// Qa Lab tests cover cli plugin behavior. import { Command } from "commander"; import type { QaRunnerCliContribution } from "openclaw/plugin-sdk/qa-runner-runtime"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const TEST_QA_RUNNER = { pluginId: "qa-runner-test", commandName: "runner-test", description: "Run the test live QA lane", } as const; function createAvailableQaRunnerContribution() { return { pluginId: TEST_QA_RUNNER.pluginId, commandName: TEST_QA_RUNNER.commandName, status: "available" as const, registration: { commandName: TEST_QA_RUNNER.commandName, register: vi.fn((qa: Command) => { qa.command(TEST_QA_RUNNER.commandName).action(() => undefined); }), }, } satisfies QaRunnerCliContribution; } function createBlockedQaRunnerContribution(): QaRunnerCliContribution { return { pluginId: TEST_QA_RUNNER.pluginId, commandName: TEST_QA_RUNNER.commandName, description: TEST_QA_RUNNER.description, status: "blocked", }; } function createConflictingQaRunnerContribution(commandName: string): QaRunnerCliContribution { return { pluginId: TEST_QA_RUNNER.pluginId, commandName, description: TEST_QA_RUNNER.description, status: "blocked", }; } const { runQaCredentialsAddCommand, runQaCredentialsListCommand, runQaCredentialsRemoveCommand, runQaCoverageReportCommand, runQaJsonlReplayCommand, runQaLabSelfCheckCommand, runQaProfileCommand, runQaProviderServerCommand, runQaSuiteCommand, runQaTelegramCommand, runMantisBeforeAfterCommand, runMantisDesktopBrowserSmokeCommand, runMantisDiscordSmokeCommand, runMantisSlackDesktopSmokeCommand, runMantisTelegramDesktopBuilderCommand, } = vi.hoisted(() => ({ runQaCredentialsAddCommand: vi.fn(), runQaCredentialsListCommand: vi.fn(), runQaCredentialsRemoveCommand: vi.fn(), runQaCoverageReportCommand: vi.fn(), runQaJsonlReplayCommand: vi.fn(), runQaLabSelfCheckCommand: vi.fn(), runQaProfileCommand: vi.fn(), runQaProviderServerCommand: vi.fn(), runQaSuiteCommand: vi.fn(), runQaTelegramCommand: vi.fn(), runMantisBeforeAfterCommand: vi.fn(), runMantisDesktopBrowserSmokeCommand: vi.fn(), runMantisDiscordSmokeCommand: vi.fn(), runMantisSlackDesktopSmokeCommand: vi.fn(), runMantisTelegramDesktopBuilderCommand: vi.fn(), })); const { listQaRunnerCliContributions } = vi.hoisted(() => ({ listQaRunnerCliContributions: vi.fn<() => QaRunnerCliContribution[]>(() => [ createAvailableQaRunnerContribution(), ]), })); function requireQaTelegramOptions() { const [call] = runQaTelegramCommand.mock.calls; if (!call) { throw new Error("expected qa telegram command call"); } const [options] = call; return options; } function requireQaSuiteOptions() { const [call] = runQaSuiteCommand.mock.calls; if (!call) { throw new Error("expected qa suite command call"); } const [options] = call; return options; } vi.mock("openclaw/plugin-sdk/qa-runner-runtime", () => ({ listQaRunnerCliContributions, })); vi.mock("./live-transports/telegram/cli.runtime.js", () => ({ runQaTelegramCommand, })); vi.mock("./mantis/cli.runtime.js", () => ({ runMantisBeforeAfterCommand, runMantisDesktopBrowserSmokeCommand, runMantisDiscordSmokeCommand, runMantisSlackDesktopSmokeCommand, runMantisTelegramDesktopBuilderCommand, })); vi.mock("./cli.runtime.js", () => ({ runQaCredentialsAddCommand, runQaCredentialsListCommand, runQaCredentialsRemoveCommand, runQaCoverageReportCommand, runQaJsonlReplayCommand, runQaLabSelfCheckCommand, runQaProfileCommand, runQaProviderServerCommand, runQaSuiteCommand, })); import { registerQaLabCli } from "./cli.js"; describe("qa cli registration", () => { let program: Command; beforeEach(() => { program = new Command(); runQaCredentialsAddCommand.mockReset(); runQaCredentialsListCommand.mockReset(); runQaCredentialsRemoveCommand.mockReset(); runQaCoverageReportCommand.mockReset(); runQaJsonlReplayCommand.mockReset(); runQaLabSelfCheckCommand.mockReset(); runQaProfileCommand.mockReset(); runQaProviderServerCommand.mockReset(); runQaSuiteCommand.mockReset(); runQaTelegramCommand.mockReset(); runMantisBeforeAfterCommand.mockReset(); runMantisDesktopBrowserSmokeCommand.mockReset(); runMantisDiscordSmokeCommand.mockReset(); runMantisSlackDesktopSmokeCommand.mockReset(); runMantisTelegramDesktopBuilderCommand.mockReset(); listQaRunnerCliContributions .mockReset() .mockReturnValue([createAvailableQaRunnerContribution()]); registerQaLabCli(program); }); afterEach(() => { vi.clearAllMocks(); }); it("registers discovered and built-in live transport subcommands", () => { const qa = program.commands.find((command) => command.name() === "qa"); if (!qa) { throw new Error("expected qa command"); } const commandNames = qa.commands.map((command) => command.name()); expect(commandNames).toContain(TEST_QA_RUNNER.commandName); expect(commandNames).toContain("telegram"); expect(commandNames).toContain("mantis"); expect(commandNames).toContain("credentials"); expect(commandNames).toContain("coverage"); }); it("does not expose a control-ui token flag on qa ui", () => { const qa = program.commands.find((command) => command.name() === "qa"); const ui = qa?.commands.find((command) => command.name() === "ui"); if (!ui) { throw new Error("expected qa ui command"); } expect(ui.options.map((option) => option.long)).not.toContain("--control-ui-token"); }); it("keeps qa run without a profile on the self-check command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "run", "--repo-root", "/tmp/openclaw-repo", "--output", ".artifacts/qa-self-check.md", ]); expect(runQaLabSelfCheckCommand).toHaveBeenCalledWith({ repoRoot: "/tmp/openclaw-repo", output: ".artifacts/qa-self-check.md", }); expect(runQaProfileCommand).not.toHaveBeenCalled(); }); it("routes qa run qa-profile flags into the taxonomy-backed profile command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "run", "--repo-root", "/tmp/openclaw-repo", "--output-dir", ".artifacts/qa-e2e/smoke-ci", "--qa-profile", "smoke-ci", "--surface", "channel-framework", "--category", "channel-framework.conversation-routing-and-delivery", "--scenario", "dm-chat-baseline", "--evidence-mode", "slim", "--transport", "qa-channel", "--provider-mode", "mock-openai", "--model", "openai/gpt-5.5", "--alt-model", "anthropic/claude-sonnet-4-6", "--concurrency", "2", "--allow-failures", "--fast", ]); expect(runQaProfileCommand).toHaveBeenCalledWith({ repoRoot: "/tmp/openclaw-repo", outputDir: ".artifacts/qa-e2e/smoke-ci", profile: "smoke-ci", surface: "channel-framework", category: "channel-framework.conversation-routing-and-delivery", scenarioIds: ["dm-chat-baseline"], evidenceMode: "slim", transportId: "qa-channel", providerMode: "mock-openai", primaryModel: "openai/gpt-5.5", alternateModel: "anthropic/claude-sonnet-4-6", concurrency: 2, allowFailures: true, fastMode: true, }); expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled(); }); it.each([ ["--output-dir", [".artifacts/qa-e2e/smoke-ci"]], ["--surface", ["agent-runtime-and-provider-execution"]], ["--category", ["channel-framework.conversation-routing-and-delivery"]], ["--scenario", ["dm-chat-baseline"]], ["--evidence-mode", ["slim"]], ["--exclude-test-execution-evidence", []], ["--transport", ["qa-channel"]], ["--provider-mode", ["mock-openai"]], ["--model", ["openai/gpt-5.5"]], ["--alt-model", ["anthropic/claude-sonnet-4-6"]], ["--concurrency", ["2"]], ["--allow-failures", []], ["--fast", []], ])("rejects qa run profile-only flag %s without --qa-profile", async (flag, values) => { await expect( program.parseAsync(["node", "openclaw", "qa", "run", flag, ...values]), ).rejects.toThrow(`qa run ${flag} requires --qa-profile`); expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled(); expect(runQaProfileCommand).not.toHaveBeenCalled(); }); it.each([["--evidence-mode", "compact"], ["--exclude-test-execution-evidence"]])( "maps deprecated compact evidence flag %s to slim", async (...flags) => { await program.parseAsync([ "node", "openclaw", "qa", "run", "--qa-profile", "release", ...flags.filter(Boolean), ]); expect(runQaProfileCommand).toHaveBeenCalledWith( expect.objectContaining({ evidenceMode: "slim", profile: "release", }), ); }, ); it("rejects conflicting deprecated evidence flags", async () => { await expect( program.parseAsync([ "node", "openclaw", "qa", "run", "--qa-profile", "release", "--evidence-mode", "full", "--exclude-test-execution-evidence", ]), ).rejects.toThrow("--exclude-test-execution-evidence conflicts with --evidence-mode full"); expect(runQaProfileCommand).not.toHaveBeenCalled(); }); it("rejects unknown qa evidence modes", async () => { const invalidProgram = new Command(); invalidProgram.exitOverride(); invalidProgram.configureOutput({ writeErr: () => {}, writeOut: () => {}, }); registerQaLabCli(invalidProgram); await expect( invalidProgram.parseAsync([ "node", "openclaw", "qa", "run", "--qa-profile", "smoke-ci", "--evidence-mode", "tiny", ]), ).rejects.toThrow("--evidence-mode must be one of full, slim."); expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled(); expect(runQaProfileCommand).not.toHaveBeenCalled(); }); it("rejects an empty qa run --qa-profile instead of falling back to self-check", async () => { await expect( program.parseAsync(["node", "openclaw", "qa", "run", "--qa-profile", ""]), ).rejects.toThrow("--qa-profile must not be empty."); expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled(); expect(runQaProfileCommand).not.toHaveBeenCalled(); }); it("rejects self-check output flags in qa run profile mode", async () => { await expect( program.parseAsync([ "node", "openclaw", "qa", "run", "--qa-profile", "smoke-ci", "--output", ".artifacts/qa-self-check.md", ]), ).rejects.toThrow("qa run --output is only valid for the self-check mode"); expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled(); expect(runQaProfileCommand).not.toHaveBeenCalled(); }); it("routes mantis discord-smoke flags into the mantis runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "mantis", "discord-smoke", "--repo-root", "/tmp/openclaw-repo", "--output-dir", ".artifacts/qa-e2e/mantis/discord-smoke", "--guild-id", "123456789012345678", "--channel-id", "223456789012345678", "--token-file", "/tmp/mantis-token", "--message", "hello from mantis", "--skip-post", ]); expect(runMantisDiscordSmokeCommand).toHaveBeenCalledWith({ repoRoot: "/tmp/openclaw-repo", outputDir: ".artifacts/qa-e2e/mantis/discord-smoke", guildId: "123456789012345678", channelId: "223456789012345678", tokenEnv: undefined, tokenFile: "/tmp/mantis-token", tokenFileEnv: undefined, message: "hello from mantis", skipPost: true, }); }); it("routes mantis before/after flags into the mantis runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "mantis", "run", "--transport", "discord", "--scenario", "discord-status-reactions-tool-only", "--baseline", "origin/main", "--candidate", "HEAD", "--repo-root", "/tmp/openclaw-repo", "--output-dir", ".artifacts/qa-e2e/mantis/local-discord-status-reactions", "--credential-source", "convex", "--credential-role", "maintainer", "--skip-install", "--skip-build", ]); expect(runMantisBeforeAfterCommand).toHaveBeenCalledWith({ baseline: "origin/main", candidate: "HEAD", credentialRole: "maintainer", credentialSource: "convex", fastMode: true, outputDir: ".artifacts/qa-e2e/mantis/local-discord-status-reactions", providerMode: "live-frontier", repoRoot: "/tmp/openclaw-repo", scenario: "discord-status-reactions-tool-only", skipBuild: true, skipInstall: true, transport: "discord", }); }); it("routes mantis desktop browser smoke flags into the mantis runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "mantis", "desktop-browser-smoke", "--repo-root", "/tmp/openclaw-repo", "--output-dir", ".artifacts/qa-e2e/mantis/desktop-browser", "--browser-url", "https://openclaw.ai/docs", "--html-file", "qa-artifacts/timeline.html", "--crabbox-bin", "/tmp/crabbox", "--provider", "hetzner", "--class", "beast", "--lease-id", "cbx_123abc", "--idle-timeout", "30m", "--ttl", "90m", "--keep-lease", ]); expect(runMantisDesktopBrowserSmokeCommand).toHaveBeenCalledWith({ browserUrl: "https://openclaw.ai/docs", crabboxBin: "/tmp/crabbox", htmlFile: "qa-artifacts/timeline.html", idleTimeout: "30m", keepLease: true, leaseId: "cbx_123abc", machineClass: "beast", outputDir: ".artifacts/qa-e2e/mantis/desktop-browser", provider: "hetzner", repoRoot: "/tmp/openclaw-repo", ttl: "90m", }); }); it("does not shadow mantis desktop browser runtime env defaults", async () => { await program.parseAsync([ "node", "openclaw", "qa", "mantis", "desktop-browser-smoke", "--repo-root", "/tmp/openclaw-repo", ]); expect(runMantisDesktopBrowserSmokeCommand).toHaveBeenCalledWith({ browserUrl: undefined, crabboxBin: undefined, htmlFile: undefined, idleTimeout: undefined, keepLease: undefined, leaseId: undefined, machineClass: undefined, outputDir: undefined, provider: undefined, repoRoot: "/tmp/openclaw-repo", ttl: undefined, }); }); it("routes mantis Slack desktop smoke flags into the mantis runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "mantis", "slack-desktop-smoke", "--repo-root", "/tmp/openclaw-repo", "--output-dir", ".artifacts/qa-e2e/mantis/slack-desktop", "--crabbox-bin", "/tmp/crabbox", "--provider", "hetzner", "--market", "on-demand", "--machine-class", "beast", "--lease-id", "cbx_123abc", "--fresh-pr", "openclaw/openclaw#85141", "--idle-timeout", "45m", "--ttl", "120m", "--slack-url", "https://app.slack.com/client/T123/C123", "--provider-mode", "live-frontier", "--model", "openai/gpt-5.5", "--alt-model", "openai/gpt-5.5", "--scenario", "slack-canary", "--credential-source", "env", "--credential-role", "maintainer", "--fast", "--keep-lease", ]); expect(runMantisSlackDesktopSmokeCommand).toHaveBeenCalledWith({ alternateModel: "openai/gpt-5.5", crabboxBin: "/tmp/crabbox", credentialRole: "maintainer", credentialSource: "env", fastMode: true, freshPr: "openclaw/openclaw#85141", gatewaySetup: undefined, idleTimeout: "45m", keepLease: true, leaseId: "cbx_123abc", machineClass: "beast", market: "on-demand", outputDir: ".artifacts/qa-e2e/mantis/slack-desktop", primaryModel: "openai/gpt-5.5", provider: "hetzner", providerMode: "live-frontier", repoRoot: "/tmp/openclaw-repo", scenarioIds: ["slack-canary"], slackChannelId: undefined, slackUrl: "https://app.slack.com/client/T123/C123", ttl: "120m", }); }); it("routes mantis Telegram desktop builder flags into the mantis runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "mantis", "telegram-desktop-builder", "--repo-root", "/tmp/openclaw-repo", "--output-dir", ".artifacts/qa-e2e/mantis/telegram-desktop", "--crabbox-bin", "/tmp/crabbox", "--provider", "hetzner", "--machine-class", "beast", "--lease-id", "cbx_123abc", "--idle-timeout", "45m", "--ttl", "120m", "--credential-source", "convex", "--credential-role", "ci", "--hydrate-mode", "prehydrated", "--telegram-profile-archive-env", "TELEGRAM_PROFILE_TGZ_B64", "--telegram-profile-dir", "/home/crabbox/.local/share/TelegramDesktop", "--no-gateway-setup", "--keep-lease", ]); expect(runMantisTelegramDesktopBuilderCommand).toHaveBeenCalledWith({ crabboxBin: "/tmp/crabbox", credentialRole: "ci", credentialSource: "convex", gatewaySetup: false, hydrateMode: "prehydrated", idleTimeout: "45m", keepLease: true, leaseId: "cbx_123abc", machineClass: "beast", outputDir: ".artifacts/qa-e2e/mantis/telegram-desktop", provider: "hetzner", repoRoot: "/tmp/openclaw-repo", telegramProfileArchiveEnv: "TELEGRAM_PROFILE_TGZ_B64", telegramProfileDir: "/home/crabbox/.local/share/TelegramDesktop", ttl: "120m", }); }); it("routes coverage report flags into the qa runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "coverage", "--repo-root", "/tmp/openclaw-repo", "--output", ".artifacts/qa-coverage.md", "--json", ]); expect(runQaCoverageReportCommand).toHaveBeenCalledWith({ repoRoot: "/tmp/openclaw-repo", output: ".artifacts/qa-coverage.md", json: true, tools: false, match: [], }); }); it("routes tool coverage report flags into the qa runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "coverage", "--repo-root", "/tmp/openclaw-repo", "--tools", "--summary", ".artifacts/runtime-summary.json", ]); expect(runQaCoverageReportCommand).toHaveBeenCalledWith({ repoRoot: "/tmp/openclaw-repo", tools: true, json: false, summary: ".artifacts/runtime-summary.json", match: [], }); }); it("routes coverage match queries into the qa runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "coverage", "--match", "image roundtrip", "--match", "native", ]); expect(runQaCoverageReportCommand).toHaveBeenCalledWith({ tools: false, json: false, match: ["image roundtrip", "native"], }); }); it("routes JSONL replay flags into the qa runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "jsonl-replay", "--repo-root", "/tmp/openclaw-repo", "--transcripts", "qa/scenarios/jsonl-replay", "--runtime-pair", "openclaw,codex", "--provider-mode", "mock-openai", "--output-dir", ".artifacts/qa-e2e/jsonl-replay-test", ]); expect(runQaJsonlReplayCommand).toHaveBeenCalledWith({ repoRoot: "/tmp/openclaw-repo", transcripts: "qa/scenarios/jsonl-replay", runtimePair: "openclaw,codex", providerMode: "mock-openai", outputDir: ".artifacts/qa-e2e/jsonl-replay-test", }); }); it("delegates discovered qa runner registration through the generic host seam", () => { const [{ registration }] = listQaRunnerCliContributions.mock.results[0].value; expect(registration.register).toHaveBeenCalledTimes(1); }); it("keeps Telegram credential flags on the shared host CLI", () => { const qa = program.commands.find((command) => command.name() === "qa"); const telegram = qa?.commands.find((command) => command.name() === "telegram"); const optionNames = telegram?.options.map((option) => option.long) ?? []; expect(optionNames).toContain("--credential-source"); expect(optionNames).toContain("--credential-role"); expect(optionNames).toContain("--list-scenarios"); }); it("registers standalone provider server commands from the provider registry", async () => { const qa = program.commands.find((command) => command.name() === "qa"); const commandNames = qa?.commands.map((command) => command.name()) ?? []; expect(commandNames).toContain("mock-openai"); expect(commandNames).toContain("aimock"); await program.parseAsync(["node", "openclaw", "qa", "aimock", "--port", "44080"]); expect(runQaProviderServerCommand).toHaveBeenCalledWith("aimock", { host: "127.0.0.1", port: 44080, }); }); it("normalizes signed decimal QA numeric option values through the shared parser", async () => { await program.parseAsync(["node", "openclaw", "qa", "aimock", "--port", "+044080"]); expect(runQaProviderServerCommand).toHaveBeenCalledWith("aimock", { host: "127.0.0.1", port: 44080, }); }); it.each([ [["qa", "suite", "--concurrency", "1.5"], "--concurrency must be a positive integer."], [["qa", "suite", "--cpus", "0x4"], "--cpus must be a positive integer."], [ ["qa", "manual", "--message", "hi", "--timeout-ms", "1e3"], "--timeout-ms must be a positive integer.", ], [["qa", "credentials", "list", "--limit", "0x10"], "--limit must be a positive integer."], [["qa", "ui", "--port", "1e4"], "--port must be a positive integer."], [ ["qa", "docker-scaffold", "--output-dir", "/tmp/qa", "--gateway-port", "1.5"], "--gateway-port must be a positive integer.", ], [["qa", "up", "--qa-lab-port", "0x43124"], "--qa-lab-port must be a positive integer."], [["qa", "aimock", "--port", "1e4"], "--port must be a positive integer."], ])("rejects non-decimal QA numeric option %j", async (args, message) => { const invalidProgram = new Command(); invalidProgram.exitOverride(); invalidProgram.configureOutput({ writeErr: () => {}, writeOut: () => {}, }); registerQaLabCli(invalidProgram); await expect(invalidProgram.parseAsync(["node", "openclaw", ...args])).rejects.toThrow(message); }); it.each([ [["qa", "ui", "--port", "65536"], "--port must be a TCP port between 1 and 65535."], [ ["qa", "ui", "--advertise-port", "999999"], "--advertise-port must be a TCP port between 1 and 65535.", ], [ ["qa", "docker-scaffold", "--output-dir", "/tmp/qa", "--gateway-port", "65536"], "--gateway-port must be a TCP port between 1 and 65535.", ], [ ["qa", "up", "--qa-lab-port", "65536"], "--qa-lab-port must be a TCP port between 1 and 65535.", ], [["qa", "aimock", "--port", "65536"], "--port must be a TCP port between 1 and 65535."], ])("rejects out-of-range QA port option %j", async (args, message) => { const invalidProgram = new Command(); invalidProgram.exitOverride(); invalidProgram.configureOutput({ writeErr: () => {}, writeOut: () => {}, }); registerQaLabCli(invalidProgram); await expect(invalidProgram.parseAsync(["node", "openclaw", ...args])).rejects.toThrow(message); }); it("shows an enable hint when a discovered runner plugin is installed but blocked", async () => { listQaRunnerCliContributions.mockReset().mockReturnValue([createBlockedQaRunnerContribution()]); const blockedProgram = new Command(); registerQaLabCli(blockedProgram); await expect( blockedProgram.parseAsync(["node", "openclaw", "qa", TEST_QA_RUNNER.commandName]), ).rejects.toThrow(`Enable or allow plugin "${TEST_QA_RUNNER.pluginId}"`); }); it("rejects discovered runners that collide with built-in qa subcommands", () => { listQaRunnerCliContributions .mockReset() .mockReturnValue([createConflictingQaRunnerContribution("manual")]); expect(() => registerQaLabCli(new Command())).toThrow( 'QA runner command "manual" conflicts with an existing qa subcommand', ); }); it("routes telegram CLI defaults into the lane runtime", async () => { await program.parseAsync(["node", "openclaw", "qa", "telegram"]); expect(runQaTelegramCommand).toHaveBeenCalledWith({ repoRoot: undefined, outputDir: undefined, providerMode: "live-frontier", primaryModel: undefined, alternateModel: undefined, fastMode: false, allowFailures: false, scenarioIds: [], listScenarios: false, sutAccountId: "sut", credentialSource: undefined, credentialRole: undefined, }); }); it("forwards --list-scenarios for telegram runs", async () => { await program.parseAsync(["node", "openclaw", "qa", "telegram", "--list-scenarios"]); const options = requireQaTelegramOptions(); expect(options.listScenarios).toBe(true); }); it("forwards --allow-failures for telegram runs", async () => { await program.parseAsync(["node", "openclaw", "qa", "telegram", "--allow-failures"]); const options = requireQaTelegramOptions(); expect(options.allowFailures).toBe(true); }); it("forwards --allow-failures for suite runs", async () => { await program.parseAsync(["node", "openclaw", "qa", "suite", "--allow-failures"]); const options = requireQaSuiteOptions(); expect(options.allowFailures).toBe(true); expect(options.providerMode).toBeUndefined(); }); it("forwards --pack for suite runs", async () => { await program.parseAsync(["node", "openclaw", "qa", "suite", "--pack", "personal-agent"]); const options = requireQaSuiteOptions(); expect(options.pack).toBe("personal-agent"); }); it("forwards --runtime-parity-tier for suite runs", async () => { await program.parseAsync([ "node", "openclaw", "qa", "suite", "--runtime-parity-tier", "standard", "--runtime-parity-tier", "optional,soak", ]); const options = requireQaSuiteOptions(); expect(options.runtimeParityTier).toEqual(["standard", "optional,soak"]); }); it("routes credential add flags into the qa runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "credentials", "add", "--kind", "telegram", "--payload-file", "qa/payload.json", "--repo-root", "/tmp/openclaw-repo", "--note", "shared lane", "--site-url", "https://first-schnauzer-821.convex.site", "--endpoint-prefix", "/qa-credentials/v1", "--actor-id", "maintainer-local", "--json", ]); expect(runQaCredentialsAddCommand).toHaveBeenCalledWith({ kind: "telegram", payloadFile: "qa/payload.json", repoRoot: "/tmp/openclaw-repo", note: "shared lane", siteUrl: "https://first-schnauzer-821.convex.site", endpointPrefix: "/qa-credentials/v1", actorId: "maintainer-local", json: true, }); }); it("routes credential remove flags into the qa runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "credentials", "remove", "--credential-id", "j57b8k419ba7bcsfw99rg05c9184p8br", "--site-url", "https://first-schnauzer-821.convex.site", "--actor-id", "maintainer-local", "--json", ]); expect(runQaCredentialsRemoveCommand).toHaveBeenCalledWith({ credentialId: "j57b8k419ba7bcsfw99rg05c9184p8br", siteUrl: "https://first-schnauzer-821.convex.site", actorId: "maintainer-local", endpointPrefix: undefined, json: true, }); }); it("routes credential list defaults into the qa runtime command", async () => { await program.parseAsync([ "node", "openclaw", "qa", "credentials", "list", "--kind", "telegram", ]); expect(runQaCredentialsListCommand).toHaveBeenCalledWith({ kind: "telegram", status: "all", limit: undefined, showSecrets: false, siteUrl: undefined, endpointPrefix: undefined, actorId: undefined, json: false, }); }); });