Wave-1 order 4 (P7, P12, the P9 test lock, the P3 plan-approval guard). All command hooks; none runs a model. - P12: pre-bash-executor answers permissionDecision "ask" before merge into main, push, tag/publish and destructive git (holds in auto mode). post-approval-log writes exactly what the person was asked to the K2 approval log: merge/push to main as `merge`, tag/publish as `release`, with a one-hour expiry; nothing when no person was asked. - P3: pre-plan-gate runs the K3 plan gate on a Voyage plan leaving plan mode and sends a refused plan back; "not measured" asks, never passes. - P9: lib/util/test-lock.mjs (K8). pre-write-executor refuses edits to locked test files; stop-test-lock refuses "done" for the locking session (stop_hook_active guarded). - P7: post-edit-check runs the target repo's own formatter/linter per edit, only during a Voyage plan run or after opt-in (.claude/voyage-guards.json); lib/verification/step-check.mjs (K9) fails the step on findings; no tool reads "not measured", never green. - STORM cap matcher also names `Agent` (the tool's name since 2.1.63). - Mod precondition stated in README, SECURITY.md and every PreToolUse stop guard: Voyage's guards are deterministic only when no installed mod handles `tool.check`. K2, K3 and K5 are stubbed in tests until orders 2 and 3 are merged. Two independent review rounds; every finding has a regression test. Suite 1600 (1598 pass / 0 fail / 2 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
4.5 KiB
JavaScript
96 lines
4.5 KiB
JavaScript
// tests/lib/repo-tools.test.mjs
|
|
// lib/verification/repo-tools.mjs finds the target repo's OWN formatter,
|
|
// linter and type checker. A tool counts only when the repo configures it AND
|
|
// it is installed locally (node_modules/.bin) or on PATH; nothing is ever
|
|
// downloaded. Exit codes are mapped per tool to clean / finding / error, and an
|
|
// error is never read as clean.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { writeFileSync, mkdirSync, realpathSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { makeRepo, fakeTool, tmp } from '../hooks/guard-helpers.mjs';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const mod = await import(join(HERE, '..', '..', 'lib', 'verification', 'repo-tools.mjs'));
|
|
const { resolveRepo, detectTools, toolsForFile, runTool } = mod;
|
|
|
|
test('resolveRepo: finds the root and git dir from a file path that does not exist yet', () => {
|
|
const repo = makeRepo('main');
|
|
const r = resolveRepo(join(repo, 'src', 'new', 'file.js'));
|
|
assert.equal(r.root, realpathSync(repo));
|
|
assert.equal(r.gitDir, realpathSync(join(repo, '.git')));
|
|
});
|
|
|
|
test('resolveRepo: null outside a repository', () => {
|
|
assert.equal(resolveRepo(tmp()), null);
|
|
});
|
|
|
|
test('detectTools: prettier counts only with config AND a local binary', () => {
|
|
const repo = makeRepo('main');
|
|
writeFileSync(join(repo, '.prettierrc'), '{}');
|
|
let tools = detectTools(repo, { env: { PATH: '/nonexistent' } });
|
|
const p = tools.find((t) => t.name === 'prettier');
|
|
assert.ok(p && !p.available, 'configured but not installed is listed as unavailable');
|
|
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier');
|
|
tools = detectTools(repo, { env: { PATH: '/nonexistent' } });
|
|
assert.ok(tools.find((t) => t.name === 'prettier').available);
|
|
});
|
|
|
|
test('detectTools: a repo with nothing configured has no tools', () => {
|
|
assert.deepEqual(detectTools(makeRepo('main'), { env: { PATH: '/nonexistent' } }), []);
|
|
});
|
|
|
|
test('detectTools: eslint and tsc (repo tier) from config + local binaries', () => {
|
|
const repo = makeRepo('main');
|
|
writeFileSync(join(repo, 'eslint.config.js'), 'export default [];');
|
|
writeFileSync(join(repo, 'tsconfig.json'), '{}');
|
|
fakeTool(join(repo, 'node_modules', '.bin'), 'eslint');
|
|
fakeTool(join(repo, 'node_modules', '.bin'), 'tsc');
|
|
const tools = detectTools(repo, { env: { PATH: '/nonexistent' } });
|
|
const eslint = tools.find((t) => t.name === 'eslint');
|
|
const tsc = tools.find((t) => t.name === 'tsc');
|
|
assert.equal(eslint.kind, 'lint');
|
|
assert.equal(eslint.tier, 'file');
|
|
assert.equal(tsc.kind, 'typecheck');
|
|
assert.equal(tsc.tier, 'repo');
|
|
});
|
|
|
|
test('detectTools: ruff from pyproject [tool.ruff] and a binary on PATH', () => {
|
|
const repo = makeRepo('main');
|
|
writeFileSync(join(repo, 'pyproject.toml'), '[tool.ruff]\nline-length = 100\n');
|
|
const bin = join(tmp(), 'bin');
|
|
fakeTool(bin, 'ruff');
|
|
const tools = detectTools(repo, { env: { PATH: bin } });
|
|
assert.ok(tools.some((t) => t.name === 'ruff-check' && t.available));
|
|
assert.ok(tools.some((t) => t.name === 'ruff-format' && t.available));
|
|
});
|
|
|
|
test('toolsForFile: by extension, file tier only', () => {
|
|
const repo = makeRepo('main');
|
|
writeFileSync(join(repo, '.prettierrc'), '{}');
|
|
writeFileSync(join(repo, 'tsconfig.json'), '{}');
|
|
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier');
|
|
fakeTool(join(repo, 'node_modules', '.bin'), 'tsc');
|
|
const tools = detectTools(repo, { env: { PATH: '/nonexistent' } });
|
|
assert.deepEqual(toolsForFile(tools, 'src/a.ts').map((t) => t.name), ['prettier']);
|
|
assert.deepEqual(toolsForFile(tools, 'image.png').map((t) => t.name), []);
|
|
});
|
|
|
|
test('runTool: clean, finding and error are three different answers', () => {
|
|
const repo = makeRepo('main');
|
|
writeFileSync(join(repo, '.prettierrc'), '{}');
|
|
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier');
|
|
mkdirSync(join(repo, 'src'));
|
|
writeFileSync(join(repo, 'src', 'good.js'), 'ok');
|
|
writeFileSync(join(repo, 'src', 'bad.js'), 'BAD');
|
|
const tool = detectTools(repo, { env: { PATH: '/nonexistent' } }).find((t) => t.name === 'prettier');
|
|
assert.equal(runTool(tool, ['src/good.js'], { cwd: repo }).status, 'clean');
|
|
const f = runTool(tool, ['src/bad.js'], { cwd: repo });
|
|
assert.equal(f.status, 'finding');
|
|
assert.match(f.output, /problem/);
|
|
// exit 7 is neither clean nor a finding for prettier
|
|
fakeTool(join(repo, 'node_modules', '.bin'), 'prettier', { findingExit: 7 });
|
|
assert.equal(runTool(tool, ['src/bad.js'], { cwd: repo }).status, 'error');
|
|
});
|