@glubean/sdk 0.1.0
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/dist/configure.d.ts +141 -0
- package/dist/configure.d.ts.map +1 -0
- package/dist/configure.js +535 -0
- package/dist/configure.js.map +1 -0
- package/dist/data.d.ts +232 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +543 -0
- package/dist/data.js.map +1 -0
- package/dist/expect.d.ts +511 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +763 -0
- package/dist/expect.js.map +1 -0
- package/dist/index.d.ts +718 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1015 -0
- package/dist/index.js.map +1 -0
- package/dist/internal.d.ts +39 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +52 -0
- package/dist/internal.js.map +1 -0
- package/dist/plugin.d.ts +56 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +57 -0
- package/dist/plugin.js.map +1 -0
- package/dist/types.d.ts +1971 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +54 -0
- package/dist/types.js.map +1 -0
- package/package.json +40 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,1971 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error thrown when a required variable or secret is missing or fails validation.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* try {
|
|
7
|
+
* const baseUrl = ctx.vars.require("BASE_URL");
|
|
8
|
+
* } catch (err) {
|
|
9
|
+
* if (err instanceof GlubeanValidationError) {
|
|
10
|
+
* ctx.log(`Missing ${err.type}: ${err.key}`);
|
|
11
|
+
* }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare class GlubeanValidationError extends Error {
|
|
16
|
+
/** The key that was being accessed (e.g., "API_KEY") */
|
|
17
|
+
readonly key: string;
|
|
18
|
+
/** Whether this is a variable or secret */
|
|
19
|
+
readonly type: "var" | "secret";
|
|
20
|
+
constructor(
|
|
21
|
+
/** The key that was being accessed (e.g., "API_KEY") */
|
|
22
|
+
key: string,
|
|
23
|
+
/** The error message */
|
|
24
|
+
message: string,
|
|
25
|
+
/** Whether this is a variable or secret */
|
|
26
|
+
type: "var" | "secret");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Error thrown when a test is dynamically skipped.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* if (!ctx.vars.get("FEATURE_FLAG")) {
|
|
34
|
+
* ctx.skip("Feature not enabled");
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare class GlubeanSkipError extends Error {
|
|
39
|
+
/** Optional reason for skipping */
|
|
40
|
+
readonly reason?: string | undefined;
|
|
41
|
+
constructor(
|
|
42
|
+
/** Optional reason for skipping */
|
|
43
|
+
reason?: string | undefined);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validator function for require() method.
|
|
47
|
+
*
|
|
48
|
+
* Return values:
|
|
49
|
+
* - `true` or `undefined` or `null`: validation passed
|
|
50
|
+
* - `false`: validation failed (generic error)
|
|
51
|
+
* - `string`: validation failed with custom error message
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* // Simple boolean validation
|
|
55
|
+
* (v) => v.length >= 16
|
|
56
|
+
*
|
|
57
|
+
* // With custom error message
|
|
58
|
+
* (v) => v.length >= 16 ? true : `must be at least 16 characters, got ${v.length}`
|
|
59
|
+
*
|
|
60
|
+
* // Alternative style
|
|
61
|
+
* (v) => {
|
|
62
|
+
* if (!v.startsWith("https://")) return "must start with https://";
|
|
63
|
+
* if (!v.includes(".")) return "must be a valid URL";
|
|
64
|
+
* }
|
|
65
|
+
*/
|
|
66
|
+
export type ValidatorFn = (value: string) => boolean | string | void | null;
|
|
67
|
+
/**
|
|
68
|
+
* Provides safe access to environment variables for a test run.
|
|
69
|
+
* Use for non-sensitive configuration such as URLs, ports, regions, and feature flags.
|
|
70
|
+
*
|
|
71
|
+
* **For credentials (API keys, tokens, passwords), use {@link SecretsAccessor | ctx.secrets} instead.**
|
|
72
|
+
* Secrets are loaded from `.secrets` files, automatically redacted in traces, and never
|
|
73
|
+
* appear in logs or dashboards.
|
|
74
|
+
*
|
|
75
|
+
* Use `require` when a value must exist to avoid silent failures.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const baseUrl = ctx.vars.require("BASE_URL");
|
|
79
|
+
* const port = ctx.vars.require("PORT", (v) => !isNaN(Number(v)));
|
|
80
|
+
*/
|
|
81
|
+
export interface VarsAccessor {
|
|
82
|
+
/**
|
|
83
|
+
* Returns the value if present, otherwise undefined.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* const region = ctx.vars.get("REGION") ?? "us-east-1";
|
|
87
|
+
*/
|
|
88
|
+
get(key: string): string | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Returns the value or throws a clear error if missing or invalid.
|
|
91
|
+
* Optionally accepts a validator function for custom validation.
|
|
92
|
+
*
|
|
93
|
+
* @param key - The variable key to retrieve
|
|
94
|
+
* @param validate - Optional validator function. Return false or error string if invalid.
|
|
95
|
+
*
|
|
96
|
+
* @example Basic usage
|
|
97
|
+
* const baseUrl = ctx.vars.require("BASE_URL");
|
|
98
|
+
*
|
|
99
|
+
* @example With boolean validation
|
|
100
|
+
* const port = ctx.vars.require("PORT", (v) => !isNaN(Number(v)));
|
|
101
|
+
*
|
|
102
|
+
* @example With custom error message
|
|
103
|
+
* const endpoint = ctx.vars.require("CALLBACK_URL", (v) =>
|
|
104
|
+
* v.startsWith("https://") ? true : "must start with https://"
|
|
105
|
+
* );
|
|
106
|
+
*/
|
|
107
|
+
require(key: string, validate?: ValidatorFn): string;
|
|
108
|
+
/**
|
|
109
|
+
* Returns a copy of all vars for diagnostics or logging.
|
|
110
|
+
* Vars contain only non-sensitive config, so this is safe to log.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ctx.log("Config", ctx.vars.all());
|
|
114
|
+
*/
|
|
115
|
+
all(): Record<string, string>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Provides safe access to secrets (API keys, tokens, passwords) for a test run.
|
|
119
|
+
*
|
|
120
|
+
* Secrets are loaded from `.secrets` files, automatically redacted in traces
|
|
121
|
+
* and logs, and never appear in dashboards. Use `require` when a secret must
|
|
122
|
+
* exist to avoid silent failures.
|
|
123
|
+
*
|
|
124
|
+
* **For non-sensitive config (URLs, ports, flags), use {@link VarsAccessor | ctx.vars} instead.**
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* const apiKey = ctx.secrets.require("API_KEY");
|
|
128
|
+
* const token = ctx.secrets.require("JWT", (v) => v.split(".").length === 3);
|
|
129
|
+
*/
|
|
130
|
+
export interface SecretsAccessor {
|
|
131
|
+
/**
|
|
132
|
+
* Returns the secret value if present, otherwise undefined.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* const token = ctx.secrets.get("REFRESH_TOKEN");
|
|
136
|
+
*/
|
|
137
|
+
get(key: string): string | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Returns the secret value or throws a clear error if missing or invalid.
|
|
140
|
+
* Optionally accepts a validator function for custom validation.
|
|
141
|
+
*
|
|
142
|
+
* @param key - The secret key to retrieve
|
|
143
|
+
* @param validate - Optional validator function. Return false or error string if invalid.
|
|
144
|
+
*
|
|
145
|
+
* @example Basic usage
|
|
146
|
+
* const apiKey = ctx.secrets.require("API_KEY");
|
|
147
|
+
*
|
|
148
|
+
* @example With custom error message
|
|
149
|
+
* const key = ctx.secrets.require("API_KEY", (v) =>
|
|
150
|
+
* v.startsWith("sk-") ? true : "must start with 'sk-'"
|
|
151
|
+
* );
|
|
152
|
+
*/
|
|
153
|
+
require(key: string, validate?: ValidatorFn): string;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The context passed to every test function.
|
|
157
|
+
* Provides access to environment variables, secrets, logging, assertions, API tracing,
|
|
158
|
+
* and a pre-configured HTTP client.
|
|
159
|
+
*
|
|
160
|
+
* @example Typical test using ctx
|
|
161
|
+
* ```ts
|
|
162
|
+
* export const getUsers = test("get-users", async (ctx) => {
|
|
163
|
+
* const baseUrl = ctx.vars.require("BASE_URL");
|
|
164
|
+
* const apiKey = ctx.secrets.require("API_KEY");
|
|
165
|
+
*
|
|
166
|
+
* const res = await ctx.http.get(`${baseUrl}/users`, {
|
|
167
|
+
* headers: { Authorization: `Bearer ${apiKey}` },
|
|
168
|
+
* });
|
|
169
|
+
* ctx.expect(res).toHaveStatus(200);
|
|
170
|
+
*
|
|
171
|
+
* const body = await res.json();
|
|
172
|
+
* ctx.expect(body.users).toHaveLength(10);
|
|
173
|
+
* });
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* @example Anti-pattern: don't use vars for credentials
|
|
177
|
+
* ```ts
|
|
178
|
+
* // ❌ BAD: credential in vars — visible in traces and dashboards
|
|
179
|
+
* const apiKey = ctx.vars.require("API_KEY");
|
|
180
|
+
*
|
|
181
|
+
* // ✅ GOOD: credential in secrets — auto-redacted in traces
|
|
182
|
+
* const apiKey = ctx.secrets.require("API_KEY");
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export interface TestContext {
|
|
186
|
+
/** Environment variables accessor (e.g., BASE_URL) */
|
|
187
|
+
vars: VarsAccessor;
|
|
188
|
+
/** Secrets accessor (e.g., API_KEY) - injected securely */
|
|
189
|
+
secrets: SecretsAccessor;
|
|
190
|
+
/**
|
|
191
|
+
* Pre-configured HTTP client with auto-tracing, auto-metrics, and retry.
|
|
192
|
+
* Powered by ky. Every request automatically records:
|
|
193
|
+
* - API trace via `ctx.trace()` (method, URL, status, duration)
|
|
194
|
+
* - Metric `http_duration_ms` via `ctx.metric()` (with method and path tags)
|
|
195
|
+
*
|
|
196
|
+
* Can be called directly or via method shortcuts.
|
|
197
|
+
*
|
|
198
|
+
* @example GET request
|
|
199
|
+
* ```ts
|
|
200
|
+
* const users = await ctx.http.get(`${baseUrl}/users`).json();
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* @example POST with JSON body
|
|
204
|
+
* ```ts
|
|
205
|
+
* const user = await ctx.http.post(`${baseUrl}/users`, {
|
|
206
|
+
* json: { name: "test" },
|
|
207
|
+
* }).json();
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* @example With retry
|
|
211
|
+
* ```ts
|
|
212
|
+
* const data = await ctx.http.get(`${baseUrl}/flaky`, { retry: 3 }).json();
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
215
|
+
* @example Callable shorthand (same as ky(url, options))
|
|
216
|
+
* ```ts
|
|
217
|
+
* const res = await ctx.http(`${baseUrl}/users`);
|
|
218
|
+
* ```
|
|
219
|
+
*
|
|
220
|
+
* @example Create scoped client with shared config
|
|
221
|
+
* ```ts
|
|
222
|
+
* const api = ctx.http.extend({
|
|
223
|
+
* prefixUrl: baseUrl,
|
|
224
|
+
* headers: { Authorization: `Bearer ${token}` },
|
|
225
|
+
* });
|
|
226
|
+
* const users = await api.get("users").json();
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
http: HttpClient;
|
|
230
|
+
/**
|
|
231
|
+
* Logging function - streams to runner stdout.
|
|
232
|
+
* @example ctx.log("User created", { id: 123 })
|
|
233
|
+
*/
|
|
234
|
+
log(message: string, data?: unknown): void;
|
|
235
|
+
/**
|
|
236
|
+
* Low-level assertion — records a pass/fail event in the test trace.
|
|
237
|
+
*
|
|
238
|
+
* **Always provide a descriptive `message`** that explains *what* is being
|
|
239
|
+
* checked and *why* it matters. Generic messages like `"status check"` are
|
|
240
|
+
* unhelpful in dashboards, CI logs, and MCP tool output. Good messages read
|
|
241
|
+
* like a sentence: `"GET /users should return 200"`.
|
|
242
|
+
*
|
|
243
|
+
* Prefer `ctx.expect` for most assertions (fluent, auto-generates actual/expected).
|
|
244
|
+
* Use `ctx.assert` when you need a simple boolean guard with a custom message.
|
|
245
|
+
*
|
|
246
|
+
* Overload 1: Simple boolean check
|
|
247
|
+
*
|
|
248
|
+
* @example Good — descriptive message
|
|
249
|
+
* ```ts
|
|
250
|
+
* ctx.assert(res.ok, "GET /users should return 2xx");
|
|
251
|
+
* ctx.assert(body.items.length > 0, "Response should contain at least one item");
|
|
252
|
+
* ctx.assert(res.status === 200, "Create user status", { actual: res.status, expected: 200 });
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* @example Bad — vague message (avoid)
|
|
256
|
+
* ```ts
|
|
257
|
+
* ctx.assert(res.ok); // no message at all
|
|
258
|
+
* ctx.assert(res.ok, "check"); // too vague
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
assert(condition: boolean, message?: string, details?: AssertionDetails): void;
|
|
262
|
+
/**
|
|
263
|
+
* Low-level assertion — records a pass/fail event in the test trace.
|
|
264
|
+
*
|
|
265
|
+
* Overload 2: Explicit result object (useful for complex logic).
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* ctx.assert(
|
|
270
|
+
* { passed: res.status === 200, actual: res.status, expected: 200 },
|
|
271
|
+
* "POST /orders should return 200",
|
|
272
|
+
* );
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
assert(result: AssertionResultInput, message?: string): void;
|
|
276
|
+
/**
|
|
277
|
+
* Fluent assertion API — Jest/Vitest style.
|
|
278
|
+
*
|
|
279
|
+
* **Soft-by-default**: failed assertions are recorded but do NOT throw.
|
|
280
|
+
* All assertions run and all failures are collected.
|
|
281
|
+
*
|
|
282
|
+
* Use `.orFail()` to guard assertions where subsequent code depends on the result.
|
|
283
|
+
* Use `.not` to negate any assertion.
|
|
284
|
+
*
|
|
285
|
+
* **Assertion messages**: Every matcher accepts an optional `message` string as
|
|
286
|
+
* its **last argument**. When provided, it is prepended to the auto-generated
|
|
287
|
+
* message, making failures far more actionable in Trace Viewer, CI, and MCP output.
|
|
288
|
+
*
|
|
289
|
+
* **Always pass a message** that describes the request or business context —
|
|
290
|
+
* e.g. `"GET /users status"`, `"created order id"`, `"auth token format"`.
|
|
291
|
+
*
|
|
292
|
+
* @example With descriptive messages (recommended)
|
|
293
|
+
* ```ts
|
|
294
|
+
* ctx.expect(res.status).toBe(200, "GET /users status");
|
|
295
|
+
* // on failure → "GET /users status: expected 401 to be 200"
|
|
296
|
+
*
|
|
297
|
+
* ctx.expect(body.items).toHaveLength(3, "search result count");
|
|
298
|
+
* ctx.expect(res).toHaveHeader("content-type", /json/, "response content type");
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* @example Guard — abort if this fails
|
|
302
|
+
* ```ts
|
|
303
|
+
* ctx.expect(res.status).toBe(200, "POST /orders").orFail();
|
|
304
|
+
* const body = await res.json(); // safe — status was 200
|
|
305
|
+
* ```
|
|
306
|
+
*
|
|
307
|
+
* @example Without message (still works, less readable in reports)
|
|
308
|
+
* ```ts
|
|
309
|
+
* ctx.expect(res.status).toBe(200);
|
|
310
|
+
* ctx.expect(body.name).toBeType("string");
|
|
311
|
+
* ```
|
|
312
|
+
*
|
|
313
|
+
* @example Negation
|
|
314
|
+
* ```ts
|
|
315
|
+
* ctx.expect(body.banned).not.toBe(true, "user should not be banned");
|
|
316
|
+
* ```
|
|
317
|
+
*
|
|
318
|
+
* @example HTTP-specific
|
|
319
|
+
* ```ts
|
|
320
|
+
* ctx.expect(res).toHaveStatus(200, "GET /users");
|
|
321
|
+
* ctx.expect(res).toHaveHeader("content-type", /json/, "content type");
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
expect<V>(actual: V): import("./expect.js").Expectation<V>;
|
|
325
|
+
/**
|
|
326
|
+
* Soft check — records a warning but never affects test pass/fail.
|
|
327
|
+
*
|
|
328
|
+
* Use `ctx.warn` for "should" conditions that are not hard requirements.
|
|
329
|
+
* Same mental model as `ctx.assert`: `condition=true` means OK, `condition=false` triggers a warning.
|
|
330
|
+
*
|
|
331
|
+
* - `assert` = **must** (failure = test fails)
|
|
332
|
+
* - `warn` = **should** (failure = recorded but test still passes)
|
|
333
|
+
*
|
|
334
|
+
* @param condition `true` if OK, `false` triggers warning
|
|
335
|
+
* @param message Human-readable description
|
|
336
|
+
*
|
|
337
|
+
* @example Performance budget
|
|
338
|
+
* ```ts
|
|
339
|
+
* ctx.warn(duration < 500, "Response should be under 500ms");
|
|
340
|
+
* ```
|
|
341
|
+
*
|
|
342
|
+
* @example Best practice check
|
|
343
|
+
* ```ts
|
|
344
|
+
* ctx.warn(res.headers.has("cache-control"), "Should have cache headers");
|
|
345
|
+
* ```
|
|
346
|
+
*
|
|
347
|
+
* @example HTTPS check
|
|
348
|
+
* ```ts
|
|
349
|
+
* ctx.warn(avatarUrl.startsWith("https"), "Avatar should use HTTPS");
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
warn(condition: boolean, message: string): void;
|
|
353
|
+
/**
|
|
354
|
+
* Validate data against a schema (Zod, Valibot, or any `SchemaLike<T>`).
|
|
355
|
+
*
|
|
356
|
+
* The runner prefers `safeParse` (no-throw) and falls back to `parse` (try/catch).
|
|
357
|
+
* Returns the parsed value on success, or `undefined` on failure.
|
|
358
|
+
*
|
|
359
|
+
* **Severity controls what happens on failure:**
|
|
360
|
+
* - `"error"` (default) — counts as a failed assertion (test fails)
|
|
361
|
+
* - `"warn"` — recorded as warning only (test still passes)
|
|
362
|
+
* - `"fatal"` — immediately aborts test execution
|
|
363
|
+
*
|
|
364
|
+
* A `schema_validation` event is always emitted regardless of severity.
|
|
365
|
+
*
|
|
366
|
+
* @param data The data to validate
|
|
367
|
+
* @param schema A schema implementing `safeParse` or `parse`
|
|
368
|
+
* @param label Human-readable label (e.g., "response body", "query params")
|
|
369
|
+
* @param options Severity and other options
|
|
370
|
+
* @returns Parsed value on success, `undefined` on failure
|
|
371
|
+
*
|
|
372
|
+
* @example Default severity (error — counts as assertion failure)
|
|
373
|
+
* ```ts
|
|
374
|
+
* const user = ctx.validate(body, UserSchema, "response body");
|
|
375
|
+
* ```
|
|
376
|
+
*
|
|
377
|
+
* @example Warning only — record but don't fail
|
|
378
|
+
* ```ts
|
|
379
|
+
* ctx.validate(body, StrictSchema, "strict contract", { severity: "warn" });
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @example Fatal — abort test on invalid response
|
|
383
|
+
* ```ts
|
|
384
|
+
* const user = ctx.validate(body, UserSchema, "response body", { severity: "fatal" });
|
|
385
|
+
* // Only reached if validation passed
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
validate<T>(data: unknown, schema: SchemaLike<T>, label?: string, options?: ValidateOptions): T | undefined;
|
|
389
|
+
/**
|
|
390
|
+
* API Tracing - manually report network calls.
|
|
391
|
+
* @example ctx.trace({ method: "GET", url: "...", status: 200, duration: 100 })
|
|
392
|
+
*/
|
|
393
|
+
trace(request: ApiTrace): void;
|
|
394
|
+
/**
|
|
395
|
+
* Record a structured action to the test timeline.
|
|
396
|
+
*
|
|
397
|
+
* Actions are the primary unit of test observability. Every plugin interaction
|
|
398
|
+
* — browser click, API call, MCP tool invocation, DB query — should be
|
|
399
|
+
* recorded as an action.
|
|
400
|
+
*
|
|
401
|
+
* Actions appear in the Glubean dashboard timeline, are filterable by category,
|
|
402
|
+
* searchable by target, and aggregatable for trend analysis.
|
|
403
|
+
*
|
|
404
|
+
* @example Browser click
|
|
405
|
+
* ```ts
|
|
406
|
+
* ctx.action({
|
|
407
|
+
* category: "browser:click",
|
|
408
|
+
* target: "#submit-btn",
|
|
409
|
+
* duration: 50,
|
|
410
|
+
* status: "ok",
|
|
411
|
+
* });
|
|
412
|
+
* ```
|
|
413
|
+
*
|
|
414
|
+
* @example MCP tool call
|
|
415
|
+
* ```ts
|
|
416
|
+
* ctx.action({
|
|
417
|
+
* category: "mcp:tool-call",
|
|
418
|
+
* target: "get_weather",
|
|
419
|
+
* duration: 300,
|
|
420
|
+
* status: "ok",
|
|
421
|
+
* detail: { args: { location: "Tokyo" } },
|
|
422
|
+
* });
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
425
|
+
action(a: GlubeanAction): void;
|
|
426
|
+
/**
|
|
427
|
+
* Emit a structured event with arbitrary payload.
|
|
428
|
+
*
|
|
429
|
+
* Use `ctx.event()` for structured data that doesn't fit the action model
|
|
430
|
+
* (no target/duration/status) but is more than a log message. Events are
|
|
431
|
+
* renderable by plugin-provided custom renderers in the dashboard.
|
|
432
|
+
*
|
|
433
|
+
* The distinction:
|
|
434
|
+
* - `ctx.log()` — text for humans reading logs
|
|
435
|
+
* - `ctx.event()` — structured data for machines/dashboard renderers
|
|
436
|
+
* - `ctx.action()` — typed interaction record for timeline/waterfall/filter
|
|
437
|
+
*
|
|
438
|
+
* @example Screenshot captured
|
|
439
|
+
* ```ts
|
|
440
|
+
* ctx.event({
|
|
441
|
+
* type: "browser:screenshot",
|
|
442
|
+
* data: { path: "/screenshots/after-login.png", fullPage: true, sizeKb: 142 },
|
|
443
|
+
* });
|
|
444
|
+
* ```
|
|
445
|
+
*
|
|
446
|
+
* @example MCP server connected
|
|
447
|
+
* ```ts
|
|
448
|
+
* ctx.event({
|
|
449
|
+
* type: "mcp:connected",
|
|
450
|
+
* data: { server: "weather-api", transport: "stdio", tools: ["get_weather"] },
|
|
451
|
+
* });
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
454
|
+
event(ev: GlubeanEvent): void;
|
|
455
|
+
/**
|
|
456
|
+
* Report a numeric metric for performance tracking and trending.
|
|
457
|
+
*
|
|
458
|
+
* Metrics are stored separately from logs/traces with longer retention (90 days)
|
|
459
|
+
* and are optimized for time-series queries and dashboards.
|
|
460
|
+
*
|
|
461
|
+
* Security note: metric names and tags are observable metadata and are not
|
|
462
|
+
* intended to carry secrets or PII. Never include tokens, API keys, emails,
|
|
463
|
+
* phone numbers, or user identifiers in `name` / `options.tags`.
|
|
464
|
+
*
|
|
465
|
+
* @param name Metric name (e.g., "api_duration_ms", "response_size_bytes")
|
|
466
|
+
* @param value Numeric value
|
|
467
|
+
* @param options Optional unit and tags
|
|
468
|
+
*
|
|
469
|
+
* @example Basic usage
|
|
470
|
+
* ```ts
|
|
471
|
+
* ctx.metric("api_duration_ms", Date.now() - start);
|
|
472
|
+
* ```
|
|
473
|
+
*
|
|
474
|
+
* @example With unit
|
|
475
|
+
* ```ts
|
|
476
|
+
* ctx.metric("response_size", body.length, { unit: "bytes" });
|
|
477
|
+
* ```
|
|
478
|
+
*
|
|
479
|
+
* @example Server-side duration from response
|
|
480
|
+
* ```ts
|
|
481
|
+
* const data = await res.json();
|
|
482
|
+
* ctx.metric("server_processing_ms", data.processing_time, { unit: "ms" });
|
|
483
|
+
* ctx.metric("route_count", data.result.summary.routes, { unit: "count" });
|
|
484
|
+
* ```
|
|
485
|
+
*
|
|
486
|
+
* @example With tags for slicing in dashboards
|
|
487
|
+
* ```ts
|
|
488
|
+
* ctx.metric("latency_ms", duration, {
|
|
489
|
+
* unit: "ms",
|
|
490
|
+
* tags: { endpoint: "/api/v2/optimize", method: "POST" },
|
|
491
|
+
* });
|
|
492
|
+
* ```
|
|
493
|
+
*
|
|
494
|
+
* @example Anti-pattern (do not do this)
|
|
495
|
+
* ```ts
|
|
496
|
+
* // Bad: secret data embedded in metric dimensions
|
|
497
|
+
* ctx.metric("token_check", 1, { tags: { token: ctx.secrets.require("API_KEY") } });
|
|
498
|
+
* ```
|
|
499
|
+
*/
|
|
500
|
+
metric(name: string, value: number, options?: MetricOptions): void;
|
|
501
|
+
/**
|
|
502
|
+
* Dynamically skip the current test.
|
|
503
|
+
* Throws a GlubeanSkipError that is caught by the runner.
|
|
504
|
+
*
|
|
505
|
+
* @param reason Optional reason for skipping
|
|
506
|
+
*
|
|
507
|
+
* @example Skip based on feature flag
|
|
508
|
+
* ```ts
|
|
509
|
+
* if (!ctx.vars.get("FEATURE_ENABLED")) {
|
|
510
|
+
* ctx.skip("Feature not enabled in this environment");
|
|
511
|
+
* }
|
|
512
|
+
* ```
|
|
513
|
+
*
|
|
514
|
+
* @example Skip if API key is missing
|
|
515
|
+
* ```ts
|
|
516
|
+
* const apiKey = ctx.secrets.get("API_KEY");
|
|
517
|
+
* if (!apiKey) {
|
|
518
|
+
* ctx.skip("API_KEY not configured");
|
|
519
|
+
* }
|
|
520
|
+
* ```
|
|
521
|
+
*/
|
|
522
|
+
skip(reason?: string): never;
|
|
523
|
+
/**
|
|
524
|
+
* Immediately fail and abort the current test.
|
|
525
|
+
* Unlike `ctx.assert(false, msg)` which records a failure but continues,
|
|
526
|
+
* `ctx.fail()` throws and stops execution immediately.
|
|
527
|
+
*
|
|
528
|
+
* Use this when execution reaches a point that should be unreachable,
|
|
529
|
+
* or when a request unexpectedly succeeds and the test should abort.
|
|
530
|
+
*
|
|
531
|
+
* @param message Reason for the failure
|
|
532
|
+
*
|
|
533
|
+
* @example Fail if a request that should error succeeds
|
|
534
|
+
* ```ts
|
|
535
|
+
* const res = await ctx.http.delete(`${ctx.vars.require("BASE_URL")}/protected`, {
|
|
536
|
+
* throwHttpErrors: false,
|
|
537
|
+
* });
|
|
538
|
+
* if (res.ok) {
|
|
539
|
+
* ctx.fail("Expected 403 but request succeeded");
|
|
540
|
+
* }
|
|
541
|
+
* ctx.expect(res.status).toBe(403);
|
|
542
|
+
* ```
|
|
543
|
+
*
|
|
544
|
+
* @example Guard against unreachable code
|
|
545
|
+
* ```ts
|
|
546
|
+
* if (status === "deleted") {
|
|
547
|
+
* ctx.fail("Resource should not be deleted at this point");
|
|
548
|
+
* }
|
|
549
|
+
* ```
|
|
550
|
+
*/
|
|
551
|
+
fail(message: string): never;
|
|
552
|
+
/**
|
|
553
|
+
* Poll a function repeatedly until it returns a truthy value or times out.
|
|
554
|
+
* Useful for eventually-consistent systems where state takes time to converge.
|
|
555
|
+
*
|
|
556
|
+
* **Behavior:**
|
|
557
|
+
* - Calls `fn()` every `intervalMs` (default 1000ms)
|
|
558
|
+
* - If `fn()` returns truthy → resolves immediately
|
|
559
|
+
* - If `fn()` returns falsy → waits and retries
|
|
560
|
+
* - If `fn()` throws → captures error, waits and retries
|
|
561
|
+
* - If `timeoutMs` exceeded and no `onTimeout` → throws Error (test fails)
|
|
562
|
+
* - If `timeoutMs` exceeded and `onTimeout` present → calls `onTimeout`, returns without throwing
|
|
563
|
+
*
|
|
564
|
+
* @param options Polling configuration
|
|
565
|
+
* @param fn Async function to poll. Return truthy to stop.
|
|
566
|
+
*
|
|
567
|
+
* @example Wait for resource to be ready (timeout = test failure)
|
|
568
|
+
* ```ts
|
|
569
|
+
* await ctx.pollUntil({ timeoutMs: 30_000 }, async () => {
|
|
570
|
+
* const res = await ctx.http.get(`${baseUrl}/status`);
|
|
571
|
+
* return res.ok;
|
|
572
|
+
* });
|
|
573
|
+
* ```
|
|
574
|
+
*
|
|
575
|
+
* @example Silent timeout — log but don't fail
|
|
576
|
+
* ```ts
|
|
577
|
+
* await ctx.pollUntil(
|
|
578
|
+
* {
|
|
579
|
+
* timeoutMs: 5_000,
|
|
580
|
+
* intervalMs: 500,
|
|
581
|
+
* onTimeout: (err) => {
|
|
582
|
+
* ctx.log(`Webhook not received: ${err?.message ?? "timeout"}`);
|
|
583
|
+
* },
|
|
584
|
+
* },
|
|
585
|
+
* async () => {
|
|
586
|
+
* const hooks = await ctx.http.get(`${baseUrl}/webhooks`).json();
|
|
587
|
+
* return hooks.length > 0;
|
|
588
|
+
* }
|
|
589
|
+
* );
|
|
590
|
+
* ```
|
|
591
|
+
*/
|
|
592
|
+
pollUntil(options: PollUntilOptions, fn: () => Promise<boolean | unknown>): Promise<void>;
|
|
593
|
+
/**
|
|
594
|
+
* Dynamically set the timeout for the current test.
|
|
595
|
+
* Must be called before any async operations.
|
|
596
|
+
*
|
|
597
|
+
* Semantics: this updates the remaining runtime budget from the moment
|
|
598
|
+
* `setTimeout()` is called (relative deadline), not from test start time.
|
|
599
|
+
*
|
|
600
|
+
* @param ms Timeout in milliseconds
|
|
601
|
+
*
|
|
602
|
+
* @example Increase timeout for slow endpoint
|
|
603
|
+
* ```ts
|
|
604
|
+
* ctx.setTimeout(60000); // 60 seconds
|
|
605
|
+
* const res = await ctx.http.get(ctx.vars.require("SLOW_API_URL"));
|
|
606
|
+
* ```
|
|
607
|
+
*
|
|
608
|
+
* @example Set timeout based on environment
|
|
609
|
+
* ```ts
|
|
610
|
+
* const isProd = ctx.vars.get("ENV") === "production";
|
|
611
|
+
* ctx.setTimeout(isProd ? 30000 : 10000);
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
setTimeout(ms: number): void;
|
|
615
|
+
/**
|
|
616
|
+
* Current execution retry count (0 for first attempt, 1+ for re-runs).
|
|
617
|
+
*
|
|
618
|
+
* Retry orchestration is owned by the runner/control plane, not by SDK user
|
|
619
|
+
* code. `ctx.retryCount` is injected into context at execution start and is
|
|
620
|
+
* read-only inside the test.
|
|
621
|
+
*
|
|
622
|
+
* Important distinction:
|
|
623
|
+
* - `ctx.retryCount` tracks whole-test re-runs.
|
|
624
|
+
* - Step retries from `StepMeta.retries` happen within one execution and do
|
|
625
|
+
* not increment `ctx.retryCount`.
|
|
626
|
+
*
|
|
627
|
+
* Useful for logging, backoff, or idempotency behavior on re-runs.
|
|
628
|
+
*
|
|
629
|
+
* @example Log retry attempts
|
|
630
|
+
* ```ts
|
|
631
|
+
* if (ctx.retryCount > 0) {
|
|
632
|
+
* ctx.log(`Retry attempt ${ctx.retryCount}`);
|
|
633
|
+
* }
|
|
634
|
+
* ```
|
|
635
|
+
*
|
|
636
|
+
* @example Different behavior on retry
|
|
637
|
+
* ```ts
|
|
638
|
+
* const timeout = ctx.retryCount === 0 ? 5000 : 10000;
|
|
639
|
+
* const res = await ctx.http.get(url, { timeout });
|
|
640
|
+
* ```
|
|
641
|
+
*/
|
|
642
|
+
readonly retryCount: number;
|
|
643
|
+
/**
|
|
644
|
+
* Get current memory usage statistics.
|
|
645
|
+
* Only available in Deno runtime (returns null in other environments).
|
|
646
|
+
* Useful for debugging memory issues and profiling tests locally.
|
|
647
|
+
*
|
|
648
|
+
* @returns Memory usage stats or null if not available
|
|
649
|
+
*
|
|
650
|
+
* @example Log current memory usage
|
|
651
|
+
* ```ts
|
|
652
|
+
* const mem = ctx.getMemoryUsage();
|
|
653
|
+
* if (mem) {
|
|
654
|
+
* ctx.log(`Heap used: ${(mem.heapUsed / 1024 / 1024).toFixed(2)} MB`);
|
|
655
|
+
* ctx.log(`Heap total: ${(mem.heapTotal / 1024 / 1024).toFixed(2)} MB`);
|
|
656
|
+
* }
|
|
657
|
+
* ```
|
|
658
|
+
*
|
|
659
|
+
* @example Track memory delta
|
|
660
|
+
* ```ts
|
|
661
|
+
* const before = ctx.getMemoryUsage();
|
|
662
|
+
* // ... perform memory-intensive operation
|
|
663
|
+
* const after = ctx.getMemoryUsage();
|
|
664
|
+
* if (before && after) {
|
|
665
|
+
* const delta = (after.heapUsed - before.heapUsed) / 1024 / 1024;
|
|
666
|
+
* ctx.log(`Memory delta: ${delta.toFixed(2)} MB`);
|
|
667
|
+
* }
|
|
668
|
+
* ```
|
|
669
|
+
*/
|
|
670
|
+
getMemoryUsage(): {
|
|
671
|
+
/** Heap memory currently used (bytes) */
|
|
672
|
+
heapUsed: number;
|
|
673
|
+
/** Total heap memory allocated (bytes) */
|
|
674
|
+
heapTotal: number;
|
|
675
|
+
/** Memory used by C++ objects bound to JS (bytes) */
|
|
676
|
+
external: number;
|
|
677
|
+
/** Resident set size - total memory allocated for the process (bytes) */
|
|
678
|
+
rss: number;
|
|
679
|
+
} | null;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Options for the `configure()` function.
|
|
683
|
+
*
|
|
684
|
+
* Declares file-level dependencies on vars, secrets, and HTTP client
|
|
685
|
+
* configuration. All declared vars and secrets are **required** — if missing
|
|
686
|
+
* at runtime, the test fails immediately with a clear error.
|
|
687
|
+
*
|
|
688
|
+
* For optional vars, use `ctx.vars.get()` directly inside the test function
|
|
689
|
+
* instead of declaring them in `configure()`.
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```ts
|
|
693
|
+
* const { vars, secrets, http } = configure({
|
|
694
|
+
* vars: { baseUrl: "BASE_URL", orgId: "ORG_ID" },
|
|
695
|
+
* secrets: { apiKey: "API_KEY" },
|
|
696
|
+
* http: {
|
|
697
|
+
* prefixUrl: "BASE_URL",
|
|
698
|
+
* headers: { Authorization: "Bearer {{API_KEY}}" },
|
|
699
|
+
* },
|
|
700
|
+
* });
|
|
701
|
+
* ```
|
|
702
|
+
*/
|
|
703
|
+
export interface ConfigureOptions {
|
|
704
|
+
/**
|
|
705
|
+
* Map of friendly property names to var keys.
|
|
706
|
+
* Each key becomes a property on the returned `vars` object.
|
|
707
|
+
* All declared vars are **required** — missing values throw at runtime.
|
|
708
|
+
*
|
|
709
|
+
* @example
|
|
710
|
+
* ```ts
|
|
711
|
+
* const { vars } = configure({
|
|
712
|
+
* vars: { baseUrl: "BASE_URL", orgId: "ORG_ID" },
|
|
713
|
+
* });
|
|
714
|
+
* vars.baseUrl; // string (required, never undefined)
|
|
715
|
+
* ```
|
|
716
|
+
*/
|
|
717
|
+
vars?: Record<string, string>;
|
|
718
|
+
/**
|
|
719
|
+
* Map of friendly property names to secret keys.
|
|
720
|
+
* Each key becomes a property on the returned `secrets` object.
|
|
721
|
+
* All declared secrets are **required** — missing values throw at runtime.
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* ```ts
|
|
725
|
+
* const { secrets } = configure({
|
|
726
|
+
* secrets: { apiKey: "API_KEY" },
|
|
727
|
+
* });
|
|
728
|
+
* secrets.apiKey; // string (required, never undefined)
|
|
729
|
+
* ```
|
|
730
|
+
*/
|
|
731
|
+
secrets?: Record<string, string>;
|
|
732
|
+
/**
|
|
733
|
+
* Pre-configure an HTTP client with shared defaults.
|
|
734
|
+
*
|
|
735
|
+
* - `prefixUrl`: A var key (string) whose runtime value becomes the base URL.
|
|
736
|
+
* - `headers`: Header values can use `{{key}}` syntax to interpolate secrets.
|
|
737
|
+
* - `timeout`, `retry`, `throwHttpErrors`: Passed through to ky.
|
|
738
|
+
*
|
|
739
|
+
* The returned `http` client inherits all `ctx.http` features
|
|
740
|
+
* (auto-tracing, auto-metrics, schema validation) and supports `.extend()`.
|
|
741
|
+
*
|
|
742
|
+
* @example
|
|
743
|
+
* ```ts
|
|
744
|
+
* const { http } = configure({
|
|
745
|
+
* http: {
|
|
746
|
+
* prefixUrl: "base_url",
|
|
747
|
+
* headers: { Authorization: "Bearer {{api_key}}" },
|
|
748
|
+
* },
|
|
749
|
+
* });
|
|
750
|
+
* // In tests:
|
|
751
|
+
* const res = await http.get("users").json();
|
|
752
|
+
* ```
|
|
753
|
+
*/
|
|
754
|
+
http?: ConfigureHttpOptions;
|
|
755
|
+
/**
|
|
756
|
+
* Plugin factories keyed by name.
|
|
757
|
+
* Each plugin is lazily instantiated on first property access during test execution.
|
|
758
|
+
* Use `definePlugin()` to create plugin factories.
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* ```ts
|
|
762
|
+
* import { graphql } from "@glubean/graphql";
|
|
763
|
+
*
|
|
764
|
+
* const { http, graphql: gql } = configure({
|
|
765
|
+
* http: { prefixUrl: "base_url" },
|
|
766
|
+
* plugins: {
|
|
767
|
+
* graphql: graphql({
|
|
768
|
+
* endpoint: "graphql_url",
|
|
769
|
+
* headers: { Authorization: "Bearer {{api_key}}" },
|
|
770
|
+
* }),
|
|
771
|
+
* },
|
|
772
|
+
* });
|
|
773
|
+
* ```
|
|
774
|
+
*/
|
|
775
|
+
plugins?: Record<string, PluginFactory<any> | PluginEntry<any>>;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* HTTP configuration for `configure()`.
|
|
779
|
+
*
|
|
780
|
+
* `prefixUrl` is a **var key** (resolved at runtime), not a literal URL.
|
|
781
|
+
* Header values can contain `{{key}}` placeholders that are resolved from
|
|
782
|
+
* the combined vars + secrets at runtime.
|
|
783
|
+
*/
|
|
784
|
+
export interface ConfigureHttpOptions {
|
|
785
|
+
/**
|
|
786
|
+
* Var key whose runtime value is used as the base URL (ky `prefixUrl`).
|
|
787
|
+
* This is a var key name, not the URL itself.
|
|
788
|
+
*
|
|
789
|
+
* @example "BASE_URL" → resolved to ctx.vars.require("BASE_URL")
|
|
790
|
+
*/
|
|
791
|
+
prefixUrl?: string;
|
|
792
|
+
/**
|
|
793
|
+
* Default headers. Values may contain `{{key}}` placeholders that are
|
|
794
|
+
* resolved from vars and secrets at runtime.
|
|
795
|
+
*
|
|
796
|
+
* @example
|
|
797
|
+
* ```ts
|
|
798
|
+
* headers: {
|
|
799
|
+
* Authorization: "Bearer {{API_KEY}}",
|
|
800
|
+
* "X-Org-Id": "{{ORG_ID}}",
|
|
801
|
+
* }
|
|
802
|
+
* ```
|
|
803
|
+
*/
|
|
804
|
+
headers?: Record<string, string>;
|
|
805
|
+
/** Default request timeout in milliseconds. */
|
|
806
|
+
timeout?: number | false;
|
|
807
|
+
/**
|
|
808
|
+
* Default retry configuration.
|
|
809
|
+
* Number for simple retry count, or object for fine-grained control.
|
|
810
|
+
*
|
|
811
|
+
* @example Simple retry count
|
|
812
|
+
* ```ts
|
|
813
|
+
* retry: 3
|
|
814
|
+
* ```
|
|
815
|
+
*
|
|
816
|
+
* @example Fine-grained control
|
|
817
|
+
* ```ts
|
|
818
|
+
* retry: {
|
|
819
|
+
* limit: 3,
|
|
820
|
+
* statusCodes: [429, 503],
|
|
821
|
+
* maxRetryAfter: 5000,
|
|
822
|
+
* }
|
|
823
|
+
* ```
|
|
824
|
+
*/
|
|
825
|
+
retry?: number | HttpRetryOptions;
|
|
826
|
+
/** Whether to throw on non-2xx responses (default: true). */
|
|
827
|
+
throwHttpErrors?: boolean;
|
|
828
|
+
/**
|
|
829
|
+
* Hooks for intercepting HTTP request/response lifecycle.
|
|
830
|
+
* Passed through to the underlying ky client.
|
|
831
|
+
*
|
|
832
|
+
* @example OAuth token injection
|
|
833
|
+
* ```ts
|
|
834
|
+
* import { oauth2 } from "@glubean/auth";
|
|
835
|
+
*
|
|
836
|
+
* const { http } = configure({
|
|
837
|
+
* http: oauth2.clientCredentials({
|
|
838
|
+
* prefixUrl: "base_url",
|
|
839
|
+
* tokenUrl: "token_url",
|
|
840
|
+
* clientId: "client_id",
|
|
841
|
+
* clientSecret: "client_secret",
|
|
842
|
+
* }),
|
|
843
|
+
* });
|
|
844
|
+
* ```
|
|
845
|
+
*/
|
|
846
|
+
hooks?: HttpHooks;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Return type of `configure()`.
|
|
850
|
+
*
|
|
851
|
+
* All properties are lazy — values are resolved from the runtime context
|
|
852
|
+
* when first accessed during test execution, not at module load time.
|
|
853
|
+
*
|
|
854
|
+
* @template V Shape of the vars object (inferred from `ConfigureOptions.vars`)
|
|
855
|
+
* @template S Shape of the secrets object (inferred from `ConfigureOptions.secrets`)
|
|
856
|
+
*
|
|
857
|
+
* @example
|
|
858
|
+
* ```ts
|
|
859
|
+
* const { vars, secrets, http } = configure({
|
|
860
|
+
* vars: { baseUrl: "BASE_URL" },
|
|
861
|
+
* secrets: { apiKey: "API_KEY" },
|
|
862
|
+
* http: { prefixUrl: "BASE_URL", headers: { Authorization: "Bearer {{API_KEY}}" } },
|
|
863
|
+
* });
|
|
864
|
+
*
|
|
865
|
+
* export const myTest = test("my-test", async (ctx) => {
|
|
866
|
+
* ctx.log(`Using ${vars.baseUrl}`);
|
|
867
|
+
* const res = await http.get("users").json();
|
|
868
|
+
* ctx.expect(res.length).toBeGreaterThan(0);
|
|
869
|
+
* });
|
|
870
|
+
* ```
|
|
871
|
+
*/
|
|
872
|
+
export interface ConfigureResult<V extends Record<string, string> = Record<string, string>, S extends Record<string, string> = Record<string, string>> {
|
|
873
|
+
/**
|
|
874
|
+
* Lazy vars accessor. Each property reads from the runtime vars
|
|
875
|
+
* using `require()` semantics — throws if missing.
|
|
876
|
+
*/
|
|
877
|
+
vars: Readonly<V>;
|
|
878
|
+
/**
|
|
879
|
+
* Lazy secrets accessor. Each property reads from the runtime secrets
|
|
880
|
+
* using `require()` semantics — throws if missing.
|
|
881
|
+
*/
|
|
882
|
+
secrets: Readonly<S>;
|
|
883
|
+
/**
|
|
884
|
+
* Pre-configured HTTP client. Lazily constructed from `ctx.http.extend()`
|
|
885
|
+
* on first use during test execution. Inherits auto-tracing and auto-metrics.
|
|
886
|
+
*
|
|
887
|
+
* Supports further `.extend()` for per-test customization.
|
|
888
|
+
*/
|
|
889
|
+
http: HttpClient;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* A plugin factory that creates a lazy instance of type T.
|
|
893
|
+
* Used with `configure({ plugins: { key: factory } })`.
|
|
894
|
+
* Plugin authors should use `definePlugin()` instead of implementing directly.
|
|
895
|
+
*
|
|
896
|
+
* @example
|
|
897
|
+
* ```ts
|
|
898
|
+
* import { definePlugin } from "@glubean/sdk";
|
|
899
|
+
*
|
|
900
|
+
* export const myPlugin = (opts: MyOptions) =>
|
|
901
|
+
* definePlugin((runtime) => new MyClient(runtime, opts));
|
|
902
|
+
* ```
|
|
903
|
+
*/
|
|
904
|
+
export interface PluginFactory<T> {
|
|
905
|
+
/** Phantom field for TypeScript inference. Not used at runtime. */
|
|
906
|
+
readonly __type: T;
|
|
907
|
+
/** Called lazily on first access during test execution. */
|
|
908
|
+
create(runtime: GlubeanRuntime): T;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Metadata about the currently running test.
|
|
912
|
+
*
|
|
913
|
+
* This is exposed to plugins so they can make deterministic activation decisions
|
|
914
|
+
* based on test identity and tags.
|
|
915
|
+
*
|
|
916
|
+
* @example
|
|
917
|
+
* ```ts
|
|
918
|
+
* definePlugin((runtime) => ({
|
|
919
|
+
* currentTest: runtime.test?.id ?? "unknown",
|
|
920
|
+
* tags: runtime.test?.tags ?? [],
|
|
921
|
+
* }));
|
|
922
|
+
* ```
|
|
923
|
+
*/
|
|
924
|
+
export interface GlubeanRuntimeTestMetadata {
|
|
925
|
+
/** Test ID currently being executed. */
|
|
926
|
+
id: string;
|
|
927
|
+
/** Normalized test tags (always an array). */
|
|
928
|
+
tags: string[];
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Matcher for request-level plugin activation.
|
|
932
|
+
*
|
|
933
|
+
* A matcher can target:
|
|
934
|
+
* - HTTP method (`GET`, `POST`, ...)
|
|
935
|
+
* - request path (`/auth/login`)
|
|
936
|
+
* - full URL (`https://api.example.com/auth/login`)
|
|
937
|
+
*
|
|
938
|
+
* String values use prefix matching. Use `RegExp` for exact/pattern matches.
|
|
939
|
+
*
|
|
940
|
+
* @example Exclude common auth endpoints
|
|
941
|
+
* ```ts
|
|
942
|
+
* requests: {
|
|
943
|
+
* exclude: [
|
|
944
|
+
* { method: "POST", path: /^\/auth\/(login|signup|logout)$/ },
|
|
945
|
+
* ],
|
|
946
|
+
* }
|
|
947
|
+
* ```
|
|
948
|
+
*/
|
|
949
|
+
export interface RequestMatcher {
|
|
950
|
+
/** HTTP method(s), case-insensitive. */
|
|
951
|
+
method?: string | string[];
|
|
952
|
+
/** Pathname matcher (e.g. `/auth/login`). */
|
|
953
|
+
path?: string | RegExp;
|
|
954
|
+
/** Full URL matcher. */
|
|
955
|
+
url?: string | RegExp;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Activation rules for SDK plugins.
|
|
959
|
+
*
|
|
960
|
+
* Defaults:
|
|
961
|
+
* - Missing activation config means "active"
|
|
962
|
+
* - `disable` rules take precedence over `enable`
|
|
963
|
+
* - `exclude` rules take precedence over `include`
|
|
964
|
+
*
|
|
965
|
+
* @example Enable plugin only for smoke tests
|
|
966
|
+
* ```ts
|
|
967
|
+
* activation: {
|
|
968
|
+
* tags: { enable: ["smoke"] },
|
|
969
|
+
* }
|
|
970
|
+
* ```
|
|
971
|
+
*
|
|
972
|
+
* @example Enable globally, except login/logout
|
|
973
|
+
* ```ts
|
|
974
|
+
* activation: {
|
|
975
|
+
* requests: {
|
|
976
|
+
* exclude: [
|
|
977
|
+
* { method: "POST", path: /^\/auth\/(login|logout)$/ },
|
|
978
|
+
* ],
|
|
979
|
+
* },
|
|
980
|
+
* }
|
|
981
|
+
* ```
|
|
982
|
+
*/
|
|
983
|
+
export interface PluginActivation {
|
|
984
|
+
/**
|
|
985
|
+
* Test-tag activation.
|
|
986
|
+
* - `enable`: active only when at least one tag matches
|
|
987
|
+
* - `disable`: force inactive when any tag matches
|
|
988
|
+
*/
|
|
989
|
+
tags?: {
|
|
990
|
+
enable?: string[];
|
|
991
|
+
disable?: string[];
|
|
992
|
+
};
|
|
993
|
+
/**
|
|
994
|
+
* Request activation for plugin-owned HTTP traffic.
|
|
995
|
+
* - `include`: active only when request matches at least one rule
|
|
996
|
+
* - `exclude`: force inactive when request matches any rule
|
|
997
|
+
*/
|
|
998
|
+
requests?: {
|
|
999
|
+
include?: RequestMatcher[];
|
|
1000
|
+
exclude?: RequestMatcher[];
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Plugin entry wrapper for `configure({ plugins })`.
|
|
1005
|
+
*
|
|
1006
|
+
* This preserves backward compatibility with the factory-only style while
|
|
1007
|
+
* enabling per-plugin activation policies.
|
|
1008
|
+
*
|
|
1009
|
+
* @template T Plugin client type returned by `factory.create()`
|
|
1010
|
+
*
|
|
1011
|
+
* @example Existing style (still supported)
|
|
1012
|
+
* ```ts
|
|
1013
|
+
* plugins: {
|
|
1014
|
+
* gql: graphql({ endpoint: "GRAPHQL_URL" }),
|
|
1015
|
+
* }
|
|
1016
|
+
* ```
|
|
1017
|
+
*
|
|
1018
|
+
* @example New style with activation
|
|
1019
|
+
* ```ts
|
|
1020
|
+
* plugins: {
|
|
1021
|
+
* authClient: {
|
|
1022
|
+
* factory: authPlugin(),
|
|
1023
|
+
* activation: {
|
|
1024
|
+
* tags: { disable: ["public"] },
|
|
1025
|
+
* requests: {
|
|
1026
|
+
* exclude: [{ path: /^\/auth\/(login|signup|logout)$/ }],
|
|
1027
|
+
* },
|
|
1028
|
+
* },
|
|
1029
|
+
* },
|
|
1030
|
+
* }
|
|
1031
|
+
* ```
|
|
1032
|
+
*/
|
|
1033
|
+
export interface PluginEntry<T> {
|
|
1034
|
+
/** The plugin factory (same object previously accepted directly). */
|
|
1035
|
+
factory: PluginFactory<T>;
|
|
1036
|
+
/** Optional activation policy for this plugin. */
|
|
1037
|
+
activation?: PluginActivation;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Runtime context available to plugin factories.
|
|
1041
|
+
* Exposes the same capabilities the harness provides to configure().
|
|
1042
|
+
*
|
|
1043
|
+
* Stability: fields may be added (minor), but existing field semantics
|
|
1044
|
+
* must not change without a major version bump. This is the contract
|
|
1045
|
+
* between the SDK and all plugins.
|
|
1046
|
+
*
|
|
1047
|
+
* @example
|
|
1048
|
+
* ```ts
|
|
1049
|
+
* definePlugin((runtime) => {
|
|
1050
|
+
* const baseUrl = runtime.requireVar("base_url");
|
|
1051
|
+
* const token = runtime.requireSecret("api_key");
|
|
1052
|
+
* const header = runtime.resolveTemplate("Bearer {{api_key}}");
|
|
1053
|
+
* return new MyClient(baseUrl, header);
|
|
1054
|
+
* });
|
|
1055
|
+
* ```
|
|
1056
|
+
*/
|
|
1057
|
+
export interface GlubeanRuntime {
|
|
1058
|
+
/** Resolved vars with env fallback */
|
|
1059
|
+
vars: Record<string, string>;
|
|
1060
|
+
/** Resolved secrets with env fallback */
|
|
1061
|
+
secrets: Record<string, string>;
|
|
1062
|
+
/** Pre-configured HTTP client with auto-tracing */
|
|
1063
|
+
http: HttpClient;
|
|
1064
|
+
/** Metadata of the currently executing test, if available. */
|
|
1065
|
+
test?: GlubeanRuntimeTestMetadata;
|
|
1066
|
+
/** Require a var (throws if missing) */
|
|
1067
|
+
requireVar(key: string): string;
|
|
1068
|
+
/** Require a secret (throws if missing) */
|
|
1069
|
+
requireSecret(key: string): string;
|
|
1070
|
+
/** Resolve {{key}} template placeholders from vars and secrets */
|
|
1071
|
+
resolveTemplate(template: string): string;
|
|
1072
|
+
/**
|
|
1073
|
+
* Record a typed interaction to the test timeline.
|
|
1074
|
+
* Available to plugins during initialization and test execution.
|
|
1075
|
+
*/
|
|
1076
|
+
action(a: GlubeanAction): void;
|
|
1077
|
+
/**
|
|
1078
|
+
* Emit a structured event. For data that doesn't fit the action model
|
|
1079
|
+
* but needs to be surfaced in the dashboard with a custom renderer.
|
|
1080
|
+
*/
|
|
1081
|
+
event(ev: GlubeanEvent): void;
|
|
1082
|
+
/**
|
|
1083
|
+
* Emit a log message. Convenience alias available to plugins
|
|
1084
|
+
* without requiring the full TestContext.
|
|
1085
|
+
*/
|
|
1086
|
+
log(message: string, data?: unknown): void;
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Maps plugin factories to their resolved types.
|
|
1090
|
+
* Used internally by `configure()` to infer the return type.
|
|
1091
|
+
*/
|
|
1092
|
+
export type ResolvePlugins<P> = {
|
|
1093
|
+
[K in keyof P]: P[K] extends PluginFactory<infer T> ? T : P[K] extends PluginEntry<infer T> ? T : never;
|
|
1094
|
+
};
|
|
1095
|
+
/**
|
|
1096
|
+
* Keys reserved by `ConfigureResult` that plugin names must not shadow.
|
|
1097
|
+
* Using one of these as a plugin key causes a compile-time error.
|
|
1098
|
+
*/
|
|
1099
|
+
export type ReservedConfigureKeys = "vars" | "secrets" | "http";
|
|
1100
|
+
/**
|
|
1101
|
+
* Response object returned when awaiting HTTP client methods.
|
|
1102
|
+
* Extends native `Response` with a typed `.json<T>()` method so you can
|
|
1103
|
+
* assert on status first, then parse with a type parameter:
|
|
1104
|
+
*
|
|
1105
|
+
* ```ts
|
|
1106
|
+
* const res = await ctx.http.post(url, { json: body });
|
|
1107
|
+
* ctx.expect(res).toHaveStatus(200);
|
|
1108
|
+
* const data = await res.json<{ id: string }>();
|
|
1109
|
+
* ```
|
|
1110
|
+
*/
|
|
1111
|
+
export interface HttpResponse extends Response {
|
|
1112
|
+
/** Parse response body as JSON with optional type parameter */
|
|
1113
|
+
json<T = unknown>(): Promise<T>;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Promise returned by HTTP client methods.
|
|
1117
|
+
* Extends native `Promise<HttpResponse>` with convenience body-parsing methods.
|
|
1118
|
+
*
|
|
1119
|
+
* @example Chain directly
|
|
1120
|
+
* ```ts
|
|
1121
|
+
* const users = await ctx.http.get(`${baseUrl}/users`).json<User[]>();
|
|
1122
|
+
* ```
|
|
1123
|
+
*
|
|
1124
|
+
* @example Await first, then parse (for asserting status before body)
|
|
1125
|
+
* ```ts
|
|
1126
|
+
* const res = await ctx.http.get(`${baseUrl}/users`);
|
|
1127
|
+
* ctx.expect(res).toHaveStatus(200);
|
|
1128
|
+
* const users = await res.json<User[]>();
|
|
1129
|
+
* ```
|
|
1130
|
+
*/
|
|
1131
|
+
export interface HttpResponsePromise extends Promise<HttpResponse> {
|
|
1132
|
+
/** Parse response body as JSON */
|
|
1133
|
+
json<T = unknown>(): Promise<T>;
|
|
1134
|
+
/** Parse response body as text */
|
|
1135
|
+
text(): Promise<string>;
|
|
1136
|
+
/** Parse response body as Blob */
|
|
1137
|
+
blob(): Promise<Blob>;
|
|
1138
|
+
/** Parse response body as ArrayBuffer */
|
|
1139
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Retry configuration for HTTP requests.
|
|
1143
|
+
*
|
|
1144
|
+
* @example
|
|
1145
|
+
* ```ts
|
|
1146
|
+
* const res = await ctx.http.get(url, {
|
|
1147
|
+
* retry: { limit: 3, statusCodes: [429, 503] },
|
|
1148
|
+
* });
|
|
1149
|
+
* ```
|
|
1150
|
+
*/
|
|
1151
|
+
export interface HttpRetryOptions {
|
|
1152
|
+
/** Number of retry attempts (default: 2) */
|
|
1153
|
+
limit?: number;
|
|
1154
|
+
/** HTTP methods to retry (default: GET, PUT, HEAD, DELETE, OPTIONS, TRACE) */
|
|
1155
|
+
methods?: string[];
|
|
1156
|
+
/** HTTP status codes that trigger a retry (default: 408, 413, 429, 500, 502, 503, 504) */
|
|
1157
|
+
statusCodes?: number[];
|
|
1158
|
+
/** Maximum delay (ms) to wait based on Retry-After header */
|
|
1159
|
+
maxRetryAfter?: number;
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Options for HTTP requests.
|
|
1163
|
+
*
|
|
1164
|
+
* **`ctx.http` is a thin wrapper around [ky](https://github.com/sindresorhus/ky).**
|
|
1165
|
+
* All ky options are supported. See https://github.com/sindresorhus/ky#options
|
|
1166
|
+
* for the complete reference.
|
|
1167
|
+
*
|
|
1168
|
+
* **There is no `form` shortcut** — ky (and therefore `ctx.http`) does not have one.
|
|
1169
|
+
* Use `body: new URLSearchParams(...)` for `application/x-www-form-urlencoded` data.
|
|
1170
|
+
*
|
|
1171
|
+
* @example POST with JSON (most common)
|
|
1172
|
+
* ```ts
|
|
1173
|
+
* const res = await ctx.http.post(url, { json: { name: "test" } });
|
|
1174
|
+
* ```
|
|
1175
|
+
*
|
|
1176
|
+
* @example POST with form-urlencoded data
|
|
1177
|
+
* ```ts
|
|
1178
|
+
* const res = await ctx.http.post(url, {
|
|
1179
|
+
* body: new URLSearchParams({
|
|
1180
|
+
* grant_type: "client_credentials",
|
|
1181
|
+
* client_id: ctx.secrets.require("CLIENT_ID"),
|
|
1182
|
+
* client_secret: ctx.secrets.require("CLIENT_SECRET"),
|
|
1183
|
+
* }),
|
|
1184
|
+
* });
|
|
1185
|
+
* ```
|
|
1186
|
+
*
|
|
1187
|
+
* @example POST with multipart form data
|
|
1188
|
+
* ```ts
|
|
1189
|
+
* const form = new FormData();
|
|
1190
|
+
* form.append("file", new Blob(["content"]), "test.txt");
|
|
1191
|
+
* const res = await ctx.http.post(url, { body: form });
|
|
1192
|
+
* ```
|
|
1193
|
+
*
|
|
1194
|
+
* @example With search params and timeout
|
|
1195
|
+
* ```ts
|
|
1196
|
+
* ctx.http.get(url, {
|
|
1197
|
+
* searchParams: { page: 1, limit: 10 },
|
|
1198
|
+
* timeout: 5000,
|
|
1199
|
+
* });
|
|
1200
|
+
* ```
|
|
1201
|
+
*/
|
|
1202
|
+
export interface HttpRequestOptions {
|
|
1203
|
+
/** JSON body (automatically serialized and sets Content-Type) */
|
|
1204
|
+
json?: unknown;
|
|
1205
|
+
/** URL search parameters */
|
|
1206
|
+
searchParams?: Record<string, string | number | boolean> | URLSearchParams | string;
|
|
1207
|
+
/** Request headers */
|
|
1208
|
+
headers?: Record<string, string> | Headers;
|
|
1209
|
+
/** Request timeout in milliseconds (default: 10000). Set `false` to disable. */
|
|
1210
|
+
timeout?: number | false;
|
|
1211
|
+
/** Retry configuration. Number for simple retry count, or object for fine-grained control. */
|
|
1212
|
+
retry?: number | HttpRetryOptions;
|
|
1213
|
+
/** Base URL prefix (prepended to the request URL) */
|
|
1214
|
+
prefixUrl?: string | URL;
|
|
1215
|
+
/** Whether to throw on non-2xx responses (default: true) */
|
|
1216
|
+
throwHttpErrors?: boolean;
|
|
1217
|
+
/** HTTP method override */
|
|
1218
|
+
method?: string;
|
|
1219
|
+
/**
|
|
1220
|
+
* Request body for non-JSON payloads.
|
|
1221
|
+
*
|
|
1222
|
+
* - `new URLSearchParams(...)` for `application/x-www-form-urlencoded`
|
|
1223
|
+
* - `new FormData()` for `multipart/form-data`
|
|
1224
|
+
* - `string` or `Blob` for raw payloads
|
|
1225
|
+
*
|
|
1226
|
+
* Do **not** use `json` and `body` together — `json` takes precedence.
|
|
1227
|
+
*/
|
|
1228
|
+
body?: BodyInit;
|
|
1229
|
+
/** AbortSignal for request cancellation */
|
|
1230
|
+
signal?: AbortSignal;
|
|
1231
|
+
/** Hooks for request/response interception */
|
|
1232
|
+
hooks?: HttpHooks;
|
|
1233
|
+
/**
|
|
1234
|
+
* Schema validation for request and response.
|
|
1235
|
+
*
|
|
1236
|
+
* Schemas are validated automatically:
|
|
1237
|
+
* - `query` and `request` — validated before the request is sent
|
|
1238
|
+
* - `response` — validated after the response is received
|
|
1239
|
+
*
|
|
1240
|
+
* Each entry can be a bare schema (severity defaults to `"error"`)
|
|
1241
|
+
* or `{ schema, severity }` for explicit control.
|
|
1242
|
+
*
|
|
1243
|
+
* @example
|
|
1244
|
+
* ```ts
|
|
1245
|
+
* const res = await ctx.http.post(url, {
|
|
1246
|
+
* json: payload,
|
|
1247
|
+
* schema: {
|
|
1248
|
+
* request: RequestBodySchema,
|
|
1249
|
+
* response: ResponseSchema,
|
|
1250
|
+
* query: { schema: QuerySchema, severity: "warn" },
|
|
1251
|
+
* },
|
|
1252
|
+
* });
|
|
1253
|
+
* ```
|
|
1254
|
+
*/
|
|
1255
|
+
schema?: HttpSchemaOptions;
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Hooks for intercepting HTTP request/response lifecycle.
|
|
1259
|
+
*
|
|
1260
|
+
* @example Log all requests
|
|
1261
|
+
* ```ts
|
|
1262
|
+
* const api = ctx.http.extend({
|
|
1263
|
+
* hooks: {
|
|
1264
|
+
* beforeRequest: [(request) => {
|
|
1265
|
+
* ctx.log(`→ ${request.method} ${request.url}`);
|
|
1266
|
+
* }],
|
|
1267
|
+
* },
|
|
1268
|
+
* });
|
|
1269
|
+
* ```
|
|
1270
|
+
*/
|
|
1271
|
+
export interface HttpHooks {
|
|
1272
|
+
/** Called before each request. Can modify or replace the request. */
|
|
1273
|
+
beforeRequest?: Array<(request: Request, options: HttpRequestOptions) => Request | Response | void | Promise<Request | Response | void>>;
|
|
1274
|
+
/** Called after each response. Can modify or replace the response. */
|
|
1275
|
+
afterResponse?: Array<(request: Request, options: HttpRequestOptions, response: Response) => Response | void | Promise<Response | void>>;
|
|
1276
|
+
/** Called before each retry attempt. */
|
|
1277
|
+
beforeRetry?: Array<(details: {
|
|
1278
|
+
request: Request;
|
|
1279
|
+
options: HttpRequestOptions;
|
|
1280
|
+
error: Error;
|
|
1281
|
+
retryCount: number;
|
|
1282
|
+
}) => void | Promise<void>>;
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* HTTP client interface powered by ky.
|
|
1286
|
+
*
|
|
1287
|
+
* Pre-configured with auto-tracing and auto-metrics.
|
|
1288
|
+
* Every request automatically records:
|
|
1289
|
+
* - API trace: method, URL, status, duration (via `ctx.trace()`)
|
|
1290
|
+
* - Metric: `http_duration_ms` with method and path tags (via `ctx.metric()`)
|
|
1291
|
+
*
|
|
1292
|
+
* Supports ky features: retry, timeout, hooks, JSON shortcuts, and `.extend()`.
|
|
1293
|
+
*
|
|
1294
|
+
* @example
|
|
1295
|
+
* ```ts
|
|
1296
|
+
* // Method shortcuts
|
|
1297
|
+
* const users = await ctx.http.get(`${baseUrl}/users`).json();
|
|
1298
|
+
* const created = await ctx.http.post(`${baseUrl}/users`, { json: { name: "test" } }).json();
|
|
1299
|
+
*
|
|
1300
|
+
* // Callable shorthand
|
|
1301
|
+
* const res = await ctx.http(`${baseUrl}/users`);
|
|
1302
|
+
*
|
|
1303
|
+
* // Create scoped client
|
|
1304
|
+
* const api = ctx.http.extend({ prefixUrl: baseUrl });
|
|
1305
|
+
* const user = await api.get("users/1").json();
|
|
1306
|
+
* ```
|
|
1307
|
+
*/
|
|
1308
|
+
export interface HttpClient {
|
|
1309
|
+
/** Make a request (generic). Same as ky(url, options). */
|
|
1310
|
+
(url: string | URL | Request, options?: HttpRequestOptions): HttpResponsePromise;
|
|
1311
|
+
/** HTTP GET request */
|
|
1312
|
+
get(url: string | URL | Request, options?: HttpRequestOptions): HttpResponsePromise;
|
|
1313
|
+
/** HTTP POST request */
|
|
1314
|
+
post(url: string | URL | Request, options?: HttpRequestOptions): HttpResponsePromise;
|
|
1315
|
+
/** HTTP PUT request */
|
|
1316
|
+
put(url: string | URL | Request, options?: HttpRequestOptions): HttpResponsePromise;
|
|
1317
|
+
/** HTTP PATCH request */
|
|
1318
|
+
patch(url: string | URL | Request, options?: HttpRequestOptions): HttpResponsePromise;
|
|
1319
|
+
/** HTTP DELETE request */
|
|
1320
|
+
delete(url: string | URL | Request, options?: HttpRequestOptions): HttpResponsePromise;
|
|
1321
|
+
/** HTTP HEAD request */
|
|
1322
|
+
head(url: string | URL | Request, options?: HttpRequestOptions): HttpResponsePromise;
|
|
1323
|
+
/**
|
|
1324
|
+
* Create a new HTTP client instance with merged defaults.
|
|
1325
|
+
* The new instance inherits auto-tracing and auto-metrics.
|
|
1326
|
+
*
|
|
1327
|
+
* @example Scoped client with base URL and auth
|
|
1328
|
+
* ```ts
|
|
1329
|
+
* const api = ctx.http.extend({
|
|
1330
|
+
* prefixUrl: ctx.vars.require("BASE_URL"),
|
|
1331
|
+
* headers: { Authorization: `Bearer ${ctx.secrets.require("API_TOKEN")}` },
|
|
1332
|
+
* });
|
|
1333
|
+
* const users = await api.get("users").json();
|
|
1334
|
+
* const user = await api.get("users/1").json();
|
|
1335
|
+
* ```
|
|
1336
|
+
*/
|
|
1337
|
+
extend(options: HttpRequestOptions): HttpClient;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Protocol interface for any schema library (Zod, Valibot, ArkType, etc.).
|
|
1341
|
+
*
|
|
1342
|
+
* A schema is `SchemaLike<T>` if it implements **at least** `.safeParse()`.
|
|
1343
|
+
* If only `.parse()` is available it is also accepted (the runner wraps it in try/catch).
|
|
1344
|
+
*
|
|
1345
|
+
* @example Zod
|
|
1346
|
+
* ```ts
|
|
1347
|
+
* import { z } from "zod";
|
|
1348
|
+
* const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
1349
|
+
* // z.ZodType satisfies SchemaLike<T> out of the box
|
|
1350
|
+
* ```
|
|
1351
|
+
*
|
|
1352
|
+
* @example Custom schema
|
|
1353
|
+
* ```ts
|
|
1354
|
+
* const MySchema: SchemaLike<User> = {
|
|
1355
|
+
* safeParse(data) {
|
|
1356
|
+
* if (isValid(data)) return { success: true, data };
|
|
1357
|
+
* return { success: false, error: { issues: [{ message: "invalid" }] } };
|
|
1358
|
+
* },
|
|
1359
|
+
* };
|
|
1360
|
+
* ```
|
|
1361
|
+
*/
|
|
1362
|
+
export interface SchemaLike<T> {
|
|
1363
|
+
/** Preferred — returns a result object without throwing. */
|
|
1364
|
+
safeParse?: (data: unknown) => {
|
|
1365
|
+
success: true;
|
|
1366
|
+
data: T;
|
|
1367
|
+
} | {
|
|
1368
|
+
success: false;
|
|
1369
|
+
error: {
|
|
1370
|
+
issues: Array<{
|
|
1371
|
+
message: string;
|
|
1372
|
+
path?: Array<string | number>;
|
|
1373
|
+
}>;
|
|
1374
|
+
};
|
|
1375
|
+
};
|
|
1376
|
+
/** Fallback — throws on failure, returns parsed value on success. */
|
|
1377
|
+
parse?: (data: unknown) => T;
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* A single issue reported by schema validation.
|
|
1381
|
+
*/
|
|
1382
|
+
export interface SchemaIssue {
|
|
1383
|
+
/** Human-readable error message */
|
|
1384
|
+
message: string;
|
|
1385
|
+
/** Property path (e.g., ["user", "email"]) */
|
|
1386
|
+
path?: Array<string | number>;
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Options for `ctx.validate()`.
|
|
1390
|
+
*/
|
|
1391
|
+
export interface ValidateOptions {
|
|
1392
|
+
/**
|
|
1393
|
+
* How a validation failure is treated:
|
|
1394
|
+
* - `"error"` (default) — counts as a failed assertion (test fails)
|
|
1395
|
+
* - `"warn"` — recorded as warning only (test still passes)
|
|
1396
|
+
* - `"fatal"` — immediately aborts test execution
|
|
1397
|
+
*/
|
|
1398
|
+
severity?: "error" | "warn" | "fatal";
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Schema validation entry for an HTTP request.
|
|
1402
|
+
* Can be a bare schema (severity defaults to `"error"`) or an object with explicit severity.
|
|
1403
|
+
*/
|
|
1404
|
+
export type SchemaEntry<T> = SchemaLike<T> | {
|
|
1405
|
+
schema: SchemaLike<T>;
|
|
1406
|
+
severity?: "error" | "warn" | "fatal";
|
|
1407
|
+
};
|
|
1408
|
+
/**
|
|
1409
|
+
* Schema configuration for automatic HTTP request/response validation.
|
|
1410
|
+
*
|
|
1411
|
+
* @example
|
|
1412
|
+
* ```ts
|
|
1413
|
+
* ctx.http.post(url, {
|
|
1414
|
+
* json: payload,
|
|
1415
|
+
* schema: {
|
|
1416
|
+
* request: RequestBodySchema,
|
|
1417
|
+
* response: ResponseSchema,
|
|
1418
|
+
* query: QueryParamsSchema,
|
|
1419
|
+
* },
|
|
1420
|
+
* });
|
|
1421
|
+
* ```
|
|
1422
|
+
*/
|
|
1423
|
+
export interface HttpSchemaOptions {
|
|
1424
|
+
/** Validate the request body (json option) before sending */
|
|
1425
|
+
request?: SchemaEntry<unknown>;
|
|
1426
|
+
/** Validate the response body after receiving */
|
|
1427
|
+
response?: SchemaEntry<unknown>;
|
|
1428
|
+
/** Validate the query/searchParams before sending */
|
|
1429
|
+
query?: SchemaEntry<unknown>;
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Details for assertion (actual/expected values).
|
|
1433
|
+
*
|
|
1434
|
+
* @example
|
|
1435
|
+
* ```ts
|
|
1436
|
+
* ctx.assert(res.status === 200, "Expected 200", {
|
|
1437
|
+
* actual: res.status,
|
|
1438
|
+
* expected: 200,
|
|
1439
|
+
* });
|
|
1440
|
+
* ```
|
|
1441
|
+
*/
|
|
1442
|
+
export interface AssertionDetails {
|
|
1443
|
+
actual?: unknown;
|
|
1444
|
+
expected?: unknown;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Input for explicit assertion result object.
|
|
1448
|
+
*
|
|
1449
|
+
* @example
|
|
1450
|
+
* ```ts
|
|
1451
|
+
* ctx.assertResult({
|
|
1452
|
+
* passed: res.status === 200,
|
|
1453
|
+
* actual: res.status,
|
|
1454
|
+
* expected: 200,
|
|
1455
|
+
* });
|
|
1456
|
+
* ```
|
|
1457
|
+
*/
|
|
1458
|
+
export interface AssertionResultInput {
|
|
1459
|
+
passed: boolean;
|
|
1460
|
+
actual?: unknown;
|
|
1461
|
+
expected?: unknown;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Options for metric reporting.
|
|
1465
|
+
*
|
|
1466
|
+
* @example
|
|
1467
|
+
* ```ts
|
|
1468
|
+
* ctx.metric("api_latency", duration, {
|
|
1469
|
+
* unit: "ms",
|
|
1470
|
+
* tags: { endpoint: "/users", method: "GET" },
|
|
1471
|
+
* });
|
|
1472
|
+
* ```
|
|
1473
|
+
*/
|
|
1474
|
+
export interface MetricOptions {
|
|
1475
|
+
/** Display unit (e.g., "ms", "bytes", "count", "%") */
|
|
1476
|
+
unit?: string;
|
|
1477
|
+
/** Key-value tags for grouping/filtering in dashboards */
|
|
1478
|
+
tags?: Record<string, string>;
|
|
1479
|
+
}
|
|
1480
|
+
/**
|
|
1481
|
+
* Aggregation function for threshold evaluation.
|
|
1482
|
+
*
|
|
1483
|
+
* - `avg`, `min`, `max`: basic statistics
|
|
1484
|
+
* - `p50`, `p90`, `p95`, `p99`: percentiles
|
|
1485
|
+
* - `count`: total number of data points
|
|
1486
|
+
*/
|
|
1487
|
+
export type ThresholdAggregation = "avg" | "min" | "max" | "p50" | "p90" | "p95" | "p99" | "count";
|
|
1488
|
+
/**
|
|
1489
|
+
* A single threshold rule: `"<200"` or `"<=500"`.
|
|
1490
|
+
*
|
|
1491
|
+
* The string format is: `operator + number`, where operator is `<` or `<=`.
|
|
1492
|
+
*/
|
|
1493
|
+
export type ThresholdExpression = string;
|
|
1494
|
+
/**
|
|
1495
|
+
* Per-metric threshold rules keyed by aggregation function.
|
|
1496
|
+
*
|
|
1497
|
+
* @example
|
|
1498
|
+
* ```ts
|
|
1499
|
+
* { p95: "<200", avg: "<100", max: "<2000" }
|
|
1500
|
+
* ```
|
|
1501
|
+
*/
|
|
1502
|
+
export type MetricThresholdRules = Partial<Record<ThresholdAggregation, ThresholdExpression>>;
|
|
1503
|
+
/**
|
|
1504
|
+
* Threshold configuration: metric name → rules (or shorthand string for avg).
|
|
1505
|
+
*
|
|
1506
|
+
* @example
|
|
1507
|
+
* ```ts
|
|
1508
|
+
* {
|
|
1509
|
+
* thresholds: {
|
|
1510
|
+
* "http_duration_ms": { p95: "<200", avg: "<100" },
|
|
1511
|
+
* "error_rate": "<0.01", // shorthand for { avg: "<0.01" }
|
|
1512
|
+
* }
|
|
1513
|
+
* }
|
|
1514
|
+
* ```
|
|
1515
|
+
*/
|
|
1516
|
+
export type ThresholdConfig = Record<string, MetricThresholdRules | ThresholdExpression>;
|
|
1517
|
+
/**
|
|
1518
|
+
* Result of evaluating a single threshold rule.
|
|
1519
|
+
*/
|
|
1520
|
+
export interface ThresholdResult {
|
|
1521
|
+
/** Metric key (e.g., "http_duration_ms") */
|
|
1522
|
+
metric: string;
|
|
1523
|
+
/** Aggregation function used (e.g., "p95") */
|
|
1524
|
+
aggregation: ThresholdAggregation;
|
|
1525
|
+
/** The threshold expression (e.g., "<200") */
|
|
1526
|
+
threshold: ThresholdExpression;
|
|
1527
|
+
/** The actual computed value */
|
|
1528
|
+
actual: number;
|
|
1529
|
+
/** Whether the threshold was met */
|
|
1530
|
+
pass: boolean;
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* Summary of all threshold evaluations for a run.
|
|
1534
|
+
*/
|
|
1535
|
+
export interface ThresholdSummary {
|
|
1536
|
+
/** All individual threshold results */
|
|
1537
|
+
results: ThresholdResult[];
|
|
1538
|
+
/** True if all thresholds passed */
|
|
1539
|
+
pass: boolean;
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* Options for `ctx.pollUntil()`.
|
|
1543
|
+
*/
|
|
1544
|
+
export interface PollUntilOptions {
|
|
1545
|
+
/** Maximum time to wait before giving up (milliseconds). */
|
|
1546
|
+
timeoutMs: number;
|
|
1547
|
+
/** Interval between poll attempts (milliseconds). Default: 1000. */
|
|
1548
|
+
intervalMs?: number;
|
|
1549
|
+
/**
|
|
1550
|
+
* Called when polling times out instead of throwing an error.
|
|
1551
|
+
* If present, `pollUntil` resolves silently on timeout.
|
|
1552
|
+
* If absent, `pollUntil` throws on timeout (test fails).
|
|
1553
|
+
*
|
|
1554
|
+
* @param lastError The last error thrown by the polling function, if any.
|
|
1555
|
+
*/
|
|
1556
|
+
onTimeout?: (lastError?: Error) => void;
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* API trace data for network call reporting.
|
|
1560
|
+
* Usually auto-generated by `ctx.http` — only use `ctx.trace()` directly
|
|
1561
|
+
* for non-HTTP calls (e.g., gRPC, WebSocket) or custom instrumentation.
|
|
1562
|
+
*
|
|
1563
|
+
* @example Manual trace for a non-HTTP call
|
|
1564
|
+
* ```ts
|
|
1565
|
+
* const start = Date.now();
|
|
1566
|
+
* const result = await grpcClient.getUser({ id: 1 });
|
|
1567
|
+
* ctx.trace({
|
|
1568
|
+
* name: "gRPC GetUser",
|
|
1569
|
+
* method: "gRPC",
|
|
1570
|
+
* url: "user-service:50051/GetUser",
|
|
1571
|
+
* status: result.ok ? 200 : 500,
|
|
1572
|
+
* duration: Date.now() - start,
|
|
1573
|
+
* });
|
|
1574
|
+
* ```
|
|
1575
|
+
*/
|
|
1576
|
+
export interface ApiTrace {
|
|
1577
|
+
/** Optional human-readable name to quickly identify the API (e.g., "Create User", "Get Orders") */
|
|
1578
|
+
name?: string;
|
|
1579
|
+
/** Optional detailed description of what this API call does */
|
|
1580
|
+
description?: string;
|
|
1581
|
+
/** HTTP method (GET, POST, etc.) */
|
|
1582
|
+
method: string;
|
|
1583
|
+
/** Request URL */
|
|
1584
|
+
url: string;
|
|
1585
|
+
/** Response status code */
|
|
1586
|
+
status: number;
|
|
1587
|
+
/** Request duration in milliseconds */
|
|
1588
|
+
duration: number;
|
|
1589
|
+
/** Optional request headers */
|
|
1590
|
+
requestHeaders?: Record<string, string>;
|
|
1591
|
+
/** Optional request body */
|
|
1592
|
+
requestBody?: unknown;
|
|
1593
|
+
/** Optional response headers */
|
|
1594
|
+
responseHeaders?: Record<string, string>;
|
|
1595
|
+
/** Optional response body */
|
|
1596
|
+
responseBody?: unknown;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* A typed interaction record emitted by plugins.
|
|
1600
|
+
*
|
|
1601
|
+
* All fields except `detail` are required, ensuring consistent timeline
|
|
1602
|
+
* rendering, filtering, and analytics across all plugin domains.
|
|
1603
|
+
*
|
|
1604
|
+
* @example Browser interaction
|
|
1605
|
+
* ```ts
|
|
1606
|
+
* ctx.action({
|
|
1607
|
+
* category: "browser:click",
|
|
1608
|
+
* target: "#submit-btn",
|
|
1609
|
+
* duration: 620,
|
|
1610
|
+
* status: "ok",
|
|
1611
|
+
* detail: { autoWaitMs: 580 },
|
|
1612
|
+
* });
|
|
1613
|
+
* ```
|
|
1614
|
+
*
|
|
1615
|
+
* @example HTTP request (auto-emitted by ctx.trace())
|
|
1616
|
+
* ```ts
|
|
1617
|
+
* ctx.action({
|
|
1618
|
+
* category: "http:request",
|
|
1619
|
+
* target: "POST /api/auth/login",
|
|
1620
|
+
* duration: 350,
|
|
1621
|
+
* status: "ok",
|
|
1622
|
+
* detail: { method: "POST", url: "/api/auth/login", httpStatus: 200 },
|
|
1623
|
+
* });
|
|
1624
|
+
* ```
|
|
1625
|
+
*/
|
|
1626
|
+
export interface GlubeanAction {
|
|
1627
|
+
/**
|
|
1628
|
+
* Namespaced action category for routing, filtering, and rendering.
|
|
1629
|
+
*
|
|
1630
|
+
* Convention: `"domain:verb"` where `domain` identifies the plugin
|
|
1631
|
+
* (`http`, `browser`, `mcp`, `db`) and `verb` identifies the operation
|
|
1632
|
+
* (`request`, `click`, `assert`, `query`).
|
|
1633
|
+
*/
|
|
1634
|
+
category: string;
|
|
1635
|
+
/**
|
|
1636
|
+
* The target of the action — what was acted upon.
|
|
1637
|
+
* Must be machine-readable for aggregation and search.
|
|
1638
|
+
*
|
|
1639
|
+
* Examples: `"#submit-btn"`, `"POST /api/users"`, `"get_weather"`.
|
|
1640
|
+
*/
|
|
1641
|
+
target: string;
|
|
1642
|
+
/** How long the action took, in milliseconds. */
|
|
1643
|
+
duration: number;
|
|
1644
|
+
/**
|
|
1645
|
+
* Outcome of the action.
|
|
1646
|
+
* - `"ok"` — completed successfully
|
|
1647
|
+
* - `"error"` — failed (e.g., element not found, assertion mismatch)
|
|
1648
|
+
* - `"timeout"` — timed out (e.g., actionability check exceeded limit)
|
|
1649
|
+
*/
|
|
1650
|
+
status: "ok" | "error" | "timeout";
|
|
1651
|
+
/**
|
|
1652
|
+
* Domain-specific payload. Optional.
|
|
1653
|
+
* Values must be JSON-serializable.
|
|
1654
|
+
*/
|
|
1655
|
+
detail?: Record<string, unknown>;
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* A generic structured event emitted by plugins.
|
|
1659
|
+
*
|
|
1660
|
+
* Unlike `GlubeanAction` (which has required fields for timeline rendering),
|
|
1661
|
+
* `GlubeanEvent` is a loosely-typed container for any structured data that
|
|
1662
|
+
* plugins want to surface in the dashboard.
|
|
1663
|
+
*
|
|
1664
|
+
* Dashboard plugins can register custom renderers keyed on `type` to
|
|
1665
|
+
* control how events are displayed. Without a custom renderer, events appear
|
|
1666
|
+
* as collapsible JSON in the detail panel.
|
|
1667
|
+
*
|
|
1668
|
+
* @example Screenshot captured
|
|
1669
|
+
* ```ts
|
|
1670
|
+
* ctx.event({
|
|
1671
|
+
* type: "browser:screenshot",
|
|
1672
|
+
* data: { path: "/screenshots/login.png", fullPage: true, sizeKb: 142 },
|
|
1673
|
+
* });
|
|
1674
|
+
* ```
|
|
1675
|
+
*
|
|
1676
|
+
* @example MCP server connected
|
|
1677
|
+
* ```ts
|
|
1678
|
+
* ctx.event({
|
|
1679
|
+
* type: "mcp:connected",
|
|
1680
|
+
* data: { server: "weather-api", transport: "stdio", tools: ["get_weather"] },
|
|
1681
|
+
* });
|
|
1682
|
+
* ```
|
|
1683
|
+
*/
|
|
1684
|
+
export interface GlubeanEvent {
|
|
1685
|
+
/**
|
|
1686
|
+
* Namespaced event type. Convention: `"domain:noun"` or `"domain:description"`.
|
|
1687
|
+
* Examples: `"browser:screenshot"`, `"mcp:connected"`, `"db:slow-query-warning"`.
|
|
1688
|
+
*/
|
|
1689
|
+
type: string;
|
|
1690
|
+
/** Structured payload. Must be JSON-serializable. */
|
|
1691
|
+
data: Record<string, unknown>;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Result of a single assertion within a test.
|
|
1695
|
+
*/
|
|
1696
|
+
export interface AssertionResult {
|
|
1697
|
+
/** Whether the assertion passed */
|
|
1698
|
+
passed: boolean;
|
|
1699
|
+
/** Human-readable description of the assertion */
|
|
1700
|
+
message: string;
|
|
1701
|
+
/** The actual value received (optional) */
|
|
1702
|
+
actual?: unknown;
|
|
1703
|
+
/** The expected value (optional) */
|
|
1704
|
+
expected?: unknown;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Metadata for a test (unified for all test types).
|
|
1708
|
+
*/
|
|
1709
|
+
export interface TestMeta {
|
|
1710
|
+
/** Unique identifier for the test */
|
|
1711
|
+
id: string;
|
|
1712
|
+
/** Human-readable name (defaults to id) */
|
|
1713
|
+
name?: string;
|
|
1714
|
+
/** Detailed description */
|
|
1715
|
+
description?: string;
|
|
1716
|
+
/**
|
|
1717
|
+
* Tags for filtering (e.g., ["smoke", "auth"]).
|
|
1718
|
+
* Accepts a single string or an array.
|
|
1719
|
+
*
|
|
1720
|
+
* @example
|
|
1721
|
+
* tags: "smoke"
|
|
1722
|
+
* tags: ["smoke", "auth"]
|
|
1723
|
+
*/
|
|
1724
|
+
tags?: string | string[];
|
|
1725
|
+
/** Timeout in milliseconds (default: 30000) */
|
|
1726
|
+
timeout?: number;
|
|
1727
|
+
/**
|
|
1728
|
+
* If true, run only focused tests in this file/run context.
|
|
1729
|
+
* If both `only` and `skip` are true, `skip` takes precedence.
|
|
1730
|
+
*/
|
|
1731
|
+
only?: boolean;
|
|
1732
|
+
/** If true, skip this test (takes precedence over `only`) */
|
|
1733
|
+
skip?: boolean;
|
|
1734
|
+
/**
|
|
1735
|
+
* Filter rows before generating tests (data-driven only).
|
|
1736
|
+
* Return `true` to include the row, `false` to exclude.
|
|
1737
|
+
* Applied before test registration — excluded rows never become tests.
|
|
1738
|
+
*
|
|
1739
|
+
* @example Exclude invalid data
|
|
1740
|
+
* ```ts
|
|
1741
|
+
* filter: (row) => !!row.endpoint && !!row.expected
|
|
1742
|
+
* ```
|
|
1743
|
+
*
|
|
1744
|
+
* @example Only include specific country
|
|
1745
|
+
* ```ts
|
|
1746
|
+
* filter: (row) => row.country === "JP"
|
|
1747
|
+
* ```
|
|
1748
|
+
*/
|
|
1749
|
+
filter?: (row: Record<string, unknown>, index: number) => boolean;
|
|
1750
|
+
/**
|
|
1751
|
+
* Auto-tag tests with values from data row fields.
|
|
1752
|
+
* Each field generates a tag in `"field:value"` format.
|
|
1753
|
+
* Accepts a single field name or an array.
|
|
1754
|
+
*
|
|
1755
|
+
* Combined with static `tags` — both are included in the final tag list.
|
|
1756
|
+
* Use `glubean run --tag country:JP` to filter at runtime.
|
|
1757
|
+
*
|
|
1758
|
+
* @example Single field
|
|
1759
|
+
* ```ts
|
|
1760
|
+
* tagFields: "country"
|
|
1761
|
+
* // Row { country: "JP" } → tags include "country:JP"
|
|
1762
|
+
* ```
|
|
1763
|
+
*
|
|
1764
|
+
* @example Multiple fields
|
|
1765
|
+
* ```ts
|
|
1766
|
+
* tagFields: ["country", "region"]
|
|
1767
|
+
* // Row { country: "JP", region: "APAC" } → tags include "country:JP", "region:APAC"
|
|
1768
|
+
* ```
|
|
1769
|
+
*/
|
|
1770
|
+
tagFields?: string | string[];
|
|
1771
|
+
/**
|
|
1772
|
+
* If true, workflow-level metrics (total duration + per-step durations)
|
|
1773
|
+
* are exported to the Prometheus metrics endpoint. Endpoint latency
|
|
1774
|
+
* metrics are always collected regardless of this flag.
|
|
1775
|
+
*
|
|
1776
|
+
* Use this for tests where workflow performance matters and you want
|
|
1777
|
+
* to track it in Grafana. Defaults to false to avoid cardinality explosion.
|
|
1778
|
+
*/
|
|
1779
|
+
enableMetrics?: boolean;
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Metadata for a step within a test.
|
|
1783
|
+
*/
|
|
1784
|
+
export interface StepMeta {
|
|
1785
|
+
/** Step name (used for display and reporting) */
|
|
1786
|
+
name: string;
|
|
1787
|
+
/** Optional timeout override for this step */
|
|
1788
|
+
timeout?: number;
|
|
1789
|
+
/** Number of retries for this step (default: 0) */
|
|
1790
|
+
retries?: number;
|
|
1791
|
+
/**
|
|
1792
|
+
* Logical group this step belongs to (set by `.group()`).
|
|
1793
|
+
* Used for visual grouping in reports and dashboards.
|
|
1794
|
+
*/
|
|
1795
|
+
group?: string;
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* The function signature for a simple test (no state).
|
|
1799
|
+
*/
|
|
1800
|
+
export type SimpleTestFunction = (ctx: TestContext) => Promise<void>;
|
|
1801
|
+
/**
|
|
1802
|
+
* The function signature for a data-driven test (test.each).
|
|
1803
|
+
* Receives TestContext and the data row from the table.
|
|
1804
|
+
*
|
|
1805
|
+
* @template T The data row type
|
|
1806
|
+
*
|
|
1807
|
+
* @example
|
|
1808
|
+
* ```ts
|
|
1809
|
+
* const fn: EachTestFunction<{ id: number; expected: number }> =
|
|
1810
|
+
* async (ctx, { id, expected }) => {
|
|
1811
|
+
* const res = await ctx.http.get(`${ctx.vars.require("BASE_URL")}/users/${id}`, {
|
|
1812
|
+
* throwHttpErrors: false,
|
|
1813
|
+
* });
|
|
1814
|
+
* ctx.expect(res.status).toBe(expected);
|
|
1815
|
+
* };
|
|
1816
|
+
* ```
|
|
1817
|
+
*/
|
|
1818
|
+
export type EachTestFunction<T> = (ctx: TestContext, data: T) => Promise<void>;
|
|
1819
|
+
/**
|
|
1820
|
+
* The function signature for a step with state.
|
|
1821
|
+
* @template S The state type passed between steps
|
|
1822
|
+
*/
|
|
1823
|
+
export type StepFunction<S = unknown> = (ctx: TestContext, state: S) => Promise<S | void>;
|
|
1824
|
+
/**
|
|
1825
|
+
* Setup function that runs before all steps.
|
|
1826
|
+
* Returns state that will be passed to steps and teardown.
|
|
1827
|
+
* @template S The state type to return
|
|
1828
|
+
*/
|
|
1829
|
+
export type SetupFunction<S = unknown> = (ctx: TestContext) => Promise<S>;
|
|
1830
|
+
/**
|
|
1831
|
+
* Teardown function that runs after all steps.
|
|
1832
|
+
*
|
|
1833
|
+
* **Important**: Teardown always runs, even if:
|
|
1834
|
+
* - Setup fails
|
|
1835
|
+
* - Any step fails
|
|
1836
|
+
* - The test times out
|
|
1837
|
+
*
|
|
1838
|
+
* Use teardown to clean up resources (close connections, delete test data, etc.).
|
|
1839
|
+
* Handle errors gracefully as teardown failures won't prevent other cleanup.
|
|
1840
|
+
*
|
|
1841
|
+
* @template S The state type received from setup
|
|
1842
|
+
*
|
|
1843
|
+
* @example
|
|
1844
|
+
* ```ts
|
|
1845
|
+
* const teardown: TeardownFunction<{ userId: string }> = async (ctx, state) => {
|
|
1846
|
+
* // Always runs for cleanup
|
|
1847
|
+
* try {
|
|
1848
|
+
* await deleteUser(state.userId);
|
|
1849
|
+
* } catch (err) {
|
|
1850
|
+
* ctx.log("Cleanup warning:", err.message);
|
|
1851
|
+
* // Don't throw - allow other cleanup to continue
|
|
1852
|
+
* }
|
|
1853
|
+
* };
|
|
1854
|
+
* ```
|
|
1855
|
+
*/
|
|
1856
|
+
export type TeardownFunction<S = unknown> = (ctx: TestContext, state: S) => Promise<void>;
|
|
1857
|
+
/**
|
|
1858
|
+
* Internal step definition (stored in builder).
|
|
1859
|
+
*/
|
|
1860
|
+
export interface StepDefinition<S = unknown> {
|
|
1861
|
+
meta: StepMeta;
|
|
1862
|
+
fn: StepFunction<S>;
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* A complete test definition (output of builder).
|
|
1866
|
+
* @template S The state type for multi-step tests
|
|
1867
|
+
*/
|
|
1868
|
+
export interface Test<S = unknown> {
|
|
1869
|
+
/** Test metadata */
|
|
1870
|
+
meta: TestMeta;
|
|
1871
|
+
/** Test type: 'simple' for single-function tests, 'steps' for multi-step */
|
|
1872
|
+
type: "simple" | "steps";
|
|
1873
|
+
/** The test function (for simple tests) */
|
|
1874
|
+
fn?: SimpleTestFunction;
|
|
1875
|
+
/** Setup function (for step-based tests) */
|
|
1876
|
+
setup?: SetupFunction<S>;
|
|
1877
|
+
/** Teardown function (for step-based tests) */
|
|
1878
|
+
teardown?: TeardownFunction<S>;
|
|
1879
|
+
/** Steps (for step-based tests) */
|
|
1880
|
+
steps?: StepDefinition<S>[];
|
|
1881
|
+
/**
|
|
1882
|
+
* Fixture definitions provided by `test.extend()`.
|
|
1883
|
+
* The runner resolves these and merges results into `TestContext`
|
|
1884
|
+
* before invoking the test function / steps.
|
|
1885
|
+
*/
|
|
1886
|
+
fixtures?: Record<string, ExtensionFn<any>>;
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Factory function for a context extension (fixture).
|
|
1890
|
+
*
|
|
1891
|
+
* Two forms:
|
|
1892
|
+
* - **Simple factory**: `(ctx) => instance` — called once per test, return value
|
|
1893
|
+
* is merged into ctx.
|
|
1894
|
+
* - **Lifecycle factory**: `(ctx, use) => { setup; await use(instance); teardown; }`
|
|
1895
|
+
* — wraps the test execution; cleanup runs after the `use` callback resolves.
|
|
1896
|
+
*
|
|
1897
|
+
* @template T The type of the fixture instance
|
|
1898
|
+
*
|
|
1899
|
+
* @example Simple factory
|
|
1900
|
+
* ```ts
|
|
1901
|
+
* const authFixture: ExtensionFn<AuthClient> = (ctx) =>
|
|
1902
|
+
* createAuth(ctx.vars.require("AUTH_URL"));
|
|
1903
|
+
* ```
|
|
1904
|
+
*
|
|
1905
|
+
* @example Lifecycle factory
|
|
1906
|
+
* ```ts
|
|
1907
|
+
* const dbFixture: ExtensionFn<DbClient> = async (ctx, use) => {
|
|
1908
|
+
* const db = await connect(ctx.vars.require("DB_URL"));
|
|
1909
|
+
* await use(db);
|
|
1910
|
+
* await db.disconnect();
|
|
1911
|
+
* };
|
|
1912
|
+
* ```
|
|
1913
|
+
*/
|
|
1914
|
+
export type ExtensionFn<T> = ((ctx: TestContext) => T | Promise<T>) | ((ctx: TestContext, use: (instance: T) => Promise<void>) => Promise<void>);
|
|
1915
|
+
/**
|
|
1916
|
+
* Resolve the instance type from a single extension factory function.
|
|
1917
|
+
*
|
|
1918
|
+
* Checks the lifecycle form first (2-param with `use` callback) because it
|
|
1919
|
+
* is more specific, then falls back to the simple factory forms.
|
|
1920
|
+
*/
|
|
1921
|
+
export type ResolveExtension<F> = F extends (ctx: TestContext, use: (instance: infer T) => Promise<void>) => Promise<void> ? T : F extends (ctx: TestContext) => Promise<infer T> ? T : F extends (ctx: TestContext) => infer T ? T : never;
|
|
1922
|
+
/**
|
|
1923
|
+
* Map of extension factory functions to their resolved instance types.
|
|
1924
|
+
*
|
|
1925
|
+
* @example
|
|
1926
|
+
* ```ts
|
|
1927
|
+
* type R = ResolveExtensions<{
|
|
1928
|
+
* auth: (ctx: TestContext) => AuthClient;
|
|
1929
|
+
* db: (ctx: TestContext, use: (i: DbClient) => Promise<void>) => Promise<void>;
|
|
1930
|
+
* }>;
|
|
1931
|
+
* // R = { auth: AuthClient; db: DbClient }
|
|
1932
|
+
* ```
|
|
1933
|
+
*/
|
|
1934
|
+
export type ResolveExtensions<E> = {
|
|
1935
|
+
[K in keyof E]: ResolveExtension<E[K]>;
|
|
1936
|
+
};
|
|
1937
|
+
/**
|
|
1938
|
+
* Metadata registered to the global registry (for scanning).
|
|
1939
|
+
*/
|
|
1940
|
+
export interface RegisteredTestMeta {
|
|
1941
|
+
/** Test ID */
|
|
1942
|
+
id: string;
|
|
1943
|
+
/** Test name */
|
|
1944
|
+
name: string;
|
|
1945
|
+
/** Test type */
|
|
1946
|
+
type: "simple" | "steps";
|
|
1947
|
+
/** Tags */
|
|
1948
|
+
tags?: string[];
|
|
1949
|
+
/** Description */
|
|
1950
|
+
description?: string;
|
|
1951
|
+
/** Step metadata (for step-based tests) */
|
|
1952
|
+
steps?: {
|
|
1953
|
+
name: string;
|
|
1954
|
+
group?: string;
|
|
1955
|
+
}[];
|
|
1956
|
+
/** Has setup hook */
|
|
1957
|
+
hasSetup?: boolean;
|
|
1958
|
+
/** Has teardown hook */
|
|
1959
|
+
hasTeardown?: boolean;
|
|
1960
|
+
/** Source file (set by scanner) */
|
|
1961
|
+
file?: string;
|
|
1962
|
+
/** Export name in the module */
|
|
1963
|
+
exportName?: string;
|
|
1964
|
+
/**
|
|
1965
|
+
* Trace grouping ID — the unresolved template ID for pick tests.
|
|
1966
|
+
* When set, the CLI uses this as the trace directory name so all
|
|
1967
|
+
* pick variants land in one directory for easy diffing.
|
|
1968
|
+
*/
|
|
1969
|
+
groupId?: string;
|
|
1970
|
+
}
|
|
1971
|
+
//# sourceMappingURL=types.d.ts.map
|