@specific.dev/spectest 0.24.0 → 0.27.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 +143 -0
- package/dist/components/k3s.js +1067 -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 +4223 -0
- package/dist/ids.d.ts +2 -0
- package/{src/ids.ts → dist/ids.js} +46 -50
- package/dist/index.d.ts +1183 -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 +516 -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 -1807
- 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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type SqlClient } from "../sql.js";
|
|
2
|
+
export interface PostgresOptions {
|
|
3
|
+
/** Image tag for the official `postgres` image. Default `"18-alpine"`.
|
|
4
|
+
* Ignored when {@link image} is set. */
|
|
5
|
+
version?: string;
|
|
6
|
+
/**
|
|
7
|
+
* Full image reference, overriding the default `postgres:<version>`. For a
|
|
8
|
+
* Bun-SQL-/wire-compatible image (timescaledb, postgis, pgvector, …) you
|
|
9
|
+
* still get the instrumented client on `ctx.svc.<name>.client`.
|
|
10
|
+
*/
|
|
11
|
+
image?: string;
|
|
12
|
+
/** Database created on first boot. */
|
|
13
|
+
database: string;
|
|
14
|
+
/** Superuser name created on first boot. */
|
|
15
|
+
user: string;
|
|
16
|
+
/** Superuser password. */
|
|
17
|
+
password: string;
|
|
18
|
+
/**
|
|
19
|
+
* Whether postgres data persists across container restarts via a bind
|
|
20
|
+
* mount under `.spectest/volumes/<name>/`. Default `true`.
|
|
21
|
+
* Snapshots/forks always preserve state regardless of this flag — the
|
|
22
|
+
* volume just means a plain `docker rm`/`docker run` cycle keeps data.
|
|
23
|
+
*/
|
|
24
|
+
persistent?: boolean;
|
|
25
|
+
/** TCP port the container listens on. Default `5432`. */
|
|
26
|
+
port?: number;
|
|
27
|
+
/**
|
|
28
|
+
* CMD args appended after the image entrypoint (keeps the entrypoint, unlike
|
|
29
|
+
* `command`). For extension tuning, e.g.
|
|
30
|
+
* `["postgres", "-c", "timescaledb.max_background_workers=0"]`.
|
|
31
|
+
*/
|
|
32
|
+
args?: string[];
|
|
33
|
+
/** Extra environment variables forwarded to the container. */
|
|
34
|
+
env?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
/** Helpers a `postgres(...)` service exposes on `ctx.svc.<name>`. */
|
|
37
|
+
export interface PostgresHelpers {
|
|
38
|
+
/** An instrumented Bun SQL client — each query lands on the test event log
|
|
39
|
+
* alongside http/exec/assertion events, and its rows come back
|
|
40
|
+
* inspect-wrapped so `expect(...)` on them links to the query. See
|
|
41
|
+
* {@link SQL}. */
|
|
42
|
+
client: SqlClient;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A ready-to-use Postgres service. Drop into `environment.services`:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* services: {
|
|
49
|
+
* db: postgres({ database: "todos", user: "todos", password: "todos" }),
|
|
50
|
+
* ...
|
|
51
|
+
* }
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* Peer services reach it at `<key>:<port>` (e.g. `db:5432` for the
|
|
55
|
+
* example above). Tests get a wired-up Bun SQL client at
|
|
56
|
+
* `ctx.svc.<key>.client` — `await ctx.svc.db.client\`SELECT 1\`` — with
|
|
57
|
+
* every query recorded on the test event log.
|
|
58
|
+
*
|
|
59
|
+
* Point it at a wire-compatible image with `image`/`args`:
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* db: postgres({
|
|
63
|
+
* image: "timescale/timescaledb:latest-pg17",
|
|
64
|
+
* args: ["postgres", "-c", "timescaledb.max_background_workers=0"],
|
|
65
|
+
* database: "app", user: "app", password: "app",
|
|
66
|
+
* }),
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
export declare function postgres(opts: PostgresOptions): {
|
|
70
|
+
env: {
|
|
71
|
+
POSTGRES_DB: string;
|
|
72
|
+
POSTGRES_USER: string;
|
|
73
|
+
POSTGRES_PASSWORD: string;
|
|
74
|
+
PGDATA: string;
|
|
75
|
+
};
|
|
76
|
+
volumes: {
|
|
77
|
+
target: string;
|
|
78
|
+
}[];
|
|
79
|
+
ports: number[];
|
|
80
|
+
readyCheck: {
|
|
81
|
+
type: "tcp";
|
|
82
|
+
port: number;
|
|
83
|
+
timeoutSecs: number;
|
|
84
|
+
};
|
|
85
|
+
helpers: ({ name }: {
|
|
86
|
+
name: string;
|
|
87
|
+
}) => PostgresHelpers;
|
|
88
|
+
args?: string[] | undefined;
|
|
89
|
+
image: {
|
|
90
|
+
type: "registry";
|
|
91
|
+
reference: string;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SQL } from "../sql.js";
|
|
2
|
+
/**
|
|
3
|
+
* A ready-to-use Postgres service. Drop into `environment.services`:
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* services: {
|
|
7
|
+
* db: postgres({ database: "todos", user: "todos", password: "todos" }),
|
|
8
|
+
* ...
|
|
9
|
+
* }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* Peer services reach it at `<key>:<port>` (e.g. `db:5432` for the
|
|
13
|
+
* example above). Tests get a wired-up Bun SQL client at
|
|
14
|
+
* `ctx.svc.<key>.client` — `await ctx.svc.db.client\`SELECT 1\`` — with
|
|
15
|
+
* every query recorded on the test event log.
|
|
16
|
+
*
|
|
17
|
+
* Point it at a wire-compatible image with `image`/`args`:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* db: postgres({
|
|
21
|
+
* image: "timescale/timescaledb:latest-pg17",
|
|
22
|
+
* args: ["postgres", "-c", "timescaledb.max_background_workers=0"],
|
|
23
|
+
* database: "app", user: "app", password: "app",
|
|
24
|
+
* }),
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function postgres(opts) {
|
|
28
|
+
const version = opts.version ?? "18-alpine";
|
|
29
|
+
const reference = opts.image ?? `postgres:${version}`;
|
|
30
|
+
const port = opts.port ?? 5432;
|
|
31
|
+
const persistent = opts.persistent ?? true;
|
|
32
|
+
// `satisfies` (instead of a return-type annotation) preserves the
|
|
33
|
+
// literal type — in particular, the `helpers` factory's return shape.
|
|
34
|
+
// The mapped type in `ServiceHandlesFor<S>` reads that to type
|
|
35
|
+
// `ctx.svc.<name>` as `{ client: SqlClient }`.
|
|
36
|
+
return {
|
|
37
|
+
image: { type: "registry", reference },
|
|
38
|
+
...(opts.args ? { args: opts.args } : {}),
|
|
39
|
+
env: {
|
|
40
|
+
POSTGRES_DB: opts.database,
|
|
41
|
+
POSTGRES_USER: opts.user,
|
|
42
|
+
POSTGRES_PASSWORD: opts.password,
|
|
43
|
+
// PGDATA lives in a subdir so postgres can initialise inside a bind
|
|
44
|
+
// mount that may not be empty on first boot.
|
|
45
|
+
PGDATA: "/var/lib/postgresql/data/pgdata",
|
|
46
|
+
...(opts.env ?? {}),
|
|
47
|
+
},
|
|
48
|
+
volumes: persistent ? [{ target: "/var/lib/postgresql/data" }] : [],
|
|
49
|
+
ports: [port],
|
|
50
|
+
readyCheck: { type: "tcp", port, timeoutSecs: 60 },
|
|
51
|
+
helpers: ({ name }) => {
|
|
52
|
+
const url = `postgres://${encodeURIComponent(opts.user)}` +
|
|
53
|
+
`:${encodeURIComponent(opts.password)}` +
|
|
54
|
+
`@${name}:${port}/${encodeURIComponent(opts.database)}`;
|
|
55
|
+
return { client: new SQL(url, { label: name }) };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { FakeDefinition } from "../index.js";
|
|
2
|
+
/** Which parts of a request define a match against the cassette. */
|
|
3
|
+
export interface ReplayMatch {
|
|
4
|
+
/** Match on HTTP method. Default `true`. */
|
|
5
|
+
method?: boolean;
|
|
6
|
+
/** Match on path (no query). Default `true`. */
|
|
7
|
+
path?: boolean;
|
|
8
|
+
/** Match on normalized query string. Default `true`. */
|
|
9
|
+
query?: boolean;
|
|
10
|
+
/** Match on body: `true` = exact decoded body, `"hash"` = sha256 only.
|
|
11
|
+
* Default off. */
|
|
12
|
+
body?: boolean | "hash";
|
|
13
|
+
/** Header names (lowercase) to include in the match. Default none. */
|
|
14
|
+
headers?: string[];
|
|
15
|
+
}
|
|
16
|
+
/** Match one request dimension. A bare string is an exact match; the
|
|
17
|
+
* object forms cover prefix and regex matching. */
|
|
18
|
+
export type StringMatch = string | {
|
|
19
|
+
exact: string;
|
|
20
|
+
} | {
|
|
21
|
+
startsWith: string;
|
|
22
|
+
} | {
|
|
23
|
+
regex: string;
|
|
24
|
+
};
|
|
25
|
+
/** Request matchers for a credential-injection rule. A rule applies only
|
|
26
|
+
* when EVERY specified dimension matches; omit `match` to always apply. */
|
|
27
|
+
export interface InjectMatch {
|
|
28
|
+
/** Match the request path (no query). */
|
|
29
|
+
path?: StringMatch;
|
|
30
|
+
/** HTTP method(s) — any one matching satisfies it. */
|
|
31
|
+
method?: string | string[];
|
|
32
|
+
/** Query-param matchers (ANDed); each key's value(s) must match. */
|
|
33
|
+
query?: Record<string, StringMatch>;
|
|
34
|
+
/** Header matchers (ANDed; header names case-insensitive). */
|
|
35
|
+
headers?: Record<string, StringMatch>;
|
|
36
|
+
}
|
|
37
|
+
/** One credential-brokering rule. When it matches a request, these headers
|
|
38
|
+
* are SET on the egress forward — overwriting whatever the app sent (so app
|
|
39
|
+
* code can't smuggle a different value past the broker). Header VALUES may
|
|
40
|
+
* embed `{{secret:REF}}` tokens; each `REF` is resolved server-side from the
|
|
41
|
+
* project's Secrets store and pushed eval-scoped — the real value never
|
|
42
|
+
* enters project code, the
|
|
43
|
+
* tarball, the warm-cache hash, or a cassette (it's redacted, fail-closed). */
|
|
44
|
+
export interface InjectRule {
|
|
45
|
+
match?: InjectMatch;
|
|
46
|
+
headers: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
/** SigV4 request-signing broker. Unlike `inject` (which SETs a static header
|
|
49
|
+
* value), this RE-SIGNS the outbound request: the app signs with throwaway
|
|
50
|
+
* dummy credentials, we strip that signature and re-sign the exact outbound
|
|
51
|
+
* request with the real credentials — the only way to broker AWS auth, whose
|
|
52
|
+
* signature is a keyed HMAC over the whole request, not a static token.
|
|
53
|
+
*
|
|
54
|
+
* The three fields are **secret refs, resolved directly** from the project's
|
|
55
|
+
* Secrets store (NOT `{{secret:}}` templates) — same eval-scoped push and
|
|
56
|
+
* fail-closed redaction as `inject`. Region + service are inferred from the
|
|
57
|
+
* incoming request (its credential scope, else the host); the app never
|
|
58
|
+
* configures them. */
|
|
59
|
+
export interface AwsSigV4Sign {
|
|
60
|
+
type: "awsSigv4";
|
|
61
|
+
/** Secret ref for the AWS access key id. */
|
|
62
|
+
accessKeyId: string;
|
|
63
|
+
/** Secret ref for the AWS secret access key. */
|
|
64
|
+
secretAccessKey: string;
|
|
65
|
+
/** Optional secret ref for a session token (temporary credentials). */
|
|
66
|
+
sessionToken?: string;
|
|
67
|
+
}
|
|
68
|
+
export type SignConfig = AwsSigV4Sign;
|
|
69
|
+
export interface ReplayFakeOptions {
|
|
70
|
+
/** Stable name — the `ctx.fakes` key. */
|
|
71
|
+
name: string;
|
|
72
|
+
/** The REAL host to fake, e.g. `"api.stripe.com"`. The fake answers for
|
|
73
|
+
* this name on both `http://` (:80) and `https://` (:443), and the same
|
|
74
|
+
* name is forwarded to the real upstream in record mode (over HTTPS,
|
|
75
|
+
* resolved via an external DNS server — see the module header). Your
|
|
76
|
+
* app points at this real host directly; no separate stand-in. */
|
|
77
|
+
host: string;
|
|
78
|
+
/** What defines a request match. Default `{ method, path, query }`. */
|
|
79
|
+
match?: ReplayMatch;
|
|
80
|
+
/** Per-fake credential brokering. Rules are evaluated in
|
|
81
|
+
* order; the first whose `match` matches wins (a rule without `match`
|
|
82
|
+
* matches everything and shadows later rules). Applied at the
|
|
83
|
+
* record-mode egress forward; resolved secret values are redacted from
|
|
84
|
+
* the cassette (fail-closed). */
|
|
85
|
+
inject?: InjectRule[];
|
|
86
|
+
/** SigV4 request signing (AWS). When set, the record-mode forward strips the
|
|
87
|
+
* app's dummy signature and re-signs the outbound request with the brokered
|
|
88
|
+
* credentials. Point your AWS SDK at the real host with any dummy static
|
|
89
|
+
* creds (e.g. `AWS_ACCESS_KEY_ID=test`) — the dummies are stripped and the
|
|
90
|
+
* real ones live only on the outbound wire (redacted from the cassette). */
|
|
91
|
+
sign?: SignConfig;
|
|
92
|
+
/** Extra substrings/patterns to scrub from stored request/response
|
|
93
|
+
* bodies and queries (every injected secret value is always scrubbed). */
|
|
94
|
+
redactPatterns?: (string | RegExp)[];
|
|
95
|
+
/** `"auto"` (default) records under eval and replays under test;
|
|
96
|
+
* `"record"` / `"replay"` force a mode. */
|
|
97
|
+
mode?: "auto" | "record" | "replay";
|
|
98
|
+
}
|
|
99
|
+
export interface CassetteRequest {
|
|
100
|
+
method: string;
|
|
101
|
+
path: string;
|
|
102
|
+
/** Normalized: keys + repeated values sorted, so matching is order-free. */
|
|
103
|
+
query: Record<string, string[]>;
|
|
104
|
+
/** Matched/readable header subset only — never `authorization`/`cookie`. */
|
|
105
|
+
headers: Record<string, string>;
|
|
106
|
+
bodyHash?: string;
|
|
107
|
+
/** Request body (omitted when empty): decoded + redacted UTF-8 text, or
|
|
108
|
+
* base64 for binary payloads (S3 object PUTs, protobuf, …). */
|
|
109
|
+
body?: string;
|
|
110
|
+
/** Encoding of `body`. Absent = `"utf8"` (back-compat with old cassettes). */
|
|
111
|
+
bodyEncoding?: "utf8" | "base64";
|
|
112
|
+
}
|
|
113
|
+
export interface CassetteResponse {
|
|
114
|
+
status: number;
|
|
115
|
+
headers: Record<string, string>;
|
|
116
|
+
/** Decoded + redacted response body. */
|
|
117
|
+
body: string;
|
|
118
|
+
bodyEncoding: "utf8" | "base64";
|
|
119
|
+
}
|
|
120
|
+
export interface CassetteInteraction {
|
|
121
|
+
request: CassetteRequest;
|
|
122
|
+
response: CassetteResponse;
|
|
123
|
+
}
|
|
124
|
+
export interface Cassette {
|
|
125
|
+
version: 1;
|
|
126
|
+
fake: string;
|
|
127
|
+
/** The real host this cassette mirrors (e.g. `api.stripe.com`). */
|
|
128
|
+
host: string;
|
|
129
|
+
interactions: CassetteInteraction[];
|
|
130
|
+
}
|
|
131
|
+
/** Forked-per-test state for one replay fake. */
|
|
132
|
+
export interface CassetteState {
|
|
133
|
+
cassette: Cassette;
|
|
134
|
+
/** Per-match-signature replay cursor (call-order sequencing). */
|
|
135
|
+
cursor: Map<string, number>;
|
|
136
|
+
/** Interactions captured this record session. */
|
|
137
|
+
recorded: CassetteInteraction[];
|
|
138
|
+
dirty: boolean;
|
|
139
|
+
}
|
|
140
|
+
/** What `replay(...)` returns — a small summary that surfaces in the test
|
|
141
|
+
* timeline (recorded as a `fake` event) so the run shows exactly which
|
|
142
|
+
* cassette a test loaded and how many interactions it carried. */
|
|
143
|
+
export interface ReplaySummary {
|
|
144
|
+
/** Filename loaded (under `spectest/recordings/`) or `"(inline)"`. */
|
|
145
|
+
cassette: string;
|
|
146
|
+
/** The real host the cassette mirrors. */
|
|
147
|
+
host: string;
|
|
148
|
+
/** Number of interactions now available for replay. */
|
|
149
|
+
interactions: number;
|
|
150
|
+
}
|
|
151
|
+
export interface ReplayHelpers extends Record<string, unknown> {
|
|
152
|
+
/** Load a cassette into THIS fork's replay set and replay from it,
|
|
153
|
+
* replacing whatever was loaded before and resetting the replay cursor.
|
|
154
|
+
* Cassettes are NOT auto-loaded — call this at the top of a test. Pass a
|
|
155
|
+
* path relative to `spectest/tests/` (e.g. `"recordings/stripe.json"`; it
|
|
156
|
+
* must stay under `spectest/tests/`) or a cassette object; a missing file
|
|
157
|
+
* throws. The returned summary is recorded as a step, so the UI timeline
|
|
158
|
+
* shows what was loaded. */
|
|
159
|
+
replay(file: string | Cassette): ReplaySummary;
|
|
160
|
+
/** Full cassette JSON (loaded interactions + newly recorded, redacted)
|
|
161
|
+
* — the MVP delivery channel: `export default ctx.fakes.<name>.dump()`,
|
|
162
|
+
* then write the result to `spectest/recordings/<name>.json`. */
|
|
163
|
+
dump(): Cassette;
|
|
164
|
+
/** Interactions captured this record session. */
|
|
165
|
+
recorded(): CassetteInteraction[];
|
|
166
|
+
/** Count of interactions captured this record session. */
|
|
167
|
+
count(): number;
|
|
168
|
+
}
|
|
169
|
+
export declare function replayFake(opts: ReplayFakeOptions): FakeDefinition<CassetteState, ReplayHelpers>;
|