Files
pbakaus_impeccable/tests/doctor.test.mjs
T
Paul BakausandClaude Code f3a6bb5a38 Retry the doctor-test scratch cleanup to kill a Node 22 CI flake
The suite runs real git subprocesses in its scratch dir, and on Node 22
the recursive afterEach delete raced git's object writes: rmdir of
.git/objects threw ENOTEMPTY and failed an unrelated PR's CI run
(seen on the #418 rebase run, checkDesignDrift suite). rmSync's
maxRetries/retryDelay options exist for exactly these transient errors.

Prepared with AI assistance (Claude Code), directed by @pbakaus.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-25 20:26:43 -07:00

538 lines
22 KiB
JavaScript

/**
* Tests for the deep staleness pass and the doctor CLI.
* Run with: node --test tests/doctor.test.mjs
*
* The Tier 1 checks are covered in tests/staleness.test.mjs. This file covers
* what only the deep pass does: git drift, ignore-list validation against the
* rule registry, hook script resolution, the monorepo sweep, and the narrow set
* of migrations `--fix` is allowed to perform.
*/
import { describe, it, beforeEach, afterEach } from 'node:test';
import { spawnSync, execFileSync } from 'node:child_process';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
import { extractPlatform } from '../skill/scripts/context.mjs';
import { parseDesignMd } from '../skill/scripts/lib/design-parser.mjs';
import { checkNativePlatformEvidence } from '../skill/scripts/lib/staleness.mjs';
import {
checkDesignCoverage,
checkDesignDrift,
checkDetectorIgnores,
checkHookInstallation,
checkLegacyLiveState,
checkWorkspaces,
loadKnownRuleIds,
} from '../skill/scripts/lib/staleness-deep.mjs';
const SCRIPTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'skill', 'scripts');
const DOCTOR_PATH = path.join(SCRIPTS_DIR, 'doctor.mjs');
let scratch;
beforeEach(() => {
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-doctor-'));
});
afterEach(() => {
// These tests run real git subprocesses in the scratch dir; on Node 22 a
// recursive delete can race git's object writes and fail the whole test
// with ENOTEMPTY (seen in CI). maxRetries/retryDelay make rmSync retry
// exactly those transient errors.
fs.rmSync(scratch, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
});
function write(rel, body) {
const abs = path.join(scratch, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, body);
return abs;
}
function ids(findings) {
return findings.map((entry) => entry.id);
}
function git(args, cwd = scratch) {
return execFileSync('git', args, { cwd, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
}
function initRepo() {
git(['init', '-q']);
git(['config', 'user.email', 'test@example.com']);
git(['config', 'user.name', 'Test']);
}
function commit(message) {
git(['add', '-A']);
git(['commit', '-q', '--no-gpg-sign', '-m', message]);
}
const CURRENT_PRODUCT = [
'# Product',
'',
'<!-- impeccable:product-schema 1 -->',
'',
'## Platform',
'',
'web',
'',
'## Positioning',
'The only one that does the thing.',
'',
].join('\n');
// ─── git drift ─────────────────────────────────────────────────────────────
describe('checkDesignDrift', () => {
it('reports nothing outside a git repository', () => {
const design = write('DESIGN.md', '---\nname: X\n---\n');
write('src/a.css', 'a{}');
assert.deepEqual(checkDesignDrift({ designPath: design, projectRoot: scratch }), []);
});
it('reports nothing when UI commits stay under the threshold', () => {
initRepo();
const design = write('DESIGN.md', '---\nname: X\n---\n');
write('src/a.css', 'a{color:red}');
commit('initial');
for (let i = 0; i < 3; i++) {
write(`src/f${i}.css`, `.f${i}{}`);
commit(`ui ${i}`);
}
assert.deepEqual(checkDesignDrift({ designPath: design, projectRoot: scratch }), []);
});
it('reports the commit count once the threshold is crossed, framed as a proxy', () => {
initRepo();
const design = write('DESIGN.md', '---\nname: X\n---\n');
write('src/a.css', 'a{color:red}');
commit('initial');
for (let i = 0; i < 4; i++) {
write(`src/f${i}.css`, `.f${i}{}`);
commit(`ui ${i}`);
}
const findings = checkDesignDrift({ designPath: design, projectRoot: scratch, threshold: 3 });
assert.deepEqual(ids(findings), ['design-md-drift']);
assert.match(findings[0].summary, /4 commits have touched/);
assert.match(findings[0].summary, /not that it is wrong/);
assert.equal(findings[0].severity, 'route');
});
it('counts only commits after the last DESIGN.md edit', () => {
initRepo();
const design = write('DESIGN.md', '---\nname: X\n---\n');
write('src/a.css', 'a{}');
commit('initial');
for (let i = 0; i < 4; i++) {
write(`src/old${i}.css`, `.o${i}{}`);
commit(`old ui ${i}`);
}
fs.writeFileSync(design, '---\nname: X2\n---\n');
commit('refresh design');
assert.deepEqual(checkDesignDrift({ designPath: design, projectRoot: scratch, threshold: 3 }), []);
});
it('reports nothing for an untracked DESIGN.md', () => {
initRepo();
write('src/a.css', 'a{}');
commit('initial');
const design = write('DESIGN.md', '---\nname: X\n---\n');
assert.deepEqual(checkDesignDrift({ designPath: design, projectRoot: scratch, threshold: 1 }), []);
});
});
// ─── DESIGN.md coverage ────────────────────────────────────────────────────
describe('checkDesignCoverage', () => {
it('names the canonical sections that carry nothing', () => {
const findings = checkDesignCoverage({
design: '---\nname: X\n---\n\n# Design System: X\n\n## Overview\n\nSomething.\n',
designPath: 'DESIGN.md',
parseDesignMd,
});
assert.deepEqual(ids(findings), ['design-md-coverage']);
assert.match(findings[0].summary, /colors, typography, components/);
assert.equal(findings[0].severity, 'mention');
});
it('stays quiet when the sections are present', () => {
const design = [
'---', 'name: X', '---', '',
'# Design System: X', '',
'## Colors', '', '### Primary', '- **Ink** (#111): Text.', '',
'## Typography', '', '**Body Font:** Inter', '',
'### Hierarchy', '- **Body** (400, 16px, 1.5): Paragraphs.', '',
'## Components', '', '### Button', '- Primary action.', '',
].join('\n');
assert.deepEqual(checkDesignCoverage({ design, designPath: 'DESIGN.md', parseDesignMd }), []);
});
it('reports nothing without a DESIGN.md', () => {
assert.deepEqual(checkDesignCoverage({ design: null, parseDesignMd }), []);
});
});
// ─── detector ignore lists ─────────────────────────────────────────────────
describe('checkDetectorIgnores', () => {
it('flags rule ids the engine does not have', () => {
write('.impeccable/config.json', JSON.stringify({
detector: { ignoreRules: ['side-tab', 'no-such-rule'] },
}));
const findings = checkDetectorIgnores({
projectRoot: scratch,
knownRuleIds: new Set(['side-tab']),
});
assert.deepEqual(ids(findings), ['detector-ignore-rules-unknown']);
assert.match(findings[0].summary, /`no-such-rule`/);
assert.doesNotMatch(findings[0].summary, /`side-tab`/);
});
it('skips validation entirely when the registry is unavailable', () => {
write('.impeccable/config.json', JSON.stringify({ detector: { ignoreRules: ['whatever'] } }));
assert.deepEqual(checkDetectorIgnores({ projectRoot: scratch, knownRuleIds: null }), []);
});
it('accepts the wildcard rule', () => {
write('.impeccable/config.json', JSON.stringify({ detector: { ignoreRules: ['*'] } }));
assert.deepEqual(
checkDetectorIgnores({ projectRoot: scratch, knownRuleIds: new Set(['side-tab']) }),
[],
);
});
it('flags ignored files that no longer exist but leaves globs alone', () => {
write('src/here.tsx', 'x');
write('.impeccable/config.json', JSON.stringify({
detector: { ignoreFiles: ['src/here.tsx', 'src/gone.tsx', 'src/**/*.stories.tsx'] },
}));
const findings = checkDetectorIgnores({ projectRoot: scratch });
assert.deepEqual(ids(findings), ['detector-ignore-files-missing']);
assert.match(findings[0].summary, /`src\/gone\.tsx`/);
assert.doesNotMatch(findings[0].summary, /stories/);
});
it('resolves the real rule registry from the source checkout', async () => {
const knownRuleIds = await loadKnownRuleIds(SCRIPTS_DIR);
assert.ok(knownRuleIds instanceof Set, 'expected the bundled detector to resolve');
assert.ok(knownRuleIds.size > 20, `expected many rule ids, got ${knownRuleIds.size}`);
assert.ok(knownRuleIds.has('side-tab'));
});
});
// ─── hook installation ─────────────────────────────────────────────────────
describe('checkHookInstallation', () => {
it('flags an installed hook whose script path does not resolve', () => {
write('.claude/settings.json', JSON.stringify({
hooks: { Stop: [{ hooks: [{ command: 'node .claude/skills/impeccable/scripts/hook.mjs' }] }] },
}));
const findings = checkHookInstallation({
projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code',
});
assert.deepEqual(ids(findings), ['hook-script-missing']);
assert.match(findings[0].summary, /no-op/);
});
it('stays quiet when the hook script is present', () => {
write('.claude/skills/impeccable/scripts/hook.mjs', '// hook\n');
write('.claude/settings.json', JSON.stringify({
hooks: { Stop: [{ hooks: [{ command: 'node .claude/skills/impeccable/scripts/hook.mjs' }] }] },
}));
assert.deepEqual(
checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' }),
[],
);
});
it('flags an installed manifest contradicted by hook.enabled false', () => {
write('.claude/skills/impeccable/scripts/hook.mjs', '// hook\n');
write('.claude/settings.json', JSON.stringify({
hooks: { Stop: [{ hooks: [{ command: 'node .claude/skills/impeccable/scripts/hook.mjs' }] }] },
}));
write('.impeccable/config.json', JSON.stringify({ hook: { enabled: false } }));
const findings = checkHookInstallation({
projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code',
});
assert.deepEqual(ids(findings), ['hook-enabled-conflict']);
});
it('reports nothing when no manifest installs the hook', () => {
write('.claude/settings.json', JSON.stringify({ hooks: { Stop: [{ hooks: [{ command: 'node other.mjs' }] }] } }));
assert.deepEqual(
checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' }),
[],
);
});
it('reports nothing for a provider with no known manifest', () => {
assert.deepEqual(
checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'source' }),
[],
);
});
// The manifest `impeccable hooks on` actually writes, verbatim: a
// `${CLAUDE_PROJECT_DIR}`-relative command. Claude Code expands the variable
// to the project dir at hook time; the doctor must expand it the same way
// (issue #402) instead of existsSync-ing the literal `${CLAUDE_PROJECT_DIR}/...`.
const claudeManifest = () => ({
hooks: {
PostToolUse: [{
matcher: 'Edit|Write|MultiEdit',
hooks: [{ type: 'command', command: 'node "${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs"' }],
}],
Stop: [{ hooks: [{ type: 'command', command: 'node "${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs"' }] }],
},
});
it('stays quiet for a ${CLAUDE_PROJECT_DIR} manifest when the script exists at root', () => {
write('.claude/skills/impeccable/scripts/hook.mjs', '// hook\n');
write('.claude/settings.json', JSON.stringify(claudeManifest()));
assert.deepEqual(
checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' }),
[],
);
});
it('still flags a genuinely missing script behind ${CLAUDE_PROJECT_DIR}', () => {
// Placeholder expands to a real path that does not exist: the check must
// stay real, not neutered into always-quiet.
write('.claude/settings.json', JSON.stringify(claudeManifest()));
const findings = checkHookInstallation({
projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code',
});
assert.deepEqual(ids(findings), ['hook-script-missing']);
});
it('handles the #399 guarded project-relative form', () => {
const p = '"${CLAUDE_PROJECT_DIR}/.claude/skills/impeccable/scripts/hook.mjs"';
const guarded = `[ ! -f ${p} ] || node ${p}`;
write('.claude/settings.json', JSON.stringify({
hooks: { Stop: [{ hooks: [{ command: guarded }] }] },
}));
// missing → flagged
assert.deepEqual(
ids(checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' })),
['hook-script-missing'],
);
// present → quiet
write('.claude/skills/impeccable/scripts/hook.mjs', '// hook\n');
assert.deepEqual(
checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' }),
[],
);
});
it('handles the #399 guarded absolute form (user-level installs)', () => {
const abs = path.join(scratch, '.claude', 'skills', 'impeccable', 'scripts', 'hook.mjs');
const p = JSON.stringify(abs);
const guarded = `[ ! -f ${p} ] || node ${p}`;
write('.claude/settings.json', JSON.stringify({
hooks: { Stop: [{ hooks: [{ command: guarded }] }] },
}));
// absolute path missing → flagged
assert.deepEqual(
ids(checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' })),
['hook-script-missing'],
);
// present → quiet
write('.claude/skills/impeccable/scripts/hook.mjs', '// hook\n');
assert.deepEqual(
checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' }),
[],
);
});
it('never reports missing for the GitHub $(git rev-parse) form', () => {
// Command substitution is not statically resolvable; a doctor must not
// assert a negative it cannot verify.
write('.github/hooks/impeccable.json', JSON.stringify({
hooks: { postToolUse: [{ bash: 'node "$(git rev-parse --show-toplevel)/.github/skills/impeccable/scripts/hook.mjs"' }] },
}));
assert.deepEqual(
checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'github' }),
[],
);
});
it('never reports missing for plugin-root placeholders the doctor cannot map', () => {
for (const token of ['${CLAUDE_PLUGIN_ROOT}', '${PLUGIN_ROOT}', '${GROK_PLUGIN_ROOT}']) {
fs.rmSync(path.join(scratch, '.claude'), { recursive: true, force: true });
write('.claude/settings.json', JSON.stringify({
hooks: { Stop: [{ hooks: [{ command: `node "${token}/skills/impeccable/scripts/hook.mjs"` }] }] },
}));
assert.deepEqual(
checkHookInstallation({ projectRoot: scratch, repoRoot: scratch, providerId: 'claude-code' }),
[],
`expected no finding for ${token}`,
);
}
});
});
// ─── retired live-mode state ───────────────────────────────────────────────
describe('checkLegacyLiveState', () => {
it('flags retired locations as automatic', () => {
write('.impeccable-live.json', '{}');
const findings = checkLegacyLiveState({ projectRoot: scratch });
assert.deepEqual(ids(findings), ['legacy-live-state']);
assert.equal(findings[0].severity, 'auto');
});
it('reports nothing on a current project', () => {
assert.deepEqual(checkLegacyLiveState({ projectRoot: scratch }), []);
});
});
// ─── monorepo sweep ────────────────────────────────────────────────────────
describe('checkWorkspaces', () => {
function sweep(candidates) {
return checkWorkspaces({
repoRoot: scratch,
candidates,
checkNativePlatformEvidence,
extractPlatform,
readFile: (filePath) => {
try { return fs.readFileSync(filePath, 'utf-8'); } catch { return null; }
},
});
}
it('flags a native workspace inheriting a web product record', () => {
write('PRODUCT.md', CURRENT_PRODUCT);
write('apps/mobile/ios/Podfile', '');
const { findings } = sweep([
{ name: 'mobile', path: 'apps/mobile', productStatus: 'inherited', productPath: 'PRODUCT.md', designStatus: 'missing' },
]);
assert.ok(ids(findings).includes('workspace-platform-native-evidence'));
const entry = findings.find((f) => f.id === 'workspace-platform-native-evidence');
assert.match(entry.summary, /inherits the repo-root PRODUCT\.md/);
assert.match(entry.fix, /its own PRODUCT\.md/);
});
it('does not flag a workspace whose own record declares the native platform', () => {
write('apps/mobile/PRODUCT.md', CURRENT_PRODUCT.replace('web', 'ios'));
write('apps/mobile/ios/Podfile', '');
const { findings } = sweep([
{ name: 'mobile', path: 'apps/mobile', productStatus: 'child', productPath: 'apps/mobile/PRODUCT.md', designStatus: 'missing' },
]);
assert.deepEqual(ids(findings), []);
});
it('reports inheritance as information, not as a defect to fix', () => {
write('PRODUCT.md', CURRENT_PRODUCT);
const { findings, workspaces } = sweep([
{ name: 'web', path: 'apps/web', productStatus: 'inherited', productPath: 'PRODUCT.md', designStatus: 'missing' },
]);
assert.deepEqual(ids(findings), ['workspace-context-inherited']);
assert.match(findings[0].summary, /is not something this check can tell/);
assert.equal(workspaces[0].platform, 'web');
});
it('returns an empty sweep with no candidates', () => {
assert.deepEqual(checkWorkspaces({ repoRoot: scratch, candidates: [] }), { findings: [], workspaces: [] });
});
});
// ─── the CLI ───────────────────────────────────────────────────────────────
describe('doctor CLI', () => {
function run(args = [], cwd = scratch) {
return spawnSync(process.execPath, [DOCTOR_PATH, ...args], {
cwd,
encoding: 'utf8',
env: { ...process.env, IMPECCABLE_NO_UPDATE_CHECK: '1' },
});
}
it('reports a clean project and exits zero', () => {
write('PRODUCT.md', CURRENT_PRODUCT);
const res = run();
assert.equal(res.status, 0, res.stderr);
assert.match(res.stdout, /No drift found/);
});
it('groups findings by severity in JSON mode', () => {
write('PRODUCT.md', '# Product\n\n## Register\n\nbrand\n\n## Users\nDesigners.\n');
const res = run(['--json']);
assert.equal(res.status, 0, res.stderr);
const report = JSON.parse(res.stdout);
assert.ok(report.findings.some((entry) => entry.id === 'product-deprecated-register'));
assert.ok(report.findings.some((entry) => entry.id === 'product-schema-legacy'));
for (const entry of report.findings) {
assert.ok(['auto', 'mention', 'route'].includes(entry.severity), entry.severity);
assert.ok(entry.summary && entry.fix, entry.id);
}
assert.equal(report.ruleRegistryAvailable, true);
});
it('applies only the automatic migrations under --fix', () => {
write('PRODUCT.md', CURRENT_PRODUCT.replace('<!-- impeccable:product-schema 1 -->\n\n', ''));
write('DESIGN.json', JSON.stringify({ schemaVersion: 2 }));
const res = run(['--fix']);
assert.equal(res.status, 0, res.stderr);
assert.match(res.stdout, /Moved DESIGN\.json to \.impeccable\/design\.json/);
assert.match(res.stdout, /Stamped PRODUCT\.md/);
assert.ok(fs.existsSync(path.join(scratch, '.impeccable', 'design.json')));
assert.ok(!fs.existsSync(path.join(scratch, 'DESIGN.json')));
assert.match(fs.readFileSync(path.join(scratch, 'PRODUCT.md'), 'utf8'), /impeccable:product-schema 1/);
});
it('leaves a legacy sidecar alone when the canonical one already exists', () => {
write('PRODUCT.md', CURRENT_PRODUCT);
write('DESIGN.json', JSON.stringify({ schemaVersion: 2, title: 'legacy' }));
write('.impeccable/design.json', JSON.stringify({ schemaVersion: 2, title: 'current' }));
const res = run(['--fix', '--json']);
assert.equal(res.status, 0, res.stderr);
const report = JSON.parse(res.stdout);
assert.equal(report.fixes.applied.length, 0);
assert.equal(
JSON.parse(fs.readFileSync(path.join(scratch, '.impeccable', 'design.json'), 'utf8')).title,
'current',
);
assert.ok(fs.existsSync(path.join(scratch, 'DESIGN.json')));
});
it('does not stamp a PRODUCT.md that init should rewrite instead', () => {
write('PRODUCT.md', '# Product\n\n## Users\nDesigners.\n');
const res = run(['--fix']);
assert.equal(res.status, 0, res.stderr);
assert.doesNotMatch(fs.readFileSync(path.join(scratch, 'PRODUCT.md'), 'utf8'), /product-schema/);
assert.match(res.stdout, /product-schema-legacy/);
});
it('lists workspaces and their context resolution in a monorepo', () => {
write('package.json', JSON.stringify({ name: 'root', workspaces: ['apps/*'] }));
write('PRODUCT.md', CURRENT_PRODUCT);
write('apps/web/package.json', JSON.stringify({ name: 'web' }));
write('apps/mobile/package.json', JSON.stringify({ name: 'mobile', dependencies: { 'react-native': '0.74' } }));
write('apps/mobile/ios/Podfile', '');
const res = run(['--json']);
assert.equal(res.status, 0, res.stderr);
const report = JSON.parse(res.stdout);
assert.equal(report.isMonorepo, true);
assert.deepEqual(report.workspaces.map((entry) => entry.path).sort(), ['apps/mobile', 'apps/web']);
assert.ok(report.findings.some((entry) => entry.id === 'workspace-platform-native-evidence'));
});
it('prints usage for --help without running any check', () => {
const res = run(['--help']);
assert.equal(res.status, 0, res.stderr);
assert.match(res.stdout, /Usage: node doctor\.mjs/);
assert.doesNotMatch(res.stdout, /No drift found/);
});
it('rejects a malformed target argument', () => {
const res = run(['--target']);
assert.notEqual(res.status, 0);
});
});