fix(cli): reject help/version flags instead of silently ignoring them (#1822)
* fix(cli): reject help/version flags instead of silently ignoring them (#1818) AI agents can hallucinate --help or --version on gsd-tools invocations. Without a guard, unknown flags were silently ignored and the command proceeded — including destructive ones like `phases clear`. Add a pre-dispatch check in main() that errors immediately if any never-valid flag (-h, --help, -?, --version, -v, --usage) is present in args after global flags are stripped. Regression test covers phases clear, generate- slug, state load, and current-timestamp with both --help and -h variants. Closes #1818 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(agents): convert gsd-verifier required_reading to inline wiring The thinking-model-guidance test requires inline @-reference wiring at decision points rather than a <required_reading> block. Convert verification-overrides.md reference from the <required_reading> block to an inline reference inside <verification_process> alongside the existing thinking-models-verification.md reference. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(tests): resolve conflict between thinking-model and verification-overrides tests thinking-model-guidance.test prohibited <required_reading> entirely, but verification-overrides.test requires gsd-verifier.md to have a <required_reading> block for verification-overrides.md between </role> and <project_context>. The tests were mutually exclusive. Fix: narrow the thinking-model assertion to check that the thinking-models reference is not *inside* a <required_reading> block (using regex extraction), rather than asserting no <required_reading> block exists at all. Restore the <required_reading> block in gsd-verifier.md. Both suites now pass (2345/2345). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
84
tests/bug-1818-unknown-flags.test.cjs
Normal file
84
tests/bug-1818-unknown-flags.test.cjs
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Regression test for bug #1818
|
||||
*
|
||||
* gsd-tools must reject unknown/invalid flags (--help, -h, etc.) with a
|
||||
* non-zero exit and an error message instead of silently ignoring them and
|
||||
* proceeding with the command — which can cause destructive operations to run
|
||||
* when an AI agent hallucinates a flag like --help.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
describe('unknown flag guard (bug #1818)', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
// ── --help flag ────────────────────────────────────────────────────────────
|
||||
|
||||
test('phases clear --help is rejected with non-zero exit', () => {
|
||||
const result = runGsdTools(['phases', 'clear', '--help'], tmpDir);
|
||||
assert.strictEqual(result.success, false, 'should fail, not run destructive clear');
|
||||
assert.match(result.error, /--help/);
|
||||
});
|
||||
|
||||
test('generate-slug hello --help is rejected', () => {
|
||||
// Non-destructive baseline: generate-slug hello succeeds without --help
|
||||
const ok = runGsdTools(['generate-slug', 'hello'], tmpDir);
|
||||
assert.strictEqual(ok.success, true, 'control: generate-slug without --help must succeed');
|
||||
|
||||
const result = runGsdTools(['generate-slug', 'hello', '--help'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
assert.match(result.error, /--help/);
|
||||
});
|
||||
|
||||
test('phase complete --help is rejected', () => {
|
||||
const result = runGsdTools(['phase', 'complete', '--help'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
assert.match(result.error, /--help/);
|
||||
});
|
||||
|
||||
test('state load --help is rejected', () => {
|
||||
const result = runGsdTools(['state', 'load', '--help'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
assert.match(result.error, /--help/);
|
||||
});
|
||||
|
||||
// ── -h shorthand ──────────────────────────────────────────────────────────
|
||||
|
||||
test('phases clear -h is rejected', () => {
|
||||
const result = runGsdTools(['phases', 'clear', '-h'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
assert.match(result.error, /-h/);
|
||||
});
|
||||
|
||||
test('generate-slug hello -h is rejected', () => {
|
||||
const result = runGsdTools(['generate-slug', 'hello', '-h'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
assert.match(result.error, /-h/);
|
||||
});
|
||||
|
||||
// ── other common hallucinated flags ───────────────────────────────────────
|
||||
|
||||
test('generate-slug hello --version is rejected', () => {
|
||||
const result = runGsdTools(['generate-slug', 'hello', '--version'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
assert.match(result.error, /--version/);
|
||||
});
|
||||
|
||||
test('current-timestamp --help is rejected', () => {
|
||||
const result = runGsdTools(['current-timestamp', '--help'], tmpDir);
|
||||
assert.strictEqual(result.success, false);
|
||||
assert.match(result.error, /--help/);
|
||||
});
|
||||
});
|
||||
@@ -219,11 +219,14 @@ describe('agent files use inline @-reference wiring at decision points', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('does NOT use <required_reading> block (inline wiring only)', () => {
|
||||
test('does NOT put thinking-models reference inside <required_reading> (inline wiring only)', () => {
|
||||
// Extract content from all <required_reading> blocks
|
||||
const reqReadingMatches = content.match(/<required_reading>([\s\S]*?)<\/required_reading>/g) || [];
|
||||
const reqReadingContent = reqReadingMatches.join('');
|
||||
assert.equal(
|
||||
content.includes('<required_reading>'),
|
||||
reqReadingContent.includes(wiring.refFile),
|
||||
false,
|
||||
`${agent}.md uses <required_reading> block — should use inline @-reference wiring instead`
|
||||
`${agent}.md puts ${wiring.refFile} inside a <required_reading> block — should use inline @-reference wiring instead`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user