From 6a7c256c6279c61ae1e674fb36eff03d06e08775 Mon Sep 17 00:00:00 2001 From: araharah-executor Date: Thu, 22 Oct 2026 08:21:27 +0800 Subject: [PATCH] test(01-04): add failing off-token audit tests (RED) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TDD harness for the D-13 audit: 13 checks across 7 test groups - clean/dirty/drift/components-layer/used-utilities fixtures under /tmp/opencode - real-build integration test (npm run build + default dist audit → exit 0) - contract: exit 0 clean, 1 violations (file:line: declaration), 2 token drift --- theme/scripts/audit.test.mjs | 209 +++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 theme/scripts/audit.test.mjs diff --git a/theme/scripts/audit.test.mjs b/theme/scripts/audit.test.mjs new file mode 100644 index 0000000..4092435 --- /dev/null +++ b/theme/scripts/audit.test.mjs @@ -0,0 +1,209 @@ +#!/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);