voyage/tests/lib/test-integrity.test.mjs
Kjell Tore Guttormsen abc3083c1e
feat(execute): one fresh helper per step, a controller on the diff, proof by command
/trekexecute no longer builds steps itself. Each plan step is built by one
fresh helper (new agent step-implementer: opus, effort high, no Agent tool)
and judged by one controller (a general helper on opus) that reads the
diff from a recorded start_sha and gives two verdicts, spec and quality.
Fix rounds go up, maximum 5: three fixes by a fresh helper, then back to
the plan, then to the operator. No cheaper-model ladder.

Proof is a command, never the model's word:
- lib/verification/red-proof.mjs: the step's test must fail at the start
  point and pass at the result (exit 0/1/2; 2 is never green). Reproduces
  the 02243c6 red-first measurement (6 of 55) through the shim fixture.
- lib/verification/test-integrity.mjs: the floor guard on the diff
  (removed asserts, skipped/deleted tests, muted checks, stubs, loosened
  thresholds). Over the last 150 commits: 149 clean, 1 real finding.
- lib/verification/done-line.mjs: DONE step N only when the test command
  exits 0.

Test first is required from plan_version 1.8. The test lock (K8) is used
through its CLI and reported as not-locked while it is missing. A run
stops only on a closed list of four classes; every other call goes into a
decision log with its cost if wrong. Without a passing run-the-thing
criterion the result is not verified end to end, never green.

Handover 5 gains optional per-step start_sha, report_path, red_proof,
test_lock, fix_rounds (0-5) and run-level decisions and end_to_end; old
progress files still validate and resume.

No isolation on the helper: measured, a worktree-isolated subagent
branches from the default branch (02f320f), not the executor's HEAD
(2c89a6b), so it would miss the earlier steps.

Borrowings credited in commands/trekexecute.md ## Credits (Superpowers,
ai-native-sdlc, Kiro, GitHub Spec Kit, agent-skills, Cherny).

Tests: 1411 -> 1457 (1455 pass, 0 fail, 2 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 19:58:18 +02:00

126 lines
5.8 KiB
JavaScript

// tests/lib/test-integrity.test.mjs
// The floor guard (P9 (2)): a diff that weakens the tests is a finding, never a
// pass. Exit 0 = clean, 1 = findings, 2 = could not run (never green).
// Borrowed: agent-skills "floor guard" (MIT) - see lib/verification/test-integrity.mjs.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { analyzeDiff, isTestPath } from '../../lib/verification/test-integrity.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const CLI = join(ROOT, 'lib', 'verification', 'test-integrity.mjs');
const FIX = join(ROOT, 'tests', 'fixtures', 'test-integrity');
const fixture = (name) => readFileSync(join(FIX, name), 'utf8');
function cli(args, opts = {}) {
const r = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8', ...opts });
return { status: r.status, stdout: r.stdout, stderr: r.stderr };
}
const kinds = (r) => r.findings.map((f) => f.kind);
test('floor guard: a clean diff (fix + a new test) is clean -> exit 0', () => {
const r = analyzeDiff(fixture('clean.diff'));
assert.equal(r.status, 'clean', JSON.stringify(r.findings));
assert.deepEqual(r.findings, []);
assert.equal(cli(['--diff', join(FIX, 'clean.diff')]).status, 0);
});
test('floor guard: a diff that removes an assert -> finding removed-assert, exit 1', () => {
const r = analyzeDiff(fixture('removed-assert.diff'));
assert.equal(r.status, 'findings');
assert.deepEqual(kinds(r), ['removed-assert']);
assert.equal(r.findings[0].file, 'tests/sum.test.mjs');
assert.equal(cli(['--diff', join(FIX, 'removed-assert.diff')]).status, 1);
});
test('floor guard: a diff that skips a test -> finding skipped-test, exit 1', () => {
const r = analyzeDiff(fixture('skipped-test.diff'));
assert.deepEqual(kinds(r), ['skipped-test']);
assert.equal(cli(['--diff', join(FIX, 'skipped-test.diff')]).status, 1);
});
test('floor guard: a deleted test, a muted check, a stub and a loosened threshold are each found', () => {
assert.deepEqual(kinds(analyzeDiff(fixture('deleted-test.diff'))), ['deleted-test']);
assert.deepEqual(kinds(analyzeDiff(fixture('muted-check.diff'))), ['muted-check']);
assert.deepEqual(kinds(analyzeDiff(fixture('stub.diff'))), ['stub']);
assert.deepEqual(kinds(analyzeDiff(fixture('loosened-threshold.diff'))), ['loosened-threshold']);
});
test('floor guard: a TIGHTENED threshold is not a finding (direction is read, not just change)', () => {
assert.deepEqual(analyzeDiff(fixture('tightened-threshold.diff')).findings, []);
});
test('floor guard: an unreadable diff -> status unreadable, exit 2 (never green)', () => {
assert.equal(analyzeDiff(fixture('unreadable.diff')).status, 'unreadable');
const r = cli(['--diff', join(FIX, 'unreadable.diff')]);
assert.equal(r.status, 2, r.stdout + r.stderr);
assert.equal(cli(['--diff', join(FIX, 'does-not-exist.diff')]).status, 2);
assert.equal(cli([]).status, 2, 'no input is "could not run", not clean');
});
test('floor guard: hunk line counts decide what is content - a removed "-- x" line is not a file header', () => {
const diff = [
'diff --git a/tests/a.test.mjs b/tests/a.test.mjs',
'--- a/tests/a.test.mjs',
'+++ b/tests/a.test.mjs',
'@@ -1,4 +1,3 @@',
" test('a', () => {",
'--- an old comment',
'+++ counter',
'- assert.equal(1, 1);',
' });',
'\\ No newline at end of file',
].join('\n');
const r = analyzeDiff(diff);
assert.deepEqual(r.findings.map((f) => [f.kind, f.file]), [['removed-assert', 'tests/a.test.mjs']]);
});
test('floor guard: an empty diff is clean (nothing changed weakens nothing)', () => {
assert.equal(analyzeDiff('').status, 'clean');
});
test('floor guard: --json prints the findings with file and line', () => {
const r = cli(['--diff', join(FIX, 'removed-assert.diff'), '--json']);
const out = JSON.parse(r.stdout);
assert.equal(out.status, 'findings');
assert.equal(out.findings[0].kind, 'removed-assert');
assert.equal(out.findings[0].file, 'tests/sum.test.mjs');
assert.equal(typeof out.findings[0].line, 'number');
});
test('floor guard: --base reads the diff from git in --cwd', () => {
const dir = mkdtempSync(join(tmpdir(), 'floor-guard-'));
const git = (...a) => {
const r = spawnSync('git', a, { cwd: dir, encoding: 'utf8' });
assert.equal(r.status, 0, `git ${a.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
};
try {
git('init', '-q');
git('config', 'user.email', 't@example.invalid');
git('config', 'user.name', 'test');
writeFileSync(join(dir, 'a.test.mjs'), "test('a', () => {\n assert.equal(1, 1);\n assert.equal(2, 2);\n});\n");
git('add', 'a.test.mjs');
git('commit', '-q', '-m', 'base');
const base = git('rev-parse', 'HEAD');
writeFileSync(join(dir, 'a.test.mjs'), "test('a', () => {\n assert.equal(1, 1);\n});\n");
const r = cli(['--base', base, '--cwd', dir, '--json']);
assert.equal(r.status, 1, r.stdout + r.stderr);
assert.deepEqual(JSON.parse(r.stdout).findings.map((f) => f.kind), ['removed-assert']);
assert.equal(cli(['--base', 'no-such-ref', '--cwd', dir]).status, 2, 'an unresolvable base is "could not run"');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('floor guard: test-path recognition covers the common layouts', () => {
for (const p of ['tests/a.mjs', 'test/a.py', 'src/__tests__/a.ts', 'a.test.js', 'pkg/a_test.go', 'app/test_a.py', 'spec/a_spec.rb', 'a.spec.ts']) {
assert.ok(isTestPath(p), p);
}
for (const p of ['lib/a.mjs', 'src/contest.ts', 'README.md']) assert.ok(!isTestPath(p), p);
});