voyage/tests/lib/artifact-commit.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

94 lines
4.9 KiB
JavaScript

// tests/lib/artifact-commit.test.mjs
// The commit in the target repo: the approval log always, the brief or plan only
// when the operator chose `git` (operator decision 2026-10-06: «Loggen alltid,
// briefen etter valg»). Never sweeps up anything else the operator has staged.
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 { artifactRepo, git } from '../helpers/artifact-repo.mjs';
const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const CLI = join(ROOT, 'lib', 'util', 'artifact-commit.mjs');
// Every call passes a valid trailer (the CLI refuses a call without one).
const TRAILER = ['--trailer', 'Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>'];
function run(args, cwd) {
return spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8' });
}
const committedFiles = (d) => git(d, 'show', '--name-only', '--format=', 'HEAD').stdout.trim().split('\n').filter(Boolean);
test('store git: the brief and the log land in one commit, and nothing else the operator staged', () => {
const { d, brief } = artifactRepo();
try {
writeFileSync(join(d, 'unrelated.txt'), 'staged by the operator\n');
git(d, 'add', 'unrelated.txt');
const r = run(['--store', 'git', '--artifact', brief, '--message', 'docs(voyage): approve intent toggle', ...TRAILER], d);
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.equal(JSON.parse(r.stdout).committed, true);
assert.deepEqual(committedFiles(d).sort(),
['.claude/projects/2026-10-06-toggle/brief.md', '.claude/voyage/approvals.jsonl']);
assert.match(git(d, 'diff', '--cached', '--name-only').stdout, /unrelated\.txt/, 'the operator\'s staged file stays staged, uncommitted');
assert.match(git(d, 'log', '-1', '--format=%s').stdout, /approve intent toggle/);
} finally { rmSync(d, { recursive: true, force: true }); }
});
test('store local: only the log is committed, and the project folder is kept out of git locally', () => {
const { d, brief } = artifactRepo();
try {
const r = run(['--store', 'local', '--artifact', brief, '--message', 'docs(voyage): approve intent toggle', ...TRAILER], d);
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.deepEqual(committedFiles(d), ['.claude/voyage/approvals.jsonl']);
const exclude = readFileSync(join(d, '.git', 'info', 'exclude'), 'utf8');
assert.match(exclude, /^\/\.claude\/projects\/2026-10-06-toggle\/$/m);
assert.equal(git(d, 'status', '--porcelain', '--', '.claude/projects').stdout.trim(), '',
'the local-only brief does not show as untracked');
// idempotent: a second run adds no second exclude line
run(['--store', 'local', '--artifact', brief, '--message', 'again', ...TRAILER], d);
const again = readFileSync(join(d, '.git', 'info', 'exclude'), 'utf8');
assert.equal(again.split('\n').filter((l) => l.includes('2026-10-06-toggle')).length, 1);
} finally { rmSync(d, { recursive: true, force: true }); }
});
test('nothing new to commit is exit 0 with committed:false (a re-run is not an error)', () => {
const { d, brief } = artifactRepo();
try {
assert.equal(run(['--store', 'git', '--artifact', brief, '--message', 'one', ...TRAILER], d).status, 0);
const r = run(['--store', 'git', '--artifact', brief, '--message', 'two', ...TRAILER], d);
assert.equal(r.status, 0, r.stdout + r.stderr);
assert.equal(JSON.parse(r.stdout).committed, false);
} finally { rmSync(d, { recursive: true, force: true }); }
});
test('a path the repo ignores is not forced in: exit 1 with the reason', () => {
const { d, brief } = artifactRepo();
try {
writeFileSync(join(d, '.gitignore'), '.claude/\n');
const r = run(['--store', 'git', '--artifact', brief, '--message', 'x', ...TRAILER], d);
assert.equal(r.status, 1, r.stdout + r.stderr);
assert.match(JSON.parse(r.stdout).reason, /ignored/i);
} finally { rmSync(d, { recursive: true, force: true }); }
});
test('not a git repository: exit 2 (could not run), never a silent pass', () => {
const d = mkdtempSync(join(tmpdir(), 'artifact-commit-nogit-'));
try {
writeFileSync(join(d, 'brief.md'), 'x');
const r = run(['--store', 'git', '--artifact', join(d, 'brief.md'), '--message', 'x', ...TRAILER], d);
assert.equal(r.status, 2, r.stdout + r.stderr);
} finally { rmSync(d, { recursive: true, force: true }); }
});
test('a missing or unknown --store is a usage error (exit 2)', () => {
const { d, brief } = artifactRepo();
try {
assert.equal(run(['--artifact', brief, '--message', 'x', ...TRAILER], d).status, 2);
assert.equal(run(['--store', 'cloud', '--artifact', brief, '--message', 'x', ...TRAILER], d).status, 2);
} finally { rmSync(d, { recursive: true, force: true }); }
});