voyage/tests/lib/test-lock.test.mjs
Kjell Tore Guttormsen ed6699da58
refactor(simplify): one source per rule, shared test helpers, a faster isolation test [skip-docs]
/simplify over a97efb6..HEAD (the plan_version 1.9 / trial-4 delivery).

- coordinator-contract: engine names come from engine-findings ENGINES;
  one markedMajorCodeReview predicate under both lowering checks;
  isEngineFinding everywhere; the unreachable branch (c) of
  illegalBriefLowering folded into (e).
- armB workflow: RULE_KEYS = Object.keys(RULE_TIERS); the engine map is a
  literal pinned equal to ENGINES by the workflow test.
- plan-schema: usesFilesGrammar() is the one 1.9 check (validator, sweep,
  testFirstFiles, stepTestsOnlyFromFields); opensEntry built on opensChunk.
- plan-validator: one issue builder for the Files and Test first grammar.
- plan-scope-sweep: the 1.9 and the 6.2.0 reader are one if/else.
- artifact-commit: TRAILER_RE/trailerProblem no longer exported (no
  importer); the order comment says the CLI finds the repo root first.
- tests: the lock drivers in session-id-isolation (d) run side by side per
  mode (~100 s -> ~30 s); a shared artifact-repo helper; severityRank and
  extractFindingsBlock reused; childEnv(env) without a spread; one copy of
  the PLAN_VERSION_CURRENT check.

Plan readers: validator errors/warnings, plannedPaths, fieldPaths,
testFirstFiles and stepTestsOnlyFromFields compared byte for byte against
HEAD over 510 plans / 1692 steps plus 210 synthetic 1.7-1.9 cases: same.
Suite 2384 (2382/0/2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 21:06:47 +02:00

105 lines
4.5 KiB
JavaScript

// tests/lib/test-lock.test.mjs
// K8 — the test lock CLI (lib/util/test-lock.mjs). The executor (wave-1 order
// 1) locks a step's test files once the test is written and seen failing, and
// unlocks them when the test passes. The lock lives inside the repository's
// git directory, so it can never be committed.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { makeRepo, gitIn, tmp } from '../hooks/guard-helpers.mjs';
import { childEnv } from '../helpers/child-env.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const CLI = join(HERE, '..', '..', 'lib', 'util', 'test-lock.mjs');
function run(cwd, args, env = {}) {
const r = spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', env: childEnv(env) });
return { code: r.status, stdout: r.stdout, stderr: r.stderr };
}
function repoWithTests() {
const repo = makeRepo('main');
mkdirSync(join(repo, 'tests'), { recursive: true });
writeFileSync(join(repo, 'tests', 'a.test.js'), 'a\n');
writeFileSync(join(repo, 'tests', 'b.test.js'), 'b\n');
return repo;
}
test('K8 lock: exit 0, status names the step and the files', () => {
const repo = repoWithTests();
const r = run(repo, ['lock', '--files', 'tests/a.test.js,tests/b.test.js', '--step', '3']);
assert.equal(r.code, 0, r.stderr);
const s = run(repo, ['status']);
assert.equal(s.code, 0);
const st = JSON.parse(s.stdout);
assert.equal(st.locked, true);
assert.equal(st.step, 3);
assert.equal(st.files.length, 2);
assert.ok(st.files.every((f) => f.endsWith('.test.js')));
});
test('K8 lock: stored outside what can be committed (git status stays clean)', () => {
const repo = repoWithTests();
gitIn(repo, 'add', '-A');
gitIn(repo, 'commit', '-q', '-m', 'tests');
assert.equal(run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '1']).code, 0);
assert.equal(gitIn(repo, 'status', '--porcelain'), '', 'the lock must not appear as a change in the repo');
const st = JSON.parse(run(repo, ['status']).stdout);
assert.ok(st.lock_path.includes(`${'.git'}`), `lock path should be inside the git dir: ${st.lock_path}`);
});
test('K8 lock records the Claude Code session it was taken in', () => {
const repo = repoWithTests();
run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '2'], { CLAUDE_CODE_SESSION_ID: 'sess-123' });
assert.equal(JSON.parse(run(repo, ['status']).stdout).session, 'sess-123');
});
test('K8 unlock: exit 0, status unlocked', () => {
const repo = repoWithTests();
run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '1']);
assert.equal(run(repo, ['unlock']).code, 0);
assert.equal(JSON.parse(run(repo, ['status']).stdout).locked, false);
});
test('K8 lock refuses (exit 1) while another step holds the lock', () => {
const repo = repoWithTests();
run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '1']);
const r = run(repo, ['lock', '--files', 'tests/b.test.js', '--step', '2']);
assert.equal(r.code, 1);
assert.match(r.stderr, /step 1/);
});
test('K8: --cwd points at another directory', () => {
const repo = repoWithTests();
assert.equal(run(tmp(), ['lock', '--cwd', repo, '--files', 'tests/a.test.js', '--step', '4']).code, 0);
assert.equal(JSON.parse(run(tmp(), ['status', '--cwd', repo]).stdout).step, 4);
});
test('K8 could not run (exit 2): not a git repository', () => {
const r = run(tmp(), ['lock', '--files', 'x.test.js', '--step', '1']);
assert.equal(r.code, 2);
});
test('K8 could not run (exit 2): bad usage', () => {
const repo = repoWithTests();
assert.equal(run(repo, ['lock', '--step', '1']).code, 2, 'no --files');
assert.equal(run(repo, ['lock', '--files', 'tests/a.test.js']).code, 2, 'no --step');
assert.equal(run(repo, ['lock', '--files', 'tests/a.test.js', '--step', 'x']).code, 2, 'step not a number');
assert.equal(run(repo, ['frobnicate']).code, 2);
});
test('K8 status on a corrupt lock: exit 2, never "unlocked"', () => {
const repo = repoWithTests();
run(repo, ['lock', '--files', 'tests/a.test.js', '--step', '1']);
const path = JSON.parse(run(repo, ['status']).stdout).lock_path;
writeFileSync(path, '{not json');
const s = run(repo, ['status']);
assert.equal(s.code, 2);
assert.ok(existsSync(path));
assert.ok(!/"locked":\s*false/.test(s.stdout));
assert.ok(readFileSync(path, 'utf8').startsWith('{not'), 'status never rewrites the lock');
});