merge: wave 1 order 1 (executor) into main

CLAUDE.md command table: each row from its owner (trekplan from order 3,
trekexecute from order 1, trekreview from order 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-06 20:42:47 +02:00
commit ca6ee8779a
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
28 changed files with 2384 additions and 116 deletions

View file

@ -168,3 +168,61 @@ test('SC5 control: the extractors report the old vocabulary', () => {
const bad = offenders(writtenStatuses(old)).join('\n');
for (const word of ['"in-progress"', '"passed"', '"running"']) assert.ok(bad.includes(word), `control must flag ${word}:\n${bad}`);
});
// --- Handover 5, wave 1 (P8/P9): the per-step fields of the step loop ---------
// fix_rounds counts the rounds a step went through after its first control:
// 0 = passed first time, 1-3 = fix rounds, 4 = sent up to the plan, 5 = sent up
// to the operator. There is no sixth round, so 6 is invalid, not a warning.
// All new fields are optional on read: a progress.json written before them
// still resumes (SC4 above validates the old shape).
const { validateProgressObject } = progressValidator;
function withStep(extra) {
return {
schema_version: '1', plan: 'plan.md', plan_version: '1.8', mode: 'execute', status: 'in_progress',
total_steps: 1, current_step: 1,
steps: { 1: { status: 'in_progress', attempts: 1, ...extra } },
};
}
const codes = (r) => r.errors.map((e) => e.code);
test('H5: fix_rounds 0-5 is accepted, 6 is rejected', () => {
for (const n of [0, 1, 2, 3, 4, 5]) {
assert.deepEqual(codes(validateProgressObject(withStep({ fix_rounds: n }))), [], `fix_rounds ${n}`);
}
for (const bad of [6, -1, 2.5, '3']) {
assert.deepEqual(codes(validateProgressObject(withStep({ fix_rounds: bad }))), ['PROGRESS_FIX_ROUNDS_INVALID'], `fix_rounds ${JSON.stringify(bad)}`);
}
});
test('H5: start_sha, report_path, red_proof and test_lock are shape-checked when present', () => {
const ok = { start_sha: '2c89a6b580f689070638f3ff83aea82d355f312f', report_path: '.claude/projects/x/steps/step-1.report.md', red_proof: 'proven', test_lock: 'not-locked' };
assert.deepEqual(codes(validateProgressObject(withStep(ok))), []);
for (const v of ['proven', 'rejected', 'not-run']) assert.deepEqual(codes(validateProgressObject(withStep({ red_proof: v }))), [], v);
for (const v of ['locked', 'released', 'not-locked']) assert.deepEqual(codes(validateProgressObject(withStep({ test_lock: v }))), [], v);
assert.deepEqual(codes(validateProgressObject(withStep({ start_sha: 'HEAD' }))), ['PROGRESS_START_SHA_INVALID']);
assert.deepEqual(codes(validateProgressObject(withStep({ report_path: '' }))), ['PROGRESS_REPORT_PATH_INVALID']);
assert.deepEqual(codes(validateProgressObject(withStep({ red_proof: 'green' }))), ['PROGRESS_RED_PROOF_INVALID']);
assert.deepEqual(codes(validateProgressObject(withStep({ test_lock: 'yes' }))), ['PROGRESS_TEST_LOCK_INVALID']);
// null = not set yet (the schema fence's fresh step).
assert.deepEqual(codes(validateProgressObject(withStep({ start_sha: null, report_path: null, red_proof: null, test_lock: null }))), []);
});
test('H5: the run-level decision log is an array of {decision, cost_if_wrong}', () => {
const run = (decisions) => ({ ...withStep({}), decisions });
assert.deepEqual(codes(validateProgressObject(run([{ step: 2, decision: 'kept the old name', cost_if_wrong: 'one rename commit' }]))), []);
assert.deepEqual(codes(validateProgressObject(run([{ step: 2, decision: 'kept the old name' }]))), ['PROGRESS_DECISION_INVALID']);
assert.deepEqual(codes(validateProgressObject(run('none'))), ['PROGRESS_DECISION_INVALID']);
});
test('H5: the progress schema fence carries the new per-step fields and the decision log', () => {
const schema = schemaObject(read());
const step = schema.steps['1'];
for (const k of ['start_sha', 'report_path', 'red_proof', 'test_lock', 'fix_rounds']) {
assert.ok(k in step, `the schema's step record must carry ${k}`);
}
assert.equal(step.fix_rounds, 0);
assert.ok(Array.isArray(schema.decisions), 'the schema must carry the run-level decisions log');
});

View file

@ -109,11 +109,16 @@ test('trekexecute — global recovery/retry budget TREKEXECUTE_MAX_RECOVERY_ITER
test('trekexecute — every recovery/retry loop bounded + 3-axis cap hierarchy (SC b)', () => {
const text = read();
assert.match(text, /maximum 2 retries|Retry cap = 3 attempts/, 'per-step retry cap must be explicit');
// Wave 1 (P8): the per-step cap is at most 5 fix rounds, and they go UP (to the
// plan, then to the operator) - never sideways into a fourth guess, never down
// a ladder of cheaper models. The old "3 attempts" cap is gone.
assert.match(text, /maximum 5 fix rounds[^\n]*upward/i, 'per-step cap must be "maximum 5 fix rounds", escalating upward');
assert.doesNotMatch(text, /maximum 2 retries|Retry cap = 3 attempts/, 'the old 3-attempt retry cap must be gone');
assert.match(text, /recovery_depth < 2/, 'recovery-dispatch cap must be explicit');
const capIdx = text.indexOf('Iteration caps');
assert.ok(capIdx >= 0, 'cap-hierarchy section ("Iteration caps") must exist');
const section = text.slice(capIdx, capIdx + 1500);
assert.match(section, /fix_rounds/, 'hierarchy must name the per-step fix_rounds axis');
assert.match(section, /attempts/, 'hierarchy must name per-step attempts axis');
assert.match(section, /recovery_depth/, 'hierarchy must name per-session recovery_depth axis');
assert.match(section, /TREKEXECUTE_MAX_RECOVERY_ITERATIONS/, 'hierarchy must name the global budget axis');

View file

@ -22,7 +22,7 @@
| trekbrief-stats | ts, task, slug, mode, interview_turns, review_iterations, brief_quality, research_topics, auto_research, auto_result, intent_approved, project_dir, order_id, order_claimed_at, order_reason | commands/trekbrief.md (orchestrator-emit Phase 7; order_* added by lib/stats/stats-append.mjs) | trekbrief.md:1124-1139 | profile, phase_models, profile_source | none |
| trekresearch-stats | ts, question, mode, scope, engine, slug, project_dir, brief_path, dimensions, dimensions_baseline, dimensions_baseline_preserved, effort, conv_turns, empty_turns, unique_sources, agents_local, agents_external, gemini_used, confidence, contradictions, open_questions | commands/trekresearch.md (orchestrator-emit Stats tracking) | trekresearch.md:634-676 | profile, phase_models, parallel_agents, external_research_enabled, profile_source | none |
| trekplan-stats | ts, task, mode, slug, brief_path, project_dir, codebase_size, codebase_files, agents_deployed, deep_dives, research_briefs_used, research_scout_used, critic_verdict, guardian_verdict, outcome | commands/trekplan.md (orchestrator-emit Phase 12) | trekplan.md:805-826 | profile, phase_models, parallel_agents, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:1890-1912 | profile, phase_models, profile_source | none |
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2325-2347 | profile, phase_models, profile_source | none |
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | commands/trekreview.md (orchestrator-emit Phase 8) | trekreview.md:255 | profile, phase_models, profile_source | none |

View file

@ -0,0 +1,22 @@
diff --git a/lib/sum.mjs b/lib/sum.mjs
index 1111111..2222222 100644
--- a/lib/sum.mjs
+++ b/lib/sum.mjs
@@ -1,3 +1,4 @@
export function sum(a, b) {
- return a - b;
+ // fixed: addition, not subtraction
+ return a + b;
}
diff --git a/tests/sum.test.mjs b/tests/sum.test.mjs
index 3333333..4444444 100644
--- a/tests/sum.test.mjs
+++ b/tests/sum.test.mjs
@@ -3,3 +3,7 @@ import { sum } from '../lib/sum.mjs';
test('sum adds', () => {
assert.equal(sum(1, 2), 3);
});
+
+test('sum adds negatives', () => {
+ assert.equal(sum(-1, -2), -3);
+});

View file

@ -0,0 +1,11 @@
diff --git a/tests/sum.test.mjs b/tests/sum.test.mjs
index 3333333..4444444 100644
--- a/tests/sum.test.mjs
+++ b/tests/sum.test.mjs
@@ -3,6 +3,3 @@ import { sum } from '../lib/sum.mjs';
test('sum adds', () => {
assert.equal(sum(1, 2), 3);
});
-test('sum adds negatives', () => {
- sum(-1, -2);
-});

View file

@ -0,0 +1,9 @@
diff --git a/tests/latency.test.mjs b/tests/latency.test.mjs
index 7777777..8888888 100644
--- a/tests/latency.test.mjs
+++ b/tests/latency.test.mjs
@@ -4,3 +4,3 @@ test('tail latency', () => {
const ms = measure();
- assert.ok(ms < 200, `tail latency ${ms}ms`);
+ assert.ok(ms < 2000, `tail latency ${ms}ms`);
});

View file

@ -0,0 +1,8 @@
diff --git a/tests/run.sh b/tests/run.sh
index 5555555..6666666 100644
--- a/tests/run.sh
+++ b/tests/run.sh
@@ -1,2 +1,2 @@
#!/bin/sh
-node --test tests/
+node --test tests/ || true

View file

@ -0,0 +1,9 @@
diff --git a/tests/sum.test.mjs b/tests/sum.test.mjs
index 3333333..4444444 100644
--- a/tests/sum.test.mjs
+++ b/tests/sum.test.mjs
@@ -3,5 +3,4 @@ import { sum } from '../lib/sum.mjs';
test('sum adds', () => {
assert.equal(sum(1, 2), 3);
- assert.equal(sum(2, 2), 4);
});

View file

@ -0,0 +1,9 @@
diff --git a/tests/sum.test.mjs b/tests/sum.test.mjs
index 3333333..4444444 100644
--- a/tests/sum.test.mjs
+++ b/tests/sum.test.mjs
@@ -3,3 +3,3 @@ import { sum } from '../lib/sum.mjs';
-test('sum adds', () => {
+test.skip('sum adds', () => {
assert.equal(sum(1, 2), 3);
});

View file

@ -0,0 +1,9 @@
diff --git a/lib/sum.mjs b/lib/sum.mjs
index 1111111..2222222 100644
--- a/lib/sum.mjs
+++ b/lib/sum.mjs
@@ -1,3 +1,3 @@
export function sum(a, b) {
- return a - b;
+ throw new Error('not implemented');
}

View file

@ -0,0 +1,9 @@
diff --git a/tests/latency.test.mjs b/tests/latency.test.mjs
index 7777777..8888888 100644
--- a/tests/latency.test.mjs
+++ b/tests/latency.test.mjs
@@ -4,3 +4,3 @@ test('tail latency', () => {
const ms = measure();
- assert.ok(ms < 2000, `tail latency ${ms}ms`);
+ assert.ok(ms < 200, `tail latency ${ms}ms`);
});

View file

@ -0,0 +1,2 @@
this is not a diff
just prose that a broken pipe left behind

View file

@ -1668,9 +1668,11 @@ for (const cmd of ['trekresearch', 'trekplan', 'trekreview', 'trekexecute']) {
// The derived set is asserted exactly, so the pin cannot go vacuous if a
// command stops spawning or a new spawning command is added.
const SPAWNING_COMMANDS = ['trekbrief', 'trekplan', 'trekresearch', 'trekreview'];
// Wave 1 (P8): /trekexecute joined the set - one fresh step helper per plan step
// plus one controller - so the no-name rule now covers five commands.
const SPAWNING_COMMANDS = ['trekbrief', 'trekexecute', 'trekplan', 'trekresearch', 'trekreview'];
test('S85: the set of agent-spawning commands is exactly the four that carry the no-name rule', () => {
test('S85: the set of agent-spawning commands is exactly the five that carry the no-name rule', () => {
const spawns = listMd('commands')
.filter((f) => /^Launch\b/m.test(read(`commands/${f}`)))
.map((f) => f.replace(/\.md$/, ''))

View file

@ -0,0 +1,93 @@
// tests/lib/done-line.test.mjs
// The done line (P9 (4)): a step's "done" is written by a script, and only when
// the test command exits 0. Exit 0 = written, 1 = the command failed (nothing
// written), 2 = could not run (nothing written, never green).
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const CLI = join(ROOT, 'lib', 'verification', 'done-line.mjs');
function sh(cwd, cmd, args) {
const r = spawnSync(cmd, args, { cwd, encoding: 'utf8' });
assert.equal(r.status, 0, `${cmd} ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
}
function repo() {
const dir = mkdtempSync(join(tmpdir(), 'done-line-'));
sh(dir, 'git', ['init', '-q']);
sh(dir, 'git', ['config', 'user.email', 't@example.invalid']);
sh(dir, 'git', ['config', 'user.name', 'test']);
mkdirSync(join(dir, 'tests'));
writeFileSync(join(dir, 'tests', 'ok.test.mjs'), "import { test } from 'node:test';\ntest('ok', () => {});\n");
writeFileSync(join(dir, 'tests', 'bad.test.mjs'), "import { test } from 'node:test';\ntest('bad', () => { throw new Error('red'); });\n");
sh(dir, 'git', ['add', 'tests']);
sh(dir, 'git', ['commit', '-q', '-m', 'tests']);
return dir;
}
const run = (args) => spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8' });
test('done line: the test command exits 0 -> one DONE line appended, exit 0', () => {
const dir = repo();
const report = join(dir, 'step-3.report.md');
try {
writeFileSync(report, '# Step 3 report\n');
const r = run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/ok.test.mjs', '--report', report]);
assert.equal(r.status, 0, r.stdout + r.stderr);
const head = sh(dir, 'git', ['rev-parse', 'HEAD']);
const lines = readFileSync(report, 'utf8').split('\n').filter((l) => l.startsWith('DONE '));
assert.equal(lines.length, 1);
assert.match(lines[0], new RegExp(`^DONE step 3: \`node --test tests/ok\\.test\\.mjs\` exit 0 at ${head} \\d{4}-\\d{2}-\\d{2}T`));
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('done line: the test command fails -> nothing written, exit 1', () => {
const dir = repo();
const report = join(dir, 'step-3.report.md');
try {
writeFileSync(report, '# Step 3 report\n');
const r = run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/bad.test.mjs', '--report', report]);
assert.equal(r.status, 1, r.stdout + r.stderr);
assert.equal(readFileSync(report, 'utf8'), '# Step 3 report\n');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('done line: a command outside the test-runner allowlist is not run -> exit 2, nothing written', () => {
const dir = repo();
const report = join(dir, 'step-3.report.md');
try {
for (const cmd of ['echo done', 'node --test tests/bad.test.mjs || true', 'true']) {
const r = run(['--cwd', dir, '--step', '3', '--cmd', cmd, '--report', report]);
assert.equal(r.status, 2, `${cmd}: ${r.stdout}${r.stderr}`);
}
assert.equal(existsSync(report), false);
assert.equal(run(['--cwd', dir, '--cmd', 'node --test tests/ok.test.mjs', '--report', report]).status, 2, 'no --step');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('done line: --check finds the line for that step only', () => {
const dir = repo();
const report = join(dir, 'step-3.report.md');
try {
assert.equal(run(['--check', '--step', '3', '--report', report]).status, 1, 'no report file = not done');
writeFileSync(report, '# Step 3 report\nDONE step 3 (typed by hand)\n');
assert.equal(run(['--check', '--step', '3', '--report', report]).status, 1, 'a hand-typed line is not the script line');
// A well-formed line typed by hand, at a sha that is not the step's commit.
writeFileSync(report, `# Step 3 report\nDONE step 3: \`node --test tests/ok.test.mjs\` exit 0 at ${'0'.repeat(40)} 2026-10-06T12:00:00.000Z\n`);
const head = sh(dir, 'git', ['rev-parse', 'HEAD']);
assert.equal(run(['--check', '--step', '3', '--report', report, '--commit', head]).status, 1, 'the line must be at the step commit');
run(['--cwd', dir, '--step', '3', '--cmd', 'node --test tests/ok.test.mjs', '--report', report]);
assert.equal(run(['--check', '--step', '3', '--report', report]).status, 0);
assert.equal(run(['--check', '--step', '3', '--report', report, '--commit', head]).status, 0);
assert.equal(run(['--check', '--step', '4', '--report', report]).status, 1);
} finally { rmSync(dir, { recursive: true, force: true }); }
});

View file

@ -0,0 +1,244 @@
// tests/lib/executor-prose-pins.test.mjs
// Wave 1, process 1 (the executor): prose pins for P8 (fresh helper per step,
// one controller with two verdicts, fix rounds that go up), P9 on the executor
// side (proof of red, the floor guard, the done line, end to end, back to the
// architecture after three failed fixes) and P4 (the closed stop list and the
// decision log). The mechanisms themselves are tested as code in
// tests/lib/{red-proof,test-integrity,done-line}.test.mjs; these pins hold the
// command prose to them. Fix the SOURCE a pin guards, not the pin.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
const CMD = 'commands/trekexecute.md';
const AGENT = 'agents/step-implementer.md';
function frontmatter(text) {
const m = text.match(/^---\n([\s\S]*?)\n---/);
assert.ok(m, 'frontmatter missing');
return m[1];
}
const fmValue = (fm, key) => (fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm')) || [])[1];
/** The text between `## {start}` (or `#### {start}`) and the next heading of the same or higher level. */
function section(text, heading) {
const at = text.indexOf(heading);
assert.ok(at >= 0, `${CMD} must have "${heading}"`);
const level = heading.match(/^#+/)[0].length;
const rest = text.slice(at + heading.length);
const next = rest.search(new RegExp(`\\n#{1,${level}} `));
return heading + (next >= 0 ? rest.slice(0, next) : rest);
}
// --- P8: the Agent tool is back, for one purpose -------------------------------
test('P8: /trekexecute may spawn - Agent is allowed and no longer disallowed', () => {
const fm = frontmatter(read(CMD));
assert.match(fmValue(fm, 'allowed-tools'), /\bAgent\b/, 'allowed-tools must list Agent');
const disallowed = fmValue(fm, 'disallowed-tools') || '';
assert.doesNotMatch(disallowed, /\bAgent\b/, 'disallowed-tools must no longer remove Agent');
assert.match(disallowed, /\bTeamCreate\b/, 'TeamCreate stays removed - helpers, never a team');
});
test('P8: Hard rule 10 is rewritten - one fresh helper per plan step, and helpers never start helpers', () => {
const text = read(CMD);
assert.doesNotMatch(text, /\*\*No sub-agents\.\*\*/, 'the old "No sub-agents" rule must be gone');
const rule = text.slice(text.indexOf('10. **'), text.indexOf('11. **'));
assert.match(rule, /fresh helper per plan step/i);
assert.match(rule, /voyage:step-implementer/);
assert.match(rule, /helpers never start helpers/i);
assert.match(rule, /controller/i);
});
test('P8: the step agent exists, is opus at effort >= high, and cannot spawn', () => {
assert.ok(existsSync(join(ROOT, AGENT)), `${AGENT} must exist`);
const fm = frontmatter(read(AGENT));
assert.equal(fmValue(fm, 'name'), 'step-implementer');
assert.equal(fmValue(fm, 'model'), 'opus');
assert.ok(['high', 'xhigh', 'max'].includes(fmValue(fm, 'effort')), 'effort must be >= high');
assert.doesNotMatch(fmValue(fm, 'tools') || '', /\bAgent\b/, 'a helper must not hold the Agent tool');
// Measured 2026-10-06 (CC 2.1.291): a worktree-isolated helper branched from
// the default branch (02f320f), not from the executor's HEAD (2c89a6b), and so
// did not see the earlier steps. A plugin cannot set worktree.baseRef. So no
// isolation: the helper works in the executor's tree, from a recorded start_sha.
assert.doesNotMatch(fm, /^isolation:/m, 'no isolation: worktree on the step agent (measured: wrong base)');
});
test('P8: the helper report contract is ran / saw / did NOT check', () => {
const body = read(AGENT);
for (const h of ['### Ran', '### Saw', '### Did NOT check']) assert.ok(body.includes(h), `${AGENT} report must have "${h}"`);
assert.ok(read(CMD).includes('Did NOT check'), 'the command must name the report contract too');
});
test('P8: hand-over is in files, from a recorded start point', () => {
const text = read(CMD);
assert.match(text, /start_sha/);
assert.match(text, /report_path/);
assert.match(text, /git diff \{start_sha\}/, 'the review package is the diff from the recorded start point');
});
test('P8: one controller, two verdicts, reads the diff not the summary, re-checks only what was fixed', () => {
const ctl = section(read(CMD), '#### Sub-step E — Control');
assert.match(ctl, /one controller/i);
assert.match(ctl, /two verdicts/i);
assert.match(ctl, /"spec"/);
assert.match(ctl, /"quality"/);
assert.match(ctl, /reads the diff, not the helper's (summary|report)/i);
assert.match(ctl, /only what (was|the round) fixed/i);
assert.match(ctl, /general-purpose/, 'the controller is a general helper with a prompt from the command, not agent no. 2');
});
test('P8: fix rounds go up - maximum 5, plan then operator, never a cheaper-model ladder', () => {
const fix = section(read(CMD), '#### Sub-step F — Fix rounds');
assert.match(fix, /maximum 5 fix rounds[^\n]*upward/i);
assert.match(fix, /back to (the )?planning/i);
assert.match(fix, /operator/i);
assert.match(fix, /does not improvise/i);
assert.match(fix, /no ladder of cheaper models/i);
// P9 (7): after three failed fixes the question is the architecture, not a fourth fix.
assert.match(fix, /three failed fix(es| rounds)[^\n]*architecture/i);
});
test('P8: the no-name rule covers the step helper and the controller', () => {
const text = read(CMD);
assert.ok(text.includes('in_process_teammate'));
assert.match(text, /never pass .*`name`/i);
assert.match(text, /^Launch\b/m, 'the spawn site starts with "Launch" (the S85 detector reads it)');
});
// --- P9 (executor side) ---------------------------------------------------------
test('P9 (1): proof of red is a command and it blocks - the old "do not block" sentence is gone', () => {
const text = read(CMD);
assert.doesNotMatch(text, /Do not block on test-first failures/);
assert.match(text, /lib\/verification\/red-proof\.mjs/);
const red = section(text, '#### Sub-step B — Test first and proof of red');
assert.match(red, /must FAIL/);
assert.match(red, /must PASS/);
assert.match(red, /not-run/);
assert.match(red, /never (a pass|green|proven)/i);
});
test('K4: Test first is required from plan_version 1.8; Interface in/out and Review focus are read, not required', () => {
const text = read(CMD);
assert.match(text, /plan_version ≥ 1\.8[^\n]*`Test first:`|`Test first:`[^\n]*plan_version ≥ 1\.8/);
for (const f of ['Interface in:', 'Interface out:', 'Review focus:']) assert.ok(text.includes(f), `${CMD} must read ${f}`);
assert.match(text, /tolerat(e|es) (their|its) absence|never required/i);
});
test('K8: the test lock is used through its CLI, and a missing CLI is said out loud', () => {
const text = read(CMD);
assert.match(text, /lib\/util\/test-lock\.mjs lock --files/);
assert.match(text, /test-lock\.mjs unlock/);
assert.match(text, /"not-locked"/);
assert.match(text, /never silent/i);
});
test('P9 (2): the floor guard runs on the step diff and exit 2 is never green', () => {
const d3 = section(read(CMD), '#### Sub-step D3 — Floor guard');
assert.match(d3, /lib\/verification\/test-integrity\.mjs --base \{start_sha\}/);
assert.match(d3, /exit 2 is never green/i);
assert.match(d3, /justified/);
});
test('P9 (4): the done line is written by a script, only when the test command exits 0, at the step commit', () => {
const g = section(read(CMD), '#### Sub-step G — Checkpoint, done line, unlock');
assert.match(g, /lib\/verification\/done-line\.mjs --step \{N\}/);
assert.match(g, /done-line\.mjs --check --step \{N\} --report \{report_path\} --commit \{hash\}/);
assert.match(g, /writes its line only when the test command\s+exits 0/i);
// The checkpoint comes first, so the line names the step's own commit.
assert.ok(g.indexOf('**Checkpoint.**') < g.indexOf('**The done line**'), 'checkpoint before the done line');
});
// --- the independent review of 2026-10-06 (15 findings): what the fixes must keep
test('review: the step test files are part of the scope and committed at the checkpoint', () => {
const text = read(CMD);
assert.match(section(text, '#### Sub-step B — Test first and proof of red'), /scope = its `Files:` \+ its test files/);
assert.match(section(text, '#### Sub-step G — Checkpoint, done line, unlock'), /its `Files:` and its test files/);
});
test('review: every new file is registered (not only listed ones), so no check is blind to it', () => {
const c = section(read(CMD), '#### Sub-step C — Implement through one fresh helper');
assert.match(c, /git ls-files --others --exclude-standard/);
assert.match(c, /git add --intent-to-add -- \{path\}/);
});
test('review: proof of red has a control run and a real git checkout as its start point', () => {
const b = section(read(CMD), '#### Sub-step B — Test first and proof of red');
assert.match(b, /control run/);
assert.match(b, /git clone --shared/);
});
test('review: Phase 7.5 audits the run from session_start_sha, never a step start_sha', () => {
const text = read(CMD);
const audit = text.slice(text.indexOf('## Phase 7.5'), text.indexOf('### Machine-verifiable completion gate'));
assert.match(audit, /\{session_start_sha\}\.\.HEAD/);
assert.doesNotMatch(audit, /\{start_sha\}/);
});
test('review: no sixth round, attempts never reset, a step at 5 is never built again', () => {
const text = read(CMD);
assert.match(section(text, '#### Sub-step F — Fix rounds'), /no sixth round/);
const c = section(text, '#### Sub-step C — Implement through one fresh helper');
assert.match(c, /never\s+reset/);
assert.match(c, /A step at `fix_rounds` 5 is never built again/);
});
test('review: the helper appends its report, and passes tests as one comma-separated list', () => {
const agent = read(AGENT);
assert.match(agent, /APPEND it to `report_path`/);
assert.match(agent, /ONE comma-separated argument/);
assert.match(agent, /A step with no `Test first:`/);
});
test('P9 (6): without a run-the-thing check the result is "not verified end to end", never green', () => {
const text = read(CMD);
assert.match(text, /not verified end to end/);
assert.match(text, /end_to_end/);
});
test('P9: the three new CLIs the prose names exist', () => {
for (const f of ['red-proof.mjs', 'test-integrity.mjs', 'done-line.mjs']) {
assert.ok(existsSync(join(ROOT, 'lib', 'verification', f)), `lib/verification/${f}`);
}
});
// --- P4: the closed stop list and the decision log ------------------------------
test('P4: the run stops only on the closed list of four, and the final report shows it', () => {
const text = read(CMD);
const list = section(text, '### Closed stop list');
for (const cls of [/cannot be undone/i, /security-sensitive/i, /outside the working copy/i, /so broken that (every|any)thing (after it )?is guesswork/i]) {
assert.match(list, cls);
}
assert.match(list, /closed/i);
// Phase 8's report template is a fence that holds its own "## " heading, so
// the phase is read up to Phase 9, not up to the next heading.
const report = text.slice(text.indexOf('## Phase 8 — Final report'), text.indexOf('## Phase 9'));
assert.match(report, /### Stops/);
assert.match(report, /### Decisions taken/);
});
test('P4: every decision the executor took is logged with what it costs if it is wrong', () => {
const text = read(CMD);
assert.match(text, /cost_if_wrong/);
assert.match(text, /"decisions"/);
});
// --- Credits: every borrowing is named, never silent ----------------------------
test('every borrowing in the executor is credited', () => {
const text = read(CMD);
const credits = section(text, '## Credits');
for (const src of [/Superpowers/, /subagent-driven-development/, /ai-native-sdlc/, /Kiro/, /Spec Kit/, /agent-skills/, /Cherny/]) {
assert.match(credits, src);
}
assert.match(credits, /MIT/);
});

View file

@ -0,0 +1,271 @@
// tests/lib/red-proof.test.mjs
// Proof of red (P9 (1)): the step's test is run by a command against the start
// point, where it must FAIL, and against the result, where it must PASS. The
// model's word that "the test was red first" is never the proof.
// Exit 0 = proven, 1 = rejected (green at the start point, or red at the result),
// 2 = could not run (never green).
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { proveRed } from '../../lib/verification/red-proof.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const CLI = join(ROOT, 'lib', 'verification', 'red-proof.mjs');
const TEST_CMD = 'node --test tests/sum.test.mjs';
const SUM_TEST = [
"import { test } from 'node:test';",
"import { strict as assert } from 'node:assert';",
"import { sum } from '../lib/sum.mjs';",
"test('sum adds', () => { assert.equal(sum(1, 2), 3); });",
'',
].join('\n');
function sh(cwd, cmd, args) {
const r = spawnSync(cmd, args, { cwd, encoding: 'utf8' });
assert.equal(r.status, 0, `${cmd} ${args.join(' ')}: ${r.stderr}`);
return r.stdout.trim();
}
/** A repo whose base commit has `body` as sum(); returns {dir, base}. */
function repo(body) {
const dir = mkdtempSync(join(tmpdir(), 'red-proof-'));
sh(dir, 'git', ['init', '-q']);
sh(dir, 'git', ['config', 'user.email', 't@example.invalid']);
sh(dir, 'git', ['config', 'user.name', 'test']);
mkdirSync(join(dir, 'lib'));
writeFileSync(join(dir, 'lib', 'sum.mjs'), `export function sum(a, b) { return ${body}; }\n`);
sh(dir, 'git', ['add', 'lib/sum.mjs']);
sh(dir, 'git', ['commit', '-q', '-m', 'base']);
return { dir, base: sh(dir, 'git', ['rev-parse', 'HEAD']) };
}
function addTest(dir) {
mkdirSync(join(dir, 'tests'), { recursive: true });
writeFileSync(join(dir, 'tests', 'sum.test.mjs'), SUM_TEST);
}
function cli(args) {
const r = spawnSync(process.execPath, [CLI, ...args], { encoding: 'utf8' });
return { status: r.status, stdout: r.stdout, stderr: r.stderr };
}
test('red proof: red at the start point, green at the result -> proven, exit 0', () => {
const { dir, base } = repo('a - b');
try {
addTest(dir);
writeFileSync(join(dir, 'lib', 'sum.mjs'), 'export function sum(a, b) { return a + b; }\n');
const r = proveRed({ cwd: dir, base, cmd: TEST_CMD, tests: ['tests/sum.test.mjs'] });
assert.equal(r.status, 'proven', JSON.stringify(r));
assert.notEqual(r.base.exit, 0);
assert.equal(r.result.exit, 0);
const c = cli(['--cwd', dir, '--base', base, '--cmd', TEST_CMD, '--tests', 'tests/sum.test.mjs', '--json']);
assert.equal(c.status, 0, c.stdout + c.stderr);
assert.equal(JSON.parse(c.stdout).status, 'proven');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('red proof: a test that already PASSES at the start point is rejected, exit 1', () => {
const { dir, base } = repo('a + b');
try {
addTest(dir);
const r = proveRed({ cwd: dir, base, cmd: TEST_CMD, tests: ['tests/sum.test.mjs'] });
assert.equal(r.status, 'rejected');
assert.equal(r.reason, 'not-red-at-base');
const c = cli(['--cwd', dir, '--base', base, '--cmd', TEST_CMD, '--tests', 'tests/sum.test.mjs']);
assert.equal(c.status, 1, c.stdout + c.stderr);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('red proof: a test that still FAILS at the result is rejected, exit 1', () => {
const { dir, base } = repo('a - b');
try {
addTest(dir);
const r = proveRed({ cwd: dir, base, cmd: TEST_CMD, tests: ['tests/sum.test.mjs'] });
assert.equal(r.status, 'rejected');
assert.equal(r.reason, 'not-green-at-result');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('red proof --red-only: the gate before building - red at the start point only, the result is not run', () => {
const { dir, base } = repo('a - b');
try {
addTest(dir); // test written, nothing built yet: the working tree is still red
const r = proveRed({ cwd: dir, base, cmd: TEST_CMD, tests: ['tests/sum.test.mjs'], redOnly: true });
assert.equal(r.status, 'proven', JSON.stringify(r));
assert.equal(r.result, undefined, '--red-only never runs the result');
const c = cli(['--red-only', '--cwd', dir, '--base', base, '--cmd', TEST_CMD, '--tests', 'tests/sum.test.mjs']);
assert.equal(c.status, 0, c.stdout + c.stderr);
} finally { rmSync(dir, { recursive: true, force: true }); }
const green = repo('a + b');
try {
addTest(green.dir);
const r = proveRed({ cwd: green.dir, base: green.base, cmd: TEST_CMD, tests: ['tests/sum.test.mjs'], redOnly: true });
assert.equal(r.reason, 'not-red-at-base');
} finally { rmSync(green.dir, { recursive: true, force: true }); }
});
test('red proof: what cannot be measured is "not-run", exit 2 - never proven', () => {
const { dir, base } = repo('a - b');
try {
addTest(dir);
writeFileSync(join(dir, 'lib', 'sum.mjs'), 'export function sum(a, b) { return a + b; }\n');
const args = (over) => ['--cwd', dir, '--base', base, '--cmd', TEST_CMD, '--tests', 'tests/sum.test.mjs', ...over];
// A command outside the test-runner allowlist never reaches a shell.
assert.equal(proveRed({ cwd: dir, base, cmd: 'node tests/sum.test.mjs', tests: ['tests/sum.test.mjs'] }).status, 'not-run');
assert.equal(proveRed({ cwd: dir, base, cmd: `${TEST_CMD} || true`, tests: ['tests/sum.test.mjs'] }).status, 'not-run');
// An unknown start point, a missing test file, no test file at all.
assert.equal(proveRed({ cwd: dir, base: 'no-such-ref', cmd: TEST_CMD, tests: ['tests/sum.test.mjs'] }).status, 'not-run');
assert.equal(proveRed({ cwd: dir, base, cmd: TEST_CMD, tests: ['tests/missing.test.mjs'] }).status, 'not-run');
assert.equal(proveRed({ cwd: dir, base, cmd: TEST_CMD, tests: [] }).status, 'not-run');
assert.equal(cli(args([])).status, 0, 'control: the same arguments do prove');
assert.equal(cli(['--cwd', dir, '--base', base, '--cmd', TEST_CMD]).status, 2, 'no --tests');
assert.equal(cli(['--cwd', join(dir, 'nope'), '--base', base, '--cmd', TEST_CMD, '--tests', 'tests/sum.test.mjs']).status, 2);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('red proof: --cwd in a subdirectory still runs at the repo root (plan paths are root-relative)', () => {
// Measured 2026-10-06: `git archive` run from a subdirectory packs only that
// subdirectory, so a start point built from a subdir cwd would miss lib/.
const { dir, base } = repo('a - b');
try {
addTest(dir);
writeFileSync(join(dir, 'lib', 'sum.mjs'), 'export function sum(a, b) { return a + b; }\n');
mkdirSync(join(dir, 'docs'));
const r = proveRed({ cwd: join(dir, 'docs'), base, cmd: TEST_CMD, tests: ['tests/sum.test.mjs'] });
assert.equal(r.status, 'proven', JSON.stringify({ status: r.status, reason: r.reason }));
} finally { rmSync(dir, { recursive: true, force: true }); }
});
// Review finding 2 (2026-10-06): a start-point copy that cannot run the test at
// all failed "red" for the wrong reason and was called proven. Three guards:
// the copy is a real git checkout, gitignored dependency dirs are linked in, and
// the same copy WITH the change must go green (the control run).
test('red proof: a gitignored dependency the test needs is linked into the start point - no false red', () => {
const { dir, base } = repo('a + b');
try {
writeFileSync(join(dir, '.gitignore'), 'node_modules/\n');
sh(dir, 'git', ['add', '.gitignore']);
sh(dir, 'git', ['commit', '-q', '-m', 'ignore']);
const base2 = sh(dir, 'git', ['rev-parse', 'HEAD']);
mkdirSync(join(dir, 'node_modules', 'dep'), { recursive: true });
writeFileSync(join(dir, 'node_modules', 'dep', 'index.mjs'), 'export const one = 1;\n');
mkdirSync(join(dir, 'tests'));
writeFileSync(join(dir, 'tests', 'dep.test.mjs'), [
"import { test } from 'node:test';",
"import { strict as assert } from 'node:assert';",
"import { one } from '../node_modules/dep/index.mjs';",
"import { sum } from '../lib/sum.mjs';",
"test('dep', () => { assert.equal(sum(one, one), 2); });",
'',
].join('\n'));
// The test already passes without any change: it proves nothing.
const r = proveRed({ cwd: dir, base: base2, cmd: 'node --test tests/dep.test.mjs', tests: ['tests/dep.test.mjs'] });
assert.equal(r.status, 'rejected', JSON.stringify({ status: r.status, reason: r.reason }));
assert.equal(r.reason, 'not-red-at-base');
void base;
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('red proof: the start point is a git checkout - a git-reading command does not fail there for lack of .git', () => {
const { dir, base } = repo('a - b');
try {
addTest(dir);
const r = proveRed({ cwd: dir, base, cmd: 'git status', tests: ['tests/sum.test.mjs'] });
assert.notEqual(r.status, 'proven', JSON.stringify({ status: r.status, reason: r.reason }));
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('red proof: a copy that cannot go green even with the change is "not-run" (environment), never proven', () => {
const { dir, base } = repo('a - b');
try {
mkdirSync(join(dir, 'tests'));
// Red anywhere outside the real working tree, whatever the code says.
writeFileSync(join(dir, 'tests', 'env.test.mjs'), [
"import { test } from 'node:test';",
"import { strict as assert } from 'node:assert';",
"test('env', () => { assert.ok(!process.cwd().includes('voyage-red-proof-')); });",
'',
].join('\n'));
writeFileSync(join(dir, 'lib', 'sum.mjs'), 'export function sum(a, b) { return a + b; }\n');
const r = proveRed({ cwd: dir, base, cmd: 'node --test tests/env.test.mjs', tests: ['tests/env.test.mjs'] });
assert.equal(r.status, 'not-run', JSON.stringify({ status: r.status, reason: r.reason }));
assert.match(r.reason, /control/);
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('red proof: an untracked file the change adds (not a listed test) is part of the result, so the control run goes green', () => {
const { dir, base } = repo('a - b');
try {
addTest(dir);
// The fix lives in a NEW untracked module the step adds.
writeFileSync(join(dir, 'lib', 'add.mjs'), 'export const add = (a, b) => a + b;\n');
writeFileSync(join(dir, 'lib', 'sum.mjs'), "export { add as sum } from './add.mjs';\n");
const r = proveRed({ cwd: dir, base, cmd: TEST_CMD, tests: ['tests/sum.test.mjs'] });
assert.equal(r.status, 'proven', JSON.stringify({ status: r.status, reason: r.reason }));
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('red proof: the start-point run leaves the working tree untouched', () => {
const { dir, base } = repo('a - b');
try {
addTest(dir);
writeFileSync(join(dir, 'lib', 'sum.mjs'), 'export function sum(a, b) { return a + b; }\n');
const before = sh(dir, 'git', ['status', '--porcelain']);
proveRed({ cwd: dir, base, cmd: TEST_CMD, tests: ['tests/sum.test.mjs'] });
assert.equal(sh(dir, 'git', ['status', '--porcelain']), before);
assert.equal(readFileSync(join(dir, 'lib', 'sum.mjs'), 'utf8'), 'export function sum(a, b) { return a + b; }\n');
} finally { rmSync(dir, { recursive: true, force: true }); }
});
// Reuse of tests/fixtures/red-first/02243c6-always-allow-shim.mjs: the measured
// red-first claim of 02243c6 ("6 of 55"), re-run through red-proof instead of by
// hand. Needs this repo's git history; a `git archive` tree has none, and there
// the test says so and skips rather than pretending to pass.
const HAS_HISTORY = spawnSync('git', ['cat-file', '-e', '02243c6^{commit}'], { cwd: ROOT }).status === 0;
test('red proof: reproduces the 02243c6 red-first measurement (6 of 55) and the bare load error (1 of 1)', { skip: !HAS_HISTORY && 'no git history (git archive tree)' }, () => {
const dir = mkdtempSync(join(tmpdir(), 'red-proof-02243c6-'));
const RUNNER = 'lib/verification/criteria-runner.mjs';
const TEST_FILE = 'tests/lib/criteria-runner.test.mjs';
const tar = join(dir, '..', `${dir.split('/').pop()}.tar`);
try {
sh(ROOT, 'git', ['archive', '--format=tar', '-o', tar, '02243c6']);
sh(dir, 'tar', ['-xf', tar]);
const parent = spawnSync('git', ['show', `02243c6^:${RUNNER}`], { cwd: ROOT, encoding: 'utf8' }).stdout;
const real = readFileSync(join(dir, RUNNER), 'utf8');
const shim = readFileSync(join(ROOT, 'tests', 'fixtures', 'red-first', '02243c6-always-allow-shim.mjs'), 'utf8');
sh(dir, 'git', ['init', '-q']);
sh(dir, 'git', ['config', 'user.email', 't@example.invalid']);
sh(dir, 'git', ['config', 'user.name', 'test']);
sh(dir, 'git', ['add', '.']);
// Start point 1: the parent's runner as it was - the test file cannot even load.
writeFileSync(join(dir, RUNNER), parent);
sh(dir, 'git', ['commit', '-q', '-am', 'parent runner']);
const bare = sh(dir, 'git', ['rev-parse', 'HEAD']);
// Start point 2: the parent's runner plus the always-allow shim.
writeFileSync(join(dir, RUNNER), parent + shim);
sh(dir, 'git', ['commit', '-q', '-am', 'parent runner + shim']);
const shimmed = sh(dir, 'git', ['rev-parse', 'HEAD']);
writeFileSync(join(dir, RUNNER), real);
const cmd = `node --test --test-reporter=tap ${TEST_FILE}`;
const withShim = proveRed({ cwd: dir, base: shimmed, cmd, tests: [TEST_FILE] });
assert.equal(withShim.status, 'proven', JSON.stringify(withShim));
assert.deepEqual(withShim.base.tap, { tests: 55, fail: 6 });
assert.deepEqual(withShim.result.tap, { tests: 55, fail: 0 });
// Only the start point matters here; the result run above already showed 0 of 55.
const bareRun = proveRed({ cwd: dir, base: bare, cmd, tests: [TEST_FILE], redOnly: true });
assert.equal(bareRun.status, 'proven');
assert.deepEqual(bareRun.base.tap, { tests: 1, fail: 1 }, 'the parent tree fails as ONE load error, not as named tests');
} finally {
rmSync(dir, { recursive: true, force: true });
rmSync(tar, { force: true });
}
});

View file

@ -0,0 +1,174 @@
// 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);
});