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>
174 lines
7.7 KiB
JavaScript
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);
|
|
});
|