Files
pbakaus_impeccable/tests/hook.test.mjs
T
672517f76e Add automatic design hook install and exceptions (#170)
* docs: add PRD for design detector hook integration

Plans a PostToolUse hook for Claude Code and Codex that runs the
existing design detector after every relevant file write and feeds
findings back to the agent as advisory system-reminder context. No
implementation in this commit; covers UX, technical design, build
pipeline changes, distribution, coverage tradeoffs, and rollout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: revise hook PRD with best-practices review

Folds in the P0/P1/P2 findings from an online best-practices critique
against the official Claude Code and Codex hook references plus 10+
2026 community guides and similar prior-art tools (claw-hooks,
claude-code-hooks-mastery).

Key changes:
- Exec form everywhere (Codex snippet was shell form), with Windows
  rationale.
- Default timeout dropped from 10s to 5s.
- Re-entrancy guard (CLAUDE_HOOK_DEPTH) and per-file edit counter.
- Session-scoped finding dedup promoted from open question to v1.
- Per-language inline-ignore syntax map (HTML/JSX/CSS/JS).
- Hard-skip rules for sensitive paths and generated/lock files.
- Honest framing about Claude Code lacking per-plugin hook disable.
- Honest framing about Bash-written files being invisible in v1.
- Codex Windows-not-supported call-out, feature flag note, trust ceremony detail.
- Optional NDJSON audit log via IMPECCABLE_HOOK_LOG.
- Findings cap lowered 8 → 5 with attention-budget rationale.
- Versioned envelope ([impeccable@1]) on rendered template.
- Expanded test plan, decision log, and stdin payload appendix.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(hooks): ship the design detector hook for Claude Code and Codex

Implements docs/hooks-prd.md: a PostToolUse hook that runs the
impeccable design detector after every Edit/Write/MultiEdit on a UI
file and pushes findings into the agent's next-turn context as a
short system reminder. Silent on clean files. Never blocks an edit.

Why this matters: today, design slop (side-tab borders, gradient
text, purple/cyan palettes, bounce easing, etc.) only gets caught
when a human notices or someone explicitly runs /impeccable audit.
The hook closes the loop at the moment slop is written.

What ships in v1
- skill/scripts/hook.mjs: PostToolUse entry. Reads stdin, runs the
  detector in-process (no `npx impeccable` cold start), emits
  hookSpecificOutput.additionalContext when fresh findings exist.
- skill/scripts/hook-lib.mjs: extracted helpers (config, cache,
  filter, render, audit log, runHook orchestrator). 100% unit-testable.
- skill/scripts/hook-session-start.mjs: SessionStart greeting,
  gated by a project-scannable probe + 30-day throttle.
- skill/scripts/hook-admin.mjs: backs /impeccable hooks
  on/off/status/ignore-rule/ignore-file/reset.

Hardening built in
- Re-entrancy guard (IMPECCABLE_HOOK_DEPTH) so the hook can never
  recursively spawn itself.
- Hard-skip regexes for sensitive paths (.env, .pem, id_rsa,
  secrets, credentials, .git) and generated/lock/build output. These
  fire before the file is even read; cannot be turned off via config.
- Path-traversal check on the inbound file_path.
- Session-scoped dedup keyed by (session, file, rule, line) so the
  same finding never lands in context twice. Prevents the ~12.5K
  wasted tokens per chatty session called out in the PRD.
- Per-(session, file) edit counter with a one-shot suppression
  notice on the 7th edit, silent after.
- Fail-open contract: every error path returns exit 0 with no
  stdout. Optional NDJSON audit log via IMPECCABLE_HOOK_LOG.

Three kill switches (precedence high to low):
1. IMPECCABLE_HOOK_DISABLED env var (1/true/yes/on, case-insensitive)
2. .impeccable/hook.json `enabled: false`
3. /impeccable hooks off slash command (writes the JSON)

Inline ignores are language-aware. `// impeccable: ignore <rule>` for
JS/TS, `<!-- impeccable: ignore <rule> -->` for HTML/Vue/Svelte/Astro,
`{/* impeccable: ignore <rule> */}` for JSX/TSX, `/* impeccable:
ignore <rule> */` for CSS. `*` matches any rule. Directive applies
to the next non-blank line. Same shape as ESLint, Stylelint, Biome.

Build pipeline
- scripts/lib/transformers/hooks.js: per-provider hooks.json
  builders, plus the slim .codex-plugin/plugin.json manifest.
- providers.js: emitHooks: 'claude' for claude-code, emitHooks:
  'codex' for codex and agents. Codex also emits emitCodexPlugin.
- factory.js: emits hooks/hooks.json next to the skills tree.
- build.js: syncs hooks/ into harness roots and into the slim
  plugin/ subtree; writes .codex-plugin/plugin.json. Build is
  idempotent (verified: 98 staged files unchanged across two runs).

Claude Code wiring uses exec form (command + args) and the
${CLAUDE_PLUGIN_ROOT} placeholder. Matcher: Edit|Write|MultiEdit.
`if:` glob filters to UI extensions before spawning Node. PostToolUse
timeout 5s, SessionStart timeout 3s.

Codex wiring uses ${PLUGIN_ROOT} (Codex's native placeholder),
matcher Edit|Write|apply_patch, no `if:` analog (the script does the
extension filter). macOS and Linux only; hooks are disabled on
Windows in current Codex builds. The trust ceremony and feature flag
are documented in README.md.

Routing
- /impeccable hooks lives outside the 23-command router table on
  purpose: it is plumbing, not a design skill. The hidden
  routing slot is added to SKILL.md alongside pin/unpin so the LLM
  knows to dispatch it. The 23-command count and all stale-count
  validators remain happy.

Tests
- tests/hook.test.mjs: 38 unit tests covering env parsing, config
  load + defaults + malformed, cache round-trip + GC,
  ignoreRules/minSeverity/inline ignores (all four languages),
  globbing with **/*/{a,b}, render template with cap + clamp + 0-line
  prefix drop, audit log NDJSON, payload event-name parameterization,
  re-entrancy, kill switches, sensitive-path + generated-path +
  traversal skips, allowlist filter, config ignoreFiles, edit
  counter cycle including the 7th-edit notice, MultiEdit and
  apply_patch payload shapes, detector throw swallow, malformed
  stdin, missing file race.
- tests/hook-build.test.mjs: 18 integration tests covering hook
  manifest shape (matcher, timeouts, exec form, if: glob, placeholders),
  Codex differences (${PLUGIN_ROOT}, no if:, no SessionStart),
  Codex plugin manifest (no inline hooks field to avoid the
  duplicate-file error), routing across the hooksJsonFor table, and
  presence of all three committed artifacts plus the bundled detector
  the runtime relative-import path depends on.

Full suite: 175 bun tests + 186 node tests, all green.

Docs
- README.md: new "Design hook" section explaining default behavior,
  per-project / global / inline disable paths, the JSON schema knobs,
  the audit log debug flag, and the slop / a11y coverage split.
- HARNESSES.md: flips the `hooks` row for Codex from No -> Yes
  (Claude was already Yes), adds a per-harness hook-surface table
  with the manifest location and matcher each provider uses.

Open questions from the PRD intentionally deferred to v2: Bash-write
blind spot, effort-aware suppression, Stop-hook session summary,
per-rule severity, async hook mode. None block v1.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Codex hook scanning: apply_patch paths and co-located stylesheets

Parse file targets from Codex apply_patch command bodies, co-scan imported
and sibling CSS when UI components are edited, drop the git-sweep PostToolUse
group, and align Codex SessionStart manifest and trust docs with the official
hooks spec.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Gitignore hook session cache and drop local test HTML

Hook dedup/throttle state in .impeccable/hook.cache.json is per-project
runtime data like other .impeccable/ sidecars. Remove an untracked
bad-nested-flexbox scratch page from site/public/.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix Claude Code hook: drop Edit-only if filter so Write/MultiEdit fire

Claude's if permission rule binds to one tool name, so Edit(*.{…}) never
spawned the hook on Write or MultiEdit despite the matcher listing them.
Extension filtering now lives in hook-lib on both Claude and Codex.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Surface Cursor design findings via stop-hook followup

Replace dropped postToolUse additional_context with afterFileEdit recording
and a one-shot stop followup_message so anti-pattern nudges reach the agent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix design hook packaging and scans

* Fix Cursor hook pending bucket fallback

* Fix Sass hook scan coverage

* Fix Cursor hook review findings

* Fix session start dead hook normalization

* Fix hook config and relative scan paths

* Remove SessionStart design hook

* Remove redundant afterFileEdit normalization

* Fix Cursor suppression and module style scans

* Fix sensitive path hook filter

* Fix disabled Cursor stop hook emission

* Refresh hook harness artifacts

* Fix Cursor hook manifest install

* Add hook ignore-value support

* Ignore hook runtime files locally

* Fix Codex plugin hook packaging

* fix: address PR review bot findings

Block numeric hook depth counters from re-entering.

Avoid following stylesheet imports from traversal-looking hook targets.

* fix: gate ignore-value suggestions by supported rules

Only render exact ignore-value commands when the same finding can be suppressed by ignoreValues.

* Package Codex plugin as hook-only

* Remove Codex plugin packaging

* Recover hook install probe plumbing

* Remove Codex hook packaging follow-up doc

* Remove extra hook docs and skill wording changes

* Install real design hooks via skills CLI

* Add provider hook smoke runner

* Fix Cursor hook delivery with preToolUse gate

* Simplify Cursor hook install to preToolUse

* Clarify confirmed hook exceptions

* Persist hook ignores in shared config

* Guard font hook exceptions

* Fix hook install after main rebase

* Fix hook scan target handling

* fix: address hook review findings

* Address hook review feedback

* Stabilize DeepSeek insert live fixture

* Fix Cursor hook Python shell write bypass

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-13 21:19:19 -07:00

1571 lines
61 KiB
JavaScript

/**
* Unit tests for the Impeccable design hook.
* Run: node --test tests/hook.test.mjs
*
* Exercises hook-lib.mjs through `runHook()` with an injected detector so the
* suite stays fast and detector-independent. A second block exercises the
* library helpers (config, cache, filter, render) directly.
*/
import { describe, it, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { execFileSync } from 'node:child_process';
import {
ENVELOPE_PREFIX,
ALLOWED_EXTS,
ACK_EXTS,
DEFAULT_CONFIG,
SENSITIVE_PATH,
GENERATED_PATH,
truthy,
getConfigPath,
getLocalConfigPath,
ensureHookGitExcludes,
readConfig,
readCache,
persistCache,
bumpEditCount,
rememberFindings,
dedupeAgainstCache,
filterFindings,
renderTemplate,
renderCleanAck,
renderPendingAck,
shouldEmitAckForFile,
matchesAnyGlob,
writeAuditLog,
suppressionNotice,
parseApplyPatchPaths,
resolveTargetFiles,
resolveHarness,
normalizeHookEvent,
expandScanTargets,
parseStaticStyleImports,
coLocatedStylesheets,
runHook,
payload,
extractFindingIgnoreValue,
} from '../skill/scripts/hook-lib.mjs';
import { detectHtml, detectText } from '../cli/engine/detect-antipatterns.mjs';
function mkTmp() {
return fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-hook-'));
}
function fakeDetector(findings) {
return {
detectText: () => findings,
detectHtml: () => findings,
};
}
function finding(id, line, extras = {}) {
return {
antipattern: id,
name: extras.name || 'Test finding',
description: extras.description || 'A test finding description.',
severity: extras.severity || 'warning',
file: extras.file || 'src/Card.tsx',
line,
snippet: extras.snippet || '<snippet>',
};
}
describe('truthy()', () => {
it('matches the documented values, case-insensitive', () => {
for (const v of ['1', 'true', 'TRUE', 'yes', 'YES', 'on', 'On']) {
assert.equal(truthy(v), true, `expected truthy("${v}")`);
}
for (const v of ['', '0', 'false', 'no', 'off', 'yep', undefined, null, 42]) {
assert.equal(truthy(v), false, `expected falsy(${JSON.stringify(v)})`);
}
});
});
describe('SENSITIVE_PATH / GENERATED_PATH', () => {
it('skips .env, .pem, id_rsa, secrets, credentials, .git', () => {
for (const p of [
'/x/.env', '/x/.env.production', '/x/server.pem', '/x/id_rsa',
'/x/id_rsa.pub', '/x/api-secret.json', '/x/client_secret.ts',
'/x/credentials.yml', '/x/.git/config',
]) {
assert.ok(SENSITIVE_PATH.test(p), `expected sensitive: ${p}`);
}
});
it('does not flag normal source files as sensitive', () => {
for (const p of [
'/x/src/Card.tsx',
'/x/app/page.html',
'/x/styles/main.css',
'/x/src/CredentialForm.tsx',
'/x/src/SecretPage.jsx',
'/x/src/secretary-dashboard.vue',
'/x/src/credentials-panel.tsx',
]) {
assert.ok(!SENSITIVE_PATH.test(p), `unexpected sensitive: ${p}`);
}
});
it('skips generated / lock / build output paths', () => {
for (const p of [
'/x/src/foo.generated.tsx', '/x/types.d.ts', '/x/bundle.min.js',
'/x/node_modules/lib/index.tsx', '/x/dist/Card.tsx', '/x/build/index.html',
'/x/pkg.lock.json', '/x/.next/server.js', '/x/coverage/report.html',
]) {
assert.ok(GENERATED_PATH.test(p), `expected generated: ${p}`);
}
});
});
describe('readConfig()', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
it('returns defaults when file missing', () => {
const cfg = readConfig(cwd);
assert.equal(cfg.enabled, true);
assert.equal(cfg.limits.maxFindings, DEFAULT_CONFIG.limits.maxFindings);
});
it('parses enabled, ignoreRules, ignoreFiles, limits', () => {
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
fs.writeFileSync(path.join(cwd, '.impeccable', 'hook.json'), JSON.stringify({
enabled: false,
ignoreRules: ['side-tab'],
ignoreFiles: ['src/legacy/**'],
minSeverity: 'error',
limits: { maxFindings: 2, maxChars: 1000 },
}));
const cfg = readConfig(cwd);
assert.equal(cfg.enabled, false);
assert.deepEqual(cfg.ignoreRules, ['side-tab']);
assert.deepEqual(cfg.ignoreFiles, ['src/legacy/**']);
assert.deepEqual(cfg.ignoreValues, []);
assert.equal(cfg.limits.maxFindings, 2);
assert.equal(cfg.limits.maxChars, 1000);
});
it('merges shared config first and local config second', () => {
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
fs.writeFileSync(getConfigPath(cwd), JSON.stringify({
enabled: false,
ignoreRules: ['side-tab'],
ignoreFiles: ['src/legacy/**'],
ignoreValues: [
{ rule: 'overused-font', value: 'inter', reason: 'team default' },
],
minSeverity: 'error',
limits: { maxFindings: 2, maxChars: 1000 },
}));
fs.writeFileSync(getLocalConfigPath(cwd), JSON.stringify({
enabled: true,
ignoreRules: ['gradient-text', 'side-tab'],
ignoreFiles: ['src/local/**'],
ignoreValues: [
{ rule: 'overused-font', value: 'Roboto' },
{ rule: 'overused-font', value: 'Inter', reason: 'local override' },
],
minSeverity: 'warning',
limits: { maxFindings: 4 },
}));
const cfg = readConfig(cwd);
assert.equal(cfg.enabled, true);
assert.deepEqual(cfg.ignoreRules, ['side-tab', 'gradient-text']);
assert.deepEqual(cfg.ignoreFiles, ['src/legacy/**', 'src/local/**']);
assert.deepEqual(cfg.ignoreValues, [
{ rule: 'overused-font', value: 'inter', reason: 'local override' },
{ rule: 'overused-font', value: 'roboto' },
]);
assert.equal(cfg.limits.maxFindings, 4);
assert.equal(cfg.limits.maxChars, 1000);
});
it('tolerates malformed JSON and falls back to defaults', () => {
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
fs.writeFileSync(path.join(cwd, '.impeccable', 'hook.json'), '{ not json');
const cfg = readConfig(cwd);
assert.equal(cfg.enabled, true);
});
it('ignores malformed local config while preserving valid shared config', () => {
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
fs.writeFileSync(getConfigPath(cwd), JSON.stringify({
enabled: false,
ignoreRules: ['side-tab'],
limits: { maxFindings: 3 },
}));
fs.writeFileSync(getLocalConfigPath(cwd), '{ not json');
const cfg = readConfig(cwd);
assert.equal(cfg.enabled, false);
assert.deepEqual(cfg.ignoreRules, ['side-tab']);
assert.equal(cfg.limits.maxFindings, 3);
});
});
describe('readCache / persistCache / bumpEditCount', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
it('round-trips a session', () => {
const cache = readCache(cwd);
bumpEditCount(cache, 'sid-1', '/x/a.tsx');
bumpEditCount(cache, 'sid-1', '/x/a.tsx');
rememberFindings(cache, 'sid-1', '/x/a.tsx', [finding('side-tab', 12)]);
persistCache(cwd, cache);
const reloaded = readCache(cwd);
const file = reloaded.sessions['sid-1'].files['/x/a.tsx'];
assert.equal(file.editCount, 2);
assert.ok(file.findings.includes('side-tab:12'));
});
it('garbage-collects oldest sessions over CACHE_MAX_SESSIONS', () => {
const cache = readCache(cwd);
// Stamp 10 sessions, each with a unique updatedAt so ordering is stable.
for (let i = 0; i < 10; i++) {
const id = `sid-${i}`;
cache.sessions[id] = { updatedAt: 1000 + i, files: {} };
}
persistCache(cwd, cache);
const reloaded = readCache(cwd);
assert.equal(Object.keys(reloaded.sessions).length, 8);
assert.ok(reloaded.sessions['sid-9'], 'newest preserved');
assert.ok(!reloaded.sessions['sid-0'], 'oldest gc-ed');
});
});
describe('ensureHookGitExcludes()', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
it('adds hook runtime files to local git info exclude, not tracked .gitignore', () => {
execFileSync('git', ['init', '-q'], { cwd });
const result = ensureHookGitExcludes(cwd);
assert.equal(result.mode, 'git-info-exclude');
assert.equal(result.changed, true);
assert.equal(fs.existsSync(path.join(cwd, '.gitignore')), false);
const exclude = fs.readFileSync(path.join(cwd, '.git', 'info', 'exclude'), 'utf-8');
assert.match(exclude, /\.impeccable\/hook\.cache\.json/);
assert.match(exclude, /\.impeccable\/hook\.pending\.json/);
assert.match(exclude, /\.impeccable\/hook\.local\.json/);
const second = ensureHookGitExcludes(cwd);
assert.equal(second.changed, false);
const rewritten = fs.readFileSync(path.join(cwd, '.git', 'info', 'exclude'), 'utf-8');
assert.equal((rewritten.match(/impeccable-hook-ignore-start/g) || []).length, 1);
});
});
describe('matchesAnyGlob()', () => {
it('handles `**`, `*`, basename, and `{}` alternation', () => {
assert.ok(matchesAnyGlob('src/legacy/Foo.tsx', ['src/legacy/**']));
assert.ok(matchesAnyGlob('src/Foo.generated.tsx', ['**/*.generated.tsx']));
assert.ok(matchesAnyGlob('src/Foo.generated.tsx', ['*.generated.tsx']));
// {ts,tsx} expands to (?:ts|tsx) so the actual file path is what matches.
assert.ok(matchesAnyGlob('src/widget/Foo.tsx', ['src/widget/Foo.{ts,tsx}']));
assert.ok(matchesAnyGlob('src/widget/Foo.ts', ['src/widget/Foo.{ts,tsx}']));
assert.ok(!matchesAnyGlob('src/widgets/Foo.tsx', ['src/legacy/**']));
assert.ok(!matchesAnyGlob('src/Foo.tsx', []));
});
});
describe('filterFindings()', () => {
it('drops by ignoreRules and ignores legacy minSeverity config', () => {
const content = [
'a', // line 1
'b', // line 2
].join('\n');
const findings = [
finding('side-tab', 1, { severity: 'warning' }),
finding('gradient-text', 2, { severity: 'warning' }),
finding('overused-font', 5, { severity: 'advisory' }),
];
const filtered = filterFindings(findings, content, '.ts', {
ignoreRules: ['side-tab'],
minSeverity: 'error',
limits: DEFAULT_CONFIG.limits,
});
assert.deepEqual(filtered.map((f) => f.antipattern), ['gradient-text', 'overused-font']);
});
it('does not treat source comments as hook suppression', () => {
const content = [
'/* impeccable: ignore * */',
'.card { font-family: "Roboto", sans-serif; }',
'<!-- impeccable: ignore side-tab -->',
'<div style="border-left: 4px solid #7c3aed; border-radius: 16px;">Card</div>',
].join('\n');
const filtered = filterFindings(
[finding('overused-font', 2), finding('side-tab', 4)],
content, '.html',
{ ignoreRules: [], minSeverity: 'warning', limits: DEFAULT_CONFIG.limits }
);
assert.deepEqual(filtered.map((f) => f.antipattern), ['overused-font', 'side-tab']);
});
it('drops only matching rule/value pairs from ignoreValues', () => {
const findings = [
finding('overused-font', 1, { snippet: 'Primary font: Inter (86% of text)' }),
finding('overused-font', 2, { snippet: 'Primary font: Roboto' }),
finding('side-tab', 3),
];
const filtered = filterFindings(findings, '', '.css', {
ignoreRules: [],
ignoreValues: [{ rule: 'overused-font', value: 'inter' }],
minSeverity: 'warning',
limits: DEFAULT_CONFIG.limits,
});
assert.deepEqual(filtered.map((f) => `${f.antipattern}:${f.line}`), ['overused-font:2', 'side-tab:3']);
});
it('extracts overused-font values from primary, CSS, and Google font snippets', () => {
assert.equal(
extractFindingIgnoreValue(finding('overused-font', 1, { snippet: 'Primary font: Open Sans (80% of text)' })),
'open sans',
);
assert.equal(
extractFindingIgnoreValue(finding('overused-font', 1, { snippet: 'body { font-family: "Inter", sans-serif; }' })),
'inter',
);
assert.equal(
extractFindingIgnoreValue(finding('overused-font', 1, { snippet: 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400' })),
'plus jakarta sans',
);
assert.equal(extractFindingIgnoreValue(finding('side-tab', 1)), '');
});
});
describe('hook-admin.mjs', () => {
let cwd;
const script = path.resolve('skill', 'scripts', 'hook-admin.mjs');
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
function runAdmin(args) {
return execFileSync(process.execPath, [script, ...args], {
cwd,
env: { ...process.env },
encoding: 'utf-8',
});
}
it('ignore-value writes shared config by default without creating local config', () => {
const out = runAdmin(['ignore-value', 'overused-font', 'Inter', '--reason', 'User confirmed Inter']);
assert.match(out, /overused-font=inter/);
assert.equal(fs.existsSync(getLocalConfigPath(cwd)), false);
const shared = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8'));
assert.equal(shared.enabled, true);
assert.deepEqual(shared.ignoreRules, []);
assert.deepEqual(shared.ignoreValues.map(({ rule, value, reason }) => ({ rule, value, reason })), [
{ rule: 'overused-font', value: 'inter', reason: 'User confirmed Inter' },
]);
assert.match(shared.ignoreValues[0].createdAt, /^\d{4}-\d{2}-\d{2}T/);
});
it('ignore-value --shared remains accepted for shared config', () => {
runAdmin(['ignore-value', 'overused-font', 'Open', 'Sans', '--shared', '--reason', 'Brand font']);
assert.equal(fs.existsSync(getLocalConfigPath(cwd)), false);
const shared = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8'));
assert.deepEqual(shared.ignoreValues.map(({ rule, value, reason }) => ({ rule, value, reason })), [
{ rule: 'overused-font', value: 'open sans', reason: 'Brand font' },
]);
});
it('ignore-value --local writes private config and status reports local ignores', () => {
runAdmin(['ignore-value', 'overused-font', 'Inter', '--local']);
runAdmin(['ignore-value', 'OVERUSED-FONT', '"Inter"', '--local', '--reason', 'Still intentional']);
assert.equal(fs.existsSync(getConfigPath(cwd)), false);
const local = JSON.parse(fs.readFileSync(getLocalConfigPath(cwd), 'utf-8'));
assert.equal(local.enabled, undefined, 'local ignore should not override shared enabled state');
assert.equal(local.ignoreValues.length, 1);
assert.equal(local.ignoreValues[0].reason, 'Still intentional');
const status = runAdmin(['status']);
assert.match(status, /local file:\s+\.impeccable\/hook\.local\.json/);
assert.match(status, /ignoreValues:\s+overused-font=inter/);
});
it('ignore-rule overused-font requires explicit broad suppression', () => {
assert.throws(
() => runAdmin(['ignore-rule', 'overused-font']),
/ignore-value overused-font <font>|--all-values/,
);
assert.equal(fs.existsSync(getConfigPath(cwd)), false);
});
it('ignore-rule overused-font --all-values writes a whole-rule suppression', () => {
const out = runAdmin(['ignore-rule', 'overused-font', '--all-values', '--reason', 'User asked to ignore overused fonts generally']);
assert.match(out, /Added "overused-font" to ignoreRules/);
const shared = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8'));
assert.deepEqual(shared.ignoreRules, ['overused-font']);
assert.deepEqual(shared.ignoreValues, []);
});
it('ignore-rule still allows non-value rules without --all-values', () => {
runAdmin(['ignore-rule', 'side-tab']);
const shared = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8'));
assert.deepEqual(shared.ignoreRules, ['side-tab']);
});
it('ignore-value rejects conflicting scope flags', () => {
assert.throws(
() => runAdmin(['ignore-value', 'overused-font', 'Inter', '--shared', '--local']),
/Pass only one scope flag/,
);
});
it('ignore-file writes shared config that suppresses a later hook run', async () => {
const file = path.join(cwd, 'src/ConfirmedCard.html');
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, '<div style="border-left: 4px solid #7c3aed; border-radius: 16px; padding: 16px;">Card</div>');
runAdmin(['ignore-file', 'src/ConfirmedCard.html']);
const shared = JSON.parse(fs.readFileSync(getConfigPath(cwd), 'utf-8'));
assert.deepEqual(shared.ignoreFiles, ['src/ConfirmedCard.html']);
const r = await runHook({
stdinJson: JSON.stringify({
session_id: 'confirmed-ignore-file',
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'Edit',
tool_input: { file_path: file },
}),
env: {},
cwd,
detector: fakeDetector([finding('side-tab', 1)]),
});
assert.equal(r.stdout, '');
assert.equal(r.audit.skipped, 'config-ignore-file');
});
});
describe('renderTemplate()', () => {
it('starts with the versioned envelope and caps to maxFindings', () => {
const findings = Array.from({ length: 12 }, (_, i) =>
finding('side-tab', i + 1, { name: `R${i}`, description: 'd' }));
const text = renderTemplate(findings, '/x/Card.tsx', DEFAULT_CONFIG, { cwd: '/x' });
assert.ok(text.startsWith(`${ENVELOPE_PREFIX} Required design corrections in Card.tsx (12 issue(s)):`));
assert.match(text, /\.\.\. and 7 more \(see \/impeccable audit\)\./);
// Exactly 5 finding lines.
const lines = text.split('\n').filter((l) => l.startsWith('- '));
assert.equal(lines.length, 5);
assert.ok(text.length <= DEFAULT_CONFIG.limits.maxChars);
});
it('emits a directive footer (imperative + exception clause + confirmed ignore guidance)', () => {
// Steers the model: imperative "fix", explicit exception for
// intentional bad UI / fixtures, and "acknowledge" so the user
// sees the correction in the chat reply. See `directiveFooter()`
// in hook-lib.mjs for the rationale.
const text = renderTemplate(
[finding('side-tab', 1, { name: 'X' })],
'/x/Card.tsx', DEFAULT_CONFIG, { cwd: '/x' }
);
assert.match(text, /Fix these in your next reply/);
assert.match(text, /Acknowledge what you changed/);
assert.match(text, /intentionally bad UI|anti-pattern example|test fixture/);
assert.match(text, /Do not add hook ignores unless the user explicitly confirms/);
assert.match(text, /Do not add source comments such as `impeccable: ignore`/);
assert.match(text, /ignore-value \.\.\. --shared/);
assert.match(text, /ignore-rule overused-font --all-values/);
assert.match(text, /\/impeccable hooks ignore-file Card\.tsx/);
assert.match(text, /ignore-rule <id>/);
assert.match(text, /\/impeccable audit/);
});
it('shows the exact value-specific command for overused-font findings', () => {
const text = renderTemplate(
[finding('overused-font', 1, { name: 'Overused font', snippet: 'body { font-family: "Roboto", sans-serif; }' })],
'/x/fonts.css', DEFAULT_CONFIG, { cwd: '/x' }
);
assert.match(text, /\/impeccable hooks ignore-value overused-font Roboto --shared/);
assert.match(text, /ignore-rule overused-font --all-values/);
});
it('drops the L<line> prefix when line is 0', () => {
const text = renderTemplate(
[finding('side-tab', 0, { name: 'X' })],
'/x/a.tsx', DEFAULT_CONFIG, { cwd: '/x' }
);
assert.match(text, /^- \[side-tab\]/m);
});
it('does not suggest ignore-value for rules that cannot be value-filtered', () => {
const text = renderTemplate(
[finding('side-tab', 1, {
name: 'Side tab',
ignoreValue: 'Inter',
})],
'/x/a.tsx', DEFAULT_CONFIG, { cwd: '/x' }
);
assert.doesNotMatch(text, /\/impeccable hooks ignore-value side-tab Inter/);
});
it('clamps oversize output to maxChars', () => {
const huge = Array.from({ length: 5 }, (_, i) =>
finding('side-tab', i + 1, { name: 'X', description: 'y'.repeat(2000) }));
const text = renderTemplate(huge, '/x/a.tsx',
{ ...DEFAULT_CONFIG, limits: { maxFindings: 5, maxChars: 500 } },
{ cwd: '/x' });
assert.ok(text.length <= 500);
});
});
describe('writeAuditLog()', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
it('appends NDJSON when IMPECCABLE_HOOK_LOG is set', () => {
const log = path.join(cwd, 'audit.ndjson');
writeAuditLog({ IMPECCABLE_HOOK_LOG: log }, { event: 'PostToolUse', emitted: true });
writeAuditLog({ IMPECCABLE_HOOK_LOG: log }, { event: 'PostToolUse', emitted: false });
const body = fs.readFileSync(log, 'utf-8');
assert.equal(body.trim().split('\n').length, 2);
for (const line of body.trim().split('\n')) {
const obj = JSON.parse(line);
assert.ok(obj.ts && obj.event === 'PostToolUse');
}
});
it('is a no-op when IMPECCABLE_HOOK_LOG is unset', () => {
assert.equal(writeAuditLog({}, { event: 'x' }), false);
});
});
describe('payload()', () => {
it('produces hookSpecificOutput for Claude/Codex', () => {
const obj = JSON.parse(payload('hello'));
assert.equal(obj.hookSpecificOutput.hookEventName, 'PostToolUse');
assert.equal(obj.hookSpecificOutput.additionalContext, 'hello');
});
it('produces additional_context for Cursor', () => {
const obj = JSON.parse(payload('hello', 'PostToolUse', 'cursor'));
assert.equal(obj.additional_context, 'hello');
assert.equal(obj.hookSpecificOutput, undefined);
});
});
describe('runHook()', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
function eventFor(file, sessionId = 'sid-1') {
return {
session_id: sessionId,
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'Edit',
tool_input: { file_path: file },
};
}
function writeFixture(rel, body) {
const abs = path.join(cwd, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, body);
return abs;
}
it('emits findings on first fire, then a pending-ack on subsequent dedup hits', async () => {
// The "no silent fires" policy turns the previously-silent dedup hit
// into a pending re-nudge that keeps the unresolved finding in the
// model's context across turns. Findings emission still wins outright
// over the nudge (`renderTemplate` text), so r1 is unchanged from
// before. r2 is what changed: silent → pending ack.
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([finding('side-tab', 1, { name: 'Side-tab' })]);
const r1 = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd, detector: det });
assert.equal(r1.exitCode, 0);
assert.ok(r1.stdout.includes(ENVELOPE_PREFIX));
assert.match(r1.stdout, /Required design corrections/);
assert.equal(r1.audit.emitted, true);
const r2 = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd, detector: det });
assert.equal(r2.exitCode, 0);
assert.ok(r2.stdout.includes(ENVELOPE_PREFIX));
assert.match(r2.stdout, /Still has 1 issue\(s\) flagged earlier this session/);
assert.match(r2.stdout, /side-tab:1/);
assert.equal(r2.audit.emitted, true);
assert.equal(r2.audit.kind, 'pending');
});
it('emits a clean ack when the file has zero findings', async () => {
// No-silent-fires policy: a successful scan that finds nothing still
// emits a short positive nudge so the hook stays a conversational
// presence on every fire.
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([]); // no findings
const r = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd, detector: det });
assert.equal(r.exitCode, 0);
assert.ok(r.stdout.includes(ENVELOPE_PREFIX));
assert.match(r.stdout, /No anti-patterns/);
assert.match(r.stdout, /typography hierarchy, spacing rhythm, and color contrast/);
assert.equal(r.audit.emitted, true);
assert.equal(r.audit.kind, 'clean');
});
it('does not emit clean acks for plain .ts files', async () => {
const file = writeFixture('src/server.ts', 'export const value = 1;');
const r = await runHook({
stdinJson: JSON.stringify(eventFor(file)),
env: {},
cwd,
detector: fakeDetector([]),
});
assert.equal(r.exitCode, 0);
assert.equal(r.stdout, '');
assert.equal(r.audit.skipped, 'non-ui-ack');
});
it('still emits findings for plain .ts files', async () => {
const file = writeFixture('src/styles.ts', 'export const css = "border-left: 4px solid #7c3aed";');
const r = await runHook({
stdinJson: JSON.stringify(eventFor(file)),
env: {},
cwd,
detector: fakeDetector([finding('side-tab', 1)]),
});
assert.match(r.stdout, /Required design corrections/);
assert.match(r.stdout, /side-tab/);
});
it('does not emit pending acks for plain .js files', async () => {
const file = writeFixture('src/build.js', 'export const value = 1;');
const det = fakeDetector([finding('side-tab', 1)]);
const first = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd, detector: det });
assert.match(first.stdout, /Required design corrections/);
const second = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd, detector: det });
assert.equal(second.stdout, '');
assert.equal(second.audit.skipped, 'non-ui-ack');
});
it('IMPECCABLE_HOOK_QUIET=1 suppresses clean and pending acks, keeps findings emission', async () => {
// The opt-out kill switch for users who want the old silent-on-clean
// behavior. Findings still emit because those are real signals; the
// QUIET switch only quiets the conversational acks.
const fileA = writeFixture('src/A.tsx', 'noop');
const fileB = writeFixture('src/B.tsx', 'noop');
// Clean file: silent under QUIET.
const detClean = fakeDetector([]);
const rClean = await runHook({
stdinJson: JSON.stringify(eventFor(fileA)),
env: { IMPECCABLE_HOOK_QUIET: '1' }, cwd, detector: detClean,
});
assert.equal(rClean.stdout, '');
assert.equal(rClean.audit.emitted, false);
assert.equal(rClean.audit.quiet, true);
// Findings file: still emits.
const detFindings = fakeDetector([finding('side-tab', 1)]);
const rFindings = await runHook({
stdinJson: JSON.stringify(eventFor(fileB)),
env: { IMPECCABLE_HOOK_QUIET: '1' }, cwd, detector: detFindings,
});
assert.ok(rFindings.stdout.includes(ENVELOPE_PREFIX));
assert.match(rFindings.stdout, /Required design corrections/);
assert.equal(rFindings.audit.emitted, true);
});
it('re-entrancy guard short-circuits when IMPECCABLE_HOOK_DEPTH is set', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([finding('side-tab', 1)]);
const r = await runHook({
stdinJson: JSON.stringify(eventFor(file)),
env: { IMPECCABLE_HOOK_DEPTH: '1' },
cwd,
detector: det,
});
assert.equal(r.stdout, '');
assert.equal(r.audit.reentrant, true);
});
it('re-entrancy guard treats numeric CLAUDE_HOOK_DEPTH values as active', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([finding('side-tab', 1)]);
const r = await runHook({
stdinJson: JSON.stringify(eventFor(file)),
env: { CLAUDE_HOOK_DEPTH: '2' },
cwd,
detector: det,
});
assert.equal(r.stdout, '');
assert.equal(r.audit.reentrant, true);
});
it('IMPECCABLE_HOOK_DISABLED kill switch', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([finding('side-tab', 1)]);
for (const v of ['1', 'true', 'yes', 'on', 'TRUE']) {
const r = await runHook({
stdinJson: JSON.stringify(eventFor(file)),
env: { IMPECCABLE_HOOK_DISABLED: v },
cwd,
detector: det,
});
assert.equal(r.stdout, '', `expected silent for value ${v}`);
assert.equal(r.audit.skipped, 'env-disabled');
}
});
it('config-disabled silences cleanly', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
fs.writeFileSync(path.join(cwd, '.impeccable', 'hook.json'), JSON.stringify({ enabled: false }));
const det = fakeDetector([finding('side-tab', 1)]);
const r = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd, detector: det });
assert.equal(r.stdout, '');
assert.equal(r.audit.skipped, 'config-disabled');
});
it('rejects sensitive paths before reading file content', async () => {
const file = path.join(cwd, '.env');
fs.writeFileSync(file, 'SECRET=42');
const det = { detectText: () => { throw new Error('should not run'); } };
const r = await runHook({
stdinJson: JSON.stringify({ ...eventFor(file), tool_input: { file_path: file } }),
env: {}, cwd, detector: det,
});
assert.equal(r.audit.skipped, 'sensitive');
});
it('rejects generated paths', async () => {
const file = writeFixture('dist/Card.tsx', 'noop');
const r = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd });
assert.equal(r.audit.skipped, 'generated');
});
it('rejects path traversal in file_path', async () => {
const r = await runHook({
stdinJson: JSON.stringify({ ...eventFor('/foo/../etc/passwd') }),
env: {}, cwd,
});
assert.equal(r.audit.skipped, 'sensitive');
});
it('rejects extensions outside the allowlist', async () => {
const file = writeFixture('docs/README.md', 'noop');
const r = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd });
assert.equal(r.audit.skipped, 'extension');
});
it('config ignoreFiles glob suppresses', async () => {
const file = writeFixture('src/legacy/Foo.tsx', 'noop');
fs.mkdirSync(path.join(cwd, '.impeccable'), { recursive: true });
fs.writeFileSync(path.join(cwd, '.impeccable', 'hook.json'), JSON.stringify({
ignoreFiles: ['src/legacy/**'],
}));
const det = fakeDetector([finding('side-tab', 1)]);
const r = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd, detector: det });
assert.equal(r.stdout, '');
assert.equal(r.audit.skipped, 'config-ignore-file');
});
it('emits one-shot suppression notice on the 7th edit and silences after', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([finding('side-tab', 1)]);
let last;
for (let i = 0; i < 8; i++) {
// Use a different line each time so we don't dedup; we want to hit
// edit-count, not the dedup cache.
const f = [finding('side-tab', i + 1)];
last = await runHook({
stdinJson: JSON.stringify(eventFor(file)),
env: {}, cwd, detector: { detectText: () => f, detectHtml: () => f },
});
}
// The 7th call (index 6) crosses the threshold; the 8th (index 7) is silent.
assert.equal(last.stdout, '', '8th edit should be silent');
assert.equal(last.audit.suppressed, true);
});
it('emits suppressionNotice text on the threshold-crossing edit', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([finding('side-tab', 1)]);
let r;
for (let i = 0; i < 7; i++) {
const f = [finding('side-tab', i + 1)];
r = await runHook({
stdinJson: JSON.stringify(eventFor(file)),
env: {}, cwd, detector: { detectText: () => f, detectHtml: () => f },
});
}
assert.ok(r.stdout.includes('Suppressing further design hints'));
assert.match(r.stdout, /More than 6 edits in this session reached/);
assert.match(r.stdout, /Run \/impeccable audit to revisit/);
});
it('handles MultiEdit and apply_patch payload shapes (file_path field)', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
const det = fakeDetector([finding('side-tab', 1)]);
for (const event of [
{ ...eventFor(file), tool_name: 'MultiEdit', tool_input: { file_path: file, edits: [] } },
{ ...eventFor(file), tool_name: 'apply_patch', tool_input: { file_path: file, command: '...' } },
]) {
const r = await runHook({ stdinJson: JSON.stringify(event), env: {}, cwd, detector: det });
assert.equal(r.exitCode, 0);
// First call emits; second is dedup-silent. Reset by using fresh session.
assert.ok(r.stdout.length >= 0);
}
});
it('parses Codex apply_patch command when file_path is omitted', async () => {
writeFixture('src/Card.tsx', '<div className="border-l-4" />');
const event = {
session_id: 'sid-codex-ap',
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'apply_patch',
tool_input: {
command: '*** Begin Patch\n*** Update File: src/Card.tsx\n*** End Patch',
},
};
const det = fakeDetector([finding('side-tab', 1)]);
const r = await runHook({ stdinJson: JSON.stringify(event), env: {}, cwd, detector: det });
assert.equal(r.exitCode, 0);
assert.match(r.stdout, /Required design corrections/);
});
it('detector throw is swallowed; never breaks turn', async () => {
const file = writeFixture('src/Card.tsx', 'noop');
const det = { detectText: () => { throw new Error('boom'); } };
const r = await runHook({ stdinJson: JSON.stringify(eventFor(file)), env: {}, cwd, detector: det });
assert.equal(r.exitCode, 0);
assert.equal(r.stdout, '');
});
it('awaits the real async HTML detector before deciding a page is clean', async () => {
const file = writeFixture('index.html', [
'<!doctype html>',
'<html><body>',
'<div style="border-left: 4px solid #6366f1; border-radius: 8px; padding: 16px;">Feature</div>',
'</body></html>',
].join('\n'));
const r = await runHook({
stdinJson: JSON.stringify(eventFor(file)),
env: {},
cwd,
detector: { detectHtml, detectText },
});
assert.match(r.stdout, /Required design corrections/);
assert.doesNotMatch(r.stdout, /No anti-patterns/);
assert.ok(r.audit.findings > 0);
});
it('malformed stdin → silent skip', async () => {
const r = await runHook({ stdinJson: '{not json', env: {}, cwd });
assert.equal(r.audit.skipped, 'stdin-malformed');
});
it('missing file → silent skip (race protection)', async () => {
const r = await runHook({
stdinJson: JSON.stringify(eventFor(path.join(cwd, 'src/Vanished.tsx'))),
env: {}, cwd,
});
assert.equal(r.audit.skipped, 'file-missing');
});
});
describe('suppressionNotice()', () => {
it('starts with envelope and mentions /impeccable audit', () => {
const text = suppressionNotice('src/Card.tsx');
assert.ok(text.startsWith(ENVELOPE_PREFIX));
assert.match(text, /More than 6 edits in this session reached/);
assert.match(text, /\/impeccable audit/);
});
});
describe('ALLOWED_EXTS', () => {
it('covers the documented design-relevant extensions', () => {
for (const ext of ['.tsx', '.jsx', '.html', '.css', '.vue', '.svelte', '.astro', '.ts', '.js', '.scss', '.sass', '.less', '.htm']) {
assert.ok(ALLOWED_EXTS.has(ext), `missing: ${ext}`);
}
for (const ext of ['.md', '.py', '.go', '.json']) {
assert.ok(!ALLOWED_EXTS.has(ext), `unexpected allowed: ${ext}`);
}
});
it('keeps clean/pending acknowledgements to UI-ish files', () => {
for (const ext of ['.tsx', '.jsx', '.html', '.css', '.vue', '.svelte', '.astro', '.scss', '.sass', '.less', '.htm']) {
assert.ok(ACK_EXTS.has(ext), `missing ack extension: ${ext}`);
assert.equal(shouldEmitAckForFile(`/x/src/App${ext}`), true);
}
for (const ext of ['.ts', '.js']) {
assert.ok(!ACK_EXTS.has(ext), `unexpected ack extension: ${ext}`);
assert.equal(shouldEmitAckForFile(`/x/src/tool${ext}`), false);
}
});
});
describe('renderCleanAck() / renderPendingAck()', () => {
it('renderCleanAck stays short and ends with the steer line', () => {
const text = renderCleanAck('/x/src/App.jsx', { cwd: '/x' });
assert.match(text, /^\[impeccable@1\] Design hook scanned src\/App\.jsx\. No anti-patterns\./);
assert.match(text, /typography hierarchy, spacing rhythm, and color contrast/);
// Budget guard: should fit comfortably under a single context-message
// injection (~200 chars). Hard upper bound 240 chars.
assert.ok(text.length < 240, `clean ack too long: ${text.length} chars`);
});
it('renderPendingAck quotes up to 3 known findings and counts the rest', () => {
const known = ['side-tab:3', 'gradient-text:4', 'ai-color-palette:8', 'overused-font:12'];
const text = renderPendingAck('/x/src/SlopCard.jsx', known, { cwd: '/x' });
assert.match(text, /^\[impeccable@1\] Design hook scanned src\/SlopCard\.jsx\./);
assert.match(text, /Still has 4 issue\(s\) flagged earlier this session/);
assert.match(text, /side-tab:3, gradient-text:4, ai-color-palette:8/);
assert.match(text, /\+1 more/); // 4 total, 3 shown
assert.match(text, /Address them before finalizing/);
});
it('renderPendingAck omits the "+N more" suffix when ≤3 known findings', () => {
const text = renderPendingAck('/x/src/A.tsx', ['side-tab:1', 'gradient-text:2'], { cwd: '/x' });
assert.ok(!text.includes('+'), 'no overflow suffix expected');
});
});
describe('parseApplyPatchPaths()', () => {
it('extracts absolute and relative paths from patch bodies', () => {
const cwd = '/proj';
const rel = parseApplyPatchPaths('*** Update File: src/App.jsx\n', cwd);
assert.deepEqual(rel, ['/proj/src/App.jsx']);
const abs = parseApplyPatchPaths('*** Add File: /tmp/x.css\n*** Update File: src/y.html\n', cwd);
assert.deepEqual(abs, ['/tmp/x.css', '/proj/src/y.html']);
});
});
describe('resolveTargetFiles()', () => {
it('uses file_path when present and falls back to apply_patch command', () => {
assert.deepEqual(resolveTargetFiles({ tool_input: { file_path: '/a/b.tsx' } }, '/proj'), ['/a/b.tsx']);
assert.deepEqual(
resolveTargetFiles({ tool_name: 'apply_patch', tool_input: { command: '*** Update File: src/x.css\n' } }, '/proj'),
['/proj/src/x.css'],
);
assert.deepEqual(resolveTargetFiles({ tool_name: 'Bash', tool_input: { command: 'echo hi' } }, '/proj'), []);
});
it('includes every apply_patch file even when file_path is also present', () => {
assert.deepEqual(
resolveTargetFiles({
tool_name: 'apply_patch',
tool_input: {
file_path: '/proj/src/App.jsx',
command: '*** Update File: src/App.jsx\n*** Update File: src/styles.css\n',
},
}, '/proj'),
['/proj/src/App.jsx', '/proj/src/styles.css'],
);
});
it('accepts Cursor Write/StrReplace path field and top-level file_path', () => {
assert.deepEqual(resolveTargetFiles({ tool_input: { path: '/a/b.tsx' } }, '/proj'), ['/a/b.tsx']);
assert.deepEqual(resolveTargetFiles({ file_path: '/a/c.css' }, '/proj'), ['/a/c.css']);
});
});
describe('resolveHarness() / normalizeHookEvent()', () => {
it('routes explicit env and Cursor conversation_id to cursor harness', () => {
assert.equal(resolveHarness({ IMPECCABLE_HOOK_HARNESS: 'cursor' }), 'cursor');
assert.equal(resolveHarness({}, { conversation_id: 'c1' }), 'cursor');
assert.equal(resolveHarness({}), 'claude');
});
it('maps Cursor postToolUse Write path into file_path + cwd', () => {
const normalized = normalizeHookEvent({
conversation_id: 'c1',
workspace_roots: ['/proj'],
tool_name: 'Write',
tool_input: { path: 'src/App.jsx' },
}, '/fallback', 'cursor');
assert.equal(normalized.session_id, 'c1');
assert.equal(normalized.cwd, '/proj');
assert.equal(normalized.tool_input.file_path, 'src/App.jsx');
});
});
describe('expandScanTargets()', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
function write(rel, body) {
const abs = path.join(cwd, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, body);
return abs;
}
it('includes co-located styles.css when the primary edit is App.jsx', () => {
const app = write('src/App.jsx', 'export default function App() { return <main className="x" />; }');
write('src/styles.css', "body { font-family: 'Inter', sans-serif; }");
const expanded = expandScanTargets([app], cwd);
assert.deepEqual(expanded, [app, path.join(cwd, 'src/styles.css')]);
});
it('includes common co-located Sass, SCSS, and Less stylesheet names', () => {
for (const name of ['index.scss', 'index.sass', 'index.less', 'global.scss', 'global.less', 'globals.scss', 'globals.less']) {
const dir = `src/${name.replaceAll('.', '-')}`;
const app = write(`${dir}/App.jsx`, 'export default function App() { return <main className="x" />; }');
const stylesheet = write(`${dir}/${name}`, ".card\n border-left: 4px solid #3b82f6");
const expanded = expandScanTargets([app], cwd);
assert.ok(expanded.includes(stylesheet), `missing ${name}`);
}
});
it('follows static stylesheet imports from the edited component', () => {
const card = write('src/Card.jsx', "import './Card.module.css';\nexport default function Card() { return null; }");
const mod = write('src/Card.module.css', '.card { border-left: 4px solid #3b82f6; }');
const expanded = expandScanTargets([card], cwd);
assert.ok(expanded.includes(mod));
});
it('includes co-located module Sass and Less stylesheets', () => {
for (const name of ['Card.module.sass', 'Card.module.less']) {
const dir = `src/${name.replaceAll('.', '-')}`;
const card = write(`${dir}/Card.jsx`, 'export default function Card() { return <main className="x" />; }');
const stylesheet = write(`${dir}/${name}`, '.card { border-left: 4px solid #3b82f6; }');
const expanded = expandScanTargets([card], cwd);
assert.ok(expanded.includes(stylesheet), `missing ${name}`);
}
});
it('resolves relative primary targets against the project cwd', () => {
write('src/Card.jsx', "import './Card.module.css';\nexport default function Card() { return null; }");
const mod = write('src/Card.module.css', '.card { border-left: 4px solid #3b82f6; }');
const expanded = expandScanTargets(['src/Card.jsx'], cwd);
assert.deepEqual(expanded, [path.join(cwd, 'src/Card.jsx'), mod]);
});
it('does not follow imports from traversal-looking primary targets', () => {
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-hook-outside-'));
try {
fs.writeFileSync(path.join(outside, 'App.jsx'), "import './styles.css';\nexport default function App() { return null; }");
fs.writeFileSync(path.join(outside, 'styles.css'), "body { font-family: 'Inter', sans-serif; }");
const traversalPrimary = `${cwd}/../${path.basename(outside)}/App.jsx`;
const expanded = expandScanTargets([traversalPrimary], cwd);
assert.deepEqual(expanded, [traversalPrimary]);
} finally {
fs.rmSync(outside, { recursive: true, force: true });
}
});
it('does not expand when the primary target is already a stylesheet', () => {
const css = write('src/styles.css', "body { font-family: 'Inter', sans-serif; }");
assert.deepEqual(expandScanTargets([css], cwd), [css]);
});
});
describe('runHook() — co-located stylesheet scan', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
function write(rel, body) {
const abs = path.join(cwd, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, body);
return abs;
}
it('flags slop in styles.css when only App.jsx was edited', async () => {
const app = write('src/App.jsx', 'export default function App() { return <main className="x" />; }');
write('src/styles.css', "body { font-family: 'Inter', sans-serif; }");
const det = {
detectText: (content, filePath) => (
filePath.endsWith('.css') ? [finding('overused-font', 8)] : []
),
detectHtml: () => [],
};
const r = await runHook({
stdinJson: JSON.stringify({
session_id: 'co-scan',
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'apply_patch',
tool_input: { command: `*** Update File: ${app}\n` },
}),
env: {},
cwd,
detector: det,
});
assert.match(r.stdout, /Required design corrections/);
assert.match(r.stdout, /styles\.css/);
});
it('flags slop in co-located .sass when only App.jsx was edited', async () => {
const app = write('src/App.jsx', 'export default function App() { return <main className="x" />; }');
write('src/styles.sass', ".card\n border-left: 4px solid #3b82f6");
const det = {
detectText: (content, filePath) => (
filePath.endsWith('.sass') ? [finding('side-tab', 2)] : []
),
detectHtml: () => [],
};
const r = await runHook({
stdinJson: JSON.stringify({
session_id: 'co-scan-sass',
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'apply_patch',
tool_input: { command: `*** Update File: ${app}\n` },
}),
env: {},
cwd,
detector: det,
});
assert.match(r.stdout, /Required design corrections/);
assert.match(r.stdout, /styles\.sass/);
});
it('emits fresh findings for every file scanned in the same hook run', async () => {
const app = write('src/App.jsx', 'export default function App() { return <main className="border-l-4 border-blue-500" />; }');
const styles = write('src/styles.css', "body { font-family: 'Inter', sans-serif; }");
const seen = [];
const det = {
detectText: (content, filePath) => {
seen.push(filePath);
if (filePath.endsWith('App.jsx')) return [finding('side-tab', 1)];
if (filePath.endsWith('styles.css')) return [finding('overused-font', 1)];
return [];
},
detectHtml: () => [],
};
const r = await runHook({
stdinJson: JSON.stringify({
session_id: 'co-scan-fresh-primary',
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'apply_patch',
tool_input: { command: `*** Update File: ${app}\n` },
}),
env: {},
cwd,
detector: det,
});
assert.match(r.stdout, /Required design corrections/);
assert.match(r.stdout, /App\.jsx/);
assert.match(r.stdout, /styles\.css/);
assert.match(r.stdout, /side-tab/);
assert.match(r.stdout, /overused-font/);
assert.ok(seen.includes(app), 'primary file should be scanned');
assert.ok(seen.includes(styles), 'co-located stylesheet should still be scanned');
assert.equal(r.emission.groups.length, 2);
const cache = readCache(cwd);
const files = cache.sessions['co-scan-fresh-primary'].files;
assert.deepEqual(files[app].findings, ['side-tab:1']);
assert.deepEqual(files[styles].findings, ['overused-font:1']);
});
it('does not bump edit count for passively co-scanned stylesheets', async () => {
const app = write('src/App.jsx', 'export default function App() { return <main className="x" />; }');
const styles = write('src/styles.css', "body { font-family: 'Inter', sans-serif; }");
const det = {
detectText: (content, filePath) => (
filePath.endsWith('styles.css') ? [finding('overused-font', 1)] : []
),
detectHtml: () => [],
};
const r = await runHook({
stdinJson: JSON.stringify({
session_id: 'co-scan-edit-count',
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'apply_patch',
tool_input: { command: `*** Update File: ${app}\n` },
}),
env: {},
cwd,
detector: det,
});
assert.match(r.stdout, /styles\.css/);
const cache = readCache(cwd);
const files = cache.sessions['co-scan-edit-count'].files;
assert.equal(files[app].editCount, 1);
assert.equal(files[styles].editCount || 0, 0);
});
it('does not scan imported styles from a traversal-looking primary path', async () => {
const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'impeccable-hook-outside-'));
try {
fs.writeFileSync(path.join(outside, 'App.jsx'), "import './styles.css';\nexport default function App() { return null; }");
fs.writeFileSync(path.join(outside, 'styles.css'), "body { font-family: 'Inter', sans-serif; }");
const traversalPrimary = `${cwd}/../${path.basename(outside)}/App.jsx`;
const det = fakeDetector([finding('overused-font', 1, { name: 'Overused font' })]);
const r = await runHook({
stdinJson: JSON.stringify({
session_id: 'co-scan-traversal',
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'Edit',
tool_input: { file_path: traversalPrimary },
}),
env: {},
cwd,
detector: det,
});
assert.equal(r.stdout, '');
assert.equal(r.audit.skipped, 'sensitive');
} finally {
fs.rmSync(outside, { recursive: true, force: true });
}
});
});
describe('runHook() — events without file_path', () => {
// The sweep fallback was removed in v5 (single-hook simplification).
// Code-execution tools that don't carry a `file_path` now hit a clean
// silent skip instead of running a git-status sweep. This keeps the
// single PostToolUse matcher (Edit/Write/MultiEdit/apply_patch) honest:
// anything else is a no-op.
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
it('returns silent skip with reason no-file-path', async () => {
const event = JSON.stringify({
session_id: 'sid-x',
cwd,
hook_event_name: 'PostToolUse',
tool_name: 'mcp__node_repl__js',
tool_input: { title: 'do work', code: 'console.log(1)' },
});
const det = fakeDetector([finding('side-tab', 1)]);
const r = await runHook({ stdinJson: event, env: {}, cwd, detector: det });
assert.equal(r.exitCode, 0);
assert.equal(r.stdout, '');
assert.equal(r.audit.skipped, 'no-file-path');
});
});
describe('Cursor hook scripts', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
it('preToolUse denies proposed writes with detector findings before they land', () => {
const logPath = path.join(cwd, 'hook.ndjson');
const filePath = path.join(cwd, 'src/Card.html');
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Write',
tool_input: {
file_path: filePath,
content: `
<style>
.card { border-left: 4px solid #7c3aed; border-radius: 16px; }
</style>
<div class="card">Hello</div>
`,
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: logPath },
encoding: 'utf-8',
});
const payload = JSON.parse(out);
assert.equal(payload.permission, 'deny');
assert.match(payload.user_message, /blocked this write/);
assert.match(payload.user_message, /side-tab/);
assert.match(payload.agent_message, /Fix these in your next reply/);
const entries = fs.readFileSync(logPath, 'utf-8').trim().split('\n').map((line) => JSON.parse(line));
assert.equal(entries[0].event, 'preToolUse');
assert.equal(entries[0].blocked, true);
assert.equal(entries[0].blockedFindings, 1);
});
it('preToolUse allows clean proposed writes', () => {
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Write',
tool_input: {
path: 'src/Card.jsx',
streamContent: 'export default function Card() { return <section className="card">Hello</section>; }',
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
assert.deepEqual(JSON.parse(out), { permission: 'allow' });
});
it('preToolUse denies shell heredoc writes that bypass the Write tool', () => {
const filePath = path.join(cwd, 'src/ShellCard.html');
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Shell',
tool_input: {
command: `cat > "${filePath}" <<'EOF'\n<style>.card { border-left: 4px solid #7c3aed; border-radius: 16px; padding: 16px; }</style>\n<div class="card">Hello</div>\nEOF\n`,
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
const payload = JSON.parse(out);
assert.equal(payload.permission, 'deny');
assert.match(payload.user_message, /ShellCard\.html/);
assert.match(payload.user_message, /side-tab/);
});
it('preToolUse denies Python heredoc file writes that bypass the Write tool', () => {
const filePath = path.join(cwd, 'src/PythonCard.html');
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Shell',
tool_input: {
command: `python3 - <<'PY'\nfrom pathlib import Path\npath = Path('${filePath}')\npath.write_text('''<style>.card { border-left: 4px solid #7c3aed; border-radius: 16px; padding: 16px; }</style>\n<div class="card">Hello</div>\n''', encoding='utf-8')\nPY\n`,
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
const payload = JSON.parse(out);
assert.equal(payload.permission, 'deny');
assert.match(payload.user_message, /PythonCard\.html/);
assert.match(payload.user_message, /side-tab/);
});
it('preToolUse denies shell append redirects that bypass the Write tool', () => {
const filePath = path.join(cwd, 'src/AppendedCard.html');
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Shell',
tool_input: {
command: `cat >> "${filePath}" <<'EOF'\n<style>.card { border-left: 4px solid #7c3aed; border-radius: 16px; padding: 16px; }</style>\n<div class="card">Hello</div>\nEOF\n`,
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
const payload = JSON.parse(out);
assert.equal(payload.permission, 'deny');
assert.match(payload.user_message, /AppendedCard\.html/);
assert.match(payload.user_message, /side-tab/);
});
it('preToolUse denies shell tee writes that bypass the Write tool', () => {
const filePath = path.join(cwd, 'src/TeeCard.html');
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Shell',
tool_input: {
command: `cat <<'EOF' | tee -a "${filePath}"\n<style>.card { border-left: 4px solid #7c3aed; border-radius: 16px; padding: 16px; }</style>\n<div class="card">Hello</div>\nEOF\n`,
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
const payload = JSON.parse(out);
assert.equal(payload.permission, 'deny');
assert.match(payload.user_message, /TeeCard\.html/);
assert.match(payload.user_message, /side-tab/);
});
it('preToolUse denies shell copy writes when copied content has detector findings', () => {
const sourcePath = path.join(cwd, 'src/SourceCard.html');
const destPath = path.join(cwd, 'src/CopiedCard.html');
fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
fs.writeFileSync(sourcePath, `
<style>.card { border-left: 4px solid #7c3aed; border-radius: 16px; padding: 16px; }</style>
<div class="card">Hello</div>
`);
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Shell',
tool_input: {
command: `cp "${sourcePath}" "${destPath}"`,
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
const payload = JSON.parse(out);
assert.equal(payload.permission, 'deny');
assert.match(payload.user_message, /CopiedCard\.html/);
assert.match(payload.user_message, /side-tab/);
});
it('preToolUse reconstructs Edit old_string/new_string into a full proposed file before scanning', () => {
const filePath = path.join(cwd, 'src/EditCard.html');
fs.mkdirSync(path.dirname(filePath), { recursive: true });
const oldString = '<div class="card">Hello</div>';
fs.writeFileSync(filePath, oldString);
const newString = '<style>.card { border-left: 4px solid #7c3aed; border-radius: 16px; padding: 16px; }</style>\n<div class="card">Hello</div>';
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Edit',
tool_input: {
file_path: filePath,
old_string: oldString,
new_string: newString,
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
const payload = JSON.parse(out);
assert.equal(payload.permission, 'deny');
assert.match(payload.user_message, /EditCard\.html/);
assert.match(payload.user_message, /side-tab/);
});
it('preToolUse allows fragment-only edits instead of denying on partial context', () => {
const filePath = path.join(cwd, 'src/MissingEditCard.html');
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
tool_name: 'Edit',
tool_input: {
file_path: filePath,
new_string: '<div style="border-left: 4px solid #7c3aed; border-radius: 16px;">Hello</div>',
},
}),
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
assert.deepEqual(JSON.parse(out), { permission: 'allow' });
});
it('preToolUse downgrades repeated identical denials to allow-with-warning after the edit threshold', () => {
const filePath = path.join(cwd, 'src/LoopCard.html');
const input = JSON.stringify({
hook_event_name: 'preToolUse',
cwd,
session_id: 'cursor-loop',
tool_name: 'Write',
tool_input: {
file_path: filePath,
content: '<style>.card { border-left: 4px solid #7c3aed; border-radius: 16px; padding: 16px; }</style><div class="card">Hello</div>',
},
});
let payload;
for (let i = 0; i < 7; i++) {
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input,
env: { ...process.env, IMPECCABLE_HOOK_LOG: '' },
encoding: 'utf-8',
});
payload = JSON.parse(out);
}
assert.equal(payload.permission, 'allow');
assert.match(payload.agent_message, /allowing this write to avoid a loop/);
const cache = readCache(cwd);
const denials = cache.sessions['cursor-loop'].files[filePath].cursorDenials;
assert.equal(Object.values(denials)[0], 7);
});
it('preToolUse honors truthy IMPECCABLE_HOOK_DISABLED values before stdin parsing', () => {
const logPath = path.join(cwd, 'hook.ndjson');
const out = execFileSync(process.execPath, [path.join('skill', 'scripts', 'hook-before-edit.mjs')], {
cwd: path.resolve('.'),
input: '{not-json',
env: {
...process.env,
IMPECCABLE_HOOK_DISABLED: 'true',
IMPECCABLE_HOOK_LOG: logPath,
},
encoding: 'utf-8',
});
assert.deepEqual(JSON.parse(out), { permission: 'allow' });
const entries = fs.readFileSync(logPath, 'utf-8').trim().split('\n').map((line) => JSON.parse(line));
assert.equal(entries[0].event, 'preToolUse');
assert.equal(entries[0].skipped, 'env-disabled');
});
});
describe('runHook() — emission enrichment', () => {
let cwd;
beforeEach(() => { cwd = mkTmp(); });
afterEach(() => fs.rmSync(cwd, { recursive: true, force: true }));
function write(rel, content) {
const abs = path.join(cwd, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, content);
return abs;
}
it('returns emission.kind fresh with findings on new hits', async () => {
write('src/styles.css', "body { font-family: 'Inter', sans-serif; }");
const r = await runHook({
stdinJson: JSON.stringify({
session_id: 'emit-fresh',
cwd,
hook_event_name: 'PostToolUse',
file_path: path.join(cwd, 'src/styles.css'),
}),
env: { IMPECCABLE_HOOK_HARNESS: 'claude' },
cwd,
detector: fakeDetector([finding('overused-font', 8)]),
});
assert.equal(r.emission?.kind, 'fresh');
assert.ok(Array.isArray(r.emission?.findings));
assert.equal(r.emission.findings.length, 1);
});
});