diff --git a/craft/scripts/fake-date-preload.cjs b/craft/scripts/fake-date-preload.cjs new file mode 100644 index 0000000..2cb97fa --- /dev/null +++ b/craft/scripts/fake-date-preload.cjs @@ -0,0 +1,9 @@ +// Test helper: freeze the wall clock for a child process. FAKE_NOW= (node -r this). +const RealDate = Date; +const fixed = new RealDate(process.env.FAKE_NOW).getTime(); +if (!Number.isFinite(fixed)) throw new Error('FAKE_NOW must be a valid ISO date'); +class FakeDate extends RealDate { + constructor(...a) { if (a.length === 0) super(fixed); else super(...a); } + static now() { return fixed; } +} +globalThis.Date = FakeDate; diff --git a/craft/scripts/gen-sitesmith-catalog.mjs b/craft/scripts/gen-sitesmith-catalog.mjs index b2b90cb..b238da5 100644 --- a/craft/scripts/gen-sitesmith-catalog.mjs +++ b/craft/scripts/gen-sitesmith-catalog.mjs @@ -106,6 +106,14 @@ function extractProps(program, file, name) { return out; } +/** + * Defaults computed from the wall clock when the editor module loads (Countdown.targetDate + * = today + 30 days). Emitting them would make the generated catalog (and the drift test) + * change every day and would teach the model a stale "default" date, so they are omitted + * from the catalog entirely. + */ +const VOLATILE_DEFAULTS = new Set(['Countdown.targetDate']); + const COLOR_RE = /^(color|bg|bgValue|gradient)$|(colou?r|bg|background|gradientfrom|gradientto)$/i; const isTokenRef = (v) => typeof v === 'string' && /^var\(--wsb-[a-z-]+\)$/.test(v); @@ -148,7 +156,7 @@ export async function buildCatalog() { if (COMMON_PROPS.has(p) || p === 'children') continue; const e = { ...info }; delete e.optional; - if (p in defaults && !(typeof defaults[p] === 'string' && defaults[p].startsWith('data:'))) e.default = slimDefault(defaults[p]); + if (p in defaults && !VOLATILE_DEFAULTS.has(`${name}.${p}`) && !(typeof defaults[p] === 'string' && defaults[p].startsWith('data:'))) e.default = slimDefault(defaults[p]); if (info.type !== 'boolean' && info.type !== 'number' && COLOR_RE.test(p)) { e.color = true; if (isTokenRef(defaults[p])) e.tokenDefault = true; diff --git a/craft/src/sitesmith/catalog.drift.test.ts b/craft/src/sitesmith/catalog.drift.test.ts index b4dc4cf..073909b 100644 --- a/craft/src/sitesmith/catalog.drift.test.ts +++ b/craft/src/sitesmith/catalog.drift.test.ts @@ -26,8 +26,11 @@ const craftRoot: string = path.resolve(decodeURIComponent(new URL('.', import.me const committedPath: string = proc.env.SITESMITH_CATALOG_FILE ?? path.resolve(craftRoot, '../../whp/web-files/libs/sitesmith-catalog.generated.json'); -function regenerate(): string { - return execFileSync(proc.execPath, ['scripts/gen-sitesmith-catalog.mjs', '--stdout'], { +function regenerate(fakeNow?: string): string { + const args = ['scripts/gen-sitesmith-catalog.mjs', '--stdout']; + if (fakeNow) args.unshift('-r', path.join(craftRoot, 'scripts/fake-date-preload.cjs')); + return execFileSync(proc.execPath, args, { + env: fakeNow ? { ...proc.env, FAKE_NOW: fakeNow } : proc.env, cwd: craftRoot, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 120_000, }); } @@ -47,6 +50,14 @@ describe('sitesmith catalog', () => { expect(fresh).toBe(committed); }, 130_000); + test('catalog is date-independent (Countdown default date is volatile and must not leak in)', () => { + const a = regenerate('2026-12-01T12:00:00Z'); + const b = regenerate('2027-03-09T12:00:00Z'); + expect(a).toBe(b); + expect(JSON.parse(a).components.Countdown.props.targetDate.default).toBeUndefined(); + expect(a).not.toMatch(/"\d{4}-\d{2}-\d{2}"/); + }, 130_000); + test('catalog covers every resolver component, with derived facts', () => { const cat = JSON.parse(regenerate()); const resolver = fs.readFileSync(path.join(craftRoot, 'src/components/resolver.ts'), 'utf8');