fix(commands): Phase 0 gives the arguments to the parser on stdin, never as shell words
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f1704f0800
commit
2c0a98c1ba
8 changed files with 157 additions and 29 deletions
|
|
@ -72,9 +72,12 @@ Execute these phases in order. Do not skip phases.
|
|||
|
||||
### Phase 1 — Parse mode and validate input
|
||||
|
||||
Run the arg-parser via Bash:
|
||||
Run the arg-parser via Bash, with the arguments as the one line inside the
|
||||
quoted heredoc (on stdin, never as shell words):
|
||||
```
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekreview -- "$@"
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekreview --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'
|
||||
$ARGUMENTS
|
||||
VOYAGE_ARGS_EOF_7F3C9E
|
||||
```
|
||||
|
||||
Pull the structured flags from its JSON output. Reject unknown flags. If
|
||||
|
|
|
|||
|
|
@ -32,9 +32,16 @@ See **Handover 7** in `docs/HANDOVER-CONTRACTS.md` for the full schema.
|
|||
Parse `$ARGUMENTS` with the shared arg-parser. Run first, exactly like this:
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekcontinue -- $ARGUMENTS
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekcontinue --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'
|
||||
$ARGUMENTS
|
||||
VOYAGE_ARGS_EOF_7F3C9E
|
||||
```
|
||||
|
||||
The arguments reach the parser as the one line inside the quoted heredoc, on
|
||||
stdin, never as shell words. It prints the parse result as JSON. On exit 1,
|
||||
print its `errors` and stop. When it prints no JSON, say that the parser did
|
||||
not run and stop.
|
||||
|
||||
Read `flags` and `positional` from the JSON it prints. Dispatch the usage
|
||||
block ONLY when one of these two conditions equals exactly true (no substring
|
||||
search, no "contains" check):
|
||||
|
|
|
|||
|
|
@ -31,14 +31,21 @@ See **Handover 7** in `docs/HANDOVER-CONTRACTS.md` for the schema.
|
|||
## Phase 0 — `--help` handling
|
||||
|
||||
Parse `$ARGUMENTS` with the arg parser. Run this via the Bash tool at runtime
|
||||
(NOT eager-exec):
|
||||
(NOT eager-exec), exactly as written: the arguments reach the parser as the one
|
||||
line inside the quoted heredoc, on stdin, never as shell words:
|
||||
|
||||
```bash
|
||||
setopt nonomatch 2>/dev/null || true
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession -- $ARGUMENTS
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'
|
||||
$ARGUMENTS
|
||||
VOYAGE_ARGS_EOF_7F3C9E
|
||||
```
|
||||
|
||||
The `setopt nonomatch` line is no longer needed for the arguments (the quoted
|
||||
heredoc expands no glob); it stays harmless.
|
||||
|
||||
It prints the parse result as JSON. On exit 1, print its `errors` and stop.
|
||||
When it prints no JSON, say that the parser did not run and stop.
|
||||
|
||||
Print the usage block below and exit cleanly ONLY when one of these two
|
||||
conditions equals exactly true (no substring search, no "contains" check):
|
||||
|
|
@ -95,8 +102,9 @@ Decision tree:
|
|||
## Phase 2 — Required args check (headless-safe)
|
||||
|
||||
Read the parse result from Phase 0: `<next-brief-path>` is `positional[0]`
|
||||
and `<next-label>` is `positional[1]`. Both are required. If either is
|
||||
missing or empty:
|
||||
and `<next-label>` is the parser's `tail` (the rest of the text as typed after
|
||||
the brief path, one outer quote pair removed, so a label with spaces stays
|
||||
whole). Both are required. If either is missing or empty:
|
||||
|
||||
```
|
||||
Error: missing required args.
|
||||
|
|
|
|||
|
|
@ -46,10 +46,13 @@ command executes inline.
|
|||
## Phase 1 — Parse mode and validate input
|
||||
|
||||
Parse the arguments with the shared arg-parser. Run it first, exactly like
|
||||
this, before anything else:
|
||||
this, before anything else (the arguments reach the parser as the one line
|
||||
inside the quoted heredoc, on stdin, never as shell words):
|
||||
|
||||
```bash
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekreview -- $ARGUMENTS
|
||||
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekreview --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'
|
||||
$ARGUMENTS
|
||||
VOYAGE_ARGS_EOF_7F3C9E
|
||||
```
|
||||
|
||||
It prints the parse as JSON on stdout. It also checks the required flag
|
||||
|
|
|
|||
2
tests/fixtures/jsonl-schemas.md
vendored
2
tests/fixtures/jsonl-schemas.md
vendored
|
|
@ -25,7 +25,7 @@
|
|||
| trekexecute-stats (Phase 9 record) | ts, plan, plan_type, mode, result, steps_total, steps_passed, steps_failed, steps_skipped, failed_at_step | lib/stats/stats-append.mjs (trekexecute.md Phase 9) | trekexecute.md:2973-2995 | profile, phase_models, profile_source | none |
|
||||
| trekexecute-stats (autonomy events) | ts, event, known_event, payload | lib/stats/event-emit.mjs `emit()` | event-emit.mjs:78-99 | payload.profile, payload.phase_models, payload.profile_source | none |
|
||||
| trekexecute-stats (PostToolUse Bash) | ts, session_id, command_excerpt, duration_ms, success | hooks/scripts/post-bash-stats.mjs (Bash PostToolUse) | post-bash-stats.mjs:42-54 | none (hook is plugin-level, not profile-aware) | command_excerpt (CWE-212) |
|
||||
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:640-690 | profile, phase_models, profile_source | none |
|
||||
| trekreview-stats | ts, slug, verdict, counts (BLOCKER/MAJOR/MINOR/SUGGESTION), reviewed_files_count, mode, duration_ms | lib/stats/stats-append.mjs (trekreview.md Phase 8) | trekreview.md:643-693 | profile, phase_models, profile_source | none |
|
||||
| trekcontinue-stats | ts, project, next_session_label, status | lib/stats/stats-append.mjs (trekcontinue.md Phase 5) | trekcontinue.md:304-308 | profile, profile_source | none |
|
||||
| token-usage | ts, session_id, scope, model, tokens_input, tokens_output, tokens_cache_creation, tokens_cache_read, cost_usd, is_estimate, price_table_version | lib/stats/token-usage.mjs `captureTokenUsage` (via hooks/scripts/otel-export.mjs Stop hook, opt-in `VOYAGE_TOKEN_METER`) | token-usage.mjs:buildRecord | n/a (SKAL-2 schema) | session_id (stripped at export) |
|
||||
|
||||
|
|
|
|||
|
|
@ -1225,7 +1225,7 @@ test('[D67] the End to end section says to record run_seen, also when no app was
|
|||
// now carries disable-model-invocation, so the model cannot start it.
|
||||
|
||||
const ENDSESSION = 'commands/trekendsession.md';
|
||||
const ENDSESSION_PARSER_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession -- $ARGUMENTS';
|
||||
const ENDSESSION_PARSER_LINE = "node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'";
|
||||
|
||||
test('[#15] /trekendsession Phase 0 has no "contains --help or -h" check, and its parser line run on fix-header gives no help', () => {
|
||||
const phase0 = section(read(ENDSESSION), '## Phase 0 — `--help` handling');
|
||||
|
|
@ -1238,24 +1238,26 @@ test('[#15] /trekendsession Phase 0 has no "contains --help or -h" check, and it
|
|||
const block = bashBlockHolding(phase0, cmd);
|
||||
assert.ok(block, 'the parser line is not in a runtime ```bash block');
|
||||
assert.equal(block.split('\n')[0], 'setopt nonomatch 2>/dev/null || true', 'the block does not open with setopt nonomatch');
|
||||
const plain = runParser(cmd, 'fix-header');
|
||||
assert.deepEqual(block.split('\n').slice(-2), ['$ARGUMENTS', 'VOYAGE_ARGS_EOF_7F3C9E'],
|
||||
'the block does not end with $ARGUMENTS as the heredoc body and its closing line');
|
||||
const plain = runParser(block, 'fix-header');
|
||||
assert.equal(plain.status, 0, `the parser line refused fix-header: ${plain.stdout}${plain.stderr}`);
|
||||
const r = JSON.parse(plain.stdout);
|
||||
assert.equal(r.flags['--help'], undefined, 'fix-header set the --help flag');
|
||||
assert.deepEqual(r.positional, ['fix-header']);
|
||||
assert.notEqual(r.positional[0], '-h');
|
||||
const help = runParser(cmd, '--help');
|
||||
const help = runParser(block, '--help');
|
||||
assert.equal(help.status, 0, help.stdout + help.stderr);
|
||||
assert.equal(JSON.parse(help.stdout).flags['--help'], true, 'control: --help must set the help flag');
|
||||
const two = runParser(cmd, 'fix-header "Session 2"');
|
||||
const two = runParser(block, 'fix-header "Session 2"');
|
||||
assert.equal(two.status, 0, two.stdout + two.stderr);
|
||||
assert.deepEqual(JSON.parse(two.stdout).positional, ['fix-header', 'Session 2']);
|
||||
});
|
||||
|
||||
test('[#15] /trekendsession Phase 2 reads the brief path and the label from the parser\'s positional', () => {
|
||||
test('[#15] /trekendsession Phase 2 reads the brief path from the parser\'s positional and the label from its tail', () => {
|
||||
const phase2 = flat(section(read(ENDSESSION), '## Phase 2 — Required args check (headless-safe)'));
|
||||
assert.match(phase2, /`positional\[0\]`/, 'Phase 2 does not read <next-brief-path> from positional[0]');
|
||||
assert.match(phase2, /`positional\[1\]`/, 'Phase 2 does not read <next-label> from positional[1]');
|
||||
assert.match(phase2, /`<next-label>` is the parser's `tail`/, 'Phase 2 does not read <next-label> from the parser\'s tail');
|
||||
});
|
||||
|
||||
test('[#67] /trekendsession sets disable-model-invocation, and CLAUDE.md names it as the one command that does', () => {
|
||||
|
|
@ -1293,7 +1295,7 @@ test('keep: commands/trekreview.md carries no disable-model-invocation field (/t
|
|||
// passes the state file, so the state-anchored staleness check runs.
|
||||
|
||||
const CONTINUE = 'commands/trekcontinue.md';
|
||||
const CONTINUE_PARSER_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekcontinue -- $ARGUMENTS';
|
||||
const CONTINUE_PARSER_LINE = "node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekcontinue --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'";
|
||||
const CONSISTENCY_LINE = 'node ${CLAUDE_PLUGIN_ROOT}/lib/validators/next-session-prompt-validator.mjs --json --consistency <RESOLVED-PATH-A> <RESOLVED-PATH-B> --state-file <RESOLVED-PATH-STATE>';
|
||||
|
||||
test('[#66] /trekcontinue Phase 0 has no parseArgs($ARGUMENTS call, and its parser line run with --help sets the help flag', () => {
|
||||
|
|
@ -1303,19 +1305,21 @@ test('[#66] /trekcontinue Phase 0 has no parseArgs($ARGUMENTS call, and its pars
|
|||
assert.equal(cmd, CONTINUE_PARSER_LINE, 'the Phase 0 parser line is not the trekcontinue parser call on $ARGUMENTS');
|
||||
const block = bashBlockHolding(phase0, cmd);
|
||||
assert.ok(block, 'the parser line is not in a runtime ```bash block');
|
||||
assert.deepEqual(block.split('\n').slice(-2), ['$ARGUMENTS', 'VOYAGE_ARGS_EOF_7F3C9E'],
|
||||
'the block does not end with $ARGUMENTS as the heredoc body and its closing line');
|
||||
assert.match(flat(phase0), /Run first, exactly like this:/);
|
||||
assert.match(flat(phase0), /Read `flags` and `positional` from the JSON it prints/);
|
||||
assert.match(flat(phase0), /`flags\['--help'\] === true`/, 'Phase 0 does not dispatch on the parsed --help flag');
|
||||
assert.match(flat(phase0), /`positional\[0\] === '-h'`/, 'Phase 0 does not dispatch on a lone -h as the first positional');
|
||||
const help = runParser(cmd, '--help');
|
||||
const help = runParser(block, '--help');
|
||||
assert.equal(help.status, 0, help.stdout + help.stderr);
|
||||
assert.equal(JSON.parse(help.stdout).flags['--help'], true, 'the parser line run with --help does not set the help flag');
|
||||
const dir = runParser(cmd, '.claude/projects/demo');
|
||||
const dir = runParser(block, '.claude/projects/demo');
|
||||
assert.equal(dir.status, 0, dir.stdout + dir.stderr);
|
||||
const r = JSON.parse(dir.stdout);
|
||||
assert.equal(r.flags['--help'], undefined, 'a project dir set the --help flag');
|
||||
assert.deepEqual(r.positional, ['.claude/projects/demo']);
|
||||
const h = runParser(cmd, '-h');
|
||||
const h = runParser(block, '-h');
|
||||
assert.equal(h.status, 0, h.stdout + h.stderr);
|
||||
assert.equal(JSON.parse(h.stdout).positional[0], '-h');
|
||||
const p05 = flat(section(read(CONTINUE), '## Phase 0.5 — Cleanup mode dispatch'));
|
||||
|
|
@ -1323,11 +1327,15 @@ test('[#66] /trekcontinue Phase 0 has no parseArgs($ARGUMENTS call, and its pars
|
|||
assert.match(p05, /After the parser has printed/);
|
||||
});
|
||||
|
||||
test('[#66] sibling: agents/review-orchestrator.md passes the arguments to the parser after --', () => {
|
||||
test('[#66] sibling: agents/review-orchestrator.md passes the arguments to the parser on stdin through the quoted heredoc', () => {
|
||||
const p1 = section(read('agents/review-orchestrator.md'), '### Phase 1 — Parse mode and validate input');
|
||||
const cmd = line(p1, 'parsers/arg-parser.mjs');
|
||||
assert.equal(cmd, 'node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekreview -- "$@"');
|
||||
const r = bash(`set -- --project .claude/projects/demo; ${cmd}`, ROOT);
|
||||
assert.equal(cmd, "node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekreview --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'");
|
||||
const lines = p1.split('\n');
|
||||
const at = lines.findIndex((l) => l.trim() === cmd);
|
||||
assert.deepEqual(lines.slice(at + 1, at + 3), ['$ARGUMENTS', 'VOYAGE_ARGS_EOF_7F3C9E'],
|
||||
'the parser line is not followed by $ARGUMENTS and the closing line');
|
||||
const r = runParser(lines.slice(at, at + 3).join('\n'), '--project .claude/projects/demo');
|
||||
assert.equal(r.status, 0, r.stdout + r.stderr);
|
||||
assert.equal(JSON.parse(r.stdout).flags['--project'], '.claude/projects/demo', 'the parser did not see --project');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,6 +23,12 @@
|
|||
// to the Pass 3 skip.
|
||||
// K3 The orchestrator's 4-pass item 3 names the brief-conflict lowering as
|
||||
// the exception to the catalogue reset.
|
||||
// ARG /trekendsession and /trekcontinue Phase 0 and /trekreview Phase 1
|
||||
// give $ARGUMENTS to the arg parser (`--stdin`) as the one body line of
|
||||
// a quoted heredoc, never as shell words.
|
||||
// M8 /trekcontinue and /trekendsession Phase 0 stop on the parser's exit 1
|
||||
// with its `errors`, and stop when the parser prints no JSON.
|
||||
// M10 /trekendsession Phase 2 reads the label from the parser's `tail`.
|
||||
// No static import of a new export: the file must load at be0b631.
|
||||
// Fix the SOURCE a pin guards, not the pin.
|
||||
|
||||
|
|
@ -202,3 +208,92 @@ test('[K3] orchestrator item 3 names the brief-conflict lowering as the exceptio
|
|||
assert.match(next, /MAJOR[^.]*\bMINOR\b/, `the exception does not say a MAJOR drops to MINOR: ${next}`);
|
||||
assert.match(next, /`brief_lowered_from: MAJOR`/, `the exception does not name brief_lowered_from: MAJOR: ${next}`);
|
||||
});
|
||||
|
||||
// --- ARG / M8 / M10 ----------------------------------------------------------------
|
||||
|
||||
const ARGS_DELIM = 'VOYAGE_ARGS_EOF_7F3C9E';
|
||||
const ARGS_OPENER = `<<'${ARGS_DELIM}'`;
|
||||
|
||||
/** The text of a `## ` section of `rel` from the line starting with `heading` to the next `## ` line outside a fence. */
|
||||
function phase(rel, heading) {
|
||||
const lines = read(rel).split('\n');
|
||||
const s = lines.findIndex((l) => l.startsWith(heading));
|
||||
assert.ok(s !== -1, `${rel} must hold a heading starting "${heading}"`);
|
||||
let fenced = false;
|
||||
for (let i = s + 1; i < lines.length; i++) {
|
||||
if (/^\s{0,3}(`{3,}|~{3,})/.test(lines[i])) fenced = !fenced;
|
||||
if (!fenced && lines[i].startsWith('## ')) return lines.slice(s, i).join('\n');
|
||||
}
|
||||
return lines.slice(s).join('\n');
|
||||
}
|
||||
|
||||
// [file, command, the section that runs the parser]
|
||||
const PARSER_PHASES = [
|
||||
['commands/trekendsession.md', 'trekendsession', '## Phase 0 — `--help` handling'],
|
||||
['commands/trekcontinue.md', 'trekcontinue', '## Phase 0 — `--help` handling'],
|
||||
['commands/trekreview.md', 'trekreview', '## Phase 1 — Parse mode and validate input'],
|
||||
];
|
||||
|
||||
/** The one runtime ```bash block of the section that runs the arg parser for `cmd`, as lines, with the parser line's index. */
|
||||
function parserBlock(rel, cmd, heading) {
|
||||
const text = phase(rel, heading);
|
||||
const blocks = text.split('\n```bash\n').slice(1).map((b) => b.split('\n```')[0])
|
||||
.filter((b) => b.includes(`lib/parsers/arg-parser.mjs --command ${cmd}`));
|
||||
assert.equal(blocks.length, 1, `${rel} ${heading} must hold one \`\`\`bash block that runs the ${cmd} arg parser; found ${blocks.length}`);
|
||||
const lines = blocks[0].split('\n');
|
||||
const at = lines.findIndex((l) => l.includes(`lib/parsers/arg-parser.mjs --command ${cmd}`));
|
||||
return { lines, at };
|
||||
}
|
||||
|
||||
test('[ARG] each of the three parser lines reads the arguments from a quoted heredoc', () => {
|
||||
for (const [rel, cmd, heading] of PARSER_PHASES) {
|
||||
const { lines, at } = parserBlock(rel, cmd, heading);
|
||||
const opener = lines[at].trim();
|
||||
assert.ok(opener.endsWith(` ${ARGS_OPENER}`),
|
||||
`${rel}: the parser line does not end with the quoted heredoc opener ${ARGS_OPENER}: ${opener}`);
|
||||
assert.equal(lines[at + 1], '$ARGUMENTS',
|
||||
`${rel}: the line inside the heredoc is not $ARGUMENTS alone: ${JSON.stringify(lines[at + 1])}`);
|
||||
assert.match(opener, new RegExp(`^node \\$\\{CLAUDE_PLUGIN_ROOT\\}/lib/parsers/arg-parser\\.mjs --command ${cmd} --stdin <<`),
|
||||
`${rel}: the parser line does not read stdin (--command ${cmd} --stdin right after arg-parser.mjs): ${opener}`);
|
||||
const filled = lines.filter((l) => l.trim() !== '');
|
||||
assert.ok(lines[at + 2] === ARGS_DELIM && filled[filled.length - 1] === ARGS_DELIM,
|
||||
`${rel}: the closing line ${ARGS_DELIM} does not follow $ARGUMENTS as the block's last non-empty line: ${JSON.stringify(lines)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[ARG] no line outside the heredoc carries $ARGUMENTS in those blocks', () => {
|
||||
for (const [rel, cmd, heading] of PARSER_PHASES) {
|
||||
const { lines } = parserBlock(rel, cmd, heading);
|
||||
const open = lines.findIndex((l) => l.trimEnd().endsWith(ARGS_OPENER));
|
||||
assert.ok(open !== -1, `${rel}: the parser block opens no ${ARGS_OPENER} heredoc: ${JSON.stringify(lines)}`);
|
||||
const close = lines.findIndex((l, i) => i > open && l.trim() === ARGS_DELIM);
|
||||
assert.ok(close !== -1, `${rel}: the parser block's heredoc has no closing line ${ARGS_DELIM}`);
|
||||
const outside = lines.filter((l, i) => (i <= open || i >= close) && l.includes('$ARGUMENTS'));
|
||||
assert.deepEqual(outside, [], `${rel}: a line outside the heredoc carries $ARGUMENTS`);
|
||||
}
|
||||
});
|
||||
|
||||
const PHASE0 = '## Phase 0 — `--help` handling';
|
||||
const STOP_COMMANDS = ['commands/trekcontinue.md', 'commands/trekendsession.md'];
|
||||
|
||||
test('[M8] /trekcontinue and /trekendsession Phase 0 stop on exit 1 with the parser\'s errors', () => {
|
||||
for (const rel of STOP_COMMANDS) {
|
||||
const hits = sentencesWith(phase(rel, PHASE0), 'exit 1');
|
||||
assert.ok(hits.some((s) => /^On exit 1, print (its|the parser's) `errors` and stop\.$/.test(s)),
|
||||
`${rel} Phase 0 does not say: on exit 1, print the parser's \`errors\` and stop: ${hits.join(' | ')}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[M8] /trekcontinue and /trekendsession Phase 0 stop when the parser prints no JSON', () => {
|
||||
for (const rel of STOP_COMMANDS) {
|
||||
const hits = sentencesWith(phase(rel, PHASE0), 'no JSON');
|
||||
assert.ok(hits.some((s) => /\bprints no JSON\b/.test(s) && /\bthe parser did not run\b/.test(s) && /\band stop\.$/.test(s)),
|
||||
`${rel} Phase 0 does not say: when the parser prints no JSON, say that it did not run and stop: ${hits.join(' | ')}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('[M10] /trekendsession Phase 2 reads the label from tail', () => {
|
||||
const p2 = flat(phase('commands/trekendsession.md', '## Phase 2 — Required args check'));
|
||||
assert.doesNotMatch(p2, /`positional\[1\]`/, 'Phase 2 still reads <next-label> from positional[1], which splits a label at spaces');
|
||||
assert.match(p2, /`<next-label>` is the parser's `tail`/, `Phase 2 does not read <next-label> from the parser's tail: ${p2}`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -149,13 +149,17 @@ test('plan-halts-without-phase-signals: the validator\'s own code must be in the
|
|||
// ---- case 5: the missing-flag error comes from code ----
|
||||
|
||||
test('review-requires-project: the arg parser, run as trekreview.md spells it, prints the error (exit 1)', () => {
|
||||
const lines = read('commands/trekreview.md').split('\n').filter((l) =>
|
||||
/^\s*node \$\{CLAUDE_PLUGIN_ROOT\}\/lib\/parsers\/arg-parser\.mjs --command trekreview\b/.test(l));
|
||||
assert.equal(lines.length, 1, 'trekreview.md must carry exactly one arg-parser line');
|
||||
const line = lines[0].trim();
|
||||
assert.match(line, /\$ARGUMENTS/, 'the parser must receive $ARGUMENTS (the Bash tool has no "$@")');
|
||||
const all = read('commands/trekreview.md').split('\n');
|
||||
const at = all.map((l, i) =>
|
||||
(/^\s*node \$\{CLAUDE_PLUGIN_ROOT\}\/lib\/parsers\/arg-parser\.mjs --command trekreview\b/.test(l) ? i : -1)).filter((i) => i >= 0);
|
||||
assert.equal(at.length, 1, 'trekreview.md must carry exactly one arg-parser line');
|
||||
const line = all[at[0]].trim();
|
||||
assert.match(line, / --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'$/, 'the parser must read the arguments on stdin from the quoted heredoc');
|
||||
const block = all.slice(at[0], at[0] + 3).map((l) => l.trim());
|
||||
assert.deepEqual(block.slice(1), ['$ARGUMENTS', 'VOYAGE_ARGS_EOF_7F3C9E'],
|
||||
'the parser must receive $ARGUMENTS as the heredoc body (the Bash tool has no "$@")');
|
||||
const run = (args) => spawnSync('bash', ['-c',
|
||||
line.split('${CLAUDE_PLUGIN_ROOT}').join(ROOT).split('$ARGUMENTS').join(args)], { encoding: 'utf8' });
|
||||
block.join('\n').split('${CLAUDE_PLUGIN_ROOT}').join(ROOT).split('$ARGUMENTS').join(args)], { encoding: 'utf8' });
|
||||
const none = run('');
|
||||
assert.equal(none.status, 1, `missing --project must exit 1: ${none.stdout} ${none.stderr}`);
|
||||
assert.deepEqual(JSON.parse(none.stdout).errors.map((e) => e.code), ['ARG_REQUIRED_MISSING']);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue