/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>
148 lines
7.5 KiB
JavaScript
148 lines
7.5 KiB
JavaScript
// tests/lib/artifact-commit-trailer.test.mjs
|
|
// Approval commits carry the caller's `Co-Authored-By:` trailer (plan step 13).
|
|
// The CLI takes `--trailer "<line>"`; a missing, multi-line or malformed one
|
|
// (an unfilled `{model}`, no `<email>`, another key) or one holding
|
|
// `Claude-Session:` is refused with exit 1 before any git call, so "nothing to
|
|
// commit" cannot mask it. A usage error (`--store`) stays exit 2 and comes
|
|
// first. Every call in commands/*.md passes the trailer, and none carries a
|
|
// `Claude-Session:` line.
|
|
|
|
import { test } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { readFileSync, readdirSync, realpathSync, rmSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { childEnv } from '../helpers/child-env.mjs';
|
|
import { artifactRepo, git } from '../helpers/artifact-repo.mjs';
|
|
import { commitArtifacts } from '../../lib/util/artifact-commit.mjs';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(HERE, '..', '..');
|
|
const CLI = join(ROOT, 'lib', 'util', 'artifact-commit.mjs');
|
|
const TRAILER = 'Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>';
|
|
|
|
function run(args, cwd) {
|
|
return spawnSync(process.execPath, [CLI, ...args], { cwd, encoding: 'utf8', env: childEnv() });
|
|
}
|
|
|
|
const head = (d) => git(d, 'rev-parse', 'HEAD').stdout.trim();
|
|
|
|
test('a passed trailer ends the commit message and git parses it as a trailer', () => {
|
|
const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer' });
|
|
try {
|
|
const r = run(['--store', 'local', '--artifact', brief, '--message', 'docs(voyage): approve intent trailer',
|
|
'--trailer', TRAILER], d);
|
|
assert.equal(r.status, 0, r.stdout + r.stderr);
|
|
assert.equal(JSON.parse(r.stdout).committed, true);
|
|
const body = git(d, 'log', '-1', '--format=%B').stdout.trimEnd().split('\n');
|
|
assert.equal(body[0], 'docs(voyage): approve intent trailer');
|
|
assert.equal(body[body.length - 1], TRAILER, 'the trailer is the last line of the message');
|
|
const parsed = git(d, 'log', '-1', '--format=%(trailers:key=Co-Authored-By,valueonly)').stdout.trim();
|
|
assert.equal(parsed, 'Claude Opus 5.5 <noreply@anthropic.com>');
|
|
} finally { rmSync(d, { recursive: true, force: true }); }
|
|
});
|
|
|
|
const REFUSED = [
|
|
['a two-line value', `${TRAILER}\nCo-Authored-By: Other <other@example.invalid>`],
|
|
['a value holding Claude-Session:', 'Co-Authored-By: Claude-Session: https://example.invalid/x <noreply@anthropic.com>'],
|
|
['a value without <email>', 'Co-Authored-By: Claude Opus 5.5'],
|
|
['a non-Co-Authored-By key', 'Signed-off-by: Claude Opus 5.5 <noreply@anthropic.com>'],
|
|
['the unfilled {model} placeholder', 'Co-Authored-By: Claude {model} <noreply@anthropic.com>'],
|
|
];
|
|
|
|
for (const [name, value] of REFUSED) {
|
|
test(`refused with exit 1, nothing committed: ${name}`, () => {
|
|
const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer' });
|
|
try {
|
|
const before = head(d);
|
|
const r = run(['--store', 'local', '--artifact', brief, '--message', 'x', '--trailer', value], d);
|
|
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
const out = JSON.parse(r.stdout);
|
|
assert.equal(out.committed, false);
|
|
assert.match(out.reason, /trailer/i);
|
|
assert.equal(head(d), before, 'HEAD unchanged');
|
|
} finally { rmSync(d, { recursive: true, force: true }); }
|
|
});
|
|
}
|
|
|
|
test('a call without a trailer exits 1 with HEAD unchanged', () => {
|
|
const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer' });
|
|
try {
|
|
const before = head(d);
|
|
const r = run(['--store', 'local', '--artifact', brief, '--message', 'x'], d);
|
|
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
assert.match(JSON.parse(r.stdout).reason, /--trailer/);
|
|
assert.equal(head(d), before, 'HEAD unchanged');
|
|
assert.equal(git(d, 'diff', '--cached', '--name-only').stdout.trim(), '', 'nothing staged either');
|
|
} finally { rmSync(d, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('the trailer check comes before any git call: "nothing to commit" does not mask a missing trailer', () => {
|
|
const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer', withLog: false });
|
|
try {
|
|
const r = run(['--store', 'local', '--artifact', brief, '--message', 'x'], d);
|
|
assert.equal(r.status, 1, r.stdout + r.stderr);
|
|
assert.match(JSON.parse(r.stdout).reason, /--trailer/);
|
|
} finally { rmSync(d, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('commitArtifacts takes the trailer too, and refuses without it', () => {
|
|
const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer' });
|
|
try {
|
|
const repoDir = realpathSync(d);
|
|
const before = head(d);
|
|
const no = commitArtifacts({ repo: repoDir, store: 'local', artifacts: [brief], message: 'x' });
|
|
assert.equal(no.exit, 1, JSON.stringify(no));
|
|
assert.equal(head(d), before);
|
|
const yes = commitArtifacts({ repo: repoDir, store: 'local', artifacts: [brief], message: 'x', trailer: TRAILER });
|
|
assert.equal(yes.exit, 0, JSON.stringify(yes));
|
|
assert.equal(yes.committed, true);
|
|
assert.equal(git(d, 'log', '-1', '--format=%(trailers:key=Co-Authored-By,valueonly)').stdout.trim(),
|
|
'Claude Opus 5.5 <noreply@anthropic.com>');
|
|
} finally { rmSync(d, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('usage before trailer: a missing --store is still exit 2, with or without a trailer', () => {
|
|
const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer' });
|
|
try {
|
|
assert.equal(run(['--artifact', brief, '--message', 'x'], d).status, 2);
|
|
assert.equal(run(['--artifact', brief, '--message', 'x', '--trailer', TRAILER], d).status, 2);
|
|
assert.equal(run(['--store', 'cloud', '--artifact', brief, '--message', 'x', '--trailer', 'bad'], d).status, 2);
|
|
} finally { rmSync(d, { recursive: true, force: true }); }
|
|
});
|
|
|
|
// ---- the calls in commands/*.md ----
|
|
|
|
const CMD_DIR = join(ROOT, 'commands');
|
|
// A call is `artifact-commit.mjs` followed by its flags, up to the end of the
|
|
// line or the closing backtick of an inline call. A prose mention without
|
|
// flags is not a call.
|
|
const CALL = /artifact-commit\.mjs[ \t]+--[^`\n]*/g;
|
|
|
|
function calls() {
|
|
const out = [];
|
|
for (const f of readdirSync(CMD_DIR).filter((n) => n.endsWith('.md')).sort()) {
|
|
const text = readFileSync(join(CMD_DIR, f), 'utf8');
|
|
for (const m of text.matchAll(CALL)) out.push({ file: f, call: m[0], index: m.index, text });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
test('every artifact-commit.mjs call in commands/*.md (exactly 3) passes --trailer, and none holds Claude-Session', () => {
|
|
const found = calls();
|
|
assert.deepEqual(found.map((c) => c.file), ['trekbrief.md', 'trekexecute.md', 'trekplan.md'],
|
|
found.map((c) => `${c.file}: ${c.call}`).join('\n'));
|
|
for (const c of found) {
|
|
assert.match(c.call, /--trailer "Co-Authored-By: Claude \{model\} <noreply@anthropic\.com>"/, `${c.file}: ${c.call}`);
|
|
assert.doesNotMatch(c.call, /Claude-Session/, `${c.file}: ${c.call}`);
|
|
}
|
|
});
|
|
|
|
test('commands/trekexecute.md says what to do when the approval commit exits 1 or 2', () => {
|
|
const c = calls().find((x) => x.file === 'trekexecute.md');
|
|
assert.ok(c, 'the inline call in /trekexecute is found');
|
|
const after = c.text.slice(c.index, c.index + 800).replace(/\s+/g, ' ');
|
|
assert.ok(after.includes('On exit 1 or 2, print the `reason` in one line and continue: the approval is logged but not committed.'),
|
|
after);
|
|
});
|