fix(approval): approval commits carry the caller's Co-Authored-By trailer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
94de23ca3b
commit
acd0e3dd47
8 changed files with 337 additions and 23 deletions
|
|
@ -16,6 +16,8 @@ const ROOT = join(HERE, '..', '..');
|
|||
const CLI = join(ROOT, 'lib', 'util', 'artifact-commit.mjs');
|
||||
const LOG = join(ROOT, 'lib', 'util', 'approval-log.mjs');
|
||||
const H1 = 'c'.repeat(64);
|
||||
// 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 git(cwd, ...args) {
|
||||
return spawnSync('git', args, { cwd, encoding: 'utf8' });
|
||||
|
|
@ -50,7 +52,7 @@ test('store git: the brief and the log land in one commit, and nothing else the
|
|||
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'], d);
|
||||
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(),
|
||||
|
|
@ -63,7 +65,7 @@ test('store git: the brief and the log land in one commit, and nothing else the
|
|||
test('store local: only the log is committed, and the project folder is kept out of git locally', () => {
|
||||
const { d, brief } = repo();
|
||||
try {
|
||||
const r = run(['--store', 'local', '--artifact', brief, '--message', 'docs(voyage): approve intent toggle'], d);
|
||||
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');
|
||||
|
|
@ -71,7 +73,7 @@ test('store local: only the log is committed, and the project folder is kept out
|
|||
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'], d);
|
||||
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 }); }
|
||||
|
|
@ -80,8 +82,8 @@ test('store local: only the log is committed, and the project folder is kept out
|
|||
test('nothing new to commit is exit 0 with committed:false (a re-run is not an error)', () => {
|
||||
const { d, brief } = repo();
|
||||
try {
|
||||
assert.equal(run(['--store', 'git', '--artifact', brief, '--message', 'one'], d).status, 0);
|
||||
const r = run(['--store', 'git', '--artifact', brief, '--message', 'two'], d);
|
||||
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 }); }
|
||||
|
|
@ -91,7 +93,7 @@ test('a path the repo ignores is not forced in: exit 1 with the reason', () => {
|
|||
const { d, brief } = repo();
|
||||
try {
|
||||
writeFileSync(join(d, '.gitignore'), '.claude/\n');
|
||||
const r = run(['--store', 'git', '--artifact', brief, '--message', 'x'], d);
|
||||
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 }); }
|
||||
|
|
@ -101,7 +103,7 @@ 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'], d);
|
||||
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 }); }
|
||||
});
|
||||
|
|
@ -109,7 +111,7 @@ test('not a git repository: exit 2 (could not run), never a silent pass', () =>
|
|||
test('a missing or unknown --store is a usage error (exit 2)', () => {
|
||||
const { d, brief } = repo();
|
||||
try {
|
||||
assert.equal(run(['--artifact', brief, '--message', 'x'], d).status, 2);
|
||||
assert.equal(run(['--store', 'cloud', '--artifact', brief, '--message', 'x'], d).status, 2);
|
||||
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 }); }
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue