mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* feat(daemon): inject compiled design-system tokens + fixture into prompts Follow-up to #1231. The prior PR landed the structured form of two brands (`default` + `kami`) and codified the schema; this PR teaches the daemon to actually consume those files when assembling the system prompt, so agents stop having to re-derive token names from DESIGN.md prose every turn. Gated behind `OD_DESIGN_TOKEN_CHANNEL=1` for the smoke-test phase — flag-off keeps the daemon byte-equivalent to today's behavior, flag-on appends two new prompt blocks (the brand's `tokens.css` :root contract and its `components.html` reference fixture) right after the existing DESIGN.md block. Brands without those sibling files (every brand except `default` and `kami` today) skip silently in either mode. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(daemon): only swallow ENOENT/ENOTDIR in readFileOptional, rethrow rest Reviewer feedback (nettee, #1385). The prior catch-all hid permission errors, EISDIR, and broken packaged-resource paths behind the same "undefined = absent" branch the legacy ~138-brand fallback uses, which would let `OD_DESIGN_TOKEN_CHANNEL=1` silently degrade to the DESIGN.md-only prompt while reporting success. That corrupts the exact signal the smoke-test rollout depends on. Now `readFileOptional` only returns undefined for ENOENT / ENOTDIR (real "file does not exist" cases) and rethrows everything else. Added a focused test that plants a directory at the tokens.css path to exercise the EISDIR branch, plus a partial-presence regression test to confirm the stricter contract preserves the legacy fallback. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: chaoxiaoche <chaoxiaoche@192.168.10.16> Co-authored-by: Cursor <cursoragent@cursor.com>
115 lines
4.9 KiB
TypeScript
115 lines
4.9 KiB
TypeScript
// Focused test for readDesignSystemAssets — the new sibling-file reader
|
|
// that lets the daemon ship the compiled (tokens.css + components.html)
|
|
// form of a brand alongside its DESIGN.md prose. The legacy reader
|
|
// (`readDesignSystem`, returning DESIGN.md content) already has implicit
|
|
// coverage through the showcase + chat-route tests; this file pins the
|
|
// new helper's contract so future changes can't silently regress the
|
|
// "either or both files may be absent" semantics that PR-C relies on
|
|
// for graceful fallback across the ~138 brands without compiled tokens
|
|
// today.
|
|
|
|
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
import { readDesignSystemAssets } from '../src/design-systems.js';
|
|
|
|
function fresh(): string {
|
|
return mkdtempSync(path.join(tmpdir(), 'od-design-system-assets-'));
|
|
}
|
|
|
|
function brandDir(root: string, id: string): string {
|
|
const dir = path.join(root, id);
|
|
mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
describe('readDesignSystemAssets', () => {
|
|
it('returns both fields when tokens.css and components.html are both present', async () => {
|
|
const root = fresh();
|
|
const dir = brandDir(root, 'sample');
|
|
writeFileSync(path.join(dir, 'tokens.css'), ':root {\n --bg: #fff;\n}\n');
|
|
writeFileSync(
|
|
path.join(dir, 'components.html'),
|
|
'<!doctype html><html><body>fixture</body></html>\n',
|
|
);
|
|
|
|
const assets = await readDesignSystemAssets(root, 'sample');
|
|
expect(assets.tokensCss).toContain('--bg: #fff');
|
|
expect(assets.fixtureHtml).toContain('fixture');
|
|
});
|
|
|
|
it('returns the single field that exists when its sibling is missing (per-file independence)', async () => {
|
|
const root = fresh();
|
|
const dir = brandDir(root, 'tokens-only');
|
|
writeFileSync(path.join(dir, 'tokens.css'), ':root { --x: 1; }');
|
|
|
|
const tokensOnly = await readDesignSystemAssets(root, 'tokens-only');
|
|
expect(tokensOnly.tokensCss).toBe(':root { --x: 1; }');
|
|
expect(tokensOnly.fixtureHtml).toBeUndefined();
|
|
|
|
const fixtureDir = brandDir(root, 'fixture-only');
|
|
writeFileSync(path.join(fixtureDir, 'components.html'), '<p>only</p>');
|
|
|
|
const fixtureOnly = await readDesignSystemAssets(root, 'fixture-only');
|
|
expect(fixtureOnly.tokensCss).toBeUndefined();
|
|
expect(fixtureOnly.fixtureHtml).toBe('<p>only</p>');
|
|
});
|
|
|
|
it('returns an empty object when the brand directory has neither file', async () => {
|
|
const root = fresh();
|
|
brandDir(root, 'prose-only');
|
|
|
|
const assets = await readDesignSystemAssets(root, 'prose-only');
|
|
expect(assets.tokensCss).toBeUndefined();
|
|
expect(assets.fixtureHtml).toBeUndefined();
|
|
});
|
|
|
|
it('returns an empty object when the brand directory itself does not exist (legacy ~138-brand fallback)', async () => {
|
|
const root = fresh();
|
|
const assets = await readDesignSystemAssets(root, 'nonexistent-brand');
|
|
expect(assets.tokensCss).toBeUndefined();
|
|
expect(assets.fixtureHtml).toBeUndefined();
|
|
});
|
|
|
|
// Reviewer feedback (nettee, PR-C #1385): the prior implementation
|
|
// swallowed every readFile() error as "absent", which would silently
|
|
// hide non-absence failures (EACCES, EISDIR, broken packaged
|
|
// resource paths, transient I/O) and ship the legacy DESIGN.md-only
|
|
// prompt as if the token channel had succeeded. That corrupts the
|
|
// exact signal the smoke-test rollout depends on. The reader now
|
|
// only swallows ENOENT / ENOTDIR; everything else must surface.
|
|
it('rejects on non-absence read failures so token-channel misconfigurations surface', async () => {
|
|
const root = fresh();
|
|
const dir = brandDir(root, 'broken-tokens');
|
|
// Plant a DIRECTORY at the tokens.css path. readFile() rejects
|
|
// with EISDIR — a real-world stand-in for permission / packaged-
|
|
// resource path bugs that should fail visibly, not silently fall
|
|
// back. EACCES would be more lifelike but is hard to simulate
|
|
// portably across CI runners; EISDIR exercises the exact same
|
|
// "non-absence error" branch.
|
|
mkdirSync(path.join(dir, 'tokens.css'));
|
|
|
|
await expect(readDesignSystemAssets(root, 'broken-tokens')).rejects.toThrow(
|
|
/EISDIR|illegal operation|directory/i,
|
|
);
|
|
});
|
|
|
|
it('still treats ENOENT as absence even when one sibling is present (per-file independence holds under the stricter contract)', async () => {
|
|
// Pin the flip side of the rejection test above: tightening the
|
|
// catch must NOT regress the legacy ~138-brand fallback. With
|
|
// tokens.css present and components.html absent, the reader
|
|
// returns the present side and undefined for the missing one,
|
|
// exactly as before.
|
|
const root = fresh();
|
|
const dir = brandDir(root, 'partial');
|
|
writeFileSync(path.join(dir, 'tokens.css'), ':root { --x: 1; }');
|
|
|
|
const assets = await readDesignSystemAssets(root, 'partial');
|
|
expect(assets.tokensCss).toBe(':root { --x: 1; }');
|
|
expect(assets.fixtureHtml).toBeUndefined();
|
|
});
|
|
});
|