open-design/apps/daemon/tests/project-design-system-routes.test.ts
Eli 18b947c25f
[codex] Land design system GitHub intake handoff (#2187)
* 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
2026-05-19 14:30:17 +08:00

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);
});
});