/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 the02243c6red-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>
126 lines
5.8 KiB
JavaScript
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);
|
|
});
|