From bcc7e537de086900ca05cf618d3c959537e2c8d4 Mon Sep 17 00:00:00 2001 From: "clagentic-builder[bot]" Date: Fri, 23 Oct 2026 10:42:26 -0400 Subject: [PATCH 1/5] fix(cli): reject unknown words and flags with a usage error (lr-cb8f8d) Co-Authored-By: Claude Sonnet 5.5 --- bin/cli.js | 8 ++ lib/cli/arg-validation.js | 50 ++++++++++++ test/cli-unknown-arg-lr-cb8f8d.test.js | 107 +++++++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 lib/cli/arg-validation.js create mode 100644 test/cli-unknown-arg-lr-cb8f8d.test.js diff --git a/bin/cli.js b/bin/cli.js index ac995045..34eec79a 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -114,6 +114,14 @@ var host = null; var multiUserMode = false; var osUsersMode = false; +// Reject unknown words/flags before any action runs (no IPC, no daemon fork). +var { findUnrecognisedArg, unrecognisedArgMessage } = require("../lib/cli/arg-validation"); +var _badArg = findUnrecognisedArg(args); +if (_badArg !== null) { + console.error(unrecognisedArgMessage(_badArg)); + process.exit(1); +} + for (var i = 0; i < args.length; i++) { if (args[i] === "-p" || args[i] === "--port") { port = parseInt(args[i + 1], 10); diff --git a/lib/cli/arg-validation.js b/lib/cli/arg-validation.js new file mode 100644 index 00000000..fb46f4b9 --- /dev/null +++ b/lib/cli/arg-validation.js @@ -0,0 +1,50 @@ +// lib/cli/arg-validation.js +// +// Rejects CLI arguments that bin/cli.js's flag loop does not recognise, so an +// unknown leading word or flag fails with a usage error instead of falling +// through to daemon control (--shutdown, --restart, ...) or a daemon start. +// Kept as a pure function so it is testable without spawning the CLI. + +// Flags that take no value. +var BOOLEAN_FLAGS = [ + "--no-https", "--local-cert", "--no-update", "--skip-update", "--dev", + "--watch", "-w", "--debug", "-y", "--yes", "--shutdown", "--restart", + "--no-restart", "--list", "--activity-diagnostics", "--process-build-status", + "--headless", "--multi-user", "--os-users", + // Removed flag: the flag loop prints its own dedicated message and exits 1. + "--dangerously-skip-permissions", +]; + +// Flags that consume the next argument as their value, whatever it looks like. +var VALUE_FLAGS = ["-p", "--port", "--host", "--bind", "--pin", "--add", "--remove"]; + +var HELP_FLAGS = ["-h", "--help"]; + +/** + * Returns the first argument that is neither a known flag nor a flag's + * consumed value, or null when every argument is recognised. Scanning stops at + * -h/--help, which prints usage and exits before any later token is acted on. + */ +function findUnrecognisedArg(args) { + for (var i = 0; i < args.length; i++) { + var arg = args[i]; + if (HELP_FLAGS.indexOf(arg) !== -1) return null; + if (VALUE_FLAGS.indexOf(arg) !== -1) { + i++; + continue; + } + if (BOOLEAN_FLAGS.indexOf(arg) !== -1) continue; + return arg; + } + return null; +} + +function unrecognisedArgMessage(arg) { + return "clagentic-console: unrecognised argument '" + arg + "'\n" + + "Run 'clagentic-console --help' for usage."; +} + +module.exports = { + findUnrecognisedArg: findUnrecognisedArg, + unrecognisedArgMessage: unrecognisedArgMessage, +}; diff --git a/test/cli-unknown-arg-lr-cb8f8d.test.js b/test/cli-unknown-arg-lr-cb8f8d.test.js new file mode 100644 index 00000000..837e5342 --- /dev/null +++ b/test/cli-unknown-arg-lr-cb8f8d.test.js @@ -0,0 +1,107 @@ +// An unknown CLI word or flag must fail with a usage error before any daemon +// action runs. Unit tests cover the validator; process tests spawn the real +// CLI with an isolated home and no daemon, so any IPC attempt would show up +// as a different exit code or output. + +var test = require("node:test"); +var assert = require("node:assert"); +var fs = require("fs"); +var os = require("os"); +var path = require("path"); +var childProcess = require("child_process"); + +var { findUnrecognisedArg } = require("../lib/cli/arg-validation"); + +var CLI = path.join(__dirname, "..", "bin", "cli.js"); + +function runCli(args) { + var home = fs.mkdtempSync(path.join(os.tmpdir(), "cb8f8d-home-")); + try { + return childProcess.spawnSync(process.execPath, [CLI].concat(args), { + encoding: "utf8", + timeout: 15000, + cwd: home, + env: Object.assign({}, process.env, { CLAGENTIC_CONSOLE_HOME: home }), + }); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } +} + +// --- validator --- + +test("findUnrecognisedArg: unknown leading word is reported", function () { + assert.strictEqual(findUnrecognisedArg(["bogus", "--shutdown"]), "bogus"); +}); + +test("findUnrecognisedArg: stray trailing word is reported", function () { + assert.strictEqual(findUnrecognisedArg(["--shutdown", "bogus"]), "bogus"); +}); + +test("findUnrecognisedArg: unknown flag is reported", function () { + assert.strictEqual(findUnrecognisedArg(["--bogus"]), "--bogus"); + assert.strictEqual(findUnrecognisedArg(["--port=2633"]), "--port=2633"); +}); + +test("findUnrecognisedArg: every documented flag is accepted", function () { + var forms = [ + [], ["--shutdown"], ["--restart"], ["--list"], ["--headless"], ["--multi-user"], + ["--os-users"], ["--dev"], ["--debug"], ["--watch"], ["-w"], ["-y"], ["--yes"], + ["--no-https"], ["--local-cert"], ["--no-update"], ["--skip-update"], + ["--no-restart"], ["--activity-diagnostics"], ["--process-build-status"], + ["-h"], ["--help"], ["--dangerously-skip-permissions"], + ["--pin", "123456", "--yes"], ["-p", "2700"], ["--port", "2700"], + ["--host", "127.0.0.1"], ["--bind", "127.0.0.1"], + ["--add", "."], ["--add", "/some/path"], ["--remove", "/some/path"], + ]; + forms.forEach(function (form) { + assert.strictEqual(findUnrecognisedArg(form), null, JSON.stringify(form)); + }); +}); + +test("findUnrecognisedArg: a flag's value is not a stray word", function () { + assert.strictEqual(findUnrecognisedArg(["--add", "bogus"]), null); + assert.strictEqual(findUnrecognisedArg(["--host", "bogus", "--shutdown"]), null); +}); + +test("findUnrecognisedArg: help wins over later tokens, as the flag loop does", function () { + assert.strictEqual(findUnrecognisedArg(["--help", "bogus"]), null); + assert.strictEqual(findUnrecognisedArg(["bogus", "--help"]), "bogus"); +}); + +// --- real CLI, isolated home, no daemon --- + +[ + ["bogus", "--shutdown"], + ["--shutdown", "bogus"], + ["bogus", "--restart", "x"], + ["bogus", "--add", "."], + ["--bogus"], +].forEach(function (args) { + test("cli exits 1 with usage error and no daemon action: " + args.join(" "), function () { + var res = runCli(args); + assert.strictEqual(res.status, 1); + assert.match(res.stderr, /unrecognised argument/); + assert.match(res.stderr, /clagentic-console --help/); + // An action that reached the daemon would print its own status or error. + assert.doesNotMatch(res.stdout + res.stderr, /daemon|No running|Shutting/i); + }); +}); + +test("cli: --help still prints usage and exits 0", function () { + var res = runCli(["--help"]); + assert.strictEqual(res.status, 0); + assert.match(res.stdout, /Usage: clagentic-console/); +}); + +test("cli: removed --dangerously-skip-permissions keeps its dedicated message", function () { + var res = runCli(["--dangerously-skip-permissions"]); + assert.strictEqual(res.status, 1); + assert.match(res.stderr, /has been removed/); +}); + +test("cli: release verb is still routed ahead of flag validation", function () { + var res = runCli(["release", "nope"]); + assert.strictEqual(res.status, 1); + assert.match(res.stderr, /Unknown release subcommand/); +}); From cfc4a2b0890d0620ef664dedf86f2841838f78ed Mon Sep 17 00:00:00 2001 From: "clagentic-builder[bot]" Date: Fri, 23 Oct 2026 11:02:44 -0400 Subject: [PATCH 2/5] test(cli): cover CREW_SOP section 6 caller shapes for the unknown-argument guard (lr-cb8f8d) Takes argv from the unit template, package.json, the updater re-exec builder, the verify-installed-build call and the --help text. Exports reExecArgs from lib/updater.js without changing behaviour. Co-Authored-By: Claude Sonnet 5.5 --- lib/updater.js | 10 +- test/cli-caller-shapes-lr-cb8f8d.test.js | 215 +++++++++++++++++++++++ 2 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 test/cli-caller-shapes-lr-cb8f8d.test.js diff --git a/lib/updater.js b/lib/updater.js index 47f05bd3..8c25e558 100644 --- a/lib/updater.js +++ b/lib/updater.js @@ -96,8 +96,14 @@ function performUpdate(version) { } } +// Argument vector handed to node on re-exec: the script path and the original +// arguments, plus --no-update. Split out so a test can read the real argv shape. +function reExecArgs(argv) { + return argv.slice(1).concat("--no-update"); +} + function reExec() { - var args = process.argv.slice(1).concat("--no-update"); + var args = reExecArgs(process.argv); var child = spawn(process.execPath, args, { stdio: "inherit" }); child.on("exit", function (code) { process.exit(code); @@ -144,4 +150,4 @@ async function checkAndUpdate(currentVersion, skipUpdate, channel) { return false; } -module.exports = { checkAndUpdate, fetchLatestVersion, fetchVersion, isNewer }; +module.exports = { checkAndUpdate, fetchLatestVersion, fetchVersion, isNewer, reExecArgs }; diff --git a/test/cli-caller-shapes-lr-cb8f8d.test.js b/test/cli-caller-shapes-lr-cb8f8d.test.js new file mode 100644 index 00000000..5530451c --- /dev/null +++ b/test/cli-caller-shapes-lr-cb8f8d.test.js @@ -0,0 +1,215 @@ +// CREW_SOP section 6 caller shapes for the unknown-argument guard in bin/cli.js. +// Each test takes its argv from the real caller source (unit template, +// package.json, the module that builds the argv, the CLI's own --help text) +// rather than a retyped copy, then asserts the guard accepts it or that routing +// still happens ahead of the guard. + +var test = require("node:test"); +var assert = require("node:assert"); +var fs = require("fs"); +var os = require("os"); +var path = require("path"); +var childProcess = require("child_process"); + +var { findUnrecognisedArg } = require("../lib/cli/arg-validation"); +var { reExecArgs } = require("../lib/updater"); +var { resolveProcessBuildStatus } = require("../scripts/verify-installed-build"); +var { SESSION_VERBS } = require("../lib/cli/session-subcommands"); + +var ROOT = path.join(__dirname, ".."); +var CLI = path.join(ROOT, "bin", "cli.js"); + +function tmpDir() { + return fs.mkdtempSync(path.join(os.tmpdir(), "cb8f8d-shape-")); +} + +function runCli(args, opts) { + var home = tmpDir(); + try { + return childProcess.spawnSync(process.execPath, [(opts && opts.script) || CLI].concat(args), { + encoding: "utf8", + timeout: 15000, + cwd: home, + env: Object.assign({}, process.env, { CLAGENTIC_CONSOLE_HOME: home }), + }); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } +} + +// Arguments that follow the CLI script in a shell command string. +function argsAfterCli(command) { + var tokens = command.trim().split(/\s+/); + var at = -1; + tokens.forEach(function (t, i) { if (/(^|\/)cli\.js$/.test(t)) at = i; }); + return at === -1 ? null : tokens.slice(at + 1); +} + +// --- shape 1: systemd unit ExecStart --- + +test("caller shape 1: unit ExecStart argv is `daemon`, routed ahead of the guard", function () { + var unit = fs.readFileSync(path.join(ROOT, "deploy", "clagentic-console.service"), "utf8"); + var line = unit.split("\n").filter(function (l) { return /^ExecStart=/.test(l); })[0]; + assert.ok(line, "unit has an ExecStart line"); + var tokens = line.replace(/^ExecStart=/, "").trim().split(/\s+/); + var args = tokens.slice(1); + assert.deepStrictEqual(args, ["daemon"]); + + // `daemon` is not a guard word; it must be handled before the guard runs. + assert.notStrictEqual(findUnrecognisedArg(args), null); + var src = fs.readFileSync(CLI, "utf8"); + var daemonRoute = src.indexOf('args[0] === "daemon"'); + var guardCall = src.indexOf("findUnrecognisedArg(args)"); + assert.ok(daemonRoute !== -1 && guardCall !== -1); + assert.ok(daemonRoute < guardCall, "daemon routing precedes the guard"); +}); + +// --- shape 2: in-app updater re-exec --- + +test("caller shape 2: updater re-exec argv is accepted for every documented form", function () { + var forms = helpForms(); + forms.forEach(function (form) { + var execArgs = reExecArgs([process.execPath, CLI].concat(form)); + assert.strictEqual(execArgs[0], CLI); + assert.strictEqual(execArgs[execArgs.length - 1], "--no-update"); + var cliArgs = execArgs.slice(1); + assert.strictEqual(findUnrecognisedArg(cliArgs), null, JSON.stringify(cliArgs)); + }); +}); + +// --- shape 3: scripts/verify-installed-build.js --- + +test("caller shape 3: verify-installed-build argv (--process-build-status) is accepted", function () { + var dir = tmpDir(); + try { + var capture = path.join(dir, "argv.json"); + var fakeBin = path.join(dir, "fake-clagentic-console"); + fs.writeFileSync( + fakeBin, + "#!" + process.execPath + "\n" + + "require('fs').writeFileSync(" + JSON.stringify(capture) + ", JSON.stringify(process.argv.slice(2)));\n" + + "console.log(JSON.stringify({ loadedBuildSha: 'x' }));\n", + { mode: 0o755 } + ); + resolveProcessBuildStatus(fakeBin); + var captured = JSON.parse(fs.readFileSync(capture, "utf8")); + assert.deepStrictEqual(captured, ["--process-build-status"]); + assert.strictEqual(findUnrecognisedArg(captured), null); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +// --- shape 4: package.json scripts and the dev entry --- + +test("caller shape 4: every package.json script that runs the CLI passes the guard", function () { + var pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")); + var seen = 0; + Object.keys(pkg.scripts).forEach(function (name) { + var args = argsAfterCli(pkg.scripts[name]); + if (args === null) return; + seen++; + assert.strictEqual(findUnrecognisedArg(args), null, name + ": " + JSON.stringify(args)); + }); + assert.ok(seen >= 1, "found at least the dev script"); + assert.deepStrictEqual(argsAfterCli(pkg.scripts.dev), ["--dev"]); +}); + +test("caller shape 4: clagentic-console-dev by name runs the CLI and the guard still applies", function () { + var dir = tmpDir(); + try { + var devBin = path.join(dir, "clagentic-console-dev"); + fs.symlinkSync(CLI, devBin); + var help = runCli(["--help"], { script: devBin }); + assert.strictEqual(help.status, 0); + assert.match(help.stdout, /Usage: clagentic-console/); + var bad = runCli(["bogus", "--shutdown"], { script: devBin }); + assert.strictEqual(bad.status, 1); + assert.match(bad.stderr, /unrecognised argument/); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + +// --- shape 5: operator starts documented in --help --- + +// Flags the --help text lists for the top-level (non-verb) command: the usage +// line, the `clagentic-console --x` lines and the Options column. +function helpFlags() { + var res = runCli(["--help"]); + assert.strictEqual(res.status, 0); + var flags = []; + var lines = res.stdout.split("\n"); + var inOptions = false; + lines.forEach(function (line) { + var text = null; + if (/^Usage: clagentic-console/.test(line)) { + text = line.replace(/^Usage: clagentic-console/, ""); + } else if (/^\s+clagentic-console --/.test(line)) { + text = line.replace(/^\s+clagentic-console\s+/, "").split(/\s{2,}/)[0]; + } else if (/^Options:/.test(line)) { + inOptions = true; + return; + } else if (inOptions && /^\s+-/.test(line)) { + text = line.trim().split(/\s{2,}/)[0]; + } + if (text === null) return; + var re = /(?:^|[\s[|,])(-{1,2}[A-Za-z][A-Za-z-]*)/g; + var m; + while ((m = re.exec(text)) !== null) flags.push(m[1]); + }); + return flags.filter(function (f, i) { return flags.indexOf(f) === i; }); +} + +// One argv per flag the help text lists. +function helpForms() { + return helpFlags().map(function (f) { return [f]; }); +} + +test("caller shape 5: every flag the --help text lists is accepted by the guard", function () { + var flags = helpFlags(); + // Guards against the parser silently matching nothing. + ["-p", "--port", "--host", "--no-https", "--pin", "--shutdown", "--restart", "--add", + "--remove", "--list", "--headless", "--multi-user", "--os-users", "--yes", "-y", + "--activity-diagnostics", "--process-build-status"].forEach(function (f) { + assert.ok(flags.indexOf(f) !== -1, "help lists " + f); + }); + flags.forEach(function (f) { + assert.strictEqual(findUnrecognisedArg([f]), null, f); + }); +}); + +test("caller shape 5: bare start and combined documented starts are accepted", function () { + [ + [], + ["--headless"], + ["-y"], + ["--multi-user", "--os-users", "--yes"], + ["--pin", "123456", "--yes"], + ["-p", "2700", "--host", "127.0.0.1"], + ["--add", "."], + ["--remove", "."], + ].forEach(function (form) { + assert.strictEqual(findUnrecognisedArg(form), null, JSON.stringify(form)); + }); +}); + +// --- shape 6: crew lanes and other agents --- + +test("caller shape 6: session verbs and schedule-input are routed ahead of the guard", function () { + var src = fs.readFileSync(CLI, "utf8"); + var guardCall = src.indexOf("findUnrecognisedArg(args)"); + assert.ok(src.indexOf("SESSION_VERBS.indexOf(args[0])") < guardCall); + assert.ok(src.indexOf('args[0] === "schedule-input"') < guardCall); + + // Behavioural check with no daemon: the verb's own error, never the guard's. + SESSION_VERBS.concat(["schedule-input"]).forEach(function (verb) { + var res = runCli([verb, "--project", "p", "--prompt", "x", "--only-this-verb-knows"]); + assert.doesNotMatch(res.stderr, /unrecognised argument/, verb); + }); +}); + +test("caller shape 6: agent flags --process-build-status and --activity-diagnostics pass the guard", function () { + assert.strictEqual(findUnrecognisedArg(["--process-build-status"]), null); + assert.strictEqual(findUnrecognisedArg(["--activity-diagnostics"]), null); +}); From 4618dd982d1d41f1e614dd0c3ecc35a5e0db2a18 Mon Sep 17 00:00:00 2001 From: "clagentic-builder[bot]" Date: Fri, 23 Oct 2026 11:11:36 -0400 Subject: [PATCH 3/5] test(cli): prove the systemd daemon argv by behaviour via the link-trap exit (lr-cb8f8d) Co-Authored-By: Claude Sonnet 5.5 --- test/cli-caller-shapes-lr-cb8f8d.test.js | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/cli-caller-shapes-lr-cb8f8d.test.js b/test/cli-caller-shapes-lr-cb8f8d.test.js index 5530451c..d2db4ebe 100644 --- a/test/cli-caller-shapes-lr-cb8f8d.test.js +++ b/test/cli-caller-shapes-lr-cb8f8d.test.js @@ -64,6 +64,36 @@ test("caller shape 1: unit ExecStart argv is `daemon`, routed ahead of the guard assert.ok(daemonRoute < guardCall, "daemon routing precedes the guard"); }); +// Runs the captured ExecStart argv through the real CLI without starting a daemon: +// the CLI is invoked through /bin/ with an npm-link symlink at +// /lib/node_modules/clagentic-console, so the daemon branch refuses with +// its own exit 78 before it would require lib/daemon. +test("caller shape 1: ExecStart argv reaches the daemon branch, not the guard (link-trap refusal, no daemon)", function () { + var unit = fs.readFileSync(path.join(ROOT, "deploy", "clagentic-console.service"), "utf8"); + var line = unit.split("\n").filter(function (l) { return /^ExecStart=/.test(l); })[0]; + var captured = line.replace(/^ExecStart=/, "").trim().split(/\s+/).slice(1); + assert.deepStrictEqual(captured, ["daemon"]); + + var dir = tmpDir(); + try { + var binDir = path.join(dir, "bin"); + var nodeModules = path.join(dir, "lib", "node_modules"); + fs.mkdirSync(binDir, { recursive: true }); + fs.mkdirSync(nodeModules, { recursive: true }); + fs.symlinkSync(CLI, path.join(binDir, "cli.js")); + fs.symlinkSync(ROOT, path.join(nodeModules, "clagentic-console")); + + [captured, captured.concat(["--bogus-cb8f8d"])].forEach(function (argv) { + var res = runCli(argv, { script: path.join(binDir, "cli.js") }); + assert.strictEqual(res.status, 78, JSON.stringify(argv) + ": " + res.stderr); + assert.match(res.stderr, /npm link trap/, JSON.stringify(argv)); + assert.doesNotMatch(res.stderr, /unrecognised argument/, JSON.stringify(argv)); + }); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + // --- shape 2: in-app updater re-exec --- test("caller shape 2: updater re-exec argv is accepted for every documented form", function () { From 0b482e75d13726cce518c3676fb18b516c785572 Mon Sep 17 00:00:00 2001 From: "clagentic-builder[bot]" Date: Fri, 23 Oct 2026 11:20:44 -0400 Subject: [PATCH 4/5] ci: re-run checks (flake lr-7da984, not this PR; lr-cb8f8d) Co-Authored-By: Claude Sonnet 5.5 From eded0211911f56d71bb26a73ace78186e64f173c Mon Sep 17 00:00:00 2001 From: "clagentic-builder[bot]" Date: Fri, 23 Oct 2026 11:30:15 -0400 Subject: [PATCH 5/5] fix(cli): refuse extra tokens after daemon and release list-betas (lr-cb8f8d) Co-Authored-By: Claude Sonnet 5.5 --- bin/cli.js | 11 ++++++++++- test/cli-caller-shapes-lr-cb8f8d.test.js | 17 +++++++++++------ test/cli-unknown-arg-lr-cb8f8d.test.js | 22 ++++++++++++++++++++++ 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index 34eec79a..c92b74b3 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -43,6 +43,12 @@ var args = process.argv.slice(2); // start exits 78 (EX_CONFIG), which the unit lists in RestartPreventExitStatus // so a mis-installed package is not restart-looped. if (args[0] === "daemon") { + // The documented form takes no further tokens. 78, not 1: the unit lists 78 in + // RestartPreventExitStatus, so a mis-edited ExecStart is refused once, not looped. + if (args.length > 1) { + console.error(require("../lib/cli/arg-validation").unrecognisedArgMessage(args[1])); + process.exit(78); + } var { findLinkTrap, linkTrapMessage, LinkTrapProbeError } = require("../lib/link-trap"); var linkTrap = null; try { @@ -65,7 +71,10 @@ if (args[0] === "daemon") { // Positional subcommand, handled before flag parsing: `clagentic-console release list-betas`. if (args[0] === "release") { var releaseSub = args[1]; - if (releaseSub === "list-betas") { + if (releaseSub === "list-betas" && args.length > 2) { + console.error(require("../lib/cli/arg-validation").unrecognisedArgMessage(args[2])); + process.exit(1); + } else if (releaseSub === "list-betas") { var releaseList = require("../lib/release-list"); var betasResult = releaseList.readBetasFile(process.cwd()); var betasLines = releaseList.formatBetasOutput(betasResult); diff --git a/test/cli-caller-shapes-lr-cb8f8d.test.js b/test/cli-caller-shapes-lr-cb8f8d.test.js index d2db4ebe..2ba31a42 100644 --- a/test/cli-caller-shapes-lr-cb8f8d.test.js +++ b/test/cli-caller-shapes-lr-cb8f8d.test.js @@ -83,12 +83,17 @@ test("caller shape 1: ExecStart argv reaches the daemon branch, not the guard (l fs.symlinkSync(CLI, path.join(binDir, "cli.js")); fs.symlinkSync(ROOT, path.join(nodeModules, "clagentic-console")); - [captured, captured.concat(["--bogus-cb8f8d"])].forEach(function (argv) { - var res = runCli(argv, { script: path.join(binDir, "cli.js") }); - assert.strictEqual(res.status, 78, JSON.stringify(argv) + ": " + res.stderr); - assert.match(res.stderr, /npm link trap/, JSON.stringify(argv)); - assert.doesNotMatch(res.stderr, /unrecognised argument/, JSON.stringify(argv)); - }); + // The documented form alone reaches the daemon branch (link-trap probe). + var ok = runCli(captured, { script: path.join(binDir, "cli.js") }); + assert.strictEqual(ok.status, 78, ok.stderr); + assert.match(ok.stderr, /npm link trap/); + assert.doesNotMatch(ok.stderr, /unrecognised argument/); + + // Any extra token is refused with exit 78 before the probe or the daemon. + var extra = runCli(captured.concat(["--bogus-cb8f8d"]), { script: path.join(binDir, "cli.js") }); + assert.strictEqual(extra.status, 78, extra.stderr); + assert.match(extra.stderr, /unrecognised argument '--bogus-cb8f8d'/); + assert.doesNotMatch(extra.stderr, /npm link trap/); } finally { fs.rmSync(dir, { recursive: true, force: true }); } diff --git a/test/cli-unknown-arg-lr-cb8f8d.test.js b/test/cli-unknown-arg-lr-cb8f8d.test.js index 837e5342..4a4a0f2b 100644 --- a/test/cli-unknown-arg-lr-cb8f8d.test.js +++ b/test/cli-unknown-arg-lr-cb8f8d.test.js @@ -100,6 +100,28 @@ test("cli: removed --dangerously-skip-permissions keeps its dedicated message", assert.match(res.stderr, /has been removed/); }); +test("cli: release list-betas with a stray token is a usage error", function () { + var res = runCli(["release", "list-betas", "extra"]); + assert.strictEqual(res.status, 1); + assert.match(res.stderr, /unrecognised argument 'extra'/); + assert.match(res.stderr, /clagentic-console --help/); +}); + +// Session verbs and schedule-input have their own parsers; a stray positional +// token is rejected there (exit 1) before any daemon call. +[ + ["list-sessions", "stray"], + ["open-session", "stray", "--project", "p", "--prompt", "x"], + ["send-input", "--project", "p", "--session", "s", "--prompt", "x", "stray"], + ["schedule-input", "list", "stray"], +].forEach(function (args) { + test("cli: stray positional token is rejected by the verb parser: " + args.join(" "), function () { + var res = runCli(args); + assert.strictEqual(res.status, 1); + assert.match(res.stderr, /Unknown option for .*: stray/); + }); +}); + test("cli: release verb is still routed ahead of flag validation", function () { var res = runCli(["release", "nope"]); assert.strictEqual(res.status, 1);