// tests/lib/artifact-commit-trailer.test.mjs // Approval commits carry the caller's `Co-Authored-By:` trailer (plan step 13). // The CLI takes `--trailer ""`; a missing, multi-line or malformed one // (an unfilled `{model}`, no ``, 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 '; 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 '); } finally { rmSync(d, { recursive: true, force: true }); } }); const REFUSED = [ ['a two-line value', `${TRAILER}\nCo-Authored-By: Other `], ['a value holding Claude-Session:', 'Co-Authored-By: Claude-Session: https://example.invalid/x '], ['a value without ', 'Co-Authored-By: Claude Opus 5.5'], ['a non-Co-Authored-By key', 'Signed-off-by: Claude Opus 5.5 '], ['the unfilled {model} placeholder', 'Co-Authored-By: Claude {model} '], ['an empty {model} (two spaces before the address)', 'Co-Authored-By: Claude '], ['a name of three spaces', 'Co-Authored-By: '], ['two spaces after the colon', 'Co-Authored-By: Claude Opus 5.5 '], ['two spaces inside the name', 'Co-Authored-By: Claude Opus 5.5 '], ]; 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 }); } }); } // Guard rows: these must stay accepted (review 8420915b tightened the name). const ACCEPTED = [ ['the session trailer', TRAILER], ['a name with a parenthesis', 'Co-Authored-By: Claude Opus 5.5 (1M context) '], ]; for (const [name, value] of ACCEPTED) { test(`accepted and committed as the last line: ${name}`, () => { const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer' }); try { const r = run(['--store', 'local', '--artifact', brief, '--message', 'x', '--trailer', value], 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[body.length - 1], value, 'the trailer is the last line of the message'); } 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 '); } 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\} "/, `${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); });