@skyramp/testbot-fault-plan 0.0.0-pr676.175

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1 @@
1
+ © Skyramp Inc. All rights reserved. Use is subject to the Skyramp Terms of Service: https://docs.skyramp.dev/terms-of-service
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @skyramp/testbot-fault-plan
2
+
3
+ Test support for Skyramp Testbot (SKYR-5196, Area G gap G1). It is not part of the engine: `@skyramp/testbot-core` uses it only in its tests, and the Testbot web app takes it as a devDependency.
4
+
5
+ It holds one idea in one place:
6
+
7
+ - **`FaultPlan`**: which calls of a test fake fail, and how.
8
+ - **`faultyFetch`**: the plan on an HTTP fake (any `fetch`).
9
+ - **`recorded`**: the real error answers of GitHub, Atlassian, Anthropic, Amazon Bedrock and the workflow steps, with where each was seen.
10
+
11
+ Adapters for one seam only stay in the repository that has that seam, built on this package: `faultyMethods` (the web app's interface fakes of GitHub, in its integration test support) and `faultySessions` (the engine's fake Claude session, in `packages/core/src/__tests__/support`).
12
+
13
+ ## How it works
14
+
15
+ A fake asks the plan before each call, with a key that the fake chooses. `faultyFetch` uses `METHOD /path` (no query string). The plan answers a fault, or nothing. With nothing, the fake gives its normal answer. The plan never changes the normal answers of a fake. A fake's older ad hoc failure switches (for example `mintError` in the web app's runner tests, or `outage` in its fake Atlassian) stay as they are, and the tests that use them do not change; new tests use the plan.
16
+
17
+ ```ts
18
+ import { FaultKind, FaultPlan, fault, faultyFetch, recorded } from "@skyramp/testbot-fault-plan";
19
+
20
+ const plan = new FaultPlan()
21
+ .on(/^PUT \/api\/v1\/runner\/attempts\/[^/]+\/report$/, fault.drop()) // the next upload: the app saves it, the answer is lost
22
+ .on("POST /repos/acme/api/dispatches", fault.recorded(recorded.github.secondaryRateLimit), { nth: 2 })
23
+ .down(/^GET \/ex\/confluence\//, fault.status(503)); // an outage, until plan.up(...)
24
+ const fetch = faultyFetch(plan, fakeApp.fetch);
25
+ // ... run the code under test ...
26
+ expect(plan.seen).toContainEqual({ key: expect.stringMatching(/report$/), kind: FaultKind.Drop });
27
+ ```
28
+
29
+ | Fault | What the caller sees | The fake |
30
+ | --- | --- | --- |
31
+ | `fault.status(status, body?, headers?)` | that answer | not called |
32
+ | `fault.recorded(error)` | a recorded real answer. A recorded text (`recorded.model.*`, a CLI line) has no HTTP answer: `faultyFetch` refuses it, and a session adapter prints it | not called |
33
+ | `fault.drop()` | `TypeError: fetch failed` | called first: the service did the work |
34
+ | `fault.timeout()` | `DOMException` named `TimeoutError`, at once, as the caller's `AbortSignal.timeout` gives | not called |
35
+ | `fault.hang()` | nothing until the caller's signal aborts, then its reason. Refused for a call with no signal | not called |
36
+
37
+ `on(match, fault, { nth, times })` fails the `nth` matching call from now (default 1), `times` calls in a row (default 1). `down(match, fault)` fails every matching call until `up(match)`. `match` is an exact key or a `RegExp` without the `g` or `y` flag. Every rule that matches a call counts it; the first rule whose turn it is gives the fault.
38
+
39
+ `plan.seen` lists each fault the plan gave. **Assert that your fault is in it.** A test whose fault never happened is "not exercised", and must not pass as a recovery test.
40
+
41
+ There is no separate "connection refused" fault: none of these clients reads a rejection before the answer differently from a timeout, so `fault.timeout()` covers it.
42
+
43
+ `timeout` rejects at once instead of waiting: the clients under test (the engine's runner client, 30 s; the web app's GitHub client, 10 s; its Atlassian client) read any rejection before the answer as a lost answer, so a real wait would only make the test slow.
44
+
45
+ ## How to add a fault
46
+
47
+ 1. **A new answer of a service:** add it to `src/recorded-errors.ts`, with `provenance` (where and when it was seen: a drill, a run id, a log) and `provisional: true` unless a drill or run showed that exact answer. Never put a service's error text in a test file.
48
+ 2. **A new way to fail:** add a `FaultKind` only when no existing kind gives the client the same input. Handle it in `faultyFetch` and in every adapter (`faultyMethods` in the web app, `faultySessions` in the engine), and add a test here.
49
+ 3. **A new fake:** wrap it. An HTTP fake: `faultyFetch(plan, fake)`. Another seam: an adapter in that repository's test support that asks `plan.take(key)` and answers each `FaultKind` as that seam's real client would.
50
+
51
+ ## Release
52
+
53
+ The package publishes with `@skyramp/testbot-core`, in the same `release-engine.yml` run, with the same version and dist-tag (for example `0.13.0-v3.<run>` under `next-v3`). Pin an exact version.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A fault plan: which calls of a test fake fail, and how. A fake asks the plan before each call with a key it
3
+ * chooses (for an HTTP fake, `METHOD /path`), and the plan answers the fault to give, or nothing. The fake keeps its
4
+ * own normal answers; the plan only adds failures. See README.md.
5
+ */
6
+ /** How a call fails. */
7
+ export declare enum FaultKind {
8
+ /** Answer an error status, with an optional body and headers. */
9
+ Status = "status",
10
+ /** Answer a recorded real error of the service (recorded-errors.ts). */
11
+ Recorded = "recorded",
12
+ /** Do the work, then lose the answer: the caller sees a broken connection after the service changed its state. */
13
+ Drop = "drop",
14
+ /** Reject at once as the caller's own time limit (`AbortSignal.timeout`) would, before the work. */
15
+ Timeout = "timeout",
16
+ /** Never answer: reject only when the caller's signal aborts. For callers with a short, injected time limit. */
17
+ Hang = "hang"
18
+ }
19
+ /** A real error answer of an HTTP service, as recorded where it was seen. */
20
+ export interface RecordedHttpError {
21
+ service: string;
22
+ status: number;
23
+ headers?: Record<string, string>;
24
+ body: unknown;
25
+ /** Where and when the answer was seen, or what it was built from. */
26
+ provenance: string;
27
+ /** True when no drill or run has shown this exact answer yet. */
28
+ provisional: boolean;
29
+ }
30
+ /** A line a tool printed, such as the Claude Code CLI's `API Error:` result or a workflow step's log. */
31
+ export interface RecordedText {
32
+ service: string;
33
+ text: string;
34
+ /** Where and when the text was seen, or what it was built from. */
35
+ provenance: string;
36
+ /** True when no drill or run has shown this exact text yet. */
37
+ provisional: boolean;
38
+ }
39
+ /** A recorded answer that is a printed line, not an HTTP answer. */
40
+ export declare const isRecordedText: (error: RecordedHttpError | RecordedText) => error is RecordedText;
41
+ export type Fault = {
42
+ kind: FaultKind.Status;
43
+ status: number;
44
+ body?: unknown;
45
+ headers?: Record<string, string>;
46
+ } | {
47
+ kind: FaultKind.Recorded;
48
+ error: RecordedHttpError | RecordedText;
49
+ } | {
50
+ kind: FaultKind.Drop;
51
+ } | {
52
+ kind: FaultKind.Timeout;
53
+ } | {
54
+ kind: FaultKind.Hang;
55
+ };
56
+ /** The faults, for short test code: `plan.on("PUT /report", fault.drop())`. */
57
+ export declare const fault: {
58
+ status: (status: number, body?: unknown, headers?: Record<string, string>) => Fault;
59
+ recorded: (error: RecordedHttpError | RecordedText) => Fault;
60
+ drop: () => Fault;
61
+ timeout: () => Fault;
62
+ hang: () => Fault;
63
+ };
64
+ /** Which matching calls fail: from the `nth` (1 = the next one), `times` calls in a row (Infinity: every one after). */
65
+ export interface FaultOptions {
66
+ nth?: number;
67
+ times?: number;
68
+ }
69
+ /** A call key, exact, or a pattern. */
70
+ export type FaultMatch = string | RegExp;
71
+ /** A fault the plan gave. */
72
+ export interface FaultSeen {
73
+ key: string;
74
+ kind: FaultKind;
75
+ }
76
+ export declare class FaultPlan {
77
+ private rules;
78
+ /** Each fault the plan gave, in order. A test asserts its fault is here, so a fault never reached fails the test. */
79
+ readonly seen: FaultSeen[];
80
+ /** Fails matching calls: from the `nth` after this one is added (default 1), `times` in a row (default 1). */
81
+ on(match: FaultMatch, failure: Fault, options?: FaultOptions): this;
82
+ /** An outage: every matching call fails with `fault` until `up(match)`. */
83
+ down(match: FaultMatch, failure: Fault): this;
84
+ /** Ends the faults added with this match. */
85
+ up(match: FaultMatch): this;
86
+ /** The fault for this call, or undefined to answer it normally. Every rule that matches counts the call. */
87
+ take(key: string): Fault | undefined;
88
+ }
89
+ /** What a caller sees when the connection broke after the service did the work (undici's `fetch failed`). */
90
+ export declare function lostAnswer(): TypeError;
91
+ /** What a caller's `AbortSignal.timeout` gives when its time limit passes. */
92
+ export declare function timeoutError(): DOMException;
93
+ /**
94
+ * Wraps an HTTP fake (a `fetch`): each call asks `plan` with the key `METHOD /path` (no query), and a fault replaces or
95
+ * follows the fake's answer. A call the plan does not fail goes to the fake unchanged.
96
+ */
97
+ export declare function faultyFetch(plan: FaultPlan, inner: typeof fetch): typeof fetch;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * A fault plan: which calls of a test fake fail, and how. A fake asks the plan before each call with a key it
3
+ * chooses (for an HTTP fake, `METHOD /path`), and the plan answers the fault to give, or nothing. The fake keeps its
4
+ * own normal answers; the plan only adds failures. See README.md.
5
+ */
6
+ /** How a call fails. */
7
+ export var FaultKind;
8
+ (function (FaultKind) {
9
+ /** Answer an error status, with an optional body and headers. */
10
+ FaultKind["Status"] = "status";
11
+ /** Answer a recorded real error of the service (recorded-errors.ts). */
12
+ FaultKind["Recorded"] = "recorded";
13
+ /** Do the work, then lose the answer: the caller sees a broken connection after the service changed its state. */
14
+ FaultKind["Drop"] = "drop";
15
+ /** Reject at once as the caller's own time limit (`AbortSignal.timeout`) would, before the work. */
16
+ FaultKind["Timeout"] = "timeout";
17
+ /** Never answer: reject only when the caller's signal aborts. For callers with a short, injected time limit. */
18
+ FaultKind["Hang"] = "hang";
19
+ })(FaultKind || (FaultKind = {}));
20
+ /** A recorded answer that is a printed line, not an HTTP answer. */
21
+ export const isRecordedText = (error) => "text" in error;
22
+ /** The faults, for short test code: `plan.on("PUT /report", fault.drop())`. */
23
+ export const fault = {
24
+ status: (status, body, headers) => ({
25
+ kind: FaultKind.Status, status, ...(body === undefined ? {} : { body }), ...(headers === undefined ? {} : { headers }),
26
+ }),
27
+ recorded: (error) => ({ kind: FaultKind.Recorded, error }),
28
+ drop: () => ({ kind: FaultKind.Drop }),
29
+ timeout: () => ({ kind: FaultKind.Timeout }),
30
+ hang: () => ({ kind: FaultKind.Hang }),
31
+ };
32
+ const matches = (match, key) => (typeof match === "string" ? match === key : match.test(key));
33
+ const sameMatch = (a, b) => typeof a === "string" || typeof b === "string" ? a === b : a.source === b.source && a.flags === b.flags;
34
+ export class FaultPlan {
35
+ rules = [];
36
+ /** Each fault the plan gave, in order. A test asserts its fault is here, so a fault never reached fails the test. */
37
+ seen = [];
38
+ /** Fails matching calls: from the `nth` after this one is added (default 1), `times` in a row (default 1). */
39
+ on(match, failure, options = {}) {
40
+ const from = options.nth ?? 1;
41
+ const times = options.times ?? 1;
42
+ if (!Number.isInteger(from) || from < 1)
43
+ throw new Error(`nth must be a whole number from 1, got ${from}`);
44
+ if (!(times >= 1))
45
+ throw new Error(`times must be 1 or more, got ${times}`);
46
+ // A global or sticky RegExp keeps lastIndex between test() calls, so it would match every other call.
47
+ if (typeof match !== "string" && (match.global || match.sticky))
48
+ throw new Error(`Use a RegExp without the g or y flag: ${match}`);
49
+ this.rules.push({ match, fault: failure, from, until: from + times - 1, calls: 0 });
50
+ return this;
51
+ }
52
+ /** An outage: every matching call fails with `fault` until `up(match)`. */
53
+ down(match, failure) {
54
+ return this.on(match, failure, { times: Infinity });
55
+ }
56
+ /** Ends the faults added with this match. */
57
+ up(match) {
58
+ this.rules = this.rules.filter(rule => !sameMatch(rule.match, match));
59
+ return this;
60
+ }
61
+ /** The fault for this call, or undefined to answer it normally. Every rule that matches counts the call. */
62
+ take(key) {
63
+ let chosen;
64
+ for (const rule of this.rules) {
65
+ if (!matches(rule.match, key))
66
+ continue;
67
+ rule.calls += 1;
68
+ if (!chosen && rule.calls >= rule.from && rule.calls <= rule.until)
69
+ chosen = rule.fault;
70
+ }
71
+ if (chosen)
72
+ this.seen.push({ key, kind: chosen.kind });
73
+ return chosen;
74
+ }
75
+ }
76
+ /** What a caller sees when the connection broke after the service did the work (undici's `fetch failed`). */
77
+ export function lostAnswer() {
78
+ return new TypeError("fetch failed", { cause: new Error("other side closed: the answer was lost after the service did the work (fault plan)") });
79
+ }
80
+ /** What a caller's `AbortSignal.timeout` gives when its time limit passes. */
81
+ export function timeoutError() {
82
+ return new DOMException("The operation was aborted due to timeout (fault plan)", "TimeoutError");
83
+ }
84
+ function errorResponse(status, body, headers = {}) {
85
+ if (body === undefined)
86
+ return new Response(null, { status, headers });
87
+ if (typeof body === "string")
88
+ return new Response(body, { status, headers });
89
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", ...headers } });
90
+ }
91
+ /** Rejects when `signal` aborts, with its reason. */
92
+ function hang(signal) {
93
+ if (!signal)
94
+ return Promise.reject(new Error("A hang fault needs the caller's signal to end it; this call has none, so use fault.timeout()"));
95
+ return new Promise((_resolve, reject) => {
96
+ if (signal.aborted)
97
+ reject(signal.reason);
98
+ else
99
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
100
+ });
101
+ }
102
+ /**
103
+ * Wraps an HTTP fake (a `fetch`): each call asks `plan` with the key `METHOD /path` (no query), and a fault replaces or
104
+ * follows the fake's answer. A call the plan does not fail goes to the fake unchanged.
105
+ */
106
+ export function faultyFetch(plan, inner) {
107
+ return (async (input, init) => {
108
+ const request = input instanceof Request ? input : undefined;
109
+ const url = new URL(request ? request.url : String(input));
110
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
111
+ const chosen = plan.take(`${method} ${url.pathname}`);
112
+ if (!chosen)
113
+ return inner(input, init);
114
+ switch (chosen.kind) {
115
+ case FaultKind.Status:
116
+ return errorResponse(chosen.status, chosen.body, chosen.headers);
117
+ case FaultKind.Recorded:
118
+ if (isRecordedText(chosen.error))
119
+ throw new Error(`A recorded text (${chosen.error.service}) has no HTTP answer: use it with a session adapter`);
120
+ return errorResponse(chosen.error.status, chosen.error.body, chosen.error.headers);
121
+ case FaultKind.Drop:
122
+ await inner(input, init);
123
+ throw lostAnswer();
124
+ case FaultKind.Timeout:
125
+ throw timeoutError();
126
+ case FaultKind.Hang:
127
+ return hang(init?.signal ?? request?.signal);
128
+ }
129
+ });
130
+ }
@@ -0,0 +1,2 @@
1
+ export { FaultKind, FaultPlan, fault, faultyFetch, isRecordedText, lostAnswer, timeoutError, type Fault, type FaultMatch, type FaultOptions, type FaultSeen, type RecordedHttpError, type RecordedText, } from "./fault-plan.js";
2
+ export * as recorded from "./recorded-errors.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { FaultKind, FaultPlan, fault, faultyFetch, isRecordedText, lostAnswer, timeoutError, } from "./fault-plan.js";
2
+ export * as recorded from "./recorded-errors.js";
@@ -0,0 +1,170 @@
1
+ /** GitHub REST API answers. */
2
+ export declare const github: {
3
+ /** A secondary rate limit, which GitHub answers with 403, never a refusal (SKYR-5191). */
4
+ secondaryRateLimit: {
5
+ service: string;
6
+ status: number;
7
+ body: {
8
+ message: string;
9
+ documentation_url: string;
10
+ };
11
+ provenance: string;
12
+ provisional: true;
13
+ };
14
+ /** A workflow dispatch to a disabled workflow (drill G-03). */
15
+ dispatchRefusedDisabledWorkflow: {
16
+ service: string;
17
+ status: number;
18
+ body: {
19
+ message: string;
20
+ documentation_url: string;
21
+ };
22
+ provenance: string;
23
+ provisional: true;
24
+ };
25
+ /** An App that may not do this, such as a permission removed from the installation. */
26
+ resourceNotAccessible: {
27
+ service: string;
28
+ status: number;
29
+ body: {
30
+ message: string;
31
+ documentation_url: string;
32
+ };
33
+ provenance: string;
34
+ provisional: true;
35
+ };
36
+ /** A token request for permissions the installation no longer grants. */
37
+ permissionsNotGranted: {
38
+ service: string;
39
+ status: number;
40
+ body: {
41
+ message: string;
42
+ documentation_url: string;
43
+ };
44
+ provenance: string;
45
+ provisional: true;
46
+ };
47
+ };
48
+ /** Atlassian (Jira, Confluence and the OAuth token endpoint) answers. */
49
+ export declare const atlassian: {
50
+ /** Jira's rate limit, with the wait it asks for. */
51
+ jiraRateLimit: {
52
+ service: string;
53
+ status: number;
54
+ headers: {
55
+ "retry-after": string;
56
+ };
57
+ body: {
58
+ errorMessages: string[];
59
+ errors: {};
60
+ };
61
+ provenance: string;
62
+ provisional: true;
63
+ };
64
+ /** A product API outage. */
65
+ outage: {
66
+ service: string;
67
+ status: number;
68
+ body: string;
69
+ provenance: string;
70
+ provisional: true;
71
+ };
72
+ /** The token endpoint's rate limit. */
73
+ tokenRateLimit: {
74
+ service: string;
75
+ status: number;
76
+ body: {
77
+ error: string;
78
+ };
79
+ provenance: string;
80
+ provisional: true;
81
+ };
82
+ };
83
+ /**
84
+ * The Claude Code CLI's result line for a model provider error, which a staged session ends with (exit 1). The engine
85
+ * reads it in `isApiErrorCrash`. Sorted by the pass rule's kinds (Area G, SKYR-5193): temporary, then permanent.
86
+ */
87
+ export declare const model: {
88
+ anthropicOverloaded: {
89
+ service: string;
90
+ text: string;
91
+ provenance: string;
92
+ provisional: true;
93
+ };
94
+ anthropicServiceUnavailable: {
95
+ service: string;
96
+ text: string;
97
+ provenance: string;
98
+ provisional: true;
99
+ };
100
+ anthropicInternalError: {
101
+ service: string;
102
+ text: string;
103
+ provenance: string;
104
+ provisional: true;
105
+ };
106
+ connectionError: {
107
+ service: string;
108
+ text: string;
109
+ provenance: string;
110
+ provisional: true;
111
+ };
112
+ connectionClosedMidResponse: {
113
+ service: string;
114
+ text: string;
115
+ provenance: string;
116
+ provisional: true;
117
+ };
118
+ anthropicTokensPerDay: {
119
+ service: string;
120
+ text: string;
121
+ provenance: string;
122
+ provisional: false;
123
+ };
124
+ anthropicTooManyRequests: {
125
+ service: string;
126
+ text: string;
127
+ provenance: string;
128
+ provisional: true;
129
+ };
130
+ bedrockUnexpectedError: {
131
+ service: string;
132
+ text: string;
133
+ provenance: string;
134
+ provisional: false;
135
+ };
136
+ usageLimit: {
137
+ service: string;
138
+ text: string;
139
+ provenance: string;
140
+ provisional: false;
141
+ };
142
+ bedrockExpiredToken: {
143
+ service: string;
144
+ text: string;
145
+ provenance: string;
146
+ provisional: false;
147
+ };
148
+ anthropicInvalidApiKey: {
149
+ service: string;
150
+ text: string;
151
+ provenance: string;
152
+ provisional: true;
153
+ };
154
+ bedrockAccessDenied: {
155
+ service: string;
156
+ text: string;
157
+ provenance: string;
158
+ provisional: true;
159
+ };
160
+ };
161
+ /** Workflow step log lines. */
162
+ export declare const workflow: {
163
+ /** The "Configure AWS credentials (Bedrock)" step with a role that does not exist (drill G-08). */
164
+ awsAssumeRoleFailed: {
165
+ service: string;
166
+ text: string;
167
+ provenance: string;
168
+ provisional: false;
169
+ };
170
+ };
@@ -0,0 +1,131 @@
1
+ /** GitHub REST API answers. */
2
+ export const github = {
3
+ /** A secondary rate limit, which GitHub answers with 403, never a refusal (SKYR-5191). */
4
+ secondaryRateLimit: {
5
+ service: "github", status: 403,
6
+ body: { message: "You have exceeded a secondary rate limit. Please wait a few minutes before you try again.", documentation_url: "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api" },
7
+ provenance: "GitHub REST API documentation wording, as testbot-installer-app#667 (SKYR-5191) tests it; no drill recorded the body yet",
8
+ provisional: true,
9
+ },
10
+ /** A workflow dispatch to a disabled workflow (drill G-03). */
11
+ dispatchRefusedDisabledWorkflow: {
12
+ service: "github", status: 404,
13
+ body: { message: "Not Found", documentation_url: "https://docs.github.com/rest/actions/workflows#create-a-workflow-dispatch-event" },
14
+ provenance: "Drill G-03, staging 2026-10-29: the Worker log shows `GitHub refused it: NOT_FOUND`; the body was not logged, so it is GitHub's usual 404 body",
15
+ provisional: true,
16
+ },
17
+ /** An App that may not do this, such as a permission removed from the installation. */
18
+ resourceNotAccessible: {
19
+ service: "github", status: 403,
20
+ body: { message: "Resource not accessible by integration", documentation_url: "https://docs.github.com/rest" },
21
+ provenance: "GitHub's answer to an App without the permission, as testbot-installer-app#667 tests it",
22
+ provisional: true,
23
+ },
24
+ /** A token request for permissions the installation no longer grants. */
25
+ permissionsNotGranted: {
26
+ service: "github", status: 422,
27
+ body: { message: "The permissions requested are not granted to this installation.", documentation_url: "https://docs.github.com/rest/apps/apps#create-an-installation-access-token-for-an-app" },
28
+ provenance: "GitHub REST API documentation of the installation token route; no drill recorded it yet",
29
+ provisional: true,
30
+ },
31
+ };
32
+ /** Atlassian (Jira, Confluence and the OAuth token endpoint) answers. */
33
+ export const atlassian = {
34
+ /** Jira's rate limit, with the wait it asks for. */
35
+ jiraRateLimit: {
36
+ service: "atlassian", status: 429, headers: { "retry-after": "30" },
37
+ body: { errorMessages: ["Rate limit exceeded."], errors: {} },
38
+ provenance: "Jira Cloud REST documentation (rate limiting); no drill recorded it yet",
39
+ provisional: true,
40
+ },
41
+ /** A product API outage. */
42
+ outage: {
43
+ service: "atlassian", status: 503, body: "upstream error",
44
+ provenance: "The shape fake-atlassian.ts answers for an outage; no drill recorded it yet",
45
+ provisional: true,
46
+ },
47
+ /** The token endpoint's rate limit. */
48
+ tokenRateLimit: {
49
+ service: "atlassian", status: 429, body: { error: "too_many_requests" },
50
+ provenance: "The shape fake-atlassian.ts answers; no drill recorded it yet",
51
+ provisional: true,
52
+ },
53
+ };
54
+ /**
55
+ * The Claude Code CLI's result line for a model provider error, which a staged session ends with (exit 1). The engine
56
+ * reads it in `isApiErrorCrash`. Sorted by the pass rule's kinds (Area G, SKYR-5193): temporary, then permanent.
57
+ */
58
+ export const model = {
59
+ // Temporary: the provider is overloaded, rate-limited, unreachable or stops in mid-answer.
60
+ anthropicOverloaded: {
61
+ service: "anthropic", text: 'API Error: 529 {"type":"overloaded_error"}',
62
+ provenance: "Built from Anthropic's documented 529 overloaded_error (agent-failure-reason.test.ts), not from a captured log",
63
+ provisional: true,
64
+ },
65
+ anthropicServiceUnavailable: {
66
+ service: "anthropic", text: "API Error: 503 Service Unavailable",
67
+ provenance: "Built from the API docs' 5xx (agent-failure-reason.test.ts)",
68
+ provisional: true,
69
+ },
70
+ anthropicInternalError: {
71
+ service: "anthropic", text: "API Error: 500 Internal server error.",
72
+ provenance: "execute-agent.test.ts result entry",
73
+ provisional: true,
74
+ },
75
+ connectionError: {
76
+ service: "anthropic", text: "API Error: Connection error.",
77
+ provenance: "Built for a stream dropped before any status (agent-failure-reason.test.ts)",
78
+ provisional: true,
79
+ },
80
+ connectionClosedMidResponse: {
81
+ service: "anthropic", text: "API Error: Connection closed mid-response. The response above may be incomplete.",
82
+ provenance: "execute-agent.test.ts result entry of a stream cut after it started",
83
+ provisional: true,
84
+ },
85
+ anthropicTokensPerDay: {
86
+ service: "anthropic", text: "API Error: Request rejected (429) · Too many tokens per day, please wait before trying again.",
87
+ provenance: "Verbatim from eval run 33602596506 (agent-failure-reason.test.ts)",
88
+ provisional: false,
89
+ },
90
+ anthropicTooManyRequests: {
91
+ service: "anthropic", text: "API Error: Request rejected (429) · Too many requests, please wait before trying again.",
92
+ provenance: "agent-failure-reason.test.ts, same form as eval run 33602596506",
93
+ provisional: true,
94
+ },
95
+ bedrockUnexpectedError: {
96
+ service: "bedrock", text: "API Error: The system encountered an unexpected error during processing. Try your request again.",
97
+ provenance: "Five of 40 eval fixture runs on 2026-10-07 (run-stage.ts isApiErrorCrash)",
98
+ provisional: false,
99
+ },
100
+ // Permanent: no permission, a bad key or credential, or a usage limit.
101
+ usageLimit: {
102
+ service: "bedrock", text: "API Error: 400 You have reached your specified workspace API usage limits. You will regain access on 2026-09-01 at 00:00 UTC.",
103
+ provenance: "Real string from a Bedrock run (agent-failure-reason.test.ts)",
104
+ provisional: false,
105
+ },
106
+ bedrockExpiredToken: {
107
+ service: "bedrock", text: "Failed to authenticate. API Error: 403 The security token included in the request is expired",
108
+ provenance: "Verbatim from eval agent-log.ndjson entries, eval-29726542076 and 9 other runs (agent-failure-reason.test.ts)",
109
+ provisional: false,
110
+ },
111
+ anthropicInvalidApiKey: {
112
+ service: "anthropic", text: "Invalid API key · Please run /login",
113
+ provenance: "The CLI's answer to a bad key (staged-api-error-retry.test.ts); not an `API Error:` line",
114
+ provisional: true,
115
+ },
116
+ bedrockAccessDenied: {
117
+ service: "bedrock",
118
+ text: "API Error: 403 User: arn:aws:sts::000000000000:assumed-role/testbot-drill-no-invoke/testbot is not authorized to perform: bedrock:InvokeModel on resource: arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-5",
119
+ provenance: "PLACEHOLDER built from AWS's AccessDeniedException wording. Drill G-09 (SKYR-5212) records the real text; replace it then",
120
+ provisional: true,
121
+ },
122
+ };
123
+ /** Workflow step log lines. */
124
+ export const workflow = {
125
+ /** The "Configure AWS credentials (Bedrock)" step with a role that does not exist (drill G-08). */
126
+ awsAssumeRoleFailed: {
127
+ service: "aws", text: "Could not assume role with OIDC: The web identity token provided could not be validated.",
128
+ provenance: "Drill G-08, staging 2026-10-29, archit/quote-service run 40928283162",
129
+ provisional: false,
130
+ },
131
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@skyramp/testbot-fault-plan",
3
+ "version": "0.0.0-pr676.175",
4
+ "description": "Test support for Skyramp Testbot: a fault plan for test fakes, and the real error answers of the services Testbot calls. Not part of the engine.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "//files": "An explicit allowlist, as for @skyramp/testbot-core: no glob, no sourcemaps, no TypeScript source.",
15
+ "files": [
16
+ "dist/index.js",
17
+ "dist/index.d.ts",
18
+ "dist/fault-plan.js",
19
+ "dist/fault-plan.d.ts",
20
+ "dist/recorded-errors.js",
21
+ "dist/recorded-errors.d.ts"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.build.json",
28
+ "test": "vitest run",
29
+ "typecheck": "tsc --noEmit",
30
+ "check:pack": "node scripts/check-pack.mjs"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/letsramp/testbot",
35
+ "directory": "packages/fault-plan"
36
+ },
37
+ "author": "Skyramp Inc",
38
+ "license": "SEE LICENSE IN LICENSE.md",
39
+ "devDependencies": {
40
+ "@types/node": "^25.2.3",
41
+ "vitest": "^5.0.0"
42
+ }
43
+ }