@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.
Files changed (74) hide show
  1. package/dist/aws-sigv4.d.ts +42 -0
  2. package/dist/aws-sigv4.js +166 -0
  3. package/dist/browser.d.ts +314 -0
  4. package/dist/browser.js +1320 -0
  5. package/dist/components/email.d.ts +135 -0
  6. package/dist/components/email.js +271 -0
  7. package/dist/components/expo.d.ts +69 -0
  8. package/dist/components/expo.js +125 -0
  9. package/dist/components/index.d.ts +8 -0
  10. package/dist/components/index.js +18 -0
  11. package/dist/components/k3s.d.ts +172 -0
  12. package/dist/components/k3s.js +1124 -0
  13. package/dist/components/postgres.d.ts +93 -0
  14. package/dist/components/postgres.js +58 -0
  15. package/dist/components/replayFake.d.ts +169 -0
  16. package/dist/components/replayFake.js +738 -0
  17. package/dist/components/s3.d.ts +99 -0
  18. package/dist/components/s3.js +81 -0
  19. package/dist/components/supabase.d.ts +197 -0
  20. package/dist/components/supabase.js +1003 -0
  21. package/dist/daemon.d.ts +1 -0
  22. package/dist/daemon.js +4611 -0
  23. package/dist/ids.d.ts +2 -0
  24. package/{src/ids.ts → dist/ids.js} +46 -50
  25. package/dist/index.d.ts +1328 -0
  26. package/dist/index.js +769 -0
  27. package/dist/ingress.d.ts +114 -0
  28. package/dist/ingress.js +210 -0
  29. package/dist/inspect.d.ts +228 -0
  30. package/dist/inspect.js +429 -0
  31. package/dist/locator.d.ts +260 -0
  32. package/dist/locator.js +293 -0
  33. package/dist/mobile.d.ts +71 -0
  34. package/dist/mobile.js +65 -0
  35. package/dist/record-secrets.d.ts +9 -0
  36. package/{src/record-secrets.ts → dist/record-secrets.js} +13 -15
  37. package/dist/recorder.d.ts +527 -0
  38. package/dist/recorder.js +219 -0
  39. package/dist/redis.d.ts +54 -0
  40. package/dist/redis.js +126 -0
  41. package/dist/replay-bundle.d.ts +38 -0
  42. package/{src/replay-bundle.ts → dist/replay-bundle.js} +29 -47
  43. package/dist/resolver.d.ts +1 -0
  44. package/dist/resolver.js +309 -0
  45. package/dist/s3.d.ts +89 -0
  46. package/dist/s3.js +198 -0
  47. package/dist/sql.d.ts +74 -0
  48. package/dist/sql.js +151 -0
  49. package/dist/terminal.d.ts +161 -0
  50. package/dist/terminal.js +538 -0
  51. package/package.json +24 -9
  52. package/src/browser.ts +0 -1819
  53. package/src/components/email.ts +0 -398
  54. package/src/components/expo.ts +0 -167
  55. package/src/components/index.ts +0 -63
  56. package/src/components/k3s.ts +0 -1312
  57. package/src/components/postgres.ts +0 -105
  58. package/src/components/replayFake.ts +0 -848
  59. package/src/components/s3.ts +0 -132
  60. package/src/components/supabase.ts +0 -1299
  61. package/src/daemon.ts +0 -4969
  62. package/src/index.ts +0 -2350
  63. package/src/ingress.ts +0 -288
  64. package/src/inspect.ts +0 -673
  65. package/src/locator.ts +0 -594
  66. package/src/mobile.ts +0 -133
  67. package/src/recorder.ts +0 -817
  68. package/src/redis.ts +0 -202
  69. package/src/resolver.ts +0 -351
  70. package/src/s3.ts +0 -333
  71. package/src/sql.ts +0 -243
  72. package/src/terminal.ts +0 -740
  73. package/src/vendor/rrweb-plugin-console-record.umd.js +0 -521
  74. package/src/vendor/rrweb-record.min.js +0 -5061
@@ -0,0 +1,114 @@
1
+ import type { Project, ServiceConfig } from "./index.js";
2
+ /** Where a DNS name points. `ingress` = the spectest-daemon listener on the
3
+ * bridge gateway (fakes, TLS-terminated proxies); `service` = a container's
4
+ * live IP on spectest-net (a plain peer alias, no daemon hop). */
5
+ export type DnsTarget = {
6
+ ingress: true;
7
+ } | {
8
+ service: string;
9
+ };
10
+ export interface CertificateDecl {
11
+ kind: "certificate";
12
+ /** Hostnames the leaf cert's SANs cover. One cert is minted per decl. */
13
+ hostnames: string[];
14
+ }
15
+ export interface DnsDecl {
16
+ kind: "dns";
17
+ hostname: string;
18
+ target: DnsTarget;
19
+ }
20
+ export interface ProxyDecl {
21
+ kind: "proxy";
22
+ hostname: string;
23
+ upstream: {
24
+ service: string;
25
+ port: number;
26
+ };
27
+ }
28
+ export type IngressDecl = CertificateDecl | DnsDecl | ProxyDecl;
29
+ /** Token a component can use in a decl where it can't know its own
30
+ * services-map key yet — resolved to that key during `lowerIngress`.
31
+ * Mirrors the `{{SPECTEST_SERVICE}}` token honoured in `files`. */
32
+ export declare const SELF_SERVICE_TOKEN = "{{SPECTEST_SERVICE}}";
33
+ /** A wildcard is `*.` + a normal multi-label hostname (e.g.
34
+ * `*.example.com`). It matches any name ending in that suffix. */
35
+ export declare function isWildcard(hostname: string): boolean;
36
+ /**
37
+ * Request a leaf certificate (signed by the in-VM root CA) covering
38
+ * `hostnames`. The daemon binds it on the HTTPS ingress (:443, SNI per
39
+ * hostname). Pairs with a `proxy(...)` (TLS-terminated reverse proxy) or a
40
+ * fake handler; on its own it just makes those hostnames serve HTTPS.
41
+ */
42
+ export declare function certificate(hostnames: string[]): CertificateDecl;
43
+ /**
44
+ * Register `hostname` in spectest's DNS. `{ ingress: true }` points it at
45
+ * the daemon (for fakes / TLS proxies — resolved to the bridge gateway and
46
+ * injected as `--add-host` into every container); `{ service }` makes it an
47
+ * extra peer alias for that container (resolved live to its IP). The
48
+ * `SELF_SERVICE_TOKEN` may be used for `service` when a component can't yet
49
+ * know its own key.
50
+ */
51
+ export declare function dnsName(hostname: string, target: DnsTarget): DnsDecl;
52
+ /**
53
+ * Reverse-proxy `hostname` to `http://<service>:<port>` on spectest-net.
54
+ * Implies the hostname resolves to the daemon ingress, so containers reach
55
+ * it without any extra `dnsName(...)`. Add a `certificate([hostname])` to
56
+ * serve it over HTTPS as well as HTTP.
57
+ */
58
+ export declare function proxy(hostname: string, upstream: {
59
+ service: string;
60
+ port: number;
61
+ }): ProxyDecl;
62
+ /**
63
+ * Attach low-level ingress decls to a service definition a component
64
+ * returns. The decls are read by `lowerIngress` at load time and never
65
+ * serialised into the wire config.
66
+ *
67
+ * ```ts
68
+ * return provides({ image, command }, [
69
+ * certificate(["app.test"]),
70
+ * dnsName("app.test", { ingress: true }),
71
+ * proxy("app.test", { service: SELF_SERVICE_TOKEN, port: 3000 }),
72
+ * ]);
73
+ * ```
74
+ */
75
+ export declare function provides<T extends ServiceConfig>(service: T, decls: IngressDecl[]): T;
76
+ /** Everything the daemon needs to stand up ingress, derived once per
77
+ * /load. The daemon executes this generically — it reads neither
78
+ * `svc.tls` nor `svc.hostnames` directly. */
79
+ export interface LoweredIngress {
80
+ /** One leaf cert per group; SANs = the group's hostnames. */
81
+ certificates: {
82
+ hostnames: string[];
83
+ }[];
84
+ /** Reverse-proxy routes: hostname → upstream service:port. */
85
+ proxies: {
86
+ hostname: string;
87
+ service: string;
88
+ port: number;
89
+ }[];
90
+ /** Hostnames that resolve to the daemon ingress gateway (DNS registry
91
+ * + `--add-host` on every container). Covers fakes, TLS proxies, and
92
+ * any `dnsName(h, { ingress: true })`. */
93
+ ingressHosts: string[];
94
+ /** service key → extra `--network-alias`es (from `hostnames` /
95
+ * `dnsName(h, { service })`). */
96
+ aliasesByService: Record<string, string[]>;
97
+ /** Wildcard suffixes (`*.example.com` → `.example.com`) and where they
98
+ * point. Resolved to a concrete IP in the daemon (service → that
99
+ * container's IP, ingress → the bridge gateway) and written to the
100
+ * resolver's wildcard table. Only the resolver answers these — they
101
+ * can't be expressed as `--network-alias`/`--add-host`. */
102
+ wildcards: {
103
+ pattern: string;
104
+ target: DnsTarget;
105
+ }[];
106
+ }
107
+ /**
108
+ * Lower a project's friendly surface (`services.*.tls`,
109
+ * `services.*.hostnames`, component `provides(...)`, and `fakes`) into the
110
+ * generic `LoweredIngress` the daemon executes. This is the framework
111
+ * abstraction the user asked for: the special-casing lives here, in one
112
+ * pure function, instead of being scattered through the daemon.
113
+ */
114
+ export declare function lowerIngress(project: Project): LoweredIngress;
@@ -0,0 +1,210 @@
1
+ // Low-level ingress primitives — the first-class building blocks the
2
+ // daemon's networking is expressed in terms of. The friendly
3
+ // `services.<name>.tls` / `services.<name>.hostnames` fields and
4
+ // `defineFake(...)` are all *sugar* that lower onto these three:
5
+ //
6
+ // certificate(hostnames) — mint a leaf cert from the in-VM root CA
7
+ // dnsName(hostname, target) — register a name → a container or the daemon
8
+ // proxy(hostname, upstream) — reverse-proxy a hostname to a service:port
9
+ //
10
+ // The daemon consumes only the lowered result (see `lowerIngress`), so it
11
+ // never special-cases tls/hostnames/fakes — it just executes a generic set
12
+ // of certs, proxy routes, DNS registrations, and aliases. A user-authored
13
+ // component emits the same primitives by attaching them with `provides(...)`,
14
+ // so it can do anything the built-ins do without any daemon change.
15
+ //
16
+ // None of this is part of the wire `EnvironmentConfig` the control plane
17
+ // deserialises: the decls live on the `Project` (daemon-side only) and,
18
+ // when attached to a service, ride on a Symbol key that `JSON.stringify`
19
+ // drops — so Rust's `deny_unknown_fields` never sees them.
20
+ /** Token a component can use in a decl where it can't know its own
21
+ * services-map key yet — resolved to that key during `lowerIngress`.
22
+ * Mirrors the `{{SPECTEST_SERVICE}}` token honoured in `files`. */
23
+ export const SELF_SERVICE_TOKEN = "{{SPECTEST_SERVICE}}";
24
+ // Multi-label hostname: at least one dot, each label 1–63 chars of
25
+ // [a-z0-9-], no leading/trailing hyphen. Kept in lockstep with index.ts.
26
+ const HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/;
27
+ function assertHostname(h, ctx) {
28
+ const lower = h.toLowerCase();
29
+ if (!HOSTNAME_RE.test(lower)) {
30
+ throw new Error(`${ctx}: invalid hostname ${JSON.stringify(h)} — must be a multi-label DNS name (e.g. "api.stripe.com")`);
31
+ }
32
+ if (lower === "internal" || lower.endsWith(".internal")) {
33
+ throw new Error(`${ctx}: hostname ${JSON.stringify(h)} ends in the reserved ".internal" TLD`);
34
+ }
35
+ }
36
+ /** A wildcard is `*.` + a normal multi-label hostname (e.g.
37
+ * `*.example.com`). It matches any name ending in that suffix. */
38
+ export function isWildcard(hostname) {
39
+ return hostname.startsWith("*.");
40
+ }
41
+ /** Validate an exact hostname OR a `*.suffix` wildcard. */
42
+ function assertNameOrWildcard(h, ctx) {
43
+ if (isWildcard(h)) {
44
+ // The bit after `*.` must itself be a valid multi-label hostname, so
45
+ // `*.example.com` is fine but `*.com` (too broad) and `*` are not.
46
+ assertHostname(h.slice(2), `${ctx} wildcard`);
47
+ return;
48
+ }
49
+ assertHostname(h, ctx);
50
+ }
51
+ /**
52
+ * Request a leaf certificate (signed by the in-VM root CA) covering
53
+ * `hostnames`. The daemon binds it on the HTTPS ingress (:443, SNI per
54
+ * hostname). Pairs with a `proxy(...)` (TLS-terminated reverse proxy) or a
55
+ * fake handler; on its own it just makes those hostnames serve HTTPS.
56
+ */
57
+ export function certificate(hostnames) {
58
+ if (!Array.isArray(hostnames) || hostnames.length === 0) {
59
+ throw new Error("certificate(): at least one hostname is required");
60
+ }
61
+ for (const h of hostnames)
62
+ assertHostname(h, "certificate()");
63
+ return { kind: "certificate", hostnames: hostnames.map((h) => h.toLowerCase()) };
64
+ }
65
+ /**
66
+ * Register `hostname` in spectest's DNS. `{ ingress: true }` points it at
67
+ * the daemon (for fakes / TLS proxies — resolved to the bridge gateway and
68
+ * injected as `--add-host` into every container); `{ service }` makes it an
69
+ * extra peer alias for that container (resolved live to its IP). The
70
+ * `SELF_SERVICE_TOKEN` may be used for `service` when a component can't yet
71
+ * know its own key.
72
+ */
73
+ export function dnsName(hostname, target) {
74
+ assertNameOrWildcard(hostname, "dnsName()");
75
+ if (!("ingress" in target) && !("service" in target)) {
76
+ throw new Error(`dnsName(${JSON.stringify(hostname)}): target must be { ingress: true } or { service }`);
77
+ }
78
+ return { kind: "dns", hostname: hostname.toLowerCase(), target };
79
+ }
80
+ /**
81
+ * Reverse-proxy `hostname` to `http://<service>:<port>` on spectest-net.
82
+ * Implies the hostname resolves to the daemon ingress, so containers reach
83
+ * it without any extra `dnsName(...)`. Add a `certificate([hostname])` to
84
+ * serve it over HTTPS as well as HTTP.
85
+ */
86
+ export function proxy(hostname, upstream) {
87
+ assertHostname(hostname, "proxy()");
88
+ if (!upstream || typeof upstream.service !== "string" || typeof upstream.port !== "number") {
89
+ throw new Error(`proxy(${JSON.stringify(hostname)}): upstream must be { service: string, port: number }`);
90
+ }
91
+ return { kind: "proxy", hostname: hostname.toLowerCase(), upstream };
92
+ }
93
+ /** Symbol key under which `provides(...)` stashes a component's ingress
94
+ * decls on its service object. A Symbol so `JSON.stringify(environment)`
95
+ * (the wire config shipped to the Rust control plane) drops it — symbol
96
+ * keys are never serialised, regardless of enumerability. Kept
97
+ * *enumerable* so it survives the common `{ ...component() }` spread (object
98
+ * spread copies enumerable symbol props but skips non-enumerable ones). */
99
+ const INGRESS_DECLS = Symbol.for("spectest.ingress.decls");
100
+ /**
101
+ * Attach low-level ingress decls to a service definition a component
102
+ * returns. The decls are read by `lowerIngress` at load time and never
103
+ * serialised into the wire config.
104
+ *
105
+ * ```ts
106
+ * return provides({ image, command }, [
107
+ * certificate(["app.test"]),
108
+ * dnsName("app.test", { ingress: true }),
109
+ * proxy("app.test", { service: SELF_SERVICE_TOKEN, port: 3000 }),
110
+ * ]);
111
+ * ```
112
+ */
113
+ export function provides(service, decls) {
114
+ // Merge with any decls already attached (e.g. a component built via
115
+ // provides() that the caller then wraps again) so neither layer is lost.
116
+ const prior = readProvided(service);
117
+ Object.defineProperty(service, INGRESS_DECLS, {
118
+ value: [...prior, ...decls],
119
+ enumerable: true,
120
+ configurable: true,
121
+ writable: true,
122
+ });
123
+ return service;
124
+ }
125
+ function readProvided(service) {
126
+ const decls = service[INGRESS_DECLS];
127
+ return Array.isArray(decls) ? decls : [];
128
+ }
129
+ function resolveSelf(service, selfKey) {
130
+ return service === SELF_SERVICE_TOKEN ? selfKey : service;
131
+ }
132
+ /**
133
+ * Lower a project's friendly surface (`services.*.tls`,
134
+ * `services.*.hostnames`, component `provides(...)`, and `fakes`) into the
135
+ * generic `LoweredIngress` the daemon executes. This is the framework
136
+ * abstraction the user asked for: the special-casing lives here, in one
137
+ * pure function, instead of being scattered through the daemon.
138
+ */
139
+ export function lowerIngress(project) {
140
+ const certificates = [];
141
+ const proxies = [];
142
+ const ingressSet = new Set();
143
+ const aliasesByService = {};
144
+ const wildcards = [];
145
+ const addAlias = (service, host) => {
146
+ (aliasesByService[service] ??= []).push(host.toLowerCase());
147
+ };
148
+ const applyDecl = (decl, selfKey) => {
149
+ switch (decl.kind) {
150
+ case "certificate":
151
+ certificates.push({ hostnames: decl.hostnames });
152
+ break;
153
+ case "proxy": {
154
+ const service = resolveSelf(decl.upstream.service, selfKey);
155
+ proxies.push({ hostname: decl.hostname, service, port: decl.upstream.port });
156
+ // A proxied hostname must route to the daemon.
157
+ ingressSet.add(decl.hostname);
158
+ break;
159
+ }
160
+ case "dns": {
161
+ // A wildcard can only be answered by the resolver (no
162
+ // --network-alias / --add-host equivalent), so route either kind
163
+ // of target through the wildcard table; the daemon resolves the IP.
164
+ if (isWildcard(decl.hostname)) {
165
+ const target = "service" in decl.target
166
+ ? { service: resolveSelf(decl.target.service, selfKey) }
167
+ : decl.target;
168
+ wildcards.push({ pattern: decl.hostname, target });
169
+ }
170
+ else if ("ingress" in decl.target) {
171
+ ingressSet.add(decl.hostname);
172
+ }
173
+ else {
174
+ addAlias(resolveSelf(decl.target.service, selfKey), decl.hostname);
175
+ }
176
+ break;
177
+ }
178
+ }
179
+ };
180
+ for (const [name, svc] of Object.entries(project.environment.services)) {
181
+ // Component-provided explicit decls.
182
+ for (const decl of readProvided(svc))
183
+ applyDecl(decl, name);
184
+ // `tls: [{ hostname, port }]` → cert + TLS-terminated reverse proxy.
185
+ for (const entry of svc.tls ?? []) {
186
+ applyDecl(certificate([entry.hostname]), name);
187
+ applyDecl(proxy(entry.hostname, { service: name, port: entry.port }), name);
188
+ }
189
+ // `hostnames: [h]` → plain peer alias (no daemon hop).
190
+ for (const h of svc.hostnames ?? []) {
191
+ applyDecl(dnsName(h, { service: name }), name);
192
+ }
193
+ }
194
+ // Fakes: in-daemon handlers. The handler routing stays keyed by hostname
195
+ // in the daemon's FAKES map; here we only contribute their networking
196
+ // (a leaf cert for HTTPS + ingress DNS).
197
+ for (const fake of Object.values(project.fakes ?? {})) {
198
+ const hostnames = fake.hostnames.map((h) => h.toLowerCase());
199
+ certificates.push({ hostnames });
200
+ for (const h of hostnames)
201
+ ingressSet.add(h);
202
+ }
203
+ return {
204
+ certificates,
205
+ proxies,
206
+ ingressHosts: [...ingressSet],
207
+ aliasesByService,
208
+ wildcards,
209
+ };
210
+ }
@@ -0,0 +1,228 @@
1
+ export declare const OP_TAG: unique symbol;
2
+ export declare const UNWRAP: unique symbol;
3
+ export interface OpTag {
4
+ /** Seq of the timeline event this value came from, or `undefined` when the
5
+ * value was wrapped without a recorded event (no recorder, paused, or the
6
+ * event was truncated). `expect()` only nests under a defined `sourceSeq`. */
7
+ sourceSeq: number | undefined;
8
+ path: readonly string[];
9
+ }
10
+ /** Read the tag if present; returns undefined for raw values. */
11
+ export declare function readTag(x: unknown): OpTag | undefined;
12
+ /** Forget any pending nullish-leaf note. Called by the recorder whenever a new
13
+ * op is recorded, so a note can't outlive the read that produced it. */
14
+ export declare function clearPendingNullish(): void;
15
+ /**
16
+ * If `value` is an untagged `null`/`undefined` that matches the most recent
17
+ * nullish-leaf read, mint a tagged {@link makeCarrier} holder for it (the same
18
+ * stand-in `field`/`retag` use) and clear the note; otherwise return `value`
19
+ * unchanged. The holder is safe because it goes straight to `expect()`, which
20
+ * `readRaw`s it before matching — it never escapes into control flow.
21
+ */
22
+ export declare function adoptNullishTag<T>(value: T): T;
23
+ /** If x is wrapped, return the raw value; otherwise return x. */
24
+ export declare function readRaw<T>(x: T): T;
25
+ /** The raw type behind a wrapper: a `Carrier`/`WrappedObject`/`WrappedArray`/
26
+ * `WrappedResponse` resolves to its `unwrap()` return type; anything else is
27
+ * already raw and passes through unchanged. */
28
+ export type Unwrap<T> = T extends {
29
+ unwrap(): infer V;
30
+ } ? V : T;
31
+ /**
32
+ * Wrap a value so reads through it carry an `OpTag`. Recursion is lazy:
33
+ * a property read on an object Proxy wraps its child on demand.
34
+ */
35
+ export declare function wrap<T>(raw: T, sourceSeq: number | undefined, path?: readonly string[]): T;
36
+ /**
37
+ * Provenance-preserving, null-safe field selector — for asserting on a leaf
38
+ * that is `null`/`undefined`. Reading such a leaf off a wrapped object hands
39
+ * back a raw `null`/`undefined` (a symbol tag can't ride on those, and minting
40
+ * a stand-in object would break every `=== null` / `if (!x)` in real code), so
41
+ * the raw value alone loses its link to the originating op. `field` reads the
42
+ * tag from the *container* and navigates the raw value, returning a tagged
43
+ * handle even when the leaf is nullish — safe because the handle only ever
44
+ * feeds `expect()`. Pass it straight to `expect(...)`; works with every matcher.
45
+ *
46
+ * Mostly redundant now: the plain `expect(dep.status.readyReplicas).toBeFalsy()`
47
+ * form recovers provenance on its own — the proxy notes each nullish leaf read
48
+ * and `expect` adopts the note (see {@link adoptNullishTag}). Reach for `field`
49
+ * when the nullish read and the `expect` are separated by another recorded op
50
+ * (which clears the note), or to make the navigation explicit.
51
+ *
52
+ * ```ts
53
+ * expect(field(created, "branched_from_environment_id")).toBe(null);
54
+ * expect(field(dep, "status", "readyReplicas")).toBeFalsy();
55
+ * ```
56
+ */
57
+ export declare function field(value: unknown, ...path: Array<string | number>): unknown;
58
+ /**
59
+ * A provenance-carrying handle to a value produced by a tracked op
60
+ * (`ctx.fetch`, a db query, `browser.evaluate`). Pass it straight to
61
+ * `expect(...)` — the matcher reads the provenance and nests the
62
+ * assertion under the originating op in the timeline.
63
+ *
64
+ * At runtime, coercion sinks recover the raw value (`` `${carrier}` ``,
65
+ * `JSON.stringify(carrier)` behave as if raw). But the *type* is honestly an
66
+ * object, not `T`, so arithmetic (`carrier + 1`), `==`/`===`, and handing it
67
+ * to a typed API are all compile errors — `carrier.unwrap()` (or `expect(...)`,
68
+ * which unwraps for you) first. This is deliberate: a wrapped value is a
69
+ * handle, and the type says so rather than masquerading as its raw type.
70
+ */
71
+ export interface Carrier<T> {
72
+ /** Recover the raw underlying value. */
73
+ unwrap(): T;
74
+ /**
75
+ * Run the raw value through `fn` and get back a provenance-carrying handle to
76
+ * the result — for asserting on a *decoded* value (base64, JSON, JWT, …) while
77
+ * keeping its link to the op that produced it. The op path is extended by a
78
+ * `<label>` marker, so a later `expect(...)` on the result still nests under
79
+ * the source op. `label` is a plain word (`"base64"`, `"json"`); the UI adds
80
+ * the `<…>` brackets, so don't include them yourself. `fn` receives the raw
81
+ * value (typed `T`), so no cast is needed. `R` types the result. A throw in
82
+ * `fn` is rethrown prefixed with `<label>:`, failing the test at the value.
83
+ *
84
+ * ```ts
85
+ * expect(secret.data.url.transform("base64", (s) => Buffer.from(s, "base64").toString()))
86
+ * .toContain("redis://");
87
+ * ```
88
+ */
89
+ transform<R = unknown>(label: string, fn: (raw: T) => R): Wrapped<R>;
90
+ /** Coerces to the raw value (arithmetic, `==`). */
91
+ valueOf(): T;
92
+ /** Renders the raw value (template interpolation). */
93
+ toString(): string;
94
+ /** Serializes as the raw value under `JSON.stringify`. */
95
+ toJSON(): T;
96
+ /** Coerces to the raw value. */
97
+ [Symbol.toPrimitive](hint?: string): T;
98
+ }
99
+ /**
100
+ * The values `expect()` accepts: anything carrying provenance from a recorded
101
+ * op (fetch / db / exec / browser / fakes / k8s) — every member of the
102
+ * {@link wrap} family ({@link Carrier}, {@link WrappedObject},
103
+ * {@link WrappedArray}, {@link WrappedResponse}) exposes `.unwrap()`, so this
104
+ * structural shape admits them all and rejects a raw primitive / object.
105
+ *
106
+ * `null`/`undefined` are also admitted: a nullish leaf can't carry the symbol
107
+ * tag, but `adoptNullishTag` recovers its provenance at runtime, so
108
+ * `expect(rows[0]?.text)` and `expect(dep.status.readyReplicas)` stay on
109
+ * `expect`. To assert on a value with no provenance (a computed number, a raw
110
+ * WebSocket frame), use `expectRaw(value, message)` instead.
111
+ */
112
+ export type Provenanced = {
113
+ unwrap(): unknown;
114
+ } | null | undefined;
115
+ /**
116
+ * What a tracked op's value looks like once wrapped — applied *deeply*, so the
117
+ * type matches the runtime at every level (the proxy lazily wraps each leaf you
118
+ * read). A primitive becomes a {@link Carrier}; an object keeps its keys but
119
+ * each property is itself `Wrapped`, plus an `.unwrap()`; an array keeps a raw
120
+ * `.length` and indexes to `Wrapped` elements (see {@link WrappedArray}).
121
+ *
122
+ * Because leaves are `Carrier`s rather than their raw type, using one as raw
123
+ * data — arithmetic, string methods, a typed client — is a compile error;
124
+ * `x.unwrap()` (or `expect(x)`, which unwraps) recovers the raw value. That's
125
+ * the whole point: the type tells you it's a handle instead of pretending to be
126
+ * the underlying value and blowing up at runtime.
127
+ *
128
+ * **Distributes over unions** so the nullish members of an optional property
129
+ * survive as `null`/`undefined` rather than collapsing into a `Carrier<undefined>`.
130
+ * That's what lets `?.` narrow a wrapped optional leaf: `Wrapped<V1PodStatus |
131
+ * undefined>` is `WrappedObject<V1PodStatus> | undefined` (so `pod.status?.phase`
132
+ * type-checks), not `Carrier<undefined> | WrappedObject<…>` (where `?.` can't see
133
+ * the `Carrier` as nullish). A nullish leaf still reaches `expect` untagged and is
134
+ * recovered at runtime by {@link adoptNullishTag}; `Provenanced` admits it because
135
+ * it includes `null | undefined`.
136
+ */
137
+ export type Wrapped<T> = T extends null | undefined ? T : T extends readonly (infer U)[] ? WrappedArray<U> : T extends (...args: never[]) => unknown ? T : T extends object ? WrappedObject<T> : Carrier<T>;
138
+ /** A wrapped object: every own property is itself {@link Wrapped}, plus an
139
+ * `.unwrap()` that recovers the fully-raw value (all nested leaves raw). */
140
+ export type WrappedObject<T> = {
141
+ readonly [K in keyof T]: Wrapped<T[K]>;
142
+ } & {
143
+ /** Recover the fully raw value (nested leaves unwrapped too). */
144
+ unwrap(): T;
145
+ /** Run the raw object through `fn`, keeping provenance (op path + `<label>`),
146
+ * and get back a navigable handle to the result. See {@link Carrier.transform}. */
147
+ transform<R = unknown>(label: string, fn: (raw: T) => R): Wrapped<R>;
148
+ };
149
+ /**
150
+ * A wrapped array. Indexing and the content-deriving methods (`find`, `map`,
151
+ * `filter`, `slice`, …) return {@link Wrapped} values so assertions on them
152
+ * still fold under the originating op; their *predicates* receive raw elements
153
+ * (the method runs on the raw target), so `=== ` comparisons inside a predicate
154
+ * keep working. `.length` stays a real `number` — provenance belongs on the
155
+ * data, not the container's size, so `rows.length === 1` must be a plain
156
+ * comparison. Iteration (`for…of`, spread) yields raw elements.
157
+ */
158
+ export interface WrappedArray<U> {
159
+ readonly length: number;
160
+ readonly [index: number]: Wrapped<U>;
161
+ /** Recover the raw array (elements unwrapped). */
162
+ unwrap(): U[];
163
+ /** Run the raw array through `fn`, keeping provenance (op path + `<label>`),
164
+ * and get back a navigable handle to the result. See {@link Carrier.transform}. */
165
+ transform<R = unknown>(label: string, fn: (raw: U[]) => R): Wrapped<R>;
166
+ at(index: number): Wrapped<U> | undefined;
167
+ find(predicate: (value: U, index: number, obj: U[]) => unknown): Wrapped<U> | undefined;
168
+ findLast(predicate: (value: U, index: number, obj: U[]) => unknown): Wrapped<U> | undefined;
169
+ findIndex(predicate: (value: U, index: number, obj: U[]) => unknown): number;
170
+ findLastIndex(predicate: (value: U, index: number, obj: U[]) => unknown): number;
171
+ filter(predicate: (value: U, index: number, array: U[]) => unknown): WrappedArray<U>;
172
+ map<R>(callback: (value: U, index: number, array: U[]) => R): WrappedArray<R>;
173
+ slice(start?: number, end?: number): WrappedArray<U>;
174
+ concat(...items: U[][]): WrappedArray<U>;
175
+ flat(): WrappedArray<unknown>;
176
+ flatMap<R>(callback: (value: U, index: number, array: U[]) => R): WrappedArray<R>;
177
+ /** Note: returns a `Carrier<boolean>` at runtime (re-wrapped for provenance),
178
+ * so use `arr.includes(x).unwrap()` or `expect(arr.includes(x))` rather than
179
+ * a bare `if`. */
180
+ includes(value: U, fromIndex?: number): Carrier<boolean>;
181
+ indexOf(value: U, fromIndex?: number): Carrier<number>;
182
+ lastIndexOf(value: U, fromIndex?: number): Carrier<number>;
183
+ some(predicate: (value: U, index: number, array: U[]) => unknown): Carrier<boolean>;
184
+ every(predicate: (value: U, index: number, array: U[]) => unknown): Carrier<boolean>;
185
+ /** Iteration yields *raw* elements (the runtime forwards the raw iterator). */
186
+ [Symbol.iterator](): IterableIterator<U>;
187
+ }
188
+ /**
189
+ * The view `ctx.fetch` resolves to in every context (a spectest op wraps
190
+ * unconditionally): a {@link Response} whose status-line accessors are
191
+ * {@link Carrier}s — so a raw `res.status === 200` is a *type error*
192
+ * (status is a `Carrier<number>`, not a number), the exact mistake that
193
+ * used to silently always be false. Compare `res.status.unwrap() === 200`
194
+ * or `res.unwrap().status === 200`, or assert with `expect(res.status)`.
195
+ *
196
+ * `json<T>()` / `text()` return {@link Wrapped} body values; `.unwrap()`
197
+ * (or {@link unwrap the whole response}) recovers the plain `Response`.
198
+ */
199
+ export interface WrappedResponse {
200
+ readonly status: Carrier<number>;
201
+ readonly ok: Carrier<boolean>;
202
+ readonly statusText: Carrier<string>;
203
+ readonly url: Carrier<string>;
204
+ readonly redirected: Carrier<boolean>;
205
+ readonly type: Carrier<string>;
206
+ readonly headers: Headers;
207
+ readonly bodyUsed: boolean;
208
+ json<T = unknown>(): Promise<Wrapped<T>>;
209
+ text(): Promise<Carrier<string>>;
210
+ arrayBuffer(): Promise<ArrayBuffer>;
211
+ blob(): Promise<Blob>;
212
+ formData(): Promise<FormData>;
213
+ clone(): WrappedResponse;
214
+ /** Recover the underlying raw {@link Response} (a real `number` status,
215
+ * an unwrapped body, etc.). */
216
+ unwrap(): Response;
217
+ }
218
+ /** The signature of `ctx.fetch`: a `fetch` that resolves to a
219
+ * {@link WrappedResponse} so reads carry provenance into assertions. */
220
+ export type SpectestFetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<WrappedResponse>;
221
+ /**
222
+ * Bespoke wrapper for `fetch` responses. Reads on `status` / `ok` /
223
+ * `statusText` / `url` / `redirected` / `type` return carriers tagged
224
+ * to `sourceSeq`. The body-reading methods (`json`, `text`) return the
225
+ * resolved value wrapped under `path: ["body"]`. Everything else passes
226
+ * through bound to the real Response.
227
+ */
228
+ export declare function wrapResponse(res: Response, sourceSeq: number | undefined): WrappedResponse;