From 255bb9784df3df05b74313f5c33c79e70971fa63 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Sat, 8 Aug 2026 19:58:46 +1000 Subject: [PATCH 1/2] test: cover malformed CLI arguments --- tests/cli.test.js | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/cli.test.js b/tests/cli.test.js index e3739bd..db0fb21 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { mkdtemp, readFile } from "node:fs/promises"; +import { access, mkdtemp, readFile } from "node:fs/promises"; import { readFileSync } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -38,3 +38,41 @@ test("CLI help exits cleanly with usage text", async () => { assert.match(output, /Usage:/); assert.match(output, /ledgerpet inspect/); }); + +test("CLI accepts json and markdown report formats", async () => { + for (const [format, reportFile] of [["json", "report.json"], ["markdown", "report.md"]]) { + const output = await mkdtemp(join(tmpdir(), `ledgerpet-${format}-`)); + const result = spawnSync(process.execPath, ["src/cli.js", "inspect", "fixtures/sample", "--output", output, "--format", format], { encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + await access(join(output, reportFile)); + } +}); + +test("CLI rejects options with missing values without writing reports", async () => { + for (const option of ["--scenario", "--output", "--format"]) { + const output = join(await mkdtemp(join(tmpdir(), "ledgerpet-missing-")), "reports"); + const args = ["src/cli.js", "inspect", "fixtures/sample", "--output", output, option]; + if (option === "--output") args.splice(4, 2); + const result = spawnSync(process.execPath, args, { encoding: "utf8" }); + assert.notEqual(result.status, 0, option); + assert.match(result.stderr, new RegExp(`Missing value for ${option}`)); + await assert.rejects(access(output)); + } +}); + +test("CLI rejects unsupported report formats without writing reports", async () => { + const output = join(await mkdtemp(join(tmpdir(), "ledgerpet-format-")), "reports"); + const result = spawnSync(process.execPath, ["src/cli.js", "inspect", "fixtures/sample", "--output", output, "--format", "yaml"], { encoding: "utf8" }); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Unsupported format: yaml\. Expected json or markdown/); + await assert.rejects(access(output)); +}); + +test("CLI fixed-form commands reject unexpected arguments", () => { + for (const command of ["scenarios", "--version", "--help"]) { + const result = spawnSync(process.execPath, ["src/cli.js", command, "unexpected"], { encoding: "utf8" }); + assert.notEqual(result.status, 0, command); + assert.match(result.stderr, /Unexpected argument/); + assert.equal(result.stdout, ""); + } +}); From 6ea8cc1e27cf6df23c7550b86f39f998478d8a36 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Sat, 8 Aug 2026 19:58:46 +1000 Subject: [PATCH 2/2] fix: validate CLI arguments before inspection --- src/cli.js | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/cli.js b/src/cli.js index 2d380a9..23ea052 100755 --- a/src/cli.js +++ b/src/cli.js @@ -9,15 +9,22 @@ const HELP = `ledgerpet — local-first synthetic finance anomaly trainer\n\nUsa export async function main(argv = process.argv.slice(2)) { const [command, maybeFixture, ...rest] = argv; - if (!command || command === "--help" || command === "-h") { + if (!command) { + console.log(HELP); + return 0; + } + if (command === "--help" || command === "-h") { + rejectUnexpectedArguments(argv.slice(1), command); console.log(HELP); return 0; } if (command === "--version" || command === "-v") { + rejectUnexpectedArguments(argv.slice(1), command); console.log(version); return 0; } if (command === "scenarios") { + rejectUnexpectedArguments(argv.slice(1), command); console.log(listScenarios().join("\n")); return 0; } @@ -40,14 +47,32 @@ function parseOptions(args) { for (let i = 0; i < args.length; i += 1) { const arg = args[i]; const value = args[i + 1]; - if (arg === "--scenario") { options.scenario = value; i += 1; continue; } - if (arg === "--output") { options.outputDir = value; i += 1; continue; } - if (arg === "--format") { options.format = value; i += 1; continue; } + if (["--scenario", "--output", "--format"].includes(arg)) { + if (value === undefined || value.startsWith("--")) { + throw new Error(`Missing value for ${arg}`); + } + if (arg === "--scenario") options.scenario = value; + if (arg === "--output") options.outputDir = value; + if (arg === "--format") { + if (!["json", "markdown"].includes(value)) { + throw new Error(`Unsupported format: ${value}. Expected json or markdown`); + } + options.format = value; + } + i += 1; + continue; + } throw new Error(`Unknown option: ${arg}`); } return options; } +function rejectUnexpectedArguments(args, command) { + if (args.length > 0) { + throw new Error(`Unexpected argument for ${command}: ${args[0]}`); + } +} + if (import.meta.url === `file://${process.argv[1]}`) { main().then((code) => { process.exitCode = code; }).catch((error) => { console.error(error.message);