diff --git a/scripts/docker-e2e.mjs b/scripts/docker-e2e.mjs index 9e3b8d18f520..2ce290c2a5c1 100644 --- a/scripts/docker-e2e.mjs +++ b/scripts/docker-e2e.mjs @@ -108,21 +108,35 @@ function failedRerunCommands(summary) { .map((lane) => lane.rerunCommand); } -const [command, file, ...args] = process.argv.slice(2); -if (!command || !file) { - throw new Error(usage()); -} - -if (command === "github-outputs") { - process.stdout.write(`${githubOutputs(readJson(file)).join("\n")}\n`); -} else if (command === "summary") { - const title = args.join(" ").trim(); - if (!title) { +function main(argv = process.argv.slice(2)) { + const [command, file, ...args] = argv; + if (command === "--help" || command === "-h") { + process.stdout.write(`${usage()}\n`); + return 0; + } + if (!command || !file) { throw new Error(usage()); } - process.stdout.write(`${summaryMarkdown(readJson(file), title)}\n`); -} else if (command === "failed-reruns") { - process.stdout.write(`${failedRerunCommands(readJson(file)).join("\n")}\n`); -} else { - throw new Error(`unknown command: ${command}\n${usage()}`); + + if (command === "github-outputs") { + process.stdout.write(`${githubOutputs(readJson(file)).join("\n")}\n`); + } else if (command === "summary") { + const title = args.join(" ").trim(); + if (!title) { + throw new Error(usage()); + } + process.stdout.write(`${summaryMarkdown(readJson(file), title)}\n`); + } else if (command === "failed-reruns") { + process.stdout.write(`${failedRerunCommands(readJson(file)).join("\n")}\n`); + } else { + throw new Error(`unknown command: ${command}\n${usage()}`); + } + return 0; +} + +try { + process.exitCode = main(); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; } diff --git a/scripts/kova-ci-summary.mjs b/scripts/kova-ci-summary.mjs index 317713128b2e..9ee62191b9e9 100644 --- a/scripts/kova-ci-summary.mjs +++ b/scripts/kova-ci-summary.mjs @@ -2,7 +2,12 @@ import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; -const args = parseArgs(process.argv.slice(2)); +const rawArgs = process.argv.slice(2); +if (rawArgs.includes("--help") || rawArgs.includes("-h")) { + usage("", 0); +} + +const args = parseArgs(rawArgs); if (!args.report) { usage("missing --report"); } @@ -205,12 +210,16 @@ function parseArgs(argv) { }; } -function usage(message) { +function usage(message, status = 2) { + const text = + "usage: node scripts/kova-ci-summary.mjs --report [--output ] [--lane ]\n"; if (message) { console.error(`error: ${message}`); } - console.error( - "usage: node scripts/kova-ci-summary.mjs --report [--output ] [--lane ]", - ); - process.exit(2); + if (status === 0 && !message) { + process.stdout.write(text); + } else { + process.stderr.write(text); + } + process.exit(status); } diff --git a/scripts/test-live-shard.mjs b/scripts/test-live-shard.mjs index 6297b1f31977..98274b9df97c 100644 --- a/scripts/test-live-shard.mjs +++ b/scripts/test-live-shard.mjs @@ -283,8 +283,10 @@ export function selectLiveShardFiles(shard, files = collectAllLiveTestFiles()) { } } -function usage() { - console.error(`Usage: node scripts/test-live-shard.mjs <${LIVE_TEST_SHARDS.join("|")}> [--list]`); +function usage(stream = process.stderr) { + stream.write( + `Usage: node scripts/test-live-shard.mjs <${LIVE_TEST_SHARDS.join("|")}> [--list]\n`, + ); } export function parseLiveShardArgs(args) { @@ -310,9 +312,17 @@ export function parseLiveShardArgs(args) { } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const rawArgs = process.argv.slice(2); + const separatorIndex = rawArgs.indexOf("--"); + const optionArgs = separatorIndex >= 0 ? rawArgs.slice(0, separatorIndex) : rawArgs; + if (optionArgs.includes("--help") || optionArgs.includes("-h")) { + usage(process.stdout); + process.exit(0); + } + let parsedArgs; try { - parsedArgs = parseLiveShardArgs(process.argv.slice(2)); + parsedArgs = parseLiveShardArgs(rawArgs); } catch (error) { console.error(error instanceof Error ? error.message : String(error)); usage(); diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index a60ca4aaf138..39dc05c4909a 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -387,8 +387,10 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ ["scripts/run-oxlint.mjs", ["test/scripts/run-oxlint.test.ts"]], ["scripts/run-node.mjs", ["src/infra/run-node.test.ts"]], ["scripts/ci-run-timings.mjs", ["test/scripts/ci-run-timings.test.ts"]], + ["scripts/docker-e2e.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]], ["scripts/docker-e2e-rerun.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]], ["scripts/docker-e2e-timings.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]], + ["scripts/kova-ci-summary.mjs", ["test/scripts/kova-ci-summary.test.ts"]], ["scripts/test-extension-batch.mjs", ["test/scripts/test-extension.test.ts"]], ["scripts/lib/extension-test-plan.mjs", ["test/scripts/test-extension.test.ts"]], ["scripts/lib/vitest-batch-runner.mjs", ["test/scripts/test-extension.test.ts"]], @@ -428,6 +430,7 @@ const TOOLING_TEST_TARGETS = new Map([ ["test/scripts/ci-docker-pull-retry.test.ts", ["test/scripts/ci-docker-pull-retry.test.ts"]], ["test/scripts/docker-build-helper.test.ts", ["test/scripts/docker-build-helper.test.ts"]], ["test/scripts/docker-e2e-helper-cli.test.ts", ["test/scripts/docker-e2e-helper-cli.test.ts"]], + ["test/scripts/kova-ci-summary.test.ts", ["test/scripts/kova-ci-summary.test.ts"]], ["test/scripts/live-docker-stage.test.ts", ["test/scripts/live-docker-stage.test.ts"]], ["test/scripts/openclaw-test-state.test.ts", ["test/scripts/openclaw-test-state.test.ts"]], [ diff --git a/test/scripts/docker-e2e-helper-cli.test.ts b/test/scripts/docker-e2e-helper-cli.test.ts index cc67d854b221..0c44cfdc0a58 100644 --- a/test/scripts/docker-e2e-helper-cli.test.ts +++ b/test/scripts/docker-e2e-helper-cli.test.ts @@ -14,6 +14,24 @@ function runHelper(script: string, ...args: string[]) { } describe("Docker E2E helper CLIs", () => { + it("prints scheduler helper help without throwing a stack trace", () => { + const result = runHelper("scripts/docker-e2e.mjs", "--help"); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain("node scripts/docker-e2e.mjs github-outputs "); + }); + + it("prints scheduler helper usage errors without a Node stack trace", () => { + const result = runHelper("scripts/docker-e2e.mjs"); + + expect(result.status).toBe(1); + expect(result.stdout).toBe(""); + expect(result.stderr).toContain("node scripts/docker-e2e.mjs github-outputs "); + expect(result.stderr).not.toContain("Error:"); + expect(result.stderr).not.toContain("at file:"); + }); + it("prints timings help without treating --help as an artifact path", () => { const result = runHelper("scripts/docker-e2e-timings.mjs", "--help"); diff --git a/test/scripts/kova-ci-summary.test.ts b/test/scripts/kova-ci-summary.test.ts new file mode 100644 index 000000000000..b0ab248806f4 --- /dev/null +++ b/test/scripts/kova-ci-summary.test.ts @@ -0,0 +1,15 @@ +import { spawnSync } from "node:child_process"; +import { describe, expect, it } from "vitest"; + +describe("scripts/kova-ci-summary", () => { + it("prints help without treating --help as a valued option", () => { + const result = spawnSync(process.execPath, ["scripts/kova-ci-summary.mjs", "--help"], { + cwd: process.cwd(), + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain("usage: node scripts/kova-ci-summary.mjs --report"); + }); +}); diff --git a/test/scripts/test-live-shard.test.ts b/test/scripts/test-live-shard.test.ts index b92f19373430..4f77ed4afbfc 100644 --- a/test/scripts/test-live-shard.test.ts +++ b/test/scripts/test-live-shard.test.ts @@ -1,3 +1,4 @@ +import { spawnSync } from "node:child_process"; import fs, { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { @@ -134,6 +135,17 @@ describe("scripts/test-live-shard", () => { ); }); + it("prints CLI help before validating shard options", () => { + const result = spawnSync(process.execPath, ["scripts/test-live-shard.mjs", "--help"], { + cwd: process.cwd(), + encoding: "utf8", + }); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain("Usage: node scripts/test-live-shard.mjs"); + }); + it("preserves Vitest passthrough args after the live shard separator", () => { expect(parseLiveShardArgs(["native-live-test", "--", "-t", "smoke"])).toEqual({ shard: "native-live-test",