open-design/apps/daemon/tests/design-system-import.test.ts
chaoxiaoche 6a08dfe111
Add design system package quality guard (#2224)
* Add design system import manifest schema

* Generate hybrid design system imports

* Read design system usage and cached manifests

* Add design system pull-file tool

* Show design system package evidence

* Wire design system import semantics

* Add design system package quality guard

---------

Co-authored-by: chaoxiaoche <chaoxiaoche@chaoxiaochedeMacBook-Pro.local>
2026-05-19 16:53:29 +08:00

218 lines
8.2 KiB
TypeScript

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { importLocalDesignSystemProject } from '../src/design-system-import.js';
import { listDesignSystems, readDesignSystemAssets } from '../src/design-systems.js';
describe('importLocalDesignSystemProject', () => {
let tempRoot: string;
let sourceRoot: string;
let userDesignSystemsRoot: string;
beforeEach(() => {
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'od-ds-import-'));
sourceRoot = path.join(tempRoot, 'source-app');
userDesignSystemsRoot = path.join(tempRoot, 'user-design-systems');
fs.mkdirSync(path.join(sourceRoot, 'src', 'components'), { recursive: true });
fs.mkdirSync(path.join(sourceRoot, 'src', 'styles'), { recursive: true });
fs.mkdirSync(path.join(sourceRoot, 'src', 'assets', 'fonts'), { recursive: true });
fs.mkdirSync(path.join(sourceRoot, 'public'), { recursive: true });
fs.writeFileSync(
path.join(sourceRoot, 'package.json'),
JSON.stringify({
name: '@acme/kami-app',
description: 'A focused workspace for AI design reviews.',
dependencies: { react: '^18.0.0', tailwindcss: '^3.0.0' },
}),
);
fs.writeFileSync(
path.join(sourceRoot, 'README.md'),
'# Kami App\n\nA calm review surface with crisp cards and bright primary actions.\n',
);
fs.writeFileSync(
path.join(sourceRoot, 'src', 'styles', 'tokens.css'),
':root { --color-primary: #ff3366; --color-background: #101014; --radius-card: 12px; }',
);
fs.writeFileSync(
path.join(sourceRoot, 'tailwind.config.ts'),
'export default { theme: { extend: { colors: {}, fontFamily: {}, borderRadius: {} } } }',
);
fs.writeFileSync(path.join(sourceRoot, 'src', 'components', 'Button.tsx'), 'export function Button() {}');
fs.writeFileSync(path.join(sourceRoot, 'public', 'logo.svg'), '<svg xmlns="http://www.w3.org/2000/svg" />');
fs.writeFileSync(path.join(sourceRoot, 'src', 'assets', 'fonts', 'AcmeSans-Regular.woff2'), 'font');
});
afterEach(() => {
fs.rmSync(tempRoot, { recursive: true, force: true });
});
it('generates a design-system project from a local app directory', async () => {
const result = await importLocalDesignSystemProject(sourceRoot, userDesignSystemsRoot, {
now: new Date('2026-05-18T09:00:00.000Z'),
});
expect(result.id).toBe('kami-app');
expect(result.files).toEqual(
expect.arrayContaining([
'USAGE.md',
'DESIGN.md',
'tokens.css',
'components.html',
'components.manifest.json',
'manifest.json',
'assets/logo.svg',
'fonts/acmesans-regular.woff2',
'preview/colors.html',
'preview/typography.html',
'preview/spacing.html',
'preview/components-buttons.html',
'preview/components-inputs.html',
'preview/app.html',
'source/scanned-files.json',
'source/evidence.md',
'source/tokens.source.json',
'source/snippets/INDEX.json',
'source/snippets/button.tsx',
]),
);
const manifest = JSON.parse(fs.readFileSync(path.join(result.dir, 'manifest.json'), 'utf8')) as Record<string, unknown>;
expect(manifest).toMatchObject({
schemaVersion: 'od-design-system-project/v1',
id: 'kami-app',
name: 'kami app',
category: 'Imported',
source: {
type: 'local',
path: fs.realpathSync.native(sourceRoot),
importedAt: '2026-05-18T09:00:00.000Z',
},
files: {
design: 'DESIGN.md',
tokens: 'tokens.css',
components: 'components.html',
},
usage: 'USAGE.md',
componentsManifest: 'components.manifest.json',
importMode: 'hybrid',
assetsDir: 'assets',
craft: {
applies: [],
suggested: ['color'],
exemptions: [],
},
preview: {
dir: 'preview',
},
sourceFiles: {
scanned: 'source/scanned-files.json',
evidence: 'source/evidence.md',
tokens: 'source/tokens.source.json',
snippets: 'source/snippets/INDEX.json',
},
});
expect((manifest.preview as { pages: unknown[] }).pages).toHaveLength(6);
expect(manifest.fonts).toMatchObject([{ family: 'AcmeSans Regular', file: 'fonts/acmesans-regular.woff2' }]);
const design = fs.readFileSync(path.join(result.dir, 'DESIGN.md'), 'utf8');
expect(design).toContain('A focused workspace for AI design reviews.');
expect(design).toContain('Button: `src/components/Button.tsx`');
expect(design).toContain('`--color-primary: #ff3366`');
const usage = fs.readFileSync(path.join(result.dir, 'USAGE.md'), 'utf8');
expect(usage).toContain('Auto-generated by Open Design importer');
expect(usage).toContain('## Read Order');
expect(usage).toContain('source/tokens.source.json');
const componentsManifest = JSON.parse(
fs.readFileSync(path.join(result.dir, 'components.manifest.json'), 'utf8'),
) as Record<string, unknown>;
expect(componentsManifest).toMatchObject({
schemaVersion: 1,
brandId: 'kami-app',
source: {
componentsHtml: 'components.html',
tokensCss: 'tokens.css',
},
});
const scannedFiles = JSON.parse(
fs.readFileSync(path.join(result.dir, 'source', 'scanned-files.json'), 'utf8'),
) as { files: Array<{ path: string; kind: string }> };
expect(scannedFiles.files).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: 'src/styles/tokens.css', kind: 'style' }),
expect.objectContaining({ path: 'src/components/Button.tsx', kind: 'component' }),
]),
);
const sourceTokens = JSON.parse(
fs.readFileSync(path.join(result.dir, 'source', 'tokens.source.json'), 'utf8'),
) as { tokenCount: number; tokens: Array<{ name: string; normalizedRole?: string }> };
expect(sourceTokens.tokenCount).toBe(3);
expect(sourceTokens.tokens).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: '--color-primary', normalizedRole: 'accent' }),
]),
);
const snippetsIndex = JSON.parse(
fs.readFileSync(path.join(result.dir, 'source', 'snippets', 'INDEX.json'), 'utf8'),
) as { snippets: Array<{ path: string; role: string; sourcePath: string }> };
expect(snippetsIndex.snippets).toEqual([
expect.objectContaining({
path: 'source/snippets/button.tsx',
role: 'button',
sourcePath: 'src/components/Button.tsx',
}),
]);
expect(fs.readFileSync(path.join(result.dir, 'preview', 'app.html'), 'utf8')).toContain('src/components/Button.tsx');
const assets = await readDesignSystemAssets(userDesignSystemsRoot, 'kami-app');
expect(assets.tokensCss).toContain('--accent: #ff3366;');
expect(assets.tokensCss).toContain('--bg: #101014;');
expect(assets.fixtureHtml).toContain('Component fixture');
const systems = await listDesignSystems(userDesignSystemsRoot);
expect(systems).toMatchObject([
{
id: 'kami-app',
title: 'kami app',
category: 'Imported',
summary: 'A focused workspace for AI design reviews.',
},
]);
});
it('allocates a new slug instead of colliding with existing systems', async () => {
const first = await importLocalDesignSystemProject(sourceRoot, userDesignSystemsRoot, {
reservedIds: ['kami-app'],
});
const second = await importLocalDesignSystemProject(sourceRoot, userDesignSystemsRoot, {
reservedIds: ['kami-app'],
});
expect(first.id).toBe('kami-app-2');
expect(second.id).toBe('kami-app-3');
});
it('writes selected importMode and applied craft semantics into manifest', async () => {
const result = await importLocalDesignSystemProject(sourceRoot, userDesignSystemsRoot, {
now: new Date('2026-05-18T09:00:00.000Z'),
importMode: 'verbatim',
craftApplies: ['color', 'accessibility-baseline', 'color'],
});
const manifest = JSON.parse(fs.readFileSync(path.join(result.dir, 'manifest.json'), 'utf8')) as Record<string, unknown>;
expect(manifest).toMatchObject({
importMode: 'verbatim',
craft: {
applies: ['color', 'accessibility-baseline'],
suggested: [],
exemptions: [],
},
});
});
});