diff --git a/scripts/qa-e2e.ts b/scripts/qa-e2e.ts index 17582e1bb351..97f3a2bd7645 100644 --- a/scripts/qa-e2e.ts +++ b/scripts/qa-e2e.ts @@ -12,6 +12,11 @@ type QaE2eDeps = { writeStdout?: (text: string) => void; }; +type QaE2eArgs = { + help: boolean; + outputPath: string; +}; + async function loadQaE2eRuntime(): Promise { return await import("../extensions/qa-lab/api.js"); } @@ -23,18 +28,80 @@ export function enablePrivateQaScriptEnv(env: NodeJS.ProcessEnv = process.env) { } export function resolveQaE2eOutputPath(argv: readonly string[] = process.argv.slice(2)) { - return argv[0]?.trim() || ".artifacts/qa-e2e/self-check.md"; + return parseQaE2eArgs(argv).outputPath; +} + +export function usage(): string { + return `Usage: pnpm qa:e2e [--output ] + +Options: + --output Markdown report output path + -h, --help Display help +`; +} + +export function parseQaE2eArgs(argv: readonly string[]): QaE2eArgs { + const args = argv[0] === "--" ? argv.slice(1) : argv; + let outputPath = ""; + let positionalMode = false; + for (let index = 0; index < args.length; index += 1) { + const arg = args[index] ?? ""; + if (positionalMode) { + if (!outputPath && arg.trim()) { + outputPath = arg.trim(); + continue; + } + throw new Error(`Unexpected qa:e2e argument: ${arg}`); + } + if (arg === "--") { + positionalMode = true; + continue; + } + if (arg === "--help" || arg === "-h") { + return { help: true, outputPath: ".artifacts/qa-e2e/self-check.md" }; + } + const inlineOutput = arg.startsWith("--output=") ? arg.slice("--output=".length).trim() : null; + if (inlineOutput !== null) { + if (!inlineOutput) { + throw new Error("--output requires a value"); + } + outputPath = inlineOutput; + continue; + } + if (arg === "--output") { + const value = args[index + 1]?.trim(); + if (!value || value.startsWith("-")) { + throw new Error("--output requires a value"); + } + outputPath = value; + index += 1; + continue; + } + if (arg.startsWith("-")) { + throw new Error(`Unknown qa:e2e option: ${arg}`); + } + if (outputPath) { + throw new Error(`Unexpected qa:e2e argument: ${arg}`); + } + outputPath = arg.trim(); + } + return { help: false, outputPath: outputPath || ".artifacts/qa-e2e/self-check.md" }; } export async function main( argv: readonly string[] = process.argv.slice(2), deps: QaE2eDeps = {}, ): Promise { + const args = parseQaE2eArgs(argv); + if (args.help) { + (deps.writeStdout ?? ((text: string) => process.stdout.write(text)))(usage()); + return 0; + } enablePrivateQaScriptEnv(deps.env ?? process.env); const { isQaSelfCheckSuccessful, runQaE2eSelfCheck } = await ( deps.loadRuntime ?? loadQaE2eRuntime )(); - const result = await runQaE2eSelfCheck({ outputPath: resolveQaE2eOutputPath(argv) }); + const result = await runQaE2eSelfCheck({ outputPath: args.outputPath }); (deps.writeStdout ?? ((text: string) => process.stdout.write(text)))( `QA self-check report: ${result.outputPath}\n`, ); @@ -47,5 +114,10 @@ function isMainModule() { } if (isMainModule()) { - process.exitCode = await main(); + try { + process.exitCode = await main(); + } catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exitCode = 1; + } } diff --git a/test/scripts/qa-e2e.test.ts b/test/scripts/qa-e2e.test.ts index 9020944f77c7..a44717a1547e 100644 --- a/test/scripts/qa-e2e.test.ts +++ b/test/scripts/qa-e2e.test.ts @@ -1,7 +1,12 @@ // Qa E2E tests cover qa e2e script behavior. import { describe, expect, it, vi } from "vitest"; import type { QaSelfCheckResult } from "../../extensions/qa-lab/api.js"; -import { enablePrivateQaScriptEnv, main, resolveQaE2eOutputPath } from "../../scripts/qa-e2e.js"; +import { + enablePrivateQaScriptEnv, + main, + parseQaE2eArgs, + resolveQaE2eOutputPath, +} from "../../scripts/qa-e2e.js"; function makeSelfCheckResult(status: "pass" | "fail"): QaSelfCheckResult { return { @@ -44,6 +49,50 @@ describe("qa-e2e script", () => { it("resolves the default self-check report path", () => { expect(resolveQaE2eOutputPath([])).toBe(".artifacts/qa-e2e/self-check.md"); expect(resolveQaE2eOutputPath([".artifacts/custom.md"])).toBe(".artifacts/custom.md"); + expect(resolveQaE2eOutputPath(["--output", ".artifacts/custom.md"])).toBe( + ".artifacts/custom.md", + ); + expect(resolveQaE2eOutputPath(["--", ".artifacts/custom.md"])).toBe(".artifacts/custom.md"); + }); + + it("prints help before enabling private QA or loading QA Lab", async () => { + const env: NodeJS.ProcessEnv = {}; + const loadRuntime = vi.fn(async () => { + throw new Error("runtime loaded"); + }); + const writeStdout = vi.fn(); + + await expect(main(["--help"], { env, loadRuntime, writeStdout })).resolves.toBe(0); + + expect(loadRuntime).not.toHaveBeenCalled(); + expect(writeStdout).toHaveBeenCalledWith(expect.stringContaining("Usage: pnpm qa:e2e")); + expect(env.OPENCLAW_BUILD_PRIVATE_QA).toBeUndefined(); + }); + + it("rejects unknown options before enabling private QA or loading QA Lab", async () => { + const env: NodeJS.ProcessEnv = {}; + const loadRuntime = vi.fn(async () => { + throw new Error("runtime loaded"); + }); + + await expect(main(["--wat"], { env, loadRuntime })).rejects.toThrow( + "Unknown qa:e2e option: --wat", + ); + + expect(loadRuntime).not.toHaveBeenCalled(); + expect(env.OPENCLAW_BUILD_PRIVATE_QA).toBeUndefined(); + }); + + it("parses explicit output flags and package-manager separators", () => { + expect(parseQaE2eArgs(["--output=.artifacts/custom.md"])).toEqual({ + help: false, + outputPath: ".artifacts/custom.md", + }); + expect(parseQaE2eArgs(["--", ".artifacts/from-separator.md"])).toEqual({ + help: false, + outputPath: ".artifacts/from-separator.md", + }); + expect(() => parseQaE2eArgs(["--output", "--help"])).toThrow("--output requires a value"); }); it.each([