voyage/tests/lib/test-integrity.test.mjs
Kjell Tore Guttormsen 44dcc779a9
fix(execute): the step loop survives an adversarial review
An independent review of abc3083 found 15 defects, the two worst verified
in scratch repos. Each fix started with a test that failed.

- Proof of red could be "proven" by the environment: a start point
  unpacked with git archive had no node_modules and no .git, so any test
  failed there. The start point is now a git clone --shared checked out at
  start_sha, gitignored top-level entries are linked in, and a control run
  (start point + the change) must pass, or the result is not-run.
- The Test first file was never committed (the template lists it apart
  from Files:) and leaked into the next step's diff. The step's scope is
  now Files: + test files, staged at the checkpoint.
- New files outside Files: were invisible to every check. Every untracked
  file is registered with --intent-to-add; a revert unregisters them.
- The floor guard paired renames (foo.test.js -> .bak read as clean) and
  missed skipIf and node's t.skip/t.todo; prose files no longer count as
  stubs.
- Phase 7.5 audited from the last step's start_sha; it now uses the run's
  session_start_sha.
- The done line is written after the checkpoint and checked with --commit.
- fix_rounds never passes 5; attempts are never reset; re-control keeps
  the spec/quality verdicts and earlier floor-guard rulings; the closed
  stop list maps every existing stop; failed vs stopped made consistent;
  the controller's effort is said, not assumed.

Tests: 1470 (1468 pass, 0 fail, 2 skipped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 20:17:39 +02:00

174 lines
7.7 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: skipIf and node\'s lowercase t.skip() / t.todo() are skips too', () => {
const diff = (line) => [
'diff --git a/tests/a.test.mjs b/tests/a.test.mjs',
'--- a/tests/a.test.mjs',
'+++ b/tests/a.test.mjs',
'@@ -1,1 +1,2 @@',
" test('a', (t) => {",
`+${line}`,
].join('\n');
for (const l of [" it.skipIf(process.env.CI)('x', () => {});", ' t.skip();', " t.todo('later');", "test('b', { todo: true }, () => {});"]) {
assert.deepEqual(analyzeDiff(diff(l)).findings.map((f) => f.kind), ['skipped-test'], l);
}
});
test('floor guard: renaming a test out of the runner\'s pattern is a deleted test (--base never pairs renames)', () => {
const dir = mkdtempSync(join(tmpdir(), 'floor-guard-rename-'));
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, 'foo.test.js'), "test('a', () => {\n assert.equal(1, 1);\n});\n");
git('add', 'foo.test.js');
git('commit', '-q', '-m', 'base');
const base = git('rev-parse', 'HEAD');
git('mv', 'foo.test.js', 'foo.test.js.bak');
const r = cli(['--base', base, '--cwd', dir, '--json']);
assert.equal(r.status, 1, r.stdout + r.stderr);
assert.ok(JSON.parse(r.stdout).findings.some((f) => f.kind === 'deleted-test'), r.stdout);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('floor guard: prose files are not code - "not implemented" in a .md is not a stub', () => {
const diff = [
'diff --git a/docs/guide.md b/docs/guide.md',
'--- a/docs/guide.md',
'+++ b/docs/guide.md',
'@@ -1,1 +1,2 @@',
' # Guide',
"+Never write `throw new Error('not implemented')` or an eslint-disable as a fix.",
].join('\n');
assert.deepEqual(analyzeDiff(diff).findings, []);
});
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);
});