#!/usr/bin/env node /* * audit.test.mjs — TDD harness for the off-token audit (D-13) * ------------------------------------------------------------------ * RED phase: every test below fails while scripts/audit.mjs does * not exist (spawn ENOENT); all must pass once it is implemented. * * Run: node scripts/audit.test.mjs (exit 0 = all green) * * Fixtures are (re)created under /tmp/opencode/audit-test/ — * never inside the repo. Zero runtime dependencies. * * Contract under test (from plan 01-04 task 1): * T1 stylesheet using ONLY token values → exit 0 * T2 stylesheet with off-token color / non-zero * border-radius / off-token font / box-shadow / * blur / backdrop-filter → exit 1, each * offending declaration listed as file:line: declaration * T3 tokens.css drifting below 40 colors → exit 2 (drift guard) * T4 any --radius-* token other than 0 → exit 2 (drift guard) * T5 real build (dist/rams.css) → exit 0 * T6 off-token values inside @layer components → flagged (exit 1) * T7 @layer utilities: only classes used by the * implementation pages are audited (compiled * mockup vocabulary is reference material per D-06, * never worn by implementation markup) */ import { spawnSync } from 'node:child_process'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const SCRIPTS = dirname(fileURLToPath(import.meta.url)); const THEME = resolve(SCRIPTS, '..'); const AUDIT = join(SCRIPTS, 'audit.mjs'); const FIX = '/tmp/opencode/audit-test'; const passes = []; const failures = []; function check(name, cond, detail = '') { if (cond) passes.push(name); else failures.push(detail ? `${name} — ${detail}` : name); } function runAudit(args, cwd = THEME) { const r = spawnSync(process.execPath, [AUDIT, ...args], { encoding: 'utf8', cwd }); return { status: r.status, error: r.error ? String(r.error.code || r.error) : null, out: `${r.stdout || ''}${r.stderr || ''}`, }; } /* ————— fixtures ————— */ mkdirSync(FIX, { recursive: true }); const clean = join(FIX, 'clean.css'); const dirty = join(FIX, 'dirty.css'); const fewTokens = join(FIX, 'tokens-few.css'); const badRadiusTokens = join(FIX, 'tokens-bad-radius.css'); const compLayerCss = join(FIX, 'components-layer.css'); const usedCss = join(FIX, 'used-utilities.css'); const usedPage = join(FIX, 'used-page.html'); /* T1 — token values only: var() refs, verbatim token hex, * token-hue+alpha 8-digit form, zero radius, sanctioned font * families, box-shadow none, non-blur filter, token-valued * custom property, non-literal color keywords. */ writeFileSync(clean, `/* clean fixture — token values only */ .ok-var { color: var(--color-signal-orange); } .ok-hex { background: #EB8008; border-color: #1a1a1a99; } .ok-radius { border-radius: 0; } .ok-radius-var { border-radius: var(--radius-md); } .ok-font { font-family: "Space Grotesk", Geist, ui-monospace, monospace; } .ok-shadow { box-shadow: none; } .ok-filter { filter: grayscale(1); } .ok-slot { --shell-slot: var(--color-surface); } .ok-keyword { color: currentColor; } .ok-rgb-token { background: rgba(235, 128, 8, 0.5); } `); /* T2 — every declaration below must be flagged, each printed * as file:line: declaration (the plan's example set plus the * rgb()/hsl() literal-evasion guard). */ writeFileSync(dirty, `/* dirty fixture — every declaration must be flagged */ .a-off-token-hex { color: #00ff00; } .b-non-zero-radius { border-radius: 12px; } .c-off-token-font { font-family: Arial; } .d-shadow { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } .e-blur { filter: blur(2px); } .f-glass { backdrop-filter: blur(4px); } .g-rgb-literal { background-color: rgb(255, 0, 0); } .h-hsl-literal { fill: hsl(120, 100%, 50%); } /* control: a verbatim token hex passes even in this file */ .i-control-token-hex { color: #d9d2c6; } `); /* T3 — token sheet that drifted below the 40-color floor. */ writeFileSync(fewTokens, `@theme static { --color-surface: #fff8f0; --color-signal-orange: #EB8008; --color-matte-black: #1A1A1A; --color-soft-beige: #D9D2C6; --color-functional-red: #ED3F1C; --spacing-space-md: 1rem; --font-sans: "Geist", ui-sans-serif, system-ui, sans-serif; --radius-xs: 0px; --radius-md: 0px; } `); /* T4 — real token sheet with one mutated radius token. */ const realTokens = readFileSync(join(THEME, 'src', 'tokens.css'), 'utf8'); if (!realTokens.includes('--radius-md: 0px;')) { console.error('fixture setup: --radius-md: 0px; not found in src/tokens.css'); process.exit(1); } writeFileSync(badRadiusTokens, realTokens.replace('--radius-md: 0px;', '--radius-md: 4px;')); /* T6 — off-token value inside @layer components (implementation- * owned layer) must never be skipped. */ writeFileSync(compLayerCss, `@layer components { .comp-bad { background: #ff00ff; } } `); /* T7 — utilities layer: used classes audited, unused * compiled vocabulary skipped. */ writeFileSync(usedCss, `@layer utilities { .used-bad { background: #00ff00; } .unused-bad { background: #00ff00; } } `); writeFileSync(usedPage, `
specimen
`); /* ————— tests ————— */ // T1 — clean stylesheet exits 0 { const r = runAudit([clean]); check('T1 clean stylesheet → exit 0', r.status === 0, `status=${r.status} error=${r.error} out=${r.out.slice(0, 300)}`); } // T2 — dirty stylesheet exits 1 and lists every offending declaration { const r = runAudit([dirty]); const markers = ['#00ff00', 'border-radius: 12px', 'font-family: Arial', 'box-shadow', 'blur', 'backdrop-filter', 'rgb(255, 0, 0)', 'hsl(120']; const missing = markers.filter((m) => !r.out.includes(m)); check('T2 dirty stylesheet → exit 1', r.status === 1, `status=${r.status} error=${r.error}`); check('T2 every violation listed', missing.length === 0, `missing markers: ${missing.join(', ')}`); check('T2 file:line: declaration format', /dirty\.css:\d+:/.test(r.out), 'no "file:line:" prefixed lines in output'); } // T3 — color-token drift guard (below 40 colors) → exit 2 { const r = runAudit(['--tokens', fewTokens, clean]); check('T3 < 40 color tokens → exit 2', r.status === 2, `status=${r.status} error=${r.error} out=${r.out.slice(0, 200)}`); } // T4 — radius-token drift guard (non-zero --radius-*) → exit 2 { const r = runAudit(['--tokens', badRadiusTokens, clean]); check('T4 non-zero --radius-* token → exit 2', r.status === 2, `status=${r.status} error=${r.error} out=${r.out.slice(0, 200)}`); } // T5 — real build audits clean (dist/rams.css from the sheet build) { const b = spawnSync('npm', ['run', 'build'], { encoding: 'utf8', cwd: THEME }); check('T5 npm run build succeeds', b.status === 0, `status=${b.status} ${(b.stderr || '').slice(0, 200)}`); const r = runAudit([]); check('T5 audit of real build → exit 0', r.status === 0, `status=${r.status} error=${r.error} out=${r.out.slice(0, 400)}`); } // T6 — @layer components is implementation-owned: fully audited { const r = runAudit([compLayerCss]); check('T6 components-layer off-token → exit 1', r.status === 1, `status=${r.status} error=${r.error}`); check('T6 violation listed', r.out.includes('#ff00ff') && r.out.includes('comp-bad'), `out=${r.out.slice(0, 300)}`); } // T7 — utilities layer: only classes used by implementation pages audited { const r = runAudit(['--pages', usedPage, usedCss]); check('T7 used off-token utility → exit 1', r.status === 1, `status=${r.status} error=${r.error}`); check('T7 used class flagged', r.out.includes('used-bad') && r.out.includes('#00ff00'), `out=${r.out.slice(0, 300)}`); check('T7 unused vocabulary skipped', !r.out.includes('unused-bad'), `out=${r.out.slice(0, 300)}`); } /* ————— report ————— */ console.log(`audit.test: ${passes.length} passed, ${failures.length} failed`); passes.forEach((p) => console.log(` PASS ${p}`)); failures.forEach((f) => console.error(` FAIL ${f}`)); process.exit(failures.length ? 1 : 0);