@specific.dev/spectest 0.26.0 → 0.28.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/aws-sigv4.d.ts +42 -0
- package/dist/aws-sigv4.js +166 -0
- package/dist/browser.d.ts +314 -0
- package/dist/browser.js +1320 -0
- package/dist/components/email.d.ts +135 -0
- package/dist/components/email.js +271 -0
- package/dist/components/expo.d.ts +69 -0
- package/dist/components/expo.js +125 -0
- package/dist/components/index.d.ts +8 -0
- package/dist/components/index.js +18 -0
- package/dist/components/k3s.d.ts +172 -0
- package/dist/components/k3s.js +1124 -0
- package/dist/components/postgres.d.ts +93 -0
- package/dist/components/postgres.js +58 -0
- package/dist/components/replayFake.d.ts +169 -0
- package/dist/components/replayFake.js +738 -0
- package/dist/components/s3.d.ts +99 -0
- package/dist/components/s3.js +81 -0
- package/dist/components/supabase.d.ts +197 -0
- package/dist/components/supabase.js +1003 -0
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.js +4611 -0
- package/dist/ids.d.ts +2 -0
- package/{src/ids.ts → dist/ids.js} +46 -50
- package/dist/index.d.ts +1328 -0
- package/dist/index.js +769 -0
- package/dist/ingress.d.ts +114 -0
- package/dist/ingress.js +210 -0
- package/dist/inspect.d.ts +228 -0
- package/dist/inspect.js +429 -0
- package/dist/locator.d.ts +260 -0
- package/dist/locator.js +293 -0
- package/dist/mobile.d.ts +71 -0
- package/dist/mobile.js +65 -0
- package/dist/record-secrets.d.ts +9 -0
- package/{src/record-secrets.ts → dist/record-secrets.js} +13 -15
- package/dist/recorder.d.ts +527 -0
- package/dist/recorder.js +219 -0
- package/dist/redis.d.ts +54 -0
- package/dist/redis.js +126 -0
- package/dist/replay-bundle.d.ts +38 -0
- package/{src/replay-bundle.ts → dist/replay-bundle.js} +29 -47
- package/dist/resolver.d.ts +1 -0
- package/dist/resolver.js +309 -0
- package/dist/s3.d.ts +89 -0
- package/dist/s3.js +198 -0
- package/dist/sql.d.ts +74 -0
- package/dist/sql.js +151 -0
- package/dist/terminal.d.ts +161 -0
- package/dist/terminal.js +538 -0
- package/package.json +24 -9
- package/src/browser.ts +0 -1819
- package/src/components/email.ts +0 -398
- package/src/components/expo.ts +0 -167
- package/src/components/index.ts +0 -63
- package/src/components/k3s.ts +0 -1312
- package/src/components/postgres.ts +0 -105
- package/src/components/replayFake.ts +0 -848
- package/src/components/s3.ts +0 -132
- package/src/components/supabase.ts +0 -1299
- package/src/daemon.ts +0 -4969
- package/src/index.ts +0 -2350
- package/src/ingress.ts +0 -288
- package/src/inspect.ts +0 -673
- package/src/locator.ts +0 -594
- package/src/mobile.ts +0 -133
- package/src/recorder.ts +0 -817
- package/src/redis.ts +0 -202
- package/src/resolver.ts +0 -351
- package/src/s3.ts +0 -333
- package/src/sql.ts +0 -243
- package/src/terminal.ts +0 -740
- package/src/vendor/rrweb-plugin-console-record.umd.js +0 -521
- package/src/vendor/rrweb-record.min.js +0 -5061
package/dist/mobile.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Mobile app handle for tests — drives an Expo/React-Native-Web app rendered
|
|
2
|
+
// in a phone-emulated headless Chromium and recorded as an rrweb session that
|
|
3
|
+
// the dashboard replays inside a phone bezel.
|
|
4
|
+
//
|
|
5
|
+
// `ctx.mobile(ctx.svc.app)` opens one of these already pointed at the app. The
|
|
6
|
+
// surface is the SAME Playwright-native one as `ctx.browser()` (the shared
|
|
7
|
+
// `Browser` session + `Locator`s from browser.ts/locator.ts) plus two mobile
|
|
8
|
+
// extensions: a `touchscreen` and `swipe`. There is no mobile-specific locator
|
|
9
|
+
// type — `getByTestId`/`getByRole`/`getByText`/… return the same `Locator`,
|
|
10
|
+
// and on a mobile session a locator's `tap()` dispatches a real CDP touch with
|
|
11
|
+
// the press dwell RN Pressables need (the `mobileStrategy` in locator.ts;
|
|
12
|
+
// desktop `tap()` throws and steers you to `click()`).
|
|
13
|
+
//
|
|
14
|
+
// This file is now just the `MobileApp` handle (`ctx.mobile` accepts it) and
|
|
15
|
+
// the `Mobile` public view of the shared backend. React Native Web renders
|
|
16
|
+
// `testID="x"` to `data-testid="x"` and `accessibilityLabel` to `aria-label`,
|
|
17
|
+
// so `getByTestId`/`getByLabel` map straight onto Playwright's built-ins.
|
|
18
|
+
import { acquirePersistentMobileBackend, openMobileBackend } from "./browser.js";
|
|
19
|
+
/** Branded handle a mobile-app component (e.g. `expo()`) exposes on
|
|
20
|
+
* `ctx.svc.<name>`. The brand is a `Symbol.for` key so `JSON.stringify`
|
|
21
|
+
* drops it (wire-invisible) while `ctx.mobile(...)` can still type-check
|
|
22
|
+
* against it. */
|
|
23
|
+
export const MOBILE_APP = Symbol.for("spectest.mobileApp");
|
|
24
|
+
/** True if `x` is a {@link MobileApp} handle. */
|
|
25
|
+
export function isMobileApp(x) {
|
|
26
|
+
return (typeof x === "object" &&
|
|
27
|
+
x !== null &&
|
|
28
|
+
x[MOBILE_APP] === true &&
|
|
29
|
+
typeof x.url === "string");
|
|
30
|
+
}
|
|
31
|
+
/** Build a {@link MobileApp} handle from a resolved URL (and an optional
|
|
32
|
+
* init script installed before the session's first navigation). */
|
|
33
|
+
export function mobileApp(url, initScript) {
|
|
34
|
+
return { [MOBILE_APP]: true, url, initScript };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Open an EPHEMERAL phone-emulated session pointed at `url` (`close()`
|
|
38
|
+
* destroys it). Library callers only — the daemon's `ctx.mobile(app)` goes
|
|
39
|
+
* through {@link openPersistentMobile} so sessions survive across tests.
|
|
40
|
+
*/
|
|
41
|
+
export async function openMobile(opts) {
|
|
42
|
+
const backend = await openMobileBackend({
|
|
43
|
+
frame: "mobile",
|
|
44
|
+
url: opts.url,
|
|
45
|
+
recorder: opts.recorder,
|
|
46
|
+
});
|
|
47
|
+
// MobileBackend is a superset of Mobile (adds the low-level primitives).
|
|
48
|
+
return backend;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Acquire the persistent phone-emulated session for an app (one per app URL,
|
|
52
|
+
* created on first use). The daemon calls this from `ctx.mobile(app)` with a
|
|
53
|
+
* per-test rrweb recorder; the resulting record carries `frame: "mobile"` so
|
|
54
|
+
* the dashboard renders a phone bezel. `detach` is the test-end hook;
|
|
55
|
+
* `mobile.close()` destroys the session for real.
|
|
56
|
+
*/
|
|
57
|
+
export async function openPersistentMobile(opts) {
|
|
58
|
+
const { browser, attached, detach } = await acquirePersistentMobileBackend(opts.url, opts.recorder, opts.initScript);
|
|
59
|
+
return {
|
|
60
|
+
mobile: browser,
|
|
61
|
+
attached,
|
|
62
|
+
detach,
|
|
63
|
+
safeAreaInsets: browser.safeAreaInsets,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Replace the eval-scoped secret set. Called by the daemon at the start
|
|
2
|
+
* of each `/eval` with the values the control plane resolved. */
|
|
3
|
+
export declare function setRecordSecrets(secrets: Record<string, string> | undefined): void;
|
|
4
|
+
/** Drop all secrets. Called from the eval's `finally` so nothing survives
|
|
5
|
+
* past the eval that supplied them. */
|
|
6
|
+
export declare function clearRecordSecrets(): void;
|
|
7
|
+
/** Resolve a secret by its platform `ref`. `undefined` if the control
|
|
8
|
+
* plane didn't supply it (ref not configured, or not on the eval path). */
|
|
9
|
+
export declare function getRecordSecret(ref: string): string | undefined;
|
|
@@ -15,27 +15,25 @@
|
|
|
15
15
|
// eval means a secret never persists into anything a replay run can reach.
|
|
16
16
|
// This module is shared (one instance per daemon process) so the daemon
|
|
17
17
|
// writes and the component reads the same map.
|
|
18
|
-
|
|
19
|
-
const SECRETS = new Map<string, string>();
|
|
20
|
-
|
|
18
|
+
const SECRETS = new Map();
|
|
21
19
|
/** Replace the eval-scoped secret set. Called by the daemon at the start
|
|
22
20
|
* of each `/eval` with the values the control plane resolved. */
|
|
23
|
-
export function setRecordSecrets(secrets
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
export function setRecordSecrets(secrets) {
|
|
22
|
+
SECRETS.clear();
|
|
23
|
+
if (!secrets)
|
|
24
|
+
return;
|
|
25
|
+
for (const [ref, value] of Object.entries(secrets)) {
|
|
26
|
+
if (typeof value === "string")
|
|
27
|
+
SECRETS.set(ref, value);
|
|
28
|
+
}
|
|
29
29
|
}
|
|
30
|
-
|
|
31
30
|
/** Drop all secrets. Called from the eval's `finally` so nothing survives
|
|
32
31
|
* past the eval that supplied them. */
|
|
33
|
-
export function clearRecordSecrets()
|
|
34
|
-
|
|
32
|
+
export function clearRecordSecrets() {
|
|
33
|
+
SECRETS.clear();
|
|
35
34
|
}
|
|
36
|
-
|
|
37
35
|
/** Resolve a secret by its platform `ref`. `undefined` if the control
|
|
38
36
|
* plane didn't supply it (ref not configured, or not on the eval path). */
|
|
39
|
-
export function getRecordSecret(ref
|
|
40
|
-
|
|
37
|
+
export function getRecordSecret(ref) {
|
|
38
|
+
return SECRETS.get(ref);
|
|
41
39
|
}
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
export type TestEvent = ExecEvent | AssertionEvent | HttpEvent | KubeEvent | BrowserEvent | DbEvent | RedisEvent | S3Event | TerminalEvent | TerminalStepEvent | WaitEvent | FakeEvent | EnvEvent | EmailEvent;
|
|
2
|
+
interface BaseEvent {
|
|
3
|
+
/** Order of *start* within the test. Reserved when an op begins (see
|
|
4
|
+
* `reserveEvent`), so an op whose nested children finish — and record —
|
|
5
|
+
* before it does still sorts ahead of them. Ops that don't reserve get
|
|
6
|
+
* their seq at record (= finish) time. */
|
|
7
|
+
seq: number;
|
|
8
|
+
/** Milliseconds since the recorder started, captured when the op began
|
|
9
|
+
* (reserved) or, absent a reservation, when it was recorded. */
|
|
10
|
+
tOffsetMs: number;
|
|
11
|
+
/**
|
|
12
|
+
* Optional grouping pointer. When set, this event was emitted inside
|
|
13
|
+
* a larger logical step (e.g. an HTTP call that fed the predicate of
|
|
14
|
+
* a `ctx.poll(...)`) and should render nested under the parent in
|
|
15
|
+
* timelines. The parent event is identified by its `seq`.
|
|
16
|
+
*/
|
|
17
|
+
parentSeq?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ExecEvent extends BaseEvent {
|
|
20
|
+
kind: "exec";
|
|
21
|
+
service: string;
|
|
22
|
+
command: string;
|
|
23
|
+
/**
|
|
24
|
+
* Working directory the command ran from (`ctx.exec(svc, cmd, { cwd })`),
|
|
25
|
+
* if any. Kept off `command` so the sidebar shows just the command; the
|
|
26
|
+
* detail view surfaces it. Absent when the call used no `cwd`.
|
|
27
|
+
*/
|
|
28
|
+
cwd?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The payload piped to the command's stdin (`ctx.exec(svc, cmd, {
|
|
31
|
+
* stdin })`), truncated to OUTPUT_SNIPPET_BYTES. Recorded because the
|
|
32
|
+
* asciicast only captures what the command *wrote* — without this, a
|
|
33
|
+
* `kubectl apply -f -` step shows its error but not the manifest that
|
|
34
|
+
* caused it. Absent when the call piped nothing.
|
|
35
|
+
*/
|
|
36
|
+
stdin?: string;
|
|
37
|
+
stdinTruncated?: boolean;
|
|
38
|
+
exitCode: number;
|
|
39
|
+
/** stdout truncated to OUTPUT_SNIPPET_BYTES. */
|
|
40
|
+
stdout: string;
|
|
41
|
+
stdoutTruncated: boolean;
|
|
42
|
+
stderr: string;
|
|
43
|
+
stderrTruncated: boolean;
|
|
44
|
+
durationMs: number;
|
|
45
|
+
/**
|
|
46
|
+
* Links this exec to the asciicast `TerminalSessionRecord` of its run
|
|
47
|
+
* (mirroring `TerminalEvent.sessionId`): one exec step covers one full
|
|
48
|
+
* CLI run, so the whole recording belongs to this event. Absent on
|
|
49
|
+
* events recorded before exec runs were captured.
|
|
50
|
+
*/
|
|
51
|
+
sessionId?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface AssertionEvent extends BaseEvent {
|
|
54
|
+
kind: "assertion";
|
|
55
|
+
/** Matcher name, e.g. "toBe", "toEqual". */
|
|
56
|
+
matcher: string;
|
|
57
|
+
/** Whether this was `expect(x).not.toBe(...)`. */
|
|
58
|
+
negated: boolean;
|
|
59
|
+
passed: boolean;
|
|
60
|
+
/** Best-effort JSON-safe serialization. */
|
|
61
|
+
actual: unknown;
|
|
62
|
+
expected?: unknown;
|
|
63
|
+
/** Failure message produced by the matcher (only when `passed` is false). */
|
|
64
|
+
error?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Author-supplied label for the assertion. **Required** for a raw assertion
|
|
67
|
+
* (`expectRaw(value, message)`), where it renders as the summary
|
|
68
|
+
* ("ASSERT <message>") and serves as the diff-alignment key since a raw
|
|
69
|
+
* assertion has no provenance `path`. **Optional** for a provenance-linked
|
|
70
|
+
* `expect(value, message)`, where it leads the summary as a clarifying note
|
|
71
|
+
* alongside the rendered matcher/target. Absent when `expect(...)` was called
|
|
72
|
+
* without a message.
|
|
73
|
+
*/
|
|
74
|
+
message?: string;
|
|
75
|
+
/**
|
|
76
|
+
* `seq` of the op (http / db / browser) whose return value this
|
|
77
|
+
* assertion drilled into. Set when `expect()` received a value wrapped
|
|
78
|
+
* by `inspect.wrap()`. The UI nests assertions with `sourceSeq` under
|
|
79
|
+
* the matching op; assertions without it render at top level.
|
|
80
|
+
*/
|
|
81
|
+
sourceSeq?: number;
|
|
82
|
+
/** JSON-path of property accesses from the op return value down to
|
|
83
|
+
* the asserted value (e.g. `["body", "user", "name"]`). Empty for
|
|
84
|
+
* direct asserts on the op return itself. */
|
|
85
|
+
path?: string[];
|
|
86
|
+
}
|
|
87
|
+
export interface DbEvent extends BaseEvent {
|
|
88
|
+
kind: "db";
|
|
89
|
+
/** Service the query ran against (the key in `environment.services`). */
|
|
90
|
+
service: string;
|
|
91
|
+
/** SQL text. For tagged-template calls, values appear as `$1`, `$2`, … */
|
|
92
|
+
query: string;
|
|
93
|
+
/** Parameter values. Best-effort JSON-safe; large/binary values stringified. */
|
|
94
|
+
params?: unknown[];
|
|
95
|
+
/** Rows returned — or rows AFFECTED when `rowsAffected` is set. */
|
|
96
|
+
rowCount?: number;
|
|
97
|
+
/** Set when `rowCount` counts affected rows (a non-RETURNING
|
|
98
|
+
* INSERT/UPDATE/DELETE) rather than a result set — rendered as
|
|
99
|
+
* "N affected" so an `UPDATE → 0 rows` can't read as "nothing updated". */
|
|
100
|
+
rowsAffected?: boolean;
|
|
101
|
+
/** Captured rows for table rendering in the web UI. Capped at
|
|
102
|
+
* MAX_DB_ROWS; `rowsTruncated` is set when there were more. */
|
|
103
|
+
rows?: unknown[];
|
|
104
|
+
rowsTruncated?: boolean;
|
|
105
|
+
/** Column names in the order Bun.SQL returned them. Derived from the
|
|
106
|
+
* first captured row, so omitted when no rows. */
|
|
107
|
+
columns?: string[];
|
|
108
|
+
durationMs: number;
|
|
109
|
+
/** Set if the driver threw. */
|
|
110
|
+
error?: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One Redis/Valkey command issued through the instrumented {@link
|
|
114
|
+
* "../redis".RedisClient} (a drop-in for `Bun.RedisClient` / `Bun.redis`).
|
|
115
|
+
*
|
|
116
|
+
* The return value is `wrap()`ped against this event's seq, so a later
|
|
117
|
+
* `expect(...)` that drills into it nests under this step in the timeline
|
|
118
|
+
* (same mechanism as http/db).
|
|
119
|
+
*/
|
|
120
|
+
export interface RedisEvent extends BaseEvent {
|
|
121
|
+
kind: "redis";
|
|
122
|
+
/** Label for the connection — the URL host (often the service name). */
|
|
123
|
+
service: string;
|
|
124
|
+
/** Command name, upper-cased (`GET`, `SET`, `HGETALL`, `PUBLISH`, …). */
|
|
125
|
+
command: string;
|
|
126
|
+
/** First argument — almost always the key the command targets. Surfaced
|
|
127
|
+
* separately so the UI can read `GET session:42` at a glance. */
|
|
128
|
+
key?: string;
|
|
129
|
+
/** Remaining arguments (best-effort JSON-safe; large/binary stringified).
|
|
130
|
+
* Omitted when there are none. */
|
|
131
|
+
args?: unknown[];
|
|
132
|
+
/** Reply, best-effort JSON-safe and capped. Omitted when the command threw
|
|
133
|
+
* or returned `null`/`undefined`. */
|
|
134
|
+
result?: unknown;
|
|
135
|
+
resultTruncated?: boolean;
|
|
136
|
+
durationMs: number;
|
|
137
|
+
/** Set if the command threw. */
|
|
138
|
+
error?: string;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* One object-storage operation through the instrumented {@link
|
|
142
|
+
* "../s3".S3Client} (a drop-in for `Bun.S3Client` / `Bun.s3`), covering both
|
|
143
|
+
* client-level calls (`write`/`delete`/`exists`/`list`/`presign`/`stat`) and
|
|
144
|
+
* the `S3File` handle `.file(path)` returns (`text`/`json`/`write`/…).
|
|
145
|
+
*
|
|
146
|
+
* The return value is `wrap()`ped against this event's seq, so `expect(...)`
|
|
147
|
+
* drilling into it nests under this step (same mechanism as http/db).
|
|
148
|
+
*/
|
|
149
|
+
export interface S3Event extends BaseEvent {
|
|
150
|
+
kind: "s3";
|
|
151
|
+
/** Operation: `write` | `read` | `delete` | `exists` | `list` |
|
|
152
|
+
* `presign` | `stat`. */
|
|
153
|
+
op: string;
|
|
154
|
+
/** Bucket, when the client/handle knows it. */
|
|
155
|
+
bucket?: string;
|
|
156
|
+
/** Object key (path) the op targeted. Omitted for bucket-wide `list`. */
|
|
157
|
+
key?: string;
|
|
158
|
+
/** Bytes written or read, when known. */
|
|
159
|
+
size?: number;
|
|
160
|
+
/** Content type, when known (read/write/stat). */
|
|
161
|
+
contentType?: string;
|
|
162
|
+
/** Small text preview of the body (read/write of text), capped. */
|
|
163
|
+
preview?: string;
|
|
164
|
+
previewTruncated?: boolean;
|
|
165
|
+
/** For `list`: number of keys returned. */
|
|
166
|
+
count?: number;
|
|
167
|
+
durationMs: number;
|
|
168
|
+
/** Set if the op threw. */
|
|
169
|
+
error?: string;
|
|
170
|
+
}
|
|
171
|
+
export interface HttpEvent extends BaseEvent {
|
|
172
|
+
kind: "http";
|
|
173
|
+
method: string;
|
|
174
|
+
url: string;
|
|
175
|
+
/** Truncated to OUTPUT_SNIPPET_BYTES. Only set for non-binary text bodies. */
|
|
176
|
+
requestBody?: string;
|
|
177
|
+
requestBodyTruncated?: boolean;
|
|
178
|
+
status?: number;
|
|
179
|
+
responseBody?: string;
|
|
180
|
+
responseBodyTruncated?: boolean;
|
|
181
|
+
durationMs: number;
|
|
182
|
+
/** Set if the request threw (network error, abort, etc.). */
|
|
183
|
+
error?: string;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* A Kubernetes API call (from `ctx.svc.<k3s>.client.*` / `apply(...)`).
|
|
187
|
+
*
|
|
188
|
+
* The k3s component routes the kube client through `globalThis.fetch`, so
|
|
189
|
+
* the daemon's fetch wrapper first records it as a plain `http` event;
|
|
190
|
+
* the component then parses the request path and *reclassifies* that event
|
|
191
|
+
* into this richer shape via `recorderAnnotate`, so the UI can show
|
|
192
|
+
* `verb resource/name` (e.g. `list pods · default`) instead of an opaque
|
|
193
|
+
* `GET https://k8s.internal:6443/api/v1/...`. The underlying HTTP fields
|
|
194
|
+
* are retained so the request/response bodies (the actual K8s objects)
|
|
195
|
+
* still render in the detail view, and `expect(...)` on the returned
|
|
196
|
+
* object nests under this event exactly as it does for `http`.
|
|
197
|
+
*/
|
|
198
|
+
export interface KubeEvent extends BaseEvent {
|
|
199
|
+
kind: "kube";
|
|
200
|
+
/** API verb derived from the HTTP method + path shape: `get` | `list` |
|
|
201
|
+
* `watch` | `create` | `update` | `patch` | `delete` |
|
|
202
|
+
* `deletecollection`. */
|
|
203
|
+
verb: string;
|
|
204
|
+
/** API group — `""` for the core group (`/api/v1`), else e.g. `"apps"`,
|
|
205
|
+
* `"networking.k8s.io"`. */
|
|
206
|
+
group?: string;
|
|
207
|
+
/** API version, e.g. `"v1"`. */
|
|
208
|
+
apiVersion?: string;
|
|
209
|
+
/** Resource plural, e.g. `"pods"`, `"deployments"`, `"ingresses"`. */
|
|
210
|
+
resource?: string;
|
|
211
|
+
/** Subresource, when addressed, e.g. `"status"`, `"scale"`, `"log"`. */
|
|
212
|
+
subresource?: string;
|
|
213
|
+
/** Object name, when the request targets a single object. */
|
|
214
|
+
name?: string;
|
|
215
|
+
/** Namespace, when namespaced. Absent for cluster-scoped requests. */
|
|
216
|
+
namespace?: string;
|
|
217
|
+
method: string;
|
|
218
|
+
url: string;
|
|
219
|
+
requestBody?: string;
|
|
220
|
+
requestBodyTruncated?: boolean;
|
|
221
|
+
status?: number;
|
|
222
|
+
responseBody?: string;
|
|
223
|
+
responseBodyTruncated?: boolean;
|
|
224
|
+
durationMs: number;
|
|
225
|
+
error?: string;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* One call to a fake's helper function (`ctx.fakes.<name>.<fn>(...)`).
|
|
229
|
+
* Helpers are user-authored functions that read or mutate the fake's
|
|
230
|
+
* private state, so this is the only window into what a test asked the
|
|
231
|
+
* fake — distinct from the `http` events the *app under test* generates
|
|
232
|
+
* when it actually calls the fake's endpoints.
|
|
233
|
+
*
|
|
234
|
+
* The return value is `wrap()`ped against this event's seq, so a later
|
|
235
|
+
* `expect(...)` that drills into it nests under this step in the
|
|
236
|
+
* timeline (same mechanism as http/db/exec).
|
|
237
|
+
*/
|
|
238
|
+
export interface FakeEvent extends BaseEvent {
|
|
239
|
+
kind: "fake";
|
|
240
|
+
/** Fake name — the key in `ctx.fakes`. */
|
|
241
|
+
fake: string;
|
|
242
|
+
/** Helper function called, e.g. `"lastCharge"`. */
|
|
243
|
+
member: string;
|
|
244
|
+
/** Call arguments (best-effort JSON-safe). */
|
|
245
|
+
args?: unknown[];
|
|
246
|
+
/** Return value (best-effort JSON-safe). Omitted when the helper threw
|
|
247
|
+
* or returned `undefined`. */
|
|
248
|
+
result?: unknown;
|
|
249
|
+
durationMs: number;
|
|
250
|
+
/** Set if the helper threw. */
|
|
251
|
+
error?: string;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* One captured email, as embedded on an {@link EmailEvent}. The HTML and
|
|
255
|
+
* text bodies ride along (truncated to the output cap) so the dashboard
|
|
256
|
+
* can render the actual email a test asserted against.
|
|
257
|
+
*/
|
|
258
|
+
export interface EmailEventMessage {
|
|
259
|
+
/** Sender address. */
|
|
260
|
+
from?: string;
|
|
261
|
+
/** Recipient addresses. */
|
|
262
|
+
to?: string[];
|
|
263
|
+
cc?: string[];
|
|
264
|
+
bcc?: string[];
|
|
265
|
+
subject?: string;
|
|
266
|
+
/** RFC date of the message, ISO-formatted. */
|
|
267
|
+
date?: string;
|
|
268
|
+
/** HTML body (truncated to the output cap). */
|
|
269
|
+
html?: string;
|
|
270
|
+
htmlTruncated?: boolean;
|
|
271
|
+
/** Plain-text body (truncated to the output cap). */
|
|
272
|
+
text?: string;
|
|
273
|
+
textTruncated?: boolean;
|
|
274
|
+
attachments?: {
|
|
275
|
+
filename: string;
|
|
276
|
+
contentType: string;
|
|
277
|
+
size: number;
|
|
278
|
+
}[];
|
|
279
|
+
}
|
|
280
|
+
/** One row of a mailbox listing embedded on an {@link EmailEvent}. */
|
|
281
|
+
export interface EmailEventSummary {
|
|
282
|
+
from?: string;
|
|
283
|
+
to?: string[];
|
|
284
|
+
subject?: string;
|
|
285
|
+
/** Plain-text preview of the body. */
|
|
286
|
+
snippet?: string;
|
|
287
|
+
date?: string;
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* One call to an email service's helpers (`ctx.svc.<name>.lastEmail(...)`
|
|
291
|
+
* etc.). Single-message ops embed the full captured message — including its
|
|
292
|
+
* HTML body — so timelines can render the email itself; listing ops embed
|
|
293
|
+
* compact summaries. The return value is `wrap()`ped against this event's
|
|
294
|
+
* seq, so `expect(...)` on it nests under this step (same mechanism as
|
|
295
|
+
* http/db/fake). Inside a `ctx.poll` predicate the event ride-alongs with
|
|
296
|
+
* the poll's iteration events: failed iterations get truncated, the winning
|
|
297
|
+
* one survives as a child of the `wait` event.
|
|
298
|
+
*/
|
|
299
|
+
export interface EmailEvent extends BaseEvent {
|
|
300
|
+
kind: "email";
|
|
301
|
+
/** Service key of the mail server (`ctx.svc.<service>`). */
|
|
302
|
+
service: string;
|
|
303
|
+
/** Helper called, e.g. `"lastEmail"`. */
|
|
304
|
+
op: string;
|
|
305
|
+
/** Human-readable match criteria, e.g. `to alice@example.com`. */
|
|
306
|
+
query?: string;
|
|
307
|
+
/** Number of matching messages (listing ops / mailbox size on error). */
|
|
308
|
+
count?: number;
|
|
309
|
+
/** The captured message (single-message ops). */
|
|
310
|
+
message?: EmailEventMessage;
|
|
311
|
+
/** Message summaries (listing ops). */
|
|
312
|
+
messages?: EmailEventSummary[];
|
|
313
|
+
durationMs: number;
|
|
314
|
+
/** Set if the op threw (e.g. the mail server's query API failed). */
|
|
315
|
+
error?: string;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* A runtime mutation of the environment — `ctx.startService` /
|
|
319
|
+
* `ctx.stopService` / `ctx.dnsName`, whether called from a test or from a
|
|
320
|
+
* fake handler reacting to the app under test. Distinct from the `http`
|
|
321
|
+
* event the app generates when it calls the fake: this is the
|
|
322
|
+
* infrastructure the fake (or test) created in response. No-op outside a
|
|
323
|
+
* running test (bootstrap / project-setup / eval), same as every record*.
|
|
324
|
+
*/
|
|
325
|
+
export interface EnvEvent extends BaseEvent {
|
|
326
|
+
kind: "env";
|
|
327
|
+
/** Which environment primitive ran. */
|
|
328
|
+
op: "startService" | "stopService" | "dnsName" | "certificate";
|
|
329
|
+
/** SANs of a minted leaf (certificate). */
|
|
330
|
+
hostnames?: string[];
|
|
331
|
+
/** Service name (startService / stopService). */
|
|
332
|
+
service?: string;
|
|
333
|
+
/** Image reference started (startService). */
|
|
334
|
+
image?: string;
|
|
335
|
+
/** Resolved IP — the new container's (startService) or the target's (dnsName). */
|
|
336
|
+
ip?: string;
|
|
337
|
+
/** Hostname registered (dnsName). */
|
|
338
|
+
hostname?: string;
|
|
339
|
+
durationMs: number;
|
|
340
|
+
/** Set if the op threw. */
|
|
341
|
+
error?: string;
|
|
342
|
+
}
|
|
343
|
+
export type BrowserAction = string;
|
|
344
|
+
export interface BrowserEvent extends BaseEvent {
|
|
345
|
+
kind: "browser";
|
|
346
|
+
action: BrowserAction;
|
|
347
|
+
/** Target URL (navigate). */
|
|
348
|
+
url?: string;
|
|
349
|
+
/** CSS selector (click, scrollTo). */
|
|
350
|
+
selector?: string;
|
|
351
|
+
/** Human-readable label for `evaluate` ops. */
|
|
352
|
+
description?: string;
|
|
353
|
+
/** Evaluated script, truncated. */
|
|
354
|
+
script?: string;
|
|
355
|
+
scriptTruncated?: boolean;
|
|
356
|
+
/** Typed text, truncated. */
|
|
357
|
+
text?: string;
|
|
358
|
+
textTruncated?: boolean;
|
|
359
|
+
/** Key name (press). */
|
|
360
|
+
key?: string;
|
|
361
|
+
/** Scroll/click coordinates. */
|
|
362
|
+
dx?: number;
|
|
363
|
+
dy?: number;
|
|
364
|
+
x?: number;
|
|
365
|
+
y?: number;
|
|
366
|
+
/** Screenshot image format. */
|
|
367
|
+
format?: string;
|
|
368
|
+
/** For `waitFor`: how many times the predicate was polled. */
|
|
369
|
+
attempts?: number;
|
|
370
|
+
durationMs: number;
|
|
371
|
+
error?: string;
|
|
372
|
+
/**
|
|
373
|
+
* Session this op belonged to. Set whenever the Browser was opened
|
|
374
|
+
* with a `BrowserSessionRecorder` attached (the daemon always does).
|
|
375
|
+
*/
|
|
376
|
+
sessionId?: string;
|
|
377
|
+
/**
|
|
378
|
+
* Wall-clock `Date.now()` captured at the moment this op *finished*.
|
|
379
|
+
* Lives in the same time base as rrweb's `event.timestamp` fields,
|
|
380
|
+
* so the dashboard can seek the player by computing
|
|
381
|
+
* `event.timestamp - events[0].timestamp`. Using the post-op time
|
|
382
|
+
* (rather than op start) means clicking a "type 'foo'" step lands
|
|
383
|
+
* on the frame where the field already shows the text.
|
|
384
|
+
*/
|
|
385
|
+
sessionTimestamp?: number;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Inline event for a single `ctx.terminal(...)` call. The heavy
|
|
389
|
+
* asciicast frames live separately on a `TerminalSessionRecord`
|
|
390
|
+
* (mirroring how `BrowserEvent` points at a `BrowserSessionRecord`).
|
|
391
|
+
* The UI shows this row in the step list and seeks the player to
|
|
392
|
+
* the session's start when clicked.
|
|
393
|
+
*/
|
|
394
|
+
export interface TerminalEvent extends BaseEvent {
|
|
395
|
+
kind: "terminal";
|
|
396
|
+
service: string;
|
|
397
|
+
command: string;
|
|
398
|
+
exitCode: number;
|
|
399
|
+
durationMs: number;
|
|
400
|
+
/** Links this event to its TerminalSessionRecord. */
|
|
401
|
+
sessionId: string;
|
|
402
|
+
/** PTY size used for the run. */
|
|
403
|
+
cols: number;
|
|
404
|
+
rows: number;
|
|
405
|
+
/** First N bytes of decoded output, for tooltip/inline preview. */
|
|
406
|
+
outputPreview: string;
|
|
407
|
+
outputTruncated: boolean;
|
|
408
|
+
/** Set if spawning the PTY itself failed. */
|
|
409
|
+
error?: string;
|
|
410
|
+
}
|
|
411
|
+
/** Action taken on an open interactive terminal session. */
|
|
412
|
+
export type TerminalStepAction = "send" | "sendLine" | "press" | "waitFor" | "exit" | "close";
|
|
413
|
+
/**
|
|
414
|
+
* One operation on an open interactive terminal — analogous to
|
|
415
|
+
* `BrowserEvent`. The session's heavy asciicast frames live on the
|
|
416
|
+
* `TerminalSessionRecord`; this event just carries metadata so the
|
|
417
|
+
* step shows up in the timeline and the UI can seek the player.
|
|
418
|
+
*/
|
|
419
|
+
export interface TerminalStepEvent extends BaseEvent {
|
|
420
|
+
kind: "terminal-step";
|
|
421
|
+
/** Links this event back to its TerminalSessionRecord. */
|
|
422
|
+
sessionId: string;
|
|
423
|
+
/** Service the terminal is attached to (mirrors TerminalEvent.service). */
|
|
424
|
+
service: string;
|
|
425
|
+
/** Which Terminal method produced this event. */
|
|
426
|
+
action: TerminalStepAction;
|
|
427
|
+
/** Bytes sent (send/sendLine), truncated. */
|
|
428
|
+
text?: string;
|
|
429
|
+
textTruncated?: boolean;
|
|
430
|
+
/** Key name passed to `press`. */
|
|
431
|
+
key?: string;
|
|
432
|
+
/** Human label passed to `waitFor`. */
|
|
433
|
+
description?: string;
|
|
434
|
+
/** How many polls `waitFor` made. */
|
|
435
|
+
attempts?: number;
|
|
436
|
+
/** Whether `waitFor` matched (false means it timed out). */
|
|
437
|
+
matched?: boolean;
|
|
438
|
+
/** Exit code observed on `close`. */
|
|
439
|
+
exitCode?: number;
|
|
440
|
+
/** Rendered screen at the moment of the op (post-action), truncated. */
|
|
441
|
+
screenPreview: string;
|
|
442
|
+
screenTruncated: boolean;
|
|
443
|
+
/**
|
|
444
|
+
* Player-relative offset, in seconds, captured against the *same*
|
|
445
|
+
* clock the asciicast frames use (the terminal factory's spawn
|
|
446
|
+
* time). The UI seeks the asciinema-player to this value when the
|
|
447
|
+
* step is clicked. Persisting it directly avoids ever subtracting
|
|
448
|
+
* `step.tOffsetMs - session.openedAtMs` on the front-end, which is
|
|
449
|
+
* fragile because the two timestamps live in slightly different
|
|
450
|
+
* Date.now() frames (recorder vs. spawn).
|
|
451
|
+
*/
|
|
452
|
+
castTimeSec: number;
|
|
453
|
+
durationMs: number;
|
|
454
|
+
error?: string;
|
|
455
|
+
}
|
|
456
|
+
/** An ordering slot claimed at op *start* and handed back to the matching
|
|
457
|
+
* `record*` call at op finish. Lets a triggering op (an `exec`/`fetch` that
|
|
458
|
+
* reaches the app, which calls a fake, which calls `ctx.startService`) keep a
|
|
459
|
+
* lower seq than the nested events it sets off — even though those nested
|
|
460
|
+
* events finish, and record, first. */
|
|
461
|
+
export interface EventReservation {
|
|
462
|
+
seq: number;
|
|
463
|
+
tOffsetMs: number;
|
|
464
|
+
}
|
|
465
|
+
export declare function pauseRecording(): void;
|
|
466
|
+
export declare function resumeRecording(): void;
|
|
467
|
+
export declare function startRecording(): void;
|
|
468
|
+
export declare function stopRecording(): TestEvent[];
|
|
469
|
+
export declare function isRecording(): boolean;
|
|
470
|
+
/** Reserve an ordering slot at the start of an op so nested events it triggers
|
|
471
|
+
* (which finish — and record — first) still sort after it. Hand the returned
|
|
472
|
+
* reservation to the matching `record*` call at op finish. Returns `undefined`
|
|
473
|
+
* when nothing is recording, in which case `record*` falls back to allocating
|
|
474
|
+
* the seq at record time. */
|
|
475
|
+
export declare function reserveEvent(): EventReservation | undefined;
|
|
476
|
+
export declare function recordExec(ev: Omit<ExecEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
477
|
+
export declare function recordAssertion(ev: Omit<AssertionEvent, "seq" | "tOffsetMs" | "kind">): number | undefined;
|
|
478
|
+
export declare function recordHttp(ev: Omit<HttpEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
479
|
+
export declare function recordBrowser(ev: Omit<BrowserEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
480
|
+
export declare function recordDb(ev: Omit<DbEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
481
|
+
export declare function recordRedis(ev: Omit<RedisEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
482
|
+
export declare function recordS3(ev: Omit<S3Event, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
483
|
+
export declare function recordTerminal(ev: Omit<TerminalEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
484
|
+
export declare function recordTerminalStep(ev: Omit<TerminalStepEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
485
|
+
export declare function recordFake(ev: Omit<FakeEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
486
|
+
export declare function recordEnv(ev: Omit<EnvEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
487
|
+
export declare function recordEmail(ev: Omit<EmailEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
488
|
+
/**
|
|
489
|
+
* Recorded once per `ctx.poll(...)` call. Stands in for the suppressed
|
|
490
|
+
* intermediate iterations and gives downstream tagged values
|
|
491
|
+
* (`wrap(polledValue, waitSeq)`) a single, stable event to point at —
|
|
492
|
+
* assertions on those values then link to the wait, not to one of N
|
|
493
|
+
* dropped polls.
|
|
494
|
+
*/
|
|
495
|
+
export interface WaitEvent extends BaseEvent {
|
|
496
|
+
kind: "wait";
|
|
497
|
+
description: string;
|
|
498
|
+
attempts: number;
|
|
499
|
+
durationMs: number;
|
|
500
|
+
passed: boolean;
|
|
501
|
+
/** Set when the predicate threw or the wait timed out. */
|
|
502
|
+
error?: string;
|
|
503
|
+
}
|
|
504
|
+
export declare function recordWait(ev: Omit<WaitEvent, "seq" | "tOffsetMs" | "kind">, reservation?: EventReservation): number | undefined;
|
|
505
|
+
/** Number of events the active recorder has accumulated. Returns 0
|
|
506
|
+
* when no recorder is active. */
|
|
507
|
+
export declare function recorderEventCount(): number;
|
|
508
|
+
/** Drop events with index >= `toLen` from the active recorder. */
|
|
509
|
+
export declare function recorderTruncate(toLen: number): void;
|
|
510
|
+
/** Stamp `parentSeq` onto every event from `startIdx` onward, so the
|
|
511
|
+
* UI groups them under the parent in the timeline. */
|
|
512
|
+
export declare function recorderMarkChildren(startIdx: number, parentSeq: number): void;
|
|
513
|
+
/** Shallow-merge `patch` into the active recorder's event with this
|
|
514
|
+
* `seq` (enrich/reclassify after the fact). No-op when nothing is
|
|
515
|
+
* recording or no event carries that seq. */
|
|
516
|
+
export declare function recorderAnnotate(seq: number, patch: Record<string, unknown>): void;
|
|
517
|
+
/** Retract the active recorder's event with this `seq` (an instrumentation
|
|
518
|
+
* site recorded it, then decided it's noise). No-op when nothing is
|
|
519
|
+
* recording or no event carries that seq. */
|
|
520
|
+
export declare function recorderRemove(seq: number): void;
|
|
521
|
+
/** Best-effort JSON-safe deep clone; falls back to `String(v)`. */
|
|
522
|
+
export declare function safeSerialize(v: unknown): unknown;
|
|
523
|
+
export declare function truncateUtf8(s: string): {
|
|
524
|
+
value: string;
|
|
525
|
+
truncated: boolean;
|
|
526
|
+
};
|
|
527
|
+
export {};
|