mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
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>
133 lines
4.6 KiB
JavaScript
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',
|
|
);
|
|
});
|
|
});
|