@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.
@@ -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