Files
pbakaus_impeccable/tests/detect-cli-design-contamination.test.mjs
T
Paul BakausandClaude Fable 5 70fdc172b8 Resolve detect DESIGN.md from each target's project, not cwd
The detect CLI loaded DESIGN.md once from process.cwd() and applied it to
every scan target. Scanning another project's files from inside a different
repo therefore judged them against the wrong project's design system
(cross-project contamination observed during eval work: running detect from
impeccable-evals against a generated artifact elsewhere applied the evals
repo's DESIGN.md).

DESIGN.md now resolves by walking up from each scan target's own location to
its design root: a directory carrying a DESIGN.md is the root; a directory
carrying a project marker (.git / package.json / .impeccable) without a
DESIGN.md is a boundary that stops the walk with no design system, so a
sibling project never inherits a parent's or cwd's rules. A target with no
design root above it falls back to no design system rather than cwd's.
Resolution is memoized per root, so a multi-file scan reads each DESIGN.md
once, and targets spanning projects each get their own. file:// URLs resolve
from their path; remote http(s) URLs get no design system.

Adds tests/detect-cli-design-contamination.test.mjs, which spawns the real
CLI to prove B's file is not judged by A's DESIGN.md, that a project still
governs its own file, that a mixed-project scan resolves per target, and that
a marker-less bare file gets no design system.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:23:06 -07:00

133 lines
4.6 KiB
JavaScript

/**
* Regression: `impeccable detect <file-or-dir>` must resolve DESIGN.md from
* EACH scan target's own project root, not from process.cwd().
*
* The bug (found during eval work): running detect from repo A against a file
* that lives in repo B applied A's DESIGN.md to B — cross-project contamination.
* These tests spawn the real CLI so the fix is exercised end to end.
*
* Run with: node --test tests/detect-cli-design-contamination.test.mjs
*/
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CLI = path.resolve(__dirname, '../cli/bin/cli.js');
// Verdana is a plain web-safe font: it is not in OVERUSED_FONTS and trips no
// standalone rule, so the only way it becomes a `design-system-font` finding is
// if a DESIGN.md that forbids it gets applied.
const PAGE_HTML =
'<!doctype html><html><head><style>.card { font-family: Verdana, sans-serif; }</style></head>' +
'<body><div class="card">Hi</div></body></html>';
// A DESIGN.md whose typography allows only Palatino — Verdana violates it.
const DESIGN_MD = `---
typography:
body:
fontFamily: "Palatino, Georgia, serif"
---
# Project A Design System
`;
const tempRoots = [];
function mkProject({ withDesign, withMarker = true }) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detect-contam-'));
tempRoots.push(dir);
if (withMarker) fs.writeFileSync(path.join(dir, 'package.json'), '{"name":"fixture"}');
if (withDesign) fs.writeFileSync(path.join(dir, 'DESIGN.md'), DESIGN_MD);
const page = path.join(dir, 'page.html');
fs.writeFileSync(page, PAGE_HTML);
return { dir, page };
}
// Run the CLI from `cwd`; force the node binary so the HTML/jsdom path never
// runs under bun (which is unusably slow).
function runDetect(cwd, targets) {
const result = spawnSync(process.execPath, [CLI, 'detect', '--json', ...targets], {
cwd,
encoding: 'utf-8',
});
let findings = [];
try {
findings = JSON.parse(result.stdout || '[]');
} catch {
throw new Error(`Non-JSON CLI output.\nstdout: ${result.stdout}\nstderr: ${result.stderr}`);
}
return findings;
}
function fontFindingsFor(findings, file) {
return findings.filter(
(f) => f.antipattern === 'design-system-font' && (!file || f.file === file),
);
}
let projA;
let projB;
before(() => {
projA = mkProject({ withDesign: true }); // DESIGN.md forbids Verdana
projB = mkProject({ withDesign: false }); // its own project, no DESIGN.md
});
after(() => {
for (const dir of tempRoots) {
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ }
}
});
describe('detect CLI DESIGN.md resolution', () => {
it('does NOT apply cwd project A\'s DESIGN.md to project B\'s file (the contamination bug)', () => {
const findings = runDetect(projA.dir, [projB.page]);
assert.deepEqual(
fontFindingsFor(findings, projB.page).map((f) => f.ignoreValue),
[],
'project B\'s Verdana must not be flagged by project A\'s DESIGN.md',
);
});
it('still applies a project\'s own DESIGN.md to its own file (positive control)', () => {
const findings = runDetect(projA.dir, [projA.page]);
assert.ok(
fontFindingsFor(findings, projA.page).some((f) => f.ignoreValue === 'verdana'),
'project A\'s own DESIGN.md must flag Verdana in project A\'s file',
);
});
it('resolves per target when one scan spans two projects', () => {
const findings = runDetect(projA.dir, [projA.page, projB.page]);
assert.ok(
fontFindingsFor(findings, projA.page).length > 0,
'A\'s file should be judged against A\'s DESIGN.md',
);
assert.equal(
fontFindingsFor(findings, projB.page).length,
0,
'B\'s file should NOT be judged against A\'s DESIGN.md',
);
});
it('falls back to no design system for a bare file with no project markers above it', () => {
// A lone file whose directory has neither .git, package.json, nor .impeccable.
const bareDir = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-detect-bare-'));
tempRoots.push(bareDir);
const barePage = path.join(bareDir, 'page.html');
fs.writeFileSync(barePage, PAGE_HTML);
const findings = runDetect(projA.dir, [barePage]);
assert.equal(
fontFindingsFor(findings, barePage).length,
0,
'a project-less file must fall back to no design system, not cwd\'s',
);
});
});