Skip to content

Commit aea44c9

Browse files
committed
fix(qa): reject profile flags without qa profile
1 parent 356e863 commit aea44c9

2 files changed

Lines changed: 101 additions & 1 deletion

File tree

extensions/qa-lab/src/cli.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,53 @@ describe("qa cli registration", () => {
248248
expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled();
249249
});
250250

251+
it.each([
252+
["--output-dir", [".artifacts/qa-e2e/smoke-ci"]],
253+
["--surface", ["agent-runtime-and-provider-execution"]],
254+
["--category", ["agent-runtime-and-provider-execution.agent-turn-execution"]],
255+
["--transport", ["qa-channel"]],
256+
["--provider-mode", ["mock-openai"]],
257+
["--model", ["openai/gpt-5.5"]],
258+
["--alt-model", ["anthropic/claude-sonnet-4-6"]],
259+
["--concurrency", ["2"]],
260+
["--allow-failures", []],
261+
["--fast", []],
262+
])("rejects qa run profile-only flag %s without --qa-profile", async (flag, values) => {
263+
await expect(
264+
program.parseAsync(["node", "openclaw", "qa", "run", flag, ...values]),
265+
).rejects.toThrow(`qa run ${flag} requires --qa-profile`);
266+
267+
expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled();
268+
expect(runQaProfileCommand).not.toHaveBeenCalled();
269+
});
270+
271+
it("rejects an empty qa run --qa-profile instead of falling back to self-check", async () => {
272+
await expect(
273+
program.parseAsync(["node", "openclaw", "qa", "run", "--qa-profile", ""]),
274+
).rejects.toThrow("--qa-profile must not be empty.");
275+
276+
expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled();
277+
expect(runQaProfileCommand).not.toHaveBeenCalled();
278+
});
279+
280+
it("rejects self-check output flags in qa run profile mode", async () => {
281+
await expect(
282+
program.parseAsync([
283+
"node",
284+
"openclaw",
285+
"qa",
286+
"run",
287+
"--qa-profile",
288+
"smoke-ci",
289+
"--output",
290+
".artifacts/qa-self-check.md",
291+
]),
292+
).rejects.toThrow("qa run --output is only valid for the self-check mode");
293+
294+
expect(runQaLabSelfCheckCommand).not.toHaveBeenCalled();
295+
expect(runQaProfileCommand).not.toHaveBeenCalled();
296+
});
297+
251298
it("routes mantis discord-smoke flags into the mantis runtime command", async () => {
252299
await program.parseAsync([
253300
"node",

extensions/qa-lab/src/cli.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,21 @@ type QaRunCliOptions = QaLabSelfCheckCommandOptions &
4242
category?: QaProfileCommandOptions["category"];
4343
};
4444

45+
const QA_RUN_PROFILE_ONLY_OPTIONS = [
46+
{ optionName: "outputDir", flag: "--output-dir" },
47+
{ optionName: "surface", flag: "--surface" },
48+
{ optionName: "category", flag: "--category" },
49+
{ optionName: "transport", flag: "--transport" },
50+
{ optionName: "providerMode", flag: "--provider-mode" },
51+
{ optionName: "model", flag: "--model" },
52+
{ optionName: "altModel", flag: "--alt-model" },
53+
{ optionName: "concurrency", flag: "--concurrency" },
54+
{ optionName: "allowFailures", flag: "--allow-failures" },
55+
{ optionName: "fast", flag: "--fast" },
56+
] as const;
57+
58+
const QA_RUN_SELF_CHECK_ONLY_OPTIONS = [{ optionName: "output", flag: "--output" }] as const;
59+
4560
type QaSuiteCliOptions = QaScenarioRunCliOptions & {
4661
runner?: QaSuiteCommandOptions["runner"];
4762
thinking?: QaSuiteCommandOptions["thinking"];
@@ -82,6 +97,43 @@ function parseQaCliPositiveIntegerOption(value: string, flag: string): number {
8297
return parsed;
8398
}
8499

100+
function collectCliSuppliedQaRunFlags(
101+
command: Command,
102+
options: readonly { optionName: string; flag: string }[],
103+
): string[] {
104+
return options
105+
.filter((option) => command.getOptionValueSource(option.optionName) === "cli")
106+
.map((option) => option.flag);
107+
}
108+
109+
function formatFlagList(flags: readonly string[]): string {
110+
return flags.length === 1 ? flags[0] : flags.join(", ");
111+
}
112+
113+
function validateQaRunMode(opts: QaRunCliOptions, command: Command) {
114+
const hasQaProfile = Boolean(opts.qaProfile?.trim());
115+
if (command.getOptionValueSource("qaProfile") === "cli" && !hasQaProfile) {
116+
throw new Error("--qa-profile must not be empty.");
117+
}
118+
119+
if (hasQaProfile) {
120+
const selfCheckFlags = collectCliSuppliedQaRunFlags(command, QA_RUN_SELF_CHECK_ONLY_OPTIONS);
121+
if (selfCheckFlags.length > 0) {
122+
throw new Error(
123+
`qa run ${formatFlagList(selfCheckFlags)} is only valid for the self-check mode without --qa-profile.`,
124+
);
125+
}
126+
return;
127+
}
128+
129+
const profileFlags = collectCliSuppliedQaRunFlags(command, QA_RUN_PROFILE_ONLY_OPTIONS);
130+
if (profileFlags.length > 0) {
131+
throw new Error(
132+
`qa run ${formatFlagList(profileFlags)} requires --qa-profile; without --qa-profile, qa run only executes the self-check.`,
133+
);
134+
}
135+
}
136+
85137
async function runQaSelfCheck(opts: QaLabSelfCheckCommandOptions) {
86138
const runtime = await loadQaLabCliRuntime();
87139
await runtime.runQaLabSelfCheckCommand(opts);
@@ -329,7 +381,8 @@ export function registerQaLabCli(program: Command) {
329381
false,
330382
)
331383
.option("--fast", "Enable provider fast mode where supported", false)
332-
.action(async (opts: QaRunCliOptions) => {
384+
.action(async (opts: QaRunCliOptions, command: Command) => {
385+
validateQaRunMode(opts, command);
333386
if (opts.qaProfile?.trim()) {
334387
await runQaProfile({
335388
repoRoot: opts.repoRoot,

0 commit comments

Comments
 (0)