fix(approval): a trailer with a blank or padded name is refused
Review finding 8420915b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
05f6b67be9
commit
1621352e67
2 changed files with 28 additions and 2 deletions
|
|
@ -38,7 +38,10 @@ import { isMain } from './is-main.mjs';
|
||||||
export const ARTIFACT_STORES = Object.freeze(['git', 'local']);
|
export const ARTIFACT_STORES = Object.freeze(['git', 'local']);
|
||||||
const DEFAULT_MESSAGE = 'docs(voyage): record approval';
|
const DEFAULT_MESSAGE = 'docs(voyage): record approval';
|
||||||
// One `Co-Authored-By:` line; `{`/`}` are refused so an unfilled `{model}` fails.
|
// One `Co-Authored-By:` line; `{`/`}` are refused so an unfilled `{model}` fails.
|
||||||
const TRAILER_RE = /^Co-Authored-By: [^<>{}\n]+ <[^\s<>@]+@[^\s<>]+>$/;
|
// The name starts and ends with a non-space, and trailerProblem refuses two
|
||||||
|
// spaces in a row, so an empty `{model}` ("Claude" + two spaces), a blank name
|
||||||
|
// and a padded one fail too (review 8420915b).
|
||||||
|
const TRAILER_RE = /^Co-Authored-By: [^\s<>{}](?:[^<>{}\n]*[^\s<>{}])? <[^\s<>@]+@[^\s<>]+>$/;
|
||||||
|
|
||||||
/** @returns {string|null} why `trailer` is refused, or null when it is valid */
|
/** @returns {string|null} why `trailer` is refused, or null when it is valid */
|
||||||
function trailerProblem(trailer) {
|
function trailerProblem(trailer) {
|
||||||
|
|
@ -47,7 +50,7 @@ function trailerProblem(trailer) {
|
||||||
}
|
}
|
||||||
if (/[\r\n]/.test(trailer)) return '--trailer must be one line — nothing committed';
|
if (/[\r\n]/.test(trailer)) return '--trailer must be one line — nothing committed';
|
||||||
if (trailer.includes('Claude-Session:')) return '--trailer must not hold a Claude-Session: line — nothing committed';
|
if (trailer.includes('Claude-Session:')) return '--trailer must not hold a Claude-Session: line — nothing committed';
|
||||||
if (!TRAILER_RE.test(trailer)) {
|
if (!TRAILER_RE.test(trailer) || trailer.includes(' ')) {
|
||||||
return `--trailer must read "Co-Authored-By: <name> <<email>>" with every placeholder filled; got "${trailer}" — nothing committed`;
|
return `--trailer must read "Co-Authored-By: <name> <<email>>" with every placeholder filled; got "${trailer}" — nothing committed`;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,10 @@ const REFUSED = [
|
||||||
['a value without <email>', 'Co-Authored-By: Claude Opus 5.5'],
|
['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>'],
|
['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>'],
|
['the unfilled {model} placeholder', 'Co-Authored-By: Claude {model} <noreply@anthropic.com>'],
|
||||||
|
['an empty {model} (two spaces before the address)', 'Co-Authored-By: Claude <noreply@anthropic.com>'],
|
||||||
|
['a name of three spaces', 'Co-Authored-By: <noreply@anthropic.com>'],
|
||||||
|
['two spaces after the colon', 'Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>'],
|
||||||
|
['two spaces inside the name', 'Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>'],
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const [name, value] of REFUSED) {
|
for (const [name, value] of REFUSED) {
|
||||||
|
|
@ -66,6 +70,25 @@ for (const [name, value] of REFUSED) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) <noreply@anthropic.com>'],
|
||||||
|
];
|
||||||
|
|
||||||
|
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', () => {
|
test('a call without a trailer exits 1 with HEAD unchanged', () => {
|
||||||
const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer' });
|
const { d, brief } = artifactRepo({ prefix: 'artifact-trailer-', project: '2026-10-12-trailer' });
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue