Skip to content

Commit 0eec92a

Browse files
committed
fix(runner): stop reading a spend-limit refusal as retryable
1 parent 177a9eb commit 0eec92a

12 files changed

Lines changed: 84 additions & 6 deletions

docs/exit-codes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CI consumers depend on consistent exit codes. The CLI commits to the following c
1111
| `4` | `network` | Apex unreachable, GCS download failure, registry unreachable, or a runner that could not serve the request now (unreachable, or its screen not yet up). |
1212
| `5` | `config` | `qawolf.config.ts` invalid, file collision during `init`, or a run file that could not be read. |
1313
| `6` | `timeout` | A `--follow` reached its `--timeout`: `runner run` before its run settled (the run may still be going), or `runner events`. |
14+
| `7` | `payment` | The QA Wolf API refused the request with HTTP 402: billing prevented it — the organization is over its monthly spend limit or has no valid payment method. |
1415

1516
## Using the helper
1617

src/core/messages/auth.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ export const authMessages = {
4444
request: {
4545
rejected401: (noun: string | undefined) =>
4646
`QA Wolf API rejected the${noun ? ` ${noun}` : ""} request (HTTP 401). Check your API key.`,
47+
rejected402: (noun: string | undefined) =>
48+
`QA Wolf API refused the${noun ? ` ${noun}` : ""} request (HTTP 402): billing prevented it.`,
4749
rejected403: (noun: string | undefined) =>
4850
`QA Wolf API rejected the${noun ? ` ${noun}` : ""} request (HTTP 403). Check that your API key has access to this environment.`,
4951
notFound404: (noun: string | undefined) =>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { describe, expect, it } from "bun:test";
2+
3+
import { handleRunnerExec } from "./evaluateSnippet.js";
4+
import { makeAuthCtx, makeTestDeps } from "./deps.testUtils.js";
5+
6+
describe("handleRunnerExec spend limit", () => {
7+
it("keeps the payment exit code on a spend-limit refusal", async () => {
8+
const { callPublicApi, ctx } = makeAuthCtx();
9+
callPublicApi.mockResolvedValue({
10+
error:
11+
"QA Wolf API refused the runner.evaluateSnippet request (HTTP 402): billing prevented it.",
12+
errorBody:
13+
"You have reached your monthly limit of $50.00 for runner usage.",
14+
exitCode: 7,
15+
ok: false,
16+
});
17+
18+
const result = await handleRunnerExec(
19+
ctx,
20+
{ contextFile: undefined, runner: "ci", source: "flow.ts" },
21+
makeTestDeps(),
22+
);
23+
24+
expect(result?.errorBody).toContain("monthly limit");
25+
expect(result?.exitCode).toBe(7);
26+
});
27+
});

src/domains/interactiveRunner/evaluateSnippet.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ export async function handleRunnerExec(
111111
runnerCallOptions,
112112
);
113113
if (!result.ok) {
114-
return { ...failureFields(result), exitCode: exitCodes.network };
114+
return {
115+
...failureFields(result),
116+
exitCode: result.exitCode ?? exitCodes.network,
117+
};
115118
}
116119

117120
if (result.value.outcome === "failure") {

src/domains/interactiveRunner/importPackage.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,25 @@ describe("handleRunnerImportPackage", () => {
149149
expect(result?.error).toContain("Retry");
150150
expect(result?.exitCode).toBe(4);
151151
});
152+
153+
it("keeps the payment exit code on a spend-limit refusal", async () => {
154+
const { callPublicApi, ctx } = makeAuthCtx();
155+
callPublicApi.mockResolvedValue({
156+
error:
157+
"QA Wolf API refused the runner.importPackage request (HTTP 402): billing prevented it.",
158+
errorBody:
159+
"You have reached your monthly limit of $50.00 for runner usage.",
160+
exitCode: 7,
161+
ok: false,
162+
});
163+
164+
const result = await handleRunnerImportPackage(
165+
ctx,
166+
{ name: "dayjs", runner: "ci", version: undefined },
167+
depsWithManifest(),
168+
);
169+
170+
expect(result?.errorBody).toContain("monthly limit");
171+
expect(result?.exitCode).toBe(7);
172+
});
152173
});

src/domains/interactiveRunner/importPackage.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ export async function handleRunnerImportPackage(
8181
runnerCallOptions,
8282
);
8383
if (!result.ok) {
84-
return { ...failureFields(result), exitCode: exitCodes.network };
84+
return {
85+
...failureFields(result),
86+
exitCode: result.exitCode ?? exitCodes.network,
87+
};
8588
}
8689

8790
if (result.value.outcome === "failure") {

src/domains/interactiveRunner/launchAndRemember.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ async function launchRunner(
3636
if (!result.ok) {
3737
return {
3838
...failureFields(result),
39-
exitCode: exitCodes.network,
39+
exitCode: result.exitCode ?? exitCodes.network,
4040
mayHaveArrived: result.mayHaveArrived ?? false,
4141
ok: false,
4242
};

src/domains/interactiveRunner/sendRunFlowRequest.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ export async function sendRunFlowRequest(
6060
);
6161
if (!result.ok) {
6262
return {
63-
failure: { ...failureFields(result), exitCode: exitCodes.network },
63+
failure: {
64+
...failureFields(result),
65+
exitCode: result.exitCode ?? exitCodes.network,
66+
},
6467
type: "failed",
6568
};
6669
}

src/shell/exit.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe("exit", () => {
5959
network: 4,
6060
config: 5,
6161
timeout: 6,
62+
payment: 7,
6263
});
6364
});
6465
});

src/shell/exit.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const exitCodes = {
66
network: 4,
77
config: 5,
88
timeout: 6,
9+
payment: 7,
910
} as const;
1011

1112
type ExitCode = (typeof exitCodes)[keyof typeof exitCodes];

0 commit comments

Comments
 (0)