mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
* Add Claude-style design system workflow * Merge design system workflow into main * Restore design system workflow UI styles * Fix design system setup scrolling * Fix design system setup connector button * Preserve connector auth link after popup block * Simplify connected GitHub setup state * Open generated design system workspace project * Summarize design system auto prompt in chat * Add bounded GitHub connector design intake * Prefer path-scoped GitHub intake tools * Restore branch GitHub design context intake * Restore design system review workspace * Restore design system manager tab * Let design system workflow routes own details * Open editable design systems as projects * Restore design system workspace coverage * Fix bounded GitHub connector intake * Hide design system review while generating * Suppress design system generation questions * Constrain GitHub design intake to bounded command * Tolerate oversized GitHub metadata during intake * Rebuild daemon CLI when sources change * Fallback when GitHub connector snapshots are rate limited * Allow GitHub intake without Composio * Use native GitHub auth for design intake * Remove design system review group heading * Improve design system extraction evidence * Align design system scaffold with Claude output * Add evidence inventory for design system intake * Add local design system evidence intake * Add design system package audit gate * Allow auditing Claude Design reference packages * Audit design system package content quality * Migrate legacy design system artifacts * Clean migrated design system artifacts * Require modular design system UI kits * Reject thin design system UI kits * Prioritize core design evidence intake * Require role-based design system UI kits * Clean stale design system manifest references * Require representative preserved design assets * Warn on generic design system visuals * Enforce design system quality warnings * Audit connected design system UI kits * Require mounted design system UI kits * Require composed design system app shells * Require runnable JSX design system kits * Require browser globals for design system components * Infer design system names from source URLs * Require source examples in design system packages * Bind preserved fonts in design system tokens * Require skill frontmatter in design system packages * Preserve build icons in design system packages * Require real assets in brand previews * Require substantive source examples * Require product overview in design system README * Require reusable UI kit README * Require reusable design system skill docs * Seed Claude-style UI kit entry contract * Preserve runtime build assets in design packages * Audit design system packages after generation * Audit design system first-run output * Audit source-backed preview cards * Align design system UI kit scaffolds * Materialize design evidence package artifacts * Show project chat during design system setup * Hand off design system setup to project chat * Auto-repair design system audit failures * Harden design system evidence preservation * Tighten design system package guidance * Add targeted design system repair guidance * Bound design system audit auto repair * Use connector statuses in design system setup * Audit design system preview manifests * Require README preview manifests for design systems * Fix design system GitHub intake handoff * Fix daemon prompt CI assertions
326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
import type http from 'node:http';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { writeFile } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { randomUUID } from 'node:crypto';
|
|
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
|
|
|
|
import { startServer } from '../src/server.js';
|
|
|
|
describe('project design system route gates', () => {
|
|
let server: http.Server;
|
|
let baseUrl: string;
|
|
const projectsToClean: string[] = [];
|
|
const designSystemsToClean: string[] = [];
|
|
const tempDirs: string[] = [];
|
|
|
|
beforeAll(async () => {
|
|
const started = (await startServer({ port: 0, returnServer: true })) as {
|
|
url: string;
|
|
server: http.Server;
|
|
};
|
|
baseUrl = started.url;
|
|
server = started.server;
|
|
});
|
|
|
|
afterEach(() => {
|
|
for (const dir of tempDirs.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
for (const id of projectsToClean.splice(0)) {
|
|
await fetch(`${baseUrl}/api/projects/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
}).catch(() => {});
|
|
}
|
|
for (const id of designSystemsToClean.splice(0)) {
|
|
await fetch(`${baseUrl}/api/design-systems/${encodeURIComponent(id)}`, {
|
|
method: 'DELETE',
|
|
}).catch(() => {});
|
|
}
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
});
|
|
|
|
function uniqueId(prefix: string): string {
|
|
return `${prefix}-${randomUUID()}`;
|
|
}
|
|
|
|
async function createUserDesignSystem(status: 'draft' | 'published') {
|
|
const resp = await fetch(`${baseUrl}/api/design-systems`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: `Route Gate ${uniqueId(status)}`,
|
|
summary: 'Route-level design system usage guard.',
|
|
status,
|
|
}),
|
|
});
|
|
expect(resp.status).toBe(201);
|
|
const body = (await resp.json()) as {
|
|
designSystem: { id: string; status: string };
|
|
};
|
|
designSystemsToClean.push(body.designSystem.id);
|
|
return body.designSystem;
|
|
}
|
|
|
|
async function createProject(body: Record<string, unknown>) {
|
|
return fetch(`${baseUrl}/api/projects`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
async function writeProjectText(projectId: string, name: string, content: string) {
|
|
const resp = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(projectId)}/files`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ name, content }),
|
|
});
|
|
expect(resp.status).toBe(200);
|
|
}
|
|
|
|
async function readProjectText(projectId: string, name: string) {
|
|
const resp = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(projectId)}/files/${name}`);
|
|
expect(resp.status).toBe(200);
|
|
return resp.text();
|
|
}
|
|
|
|
it('rejects draft design systems when creating a project', async () => {
|
|
const draft = await createUserDesignSystem('draft');
|
|
const id = uniqueId('project-draft-ds');
|
|
|
|
const resp = await createProject({
|
|
id,
|
|
name: 'Draft Design System Project',
|
|
designSystemId: draft.id,
|
|
});
|
|
|
|
expect(resp.status).toBe(400);
|
|
const body = (await resp.json()) as { error?: { message?: string } };
|
|
expect(body.error?.message).toMatch(/draft design systems cannot be used/i);
|
|
});
|
|
|
|
it('allows published design systems when creating a project', async () => {
|
|
const published = await createUserDesignSystem('published');
|
|
const id = uniqueId('project-published-ds');
|
|
|
|
const resp = await createProject({
|
|
id,
|
|
name: 'Published Design System Project',
|
|
designSystemId: published.id,
|
|
});
|
|
|
|
expect(resp.status).toBe(200);
|
|
projectsToClean.push(id);
|
|
const body = (await resp.json()) as {
|
|
project: { id: string; designSystemId: string | null };
|
|
};
|
|
expect(body.project.designSystemId).toBe(published.id);
|
|
});
|
|
|
|
it('preserves a pending first agent task when a design-system workspace is re-opened', async () => {
|
|
const draft = await createUserDesignSystem('draft');
|
|
|
|
const workspaceResp = await fetch(
|
|
`${baseUrl}/api/design-systems/${encodeURIComponent(draft.id)}/workspace`,
|
|
{ method: 'POST' },
|
|
);
|
|
expect(workspaceResp.status).toBe(201);
|
|
const workspaceBody = (await workspaceResp.json()) as {
|
|
project: { id: string; pendingPrompt?: string | null };
|
|
};
|
|
const projectId = workspaceBody.project.id;
|
|
projectsToClean.push(projectId);
|
|
|
|
const prompt =
|
|
'Create this project as a complete Open Design design system workspace.';
|
|
const patchResp = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(projectId)}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ pendingPrompt: prompt }),
|
|
});
|
|
expect(patchResp.status).toBe(200);
|
|
|
|
const reopenedResp = await fetch(
|
|
`${baseUrl}/api/design-systems/${encodeURIComponent(draft.id)}/workspace`,
|
|
{ method: 'POST' },
|
|
);
|
|
expect(reopenedResp.status).toBe(201);
|
|
const reopenedBody = (await reopenedResp.json()) as {
|
|
project: { id: string; pendingPrompt?: string | null };
|
|
};
|
|
|
|
expect(reopenedBody.project.id).toBe(projectId);
|
|
expect(reopenedBody.project.pendingPrompt).toBe(prompt);
|
|
});
|
|
|
|
it('audits generated design-system package files from the project workspace', async () => {
|
|
const projectId = uniqueId('project-ds-audit');
|
|
const createResp = await createProject({
|
|
id: projectId,
|
|
name: 'Package Audit Project',
|
|
skillId: null,
|
|
designSystemId: null,
|
|
});
|
|
expect(createResp.status).toBe(200);
|
|
projectsToClean.push(projectId);
|
|
|
|
await writeProjectText(projectId, 'DESIGN.md', '# Package Audit Project\n\nOnly the rules file exists so far.\n');
|
|
|
|
const auditResp = await fetch(
|
|
`${baseUrl}/api/projects/${encodeURIComponent(projectId)}/design-system-package-audit`,
|
|
);
|
|
expect(auditResp.status).toBe(200);
|
|
const body = (await auditResp.json()) as {
|
|
audit: {
|
|
ok: boolean;
|
|
filesInspected: number;
|
|
errors: Array<{ code: string; path?: string }>;
|
|
};
|
|
};
|
|
|
|
expect(body.audit.ok).toBe(false);
|
|
expect(body.audit.filesInspected).toBeGreaterThan(0);
|
|
expect(body.audit.errors).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ code: 'missing_required_file', path: 'README.md' }),
|
|
expect.objectContaining({ code: 'missing_required_file', path: 'SKILL.md' }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it('removes legacy design-system artifact names when re-opening a migrated workspace', async () => {
|
|
const draft = await createUserDesignSystem('draft');
|
|
|
|
const workspaceResp = await fetch(
|
|
`${baseUrl}/api/design-systems/${encodeURIComponent(draft.id)}/workspace`,
|
|
{ method: 'POST' },
|
|
);
|
|
expect(workspaceResp.status).toBe(201);
|
|
const workspaceBody = (await workspaceResp.json()) as {
|
|
project: { id: string };
|
|
};
|
|
const projectId = workspaceBody.project.id;
|
|
projectsToClean.push(projectId);
|
|
|
|
await writeProjectText(
|
|
projectId,
|
|
'preview/typography-scale.html',
|
|
'<!doctype html><html><body>old type</body></html>',
|
|
);
|
|
await writeProjectText(
|
|
projectId,
|
|
'preview/colors-ui-palette.html',
|
|
'<!doctype html><html><body>old colors</body></html>',
|
|
);
|
|
await writeProjectText(
|
|
projectId,
|
|
'ui_kits/generated_interface/index.html',
|
|
'<!doctype html><html><body>old app</body></html>',
|
|
);
|
|
|
|
const reopenedResp = await fetch(
|
|
`${baseUrl}/api/design-systems/${encodeURIComponent(draft.id)}/workspace`,
|
|
{ method: 'POST' },
|
|
);
|
|
expect(reopenedResp.status).toBe(201);
|
|
const reopenedBody = (await reopenedResp.json()) as {
|
|
files: Array<{ path: string }>;
|
|
};
|
|
const paths = reopenedBody.files.map((file) => file.path);
|
|
|
|
expect(paths).toEqual(expect.arrayContaining([
|
|
'preview/typography-specimens.html',
|
|
'preview/colors-primary.html',
|
|
'ui_kits/app/index.html',
|
|
]));
|
|
expect(paths).not.toEqual(expect.arrayContaining([
|
|
'preview/typography-scale.html',
|
|
'preview/colors-ui-palette.html',
|
|
'ui_kits/generated_interface/index.html',
|
|
]));
|
|
});
|
|
|
|
it('refreshes stale design-system workspace docs that still point at legacy package paths', async () => {
|
|
const draft = await createUserDesignSystem('draft');
|
|
|
|
const workspaceResp = await fetch(
|
|
`${baseUrl}/api/design-systems/${encodeURIComponent(draft.id)}/workspace`,
|
|
{ method: 'POST' },
|
|
);
|
|
expect(workspaceResp.status).toBe(201);
|
|
const workspaceBody = (await workspaceResp.json()) as {
|
|
project: { id: string };
|
|
};
|
|
const projectId = workspaceBody.project.id;
|
|
projectsToClean.push(projectId);
|
|
|
|
await writeProjectText(
|
|
projectId,
|
|
'README.md',
|
|
'# Stale README\n\nReview preview/typography-scale.html and ui_kits/generated_interface/index.html.\n',
|
|
);
|
|
await writeProjectText(
|
|
projectId,
|
|
'SKILL.md',
|
|
'# Stale Skill\n\nUse preview/colors-ui-palette.html and ui_kits/generated_interface/.\n',
|
|
);
|
|
|
|
const reopenedResp = await fetch(
|
|
`${baseUrl}/api/design-systems/${encodeURIComponent(draft.id)}/workspace`,
|
|
{ method: 'POST' },
|
|
);
|
|
expect(reopenedResp.status).toBe(201);
|
|
|
|
const readme = await readProjectText(projectId, 'README.md');
|
|
const skill = await readProjectText(projectId, 'SKILL.md');
|
|
expect(readme).toContain('preview/');
|
|
expect(readme).not.toContain('ui_kits/generated_interface');
|
|
expect(readme).not.toContain('preview/typography-scale.html');
|
|
expect(skill).not.toContain('ui_kits/generated_interface');
|
|
expect(skill).not.toContain('preview/colors-ui-palette.html');
|
|
});
|
|
|
|
it('rejects patching an existing project to a draft design system', async () => {
|
|
const draft = await createUserDesignSystem('draft');
|
|
const id = uniqueId('project-patch-draft-ds');
|
|
const createResp = await createProject({ id, name: 'Patch Guard Project' });
|
|
expect(createResp.status).toBe(200);
|
|
projectsToClean.push(id);
|
|
|
|
const resp = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(id)}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ designSystemId: draft.id }),
|
|
});
|
|
|
|
expect(resp.status).toBe(400);
|
|
const body = (await resp.json()) as { error?: { message?: string } };
|
|
expect(body.error?.message).toMatch(/draft design systems cannot be used/i);
|
|
});
|
|
|
|
it('rejects draft design systems when importing a folder as a project', async () => {
|
|
const draft = await createUserDesignSystem('draft');
|
|
const folder = mkdtempSync(path.join(tmpdir(), 'od-import-draft-ds-'));
|
|
tempDirs.push(folder);
|
|
await writeFile(path.join(folder, 'index.html'), '<!doctype html>');
|
|
|
|
const resp = await fetch(`${baseUrl}/api/import/folder`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
baseDir: folder,
|
|
name: 'Imported Draft Design System Project',
|
|
designSystemId: draft.id,
|
|
}),
|
|
});
|
|
|
|
expect(resp.status).toBe(400);
|
|
const body = (await resp.json()) as { error?: { message?: string } };
|
|
expect(body.error?.message).toMatch(/draft design systems cannot be used/i);
|
|
});
|
|
});
|