// tests/lib/step-check.test.mjs // K9 — the step check (lib/verification/step-check.mjs --cwd ). At step // end it runs the repo's own tools over the changed files (and the repo-wide // ones, such as the type checker), reads the post-edit guard's log, and // answers 0 = green, 1 = findings, 2 = "not measured" (never green). JSON on // stdout. The architecture-rule runner (K5, wave-1 order 3) is optional: when // it is missing, it is listed as not measured. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { spawnSync } from 'node:child_process'; import { writeFileSync, mkdirSync, appendFileSync } from 'node:fs'; import { join } from 'node:path'; import { ROOT, makeRepo, gitIn, fakeTool, tmp, pluginCopy } from '../hooks/guard-helpers.mjs'; import { childEnv } from '../helpers/child-env.mjs'; const CLI = join(ROOT, 'lib', 'verification', 'step-check.mjs'); function run(cli, args, env = {}) { const r = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8', env: childEnv({ PATH: '/usr/bin:/bin', ...env }) }); let json = null; try { json = JSON.parse(r.stdout); } catch { json = null; } return { code: r.status, stdout: r.stdout, stderr: r.stderr, json }; } function toolRepo() { const repo = makeRepo('main'); writeFileSync(join(repo, '.prettierrc'), '{}'); writeFileSync(join(repo, '.gitignore'), 'node_modules/\n'); gitIn(repo, 'add', '.prettierrc', '.gitignore'); gitIn(repo, 'commit', '-q', '-m', 'tools'); fakeTool(join(repo, 'node_modules', '.bin'), 'prettier'); return repo; } test('K9 green (0): the changed files pass the repo\'s own tools', () => { const repo = toolRepo(); writeFileSync(join(repo, 'a.js'), 'ok\n'); const r = run(CLI, ['--cwd', repo]); assert.equal(r.code, 0, r.stdout + r.stderr); assert.equal(r.json.status, 'green'); assert.ok(r.json.checks.some((c) => c.name === 'prettier' && c.status === 'clean')); }); test('K9 findings (1): a changed file the formatter rejects', () => { const repo = toolRepo(); writeFileSync(join(repo, 'a.js'), 'BAD\n'); const r = run(CLI, ['--cwd', repo]); assert.equal(r.code, 1, r.stdout); assert.equal(r.json?.status, 'findings'); }); test('K9 findings (1): the post-edit log holds a finding for a changed file no tool can re-check', () => { const repo = makeRepo('main'); writeFileSync(join(repo, 'a.js'), 'x\n'); mkdirSync(join(repo, '.git', 'voyage'), { recursive: true }); appendFileSync(join(repo, '.git', 'voyage', 'post-edit.jsonl'), JSON.stringify({ ts: new Date().toISOString(), file: 'a.js', status: 'finding', results: [{ name: 'prettier', status: 'finding' }] }) + '\n'); const r = run(CLI, ['--cwd', repo]); assert.equal(r.code, 1, r.stdout); assert.equal(r.json?.status, 'findings', 'exit 1 must be a finding, not a crash'); }); test('K9 not measured (2): a repo with no tools at all -> never green', () => { const repo = makeRepo('main'); writeFileSync(join(repo, 'a.js'), 'BAD\n'); const r = run(CLI, ['--cwd', repo]); assert.equal(r.code, 2, r.stdout); assert.equal(r.json.status, 'not_measured'); }); test('K9 not measured (2): not a git repository', () => { const r = run(CLI, ['--cwd', tmp()]); assert.equal(r.code, 2); }); test('K9 usage error (2): no --cwd', () => { assert.equal(run(CLI, []).code, 2); }); test('K9 --full: checks tracked files, not only the diff', () => { const repo = toolRepo(); writeFileSync(join(repo, 'old.js'), 'BAD\n'); gitIn(repo, 'add', 'old.js'); gitIn(repo, 'commit', '-q', '-m', 'old'); assert.notEqual(run(CLI, ['--cwd', repo]).json.status, 'findings', 'old.js is not in the diff'); assert.equal(run(CLI, ['--cwd', repo, '--full']).code, 1); }); test('K5 optional: arch-rules missing is listed as not measured and does not turn green into red', () => { const plugin = pluginCopy({ 'lib/verification/arch-rules.mjs': 'missing' }); const repo = toolRepo(); writeFileSync(join(repo, 'a.js'), 'ok\n'); const r = run(join(plugin, 'lib', 'verification', 'step-check.mjs'), ['--cwd', repo]); assert.equal(r.code, 0, r.stdout); const arch = r.json.checks.find((c) => c.name === 'arch-rules'); assert.equal(arch.status, 'not_measured'); }); test('K5 optional: arch-rules stub reporting a breach (exit 1) makes the step red', () => { const plugin = pluginCopy({ 'lib/verification/arch-rules.mjs': 'stub' }); const repo = toolRepo(); writeFileSync(join(repo, 'a.js'), 'ok\n'); const r = run(join(plugin, 'lib', 'verification', 'step-check.mjs'), ['--cwd', repo], { STUB_EXIT: '1' }); assert.equal(r.code, 1, r.stdout); assert.equal(r.json?.checks.find((c) => c.name === 'arch-rules')?.status, 'finding'); });