@specific.dev/spectest 0.31.0 → 0.32.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/components/index.d.ts +1 -1
- package/dist/components/k3s.d.ts +175 -12
- package/dist/components/k3s.js +403 -62
- package/dist/daemon.js +182 -109
- package/dist/index.d.ts +251 -217
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -168,58 +168,240 @@ export interface ServiceConfig {
|
|
|
168
168
|
cgroupns?: string;
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
171
|
+
* Everything test-side code can reach inside the VM, in every
|
|
172
|
+
* context it runs in: a test body, `spectest env eval`, a project-level
|
|
173
|
+
* `setup`, and a component's service-level `setup` / `helpers` hooks.
|
|
174
|
+
*
|
|
175
|
+
* This is deliberately ONE type. The setup surfaces used to be two
|
|
176
|
+
* disjoint sets — a component hook could read project files but not mint
|
|
177
|
+
* a certificate; a project `setup` could mint a certificate but not read
|
|
178
|
+
* a project file — so bring-up logic got split across files by which
|
|
179
|
+
* context happened to carry which method. A component is now written
|
|
180
|
+
* against exactly the primitives an end user has in a test.
|
|
181
|
+
*
|
|
182
|
+
* The one member that differs by context is {@link exec}: in a test it
|
|
183
|
+
* returns a {@link Wrapped} result (its output carries provenance into
|
|
184
|
+
* the timeline), everywhere else a plain one. See {@link TestContext}.
|
|
185
|
+
*
|
|
186
|
+
* `svc` is scoped in a service-level hook: a service's `setup`/`helpers`
|
|
187
|
+
* can reach its own handle and those of its (transitive) `dependsOn`
|
|
188
|
+
* services — the ones the DAG guarantees are already up — and reading
|
|
189
|
+
* any other key throws with the dependency it's missing. Everywhere else
|
|
190
|
+
* (tests, eval, project `setup`) the whole map is live.
|
|
176
191
|
*/
|
|
177
|
-
export interface
|
|
192
|
+
export interface SpectestContext<S extends ServicesMap = ServicesMap, F extends FakesMap = FakesMap> {
|
|
178
193
|
/**
|
|
179
194
|
* Absolute path of the extracted project root inside the VM — the
|
|
180
195
|
* directory the user's repo lands in, with `spectest/` directly under
|
|
181
|
-
* it. Use this (never a hard-coded path
|
|
182
|
-
*
|
|
196
|
+
* it. Use this (never a hard-coded path, and never `import.meta.url` —
|
|
197
|
+
* that only reaches files under `spectest/`) to locate project files:
|
|
198
|
+
* a manifest set, `supabase/migrations/**`, a fixture.
|
|
183
199
|
*/
|
|
184
200
|
projectRoot: string;
|
|
185
201
|
/** Read a project file as UTF-8. Relative paths resolve against
|
|
186
202
|
* {@link projectRoot}; absolute paths are read as-is. */
|
|
187
203
|
readProjectFile(path: string): Promise<string>;
|
|
188
204
|
/**
|
|
189
|
-
* Run a command inside a service container
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* `opts.stdin` is piped to the process — the natural way to feed a SQL
|
|
194
|
-
* file to `psql -f -` or a manifest to `kubectl apply -f -`.
|
|
205
|
+
* Run a command inside a service container. Pass an **array** for exact
|
|
206
|
+
* argv with no shell (`["psql", "-f", "-"]`), or a **string** to run via
|
|
207
|
+
* `sh -lc`. `opts.stdin` is piped to the process — the natural way to
|
|
208
|
+
* feed a SQL file to `psql -f -` or a manifest to `kubectl apply -f -`.
|
|
195
209
|
*
|
|
196
210
|
* Never throws on non-zero exit — inspect `exitCode` yourself.
|
|
211
|
+
*
|
|
212
|
+
* In a test this is the instrumented variant (recorded on the timeline,
|
|
213
|
+
* result {@link Wrapped}); in `setup`/`helpers` there is no timeline, so
|
|
214
|
+
* the result is a plain {@link ExecResult}.
|
|
215
|
+
*/
|
|
216
|
+
exec(service: string, command: string | string[], opts?: ExecOpts): Promise<ExecResult>;
|
|
217
|
+
/**
|
|
218
|
+
* Instrumented `fetch`. In a test each call is recorded on the timeline
|
|
219
|
+
* and resolves to a {@link WrappedResponse}: reads carry provenance so
|
|
220
|
+
* `expect(res.status)` / `expect(await res.json())` nest under the HTTP
|
|
221
|
+
* call. Because the status-line accessors are {@link Carrier}s, a raw
|
|
222
|
+
* `res.status === 200` is a *type error* — use `res.status.unwrap()` /
|
|
223
|
+
* `res.unwrap().status`, or assert via `expect`. Outside a test the
|
|
224
|
+
* result is wrapped the same way, just with no timeline to link to.
|
|
225
|
+
*
|
|
226
|
+
* (The plain global `fetch` is wrapped the same way at runtime but keeps
|
|
227
|
+
* the standard `Response` type, so prefer `ctx.fetch` for honestly-typed
|
|
228
|
+
* results.)
|
|
229
|
+
*/
|
|
230
|
+
fetch: SpectestFetch;
|
|
231
|
+
/**
|
|
232
|
+
* Per-service helper namespaces, keyed by service name. Only services
|
|
233
|
+
* whose definition ships a `helpers` factory appear here; the value at
|
|
234
|
+
* `ctx.svc.<name>` is exactly the record that factory returned. For a
|
|
235
|
+
* `postgres(...)` service that ships `{ client }`, tests do
|
|
236
|
+
* `await ctx.svc.db.client\`SELECT 1\``.
|
|
237
|
+
*
|
|
238
|
+
* In a service-level `setup`/`helpers` hook only the service itself and
|
|
239
|
+
* its transitive `dependsOn` are reachable (see {@link SpectestContext}).
|
|
240
|
+
*/
|
|
241
|
+
readonly svc: ServiceHandlesFor<S>;
|
|
242
|
+
/**
|
|
243
|
+
* Per-fake helper namespaces, keyed by fake name. Each is the record
|
|
244
|
+
* of functions the fake's `helpers` factory returned (or `{ state }`
|
|
245
|
+
* when it ships none — tests never touch a fake's private state
|
|
246
|
+
* directly). Those functions read/mutate the fake's state internally;
|
|
247
|
+
* the state itself is in-process and lives across the fork along with
|
|
248
|
+
* the rest of daemon memory, so calls in a child test see the fork's
|
|
249
|
+
* own copy as mutated by its ancestors. In a test every helper call is
|
|
250
|
+
* recorded as a step and its return value tracked, so assertions on it
|
|
251
|
+
* nest under the call in the timeline.
|
|
252
|
+
*
|
|
253
|
+
* Strongly typed against the project's fakes map when fakes are
|
|
254
|
+
* declared in `defineEnvironment({ ..., fakes })`: `ctx.fakes.stripe`
|
|
255
|
+
* is exactly the helpers record `defineFake`'s `helpers` factory
|
|
256
|
+
* returned — no cast. (Falls back to a loose record only when the
|
|
257
|
+
* environment declares no fakes.)
|
|
258
|
+
*/
|
|
259
|
+
readonly fakes: FakeHandlesFor<F>;
|
|
260
|
+
/**
|
|
261
|
+
* Poll a predicate until it returns a truthy value, then return that
|
|
262
|
+
* value. In a test it records one `wait` event for the whole loop (with
|
|
263
|
+
* attempt count, total duration, and the description) instead of one
|
|
264
|
+
* event per probe — useful for "wait until pod Running"-style checks
|
|
265
|
+
* where the intermediate states are noise. In `setup` there's no
|
|
266
|
+
* timeline; it's just the loop you'd otherwise hand-roll.
|
|
267
|
+
*
|
|
268
|
+
* - `null`, `undefined`, or `false` from `fn` mean "not yet" — wait
|
|
269
|
+
* `intervalMs` and try again.
|
|
270
|
+
* - Anything else is the success value and is returned, tagged with
|
|
271
|
+
* the wait event's seq. Downstream `expect(...)` on it links to
|
|
272
|
+
* the wait (one logical step), not to N suppressed HTTP calls.
|
|
273
|
+
* - Throws from `fn` propagate out immediately; the wait event is
|
|
274
|
+
* still recorded (with `error` set) so the timeline reflects the
|
|
275
|
+
* abort.
|
|
276
|
+
* - Defaults: `timeoutMs = 30_000`, `intervalMs = 1_000`.
|
|
277
|
+
*
|
|
278
|
+
* Side-effect calls inside `fn` (fetch, ctx.svc.* helpers, etc.)
|
|
279
|
+
* don't show up on the event log — the recorder is paused for the
|
|
280
|
+
* duration. Use `ctx.poll` for read-only observation, not for
|
|
281
|
+
* stateful work you want recorded.
|
|
282
|
+
*/
|
|
283
|
+
poll<T>(description: string, fn: () => T | null | undefined | false | Promise<T | null | undefined | false>, opts?: {
|
|
284
|
+
timeoutMs?: number;
|
|
285
|
+
intervalMs?: number;
|
|
286
|
+
}): Promise<Wrapped<T>>;
|
|
287
|
+
/**
|
|
288
|
+
* Register a DNS name so the rest of this context (and anything
|
|
289
|
+
* downstream of it) can reach it. `{ ingress: true }` points the name at
|
|
290
|
+
* the daemon (a fake / TLS proxy); `{ service }` points it at a
|
|
291
|
+
* container's live IP; a `*.suffix` wildcard (e.g. `"*.example.com"`)
|
|
292
|
+
* routes a whole domain — the natural fit for k3s Ingress hosts.
|
|
293
|
+
*
|
|
294
|
+
* Answered by spectest-resolver, so it works for VM-host/test code,
|
|
295
|
+
* `ctx.browser()`, and peer containers (Docker forwards unknown names to
|
|
296
|
+
* the host resolver). It does NOT land in any container's `/etc/hosts`.
|
|
297
|
+
* The registration mutates in-daemon state, so from a test it's isolated
|
|
298
|
+
* to that test's fork (like fake state); from `setup` it's captured by
|
|
299
|
+
* the warm-template snapshot and every test inherits it.
|
|
300
|
+
*
|
|
301
|
+
* ```ts
|
|
302
|
+
* await ctx.svc.k8s.apply(ingressFor("foo.example.com"));
|
|
303
|
+
* await ctx.dnsName("foo.example.com", { service: "k8s" });
|
|
304
|
+
* const res = await ctx.fetch("http://foo.example.com");
|
|
305
|
+
* ```
|
|
306
|
+
*/
|
|
307
|
+
dnsName(hostname: string, target: DnsTarget): Promise<void>;
|
|
308
|
+
/**
|
|
309
|
+
* Mint a leaf certificate from the in-VM root CA and return the PEMs.
|
|
310
|
+
*
|
|
311
|
+
* The value-returning counterpart to the `certificates` service field
|
|
312
|
+
* (see {@link ServiceConfig.certificates}): that one hands a
|
|
313
|
+
* certificate to a container before it boots, this one hands it to
|
|
314
|
+
* *you* — for loading into a Kubernetes `kubernetes.io/tls` Secret,
|
|
315
|
+
* posting to a control-plane API that provisions TLS endpoints, or
|
|
316
|
+
* driving a client-certificate handshake.
|
|
317
|
+
*
|
|
318
|
+
* The returned `ca` is the same root the whole environment already
|
|
319
|
+
* trusts (`ctx.fetch`, `ctx.browser()`, every service container), so a
|
|
320
|
+
* server configured with these PEMs verifies cleanly — no
|
|
321
|
+
* `rejectUnauthorized: false`, no `sslmode=require` downgrade.
|
|
322
|
+
* Wildcards (`*.example.com`) are allowed in `hostnames`.
|
|
323
|
+
*
|
|
324
|
+
* ```ts
|
|
325
|
+
* const { cert, key } = await ctx.certificate(["*.apps.test"]);
|
|
326
|
+
* await ctx.svc.k8s.apply(`
|
|
327
|
+
* apiVersion: v1
|
|
328
|
+
* kind: Secret
|
|
329
|
+
* metadata: { name: apps-tls, namespace: default }
|
|
330
|
+
* type: kubernetes.io/tls
|
|
331
|
+
* stringData:
|
|
332
|
+
* tls.crt: |
|
|
333
|
+
* ${cert.replace(/^/gm, " ")}
|
|
334
|
+
* tls.key: |
|
|
335
|
+
* ${key.replace(/^/gm, " ")}
|
|
336
|
+
* `);
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
certificate(hostnames: readonly string[]): Promise<CertificateMaterial>;
|
|
340
|
+
/**
|
|
341
|
+
* Start a real container on `spectest-net` at runtime — a peer machine
|
|
342
|
+
* with its own IP, reachable like any boot service. Returns once the
|
|
343
|
+
* container is up and its `readyCheck` (if any) has passed.
|
|
344
|
+
*
|
|
345
|
+
* Started from a test, the new container is part of that test's
|
|
346
|
+
* post-state snapshot, so a `dependsOn` child inherits it (same PID,
|
|
347
|
+
* same data) while siblings, which fork from the parent's earlier
|
|
348
|
+
* snapshot, never see it — the same isolation fake `state` and
|
|
349
|
+
* {@link dnsName} get. Started from `setup`, it's captured into the
|
|
350
|
+
* warm template and every test inherits it. Reach it by `name`
|
|
351
|
+
* (single-label, via the resolver) or by any `hostnames` you pass; map a
|
|
352
|
+
* multi-label name onto it with `ctx.dnsName(host, { service: name })`.
|
|
353
|
+
*
|
|
354
|
+
* The image is pulled on first use (fast through the host cache). See
|
|
355
|
+
* {@link RuntimeServiceSpec}.
|
|
356
|
+
*
|
|
357
|
+
* ```ts
|
|
358
|
+
* const { name } = await ctx.startService({
|
|
359
|
+
* name: `db-${crypto.randomUUID().slice(0, 8)}`,
|
|
360
|
+
* image: { type: "registry", reference: "postgres:16-alpine" },
|
|
361
|
+
* env: { POSTGRES_PASSWORD: "secret" },
|
|
362
|
+
* readyCheck: { type: "exec", command: "pg_isready -h 127.0.0.1 -p 5432" },
|
|
363
|
+
* });
|
|
364
|
+
* const sql = new Bun.SQL(`postgres://postgres:secret@${name}:5432/postgres`);
|
|
365
|
+
* ```
|
|
197
366
|
*/
|
|
198
|
-
|
|
367
|
+
startService(spec: RuntimeServiceSpec): Promise<RuntimeServiceHandle>;
|
|
368
|
+
/** Stop and remove a runtime service started via {@link startService}
|
|
369
|
+
* (no-op if it's already gone). */
|
|
370
|
+
stopService(name: string): Promise<void>;
|
|
199
371
|
}
|
|
200
372
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
|
|
205
|
-
|
|
373
|
+
* @deprecated Use {@link SpectestContext} — component hooks and test
|
|
374
|
+
* bodies now receive the same capabilities. Kept as an alias so existing
|
|
375
|
+
* component code keeps compiling.
|
|
376
|
+
*/
|
|
377
|
+
export type ComponentContext = SpectestContext;
|
|
378
|
+
/**
|
|
379
|
+
* Options for {@link SpectestContext.exec} — one type for every context,
|
|
380
|
+
* deliberately aliased rather than redeclared: the surfaces drifted once
|
|
381
|
+
* (the component one grew `stdin`/`timeoutMs`, the test one silently
|
|
382
|
+
* ignored them), and an alias makes that impossible to repeat.
|
|
206
383
|
*
|
|
207
|
-
* The one behavioural difference is the `timeoutMs` default: a
|
|
208
|
-
* exec runs during boot, where nothing
|
|
209
|
-
* 120 s.
|
|
210
|
-
* timeout is the ceiling
|
|
384
|
+
* The one behavioural difference is the `timeoutMs` default: a
|
|
385
|
+
* service-level `setup`/`helpers` exec runs during boot, where nothing
|
|
386
|
+
* else bounds it, so it defaults to 120 s. Elsewhere there is no default —
|
|
387
|
+
* in a test the enclosing test's own timeout is the ceiling, and a
|
|
388
|
+
* project `setup` runs unbounded (it routinely waits on rollouts).
|
|
211
389
|
*/
|
|
212
390
|
export type ComponentExecOpts = ExecOpts;
|
|
213
|
-
/** What a service's `setup` hook receives
|
|
214
|
-
|
|
391
|
+
/** What a service's `setup` hook receives: the full
|
|
392
|
+
* {@link SpectestContext} plus the service's own name and helpers. */
|
|
393
|
+
export interface ServiceSetupContext<H extends Record<string, any> = Record<string, never>> extends SpectestContext {
|
|
215
394
|
/** The service's key in the services map (container + DNS name). */
|
|
216
395
|
name: string;
|
|
217
396
|
/** The record the service's `helpers` factory returned (cached — setup
|
|
218
397
|
* and tests share one instance), or `{}` when it ships none. */
|
|
219
398
|
helpers: H;
|
|
220
399
|
}
|
|
221
|
-
/** What a service's `helpers` factory receives
|
|
222
|
-
|
|
400
|
+
/** What a service's `helpers` factory receives: the full
|
|
401
|
+
* {@link SpectestContext} plus the service's own name. Note `ctx.svc`
|
|
402
|
+
* here reaches the service's `dependsOn` only — never itself, since this
|
|
403
|
+
* hook is what builds that handle. */
|
|
404
|
+
export interface ServiceHelpersContext extends SpectestContext {
|
|
223
405
|
/** The service's key in the services map (container + DNS name). */
|
|
224
406
|
name: string;
|
|
225
407
|
}
|
|
@@ -638,25 +820,13 @@ export type TestFn<T = void, P = undefined, S extends ServicesMap = ServicesMap,
|
|
|
638
820
|
* `ctx.svc` only includes services with a `client` factory, each typed
|
|
639
821
|
* as the factory's output (e.g. a Bun SQL pool for `postgres(...)`).
|
|
640
822
|
*/
|
|
641
|
-
export interface TestContext<P = undefined, S extends ServicesMap = ServicesMap, F extends FakesMap = FakesMap> {
|
|
642
|
-
/**
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* `res.status.unwrap()` / `res.unwrap().status`, or assert via `expect`.
|
|
649
|
-
*
|
|
650
|
-
* (The plain global `fetch` is wrapped the same way at runtime but keeps
|
|
651
|
-
* the standard `Response` type, so prefer `ctx.fetch` for honestly-typed
|
|
652
|
-
* results.)
|
|
653
|
-
*/
|
|
654
|
-
fetch: SpectestFetch;
|
|
655
|
-
/** Run `sh -lc <command>` inside a service container. The result is
|
|
656
|
-
* {@link Wrapped}, so `res.stdout` is a `Carrier<string>` — assert via
|
|
657
|
-
* `expect(res.stdout)` or recover the raw string with `res.stdout.unwrap()`.
|
|
658
|
-
* (In `setup`/`eval` the result is wrapped too, just without a timeline
|
|
659
|
-
* link, so `.unwrap()` works there the same way.)
|
|
823
|
+
export interface TestContext<P = undefined, S extends ServicesMap = ServicesMap, F extends FakesMap = FakesMap> extends Omit<SpectestContext<S, F>, "exec"> {
|
|
824
|
+
/** Run a command inside a service container — a string via `sh -lc`,
|
|
825
|
+
* or an array as exact argv. The result is {@link Wrapped}, so
|
|
826
|
+
* `res.stdout` is a `Carrier<string>` — assert via `expect(res.stdout)`
|
|
827
|
+
* or recover the raw string with `res.stdout.unwrap()`. (This is the
|
|
828
|
+
* one place {@link SpectestContext.exec} differs: in `setup`/`helpers`
|
|
829
|
+
* there's no timeline to link to, so the result is plain.)
|
|
660
830
|
*
|
|
661
831
|
* The full run is also captured as an asciicast and replayed in the
|
|
662
832
|
* web UI — one recording per call, with output timestamped as it
|
|
@@ -671,7 +841,7 @@ export interface TestContext<P = undefined, S extends ServicesMap = ServicesMap,
|
|
|
671
841
|
* prefixing it with `cd <dir> && ` — the cwd is kept off the command
|
|
672
842
|
* string, so the timeline sidebar shows just the command and the
|
|
673
843
|
* directory surfaces in the detail view. */
|
|
674
|
-
exec(service: string, command: string, opts?: ExecOpts): Promise<Wrapped<ExecResult>>;
|
|
844
|
+
exec(service: string, command: string | string[], opts?: ExecOpts): Promise<Wrapped<ExecResult>>;
|
|
675
845
|
/**
|
|
676
846
|
* Run a command inside a service container under a PTY and record the
|
|
677
847
|
* full terminal session as an asciicast for replay in the web UI.
|
|
@@ -755,140 +925,6 @@ export interface TestContext<P = undefined, S extends ServicesMap = ServicesMap,
|
|
|
755
925
|
* the parent returned nothing.
|
|
756
926
|
*/
|
|
757
927
|
readonly parent: P;
|
|
758
|
-
/**
|
|
759
|
-
* Per-service helper namespaces, keyed by service name. Only services
|
|
760
|
-
* whose definition ships a `helpers` factory appear here; the value at
|
|
761
|
-
* `ctx.svc.<name>` is exactly the record that factory returned. For a
|
|
762
|
-
* `postgres(...)` service that ships `{ client }`, tests do
|
|
763
|
-
* `await ctx.svc.db.client\`SELECT 1\``.
|
|
764
|
-
*/
|
|
765
|
-
readonly svc: ServiceHandlesFor<S>;
|
|
766
|
-
/**
|
|
767
|
-
* Per-fake helper namespaces, keyed by fake name. Each is the record
|
|
768
|
-
* of functions the fake's `helpers` factory returned (or `{ state }`
|
|
769
|
-
* when it ships none — tests never touch a fake's private state
|
|
770
|
-
* directly). Those functions read/mutate the fake's state internally;
|
|
771
|
-
* the state itself is in-process and lives across the fork along with
|
|
772
|
-
* the rest of daemon memory, so calls in a child test see the fork's
|
|
773
|
-
* own copy as mutated by its ancestors. Every helper call is recorded
|
|
774
|
-
* as a step and its return value tracked, so assertions on it nest
|
|
775
|
-
* under the call in the timeline.
|
|
776
|
-
*
|
|
777
|
-
* Strongly typed against the project's fakes map when fakes are
|
|
778
|
-
* declared in `defineEnvironment({ ..., fakes })`: `ctx.fakes.stripe`
|
|
779
|
-
* is exactly the helpers record `defineFake`'s `helpers` factory
|
|
780
|
-
* returned — no cast. (Falls back to a loose record only when the
|
|
781
|
-
* environment declares no fakes.)
|
|
782
|
-
*/
|
|
783
|
-
readonly fakes: FakeHandlesFor<F>;
|
|
784
|
-
/**
|
|
785
|
-
* Poll a predicate until it returns a truthy value, then return that
|
|
786
|
-
* value. Records one `wait` event for the whole loop (with attempt
|
|
787
|
-
* count, total duration, and the description) instead of one event
|
|
788
|
-
* per probe — useful for "wait until pod Running"-style checks where
|
|
789
|
-
* the intermediate states are noise.
|
|
790
|
-
*
|
|
791
|
-
* - `null`, `undefined`, or `false` from `fn` mean "not yet" — wait
|
|
792
|
-
* `intervalMs` and try again.
|
|
793
|
-
* - Anything else is the success value and is returned, tagged with
|
|
794
|
-
* the wait event's seq. Downstream `expect(...)` on it links to
|
|
795
|
-
* the wait (one logical step), not to N suppressed HTTP calls.
|
|
796
|
-
* - Throws from `fn` propagate out immediately; the wait event is
|
|
797
|
-
* still recorded (with `error` set) so the timeline reflects the
|
|
798
|
-
* abort.
|
|
799
|
-
* - Defaults: `timeoutMs = 30_000`, `intervalMs = 1_000`.
|
|
800
|
-
*
|
|
801
|
-
* Side-effect calls inside `fn` (fetch, ctx.svc.* helpers, etc.)
|
|
802
|
-
* don't show up on the event log — the recorder is paused for the
|
|
803
|
-
* duration. Use `ctx.poll` for read-only observation, not for
|
|
804
|
-
* stateful work you want recorded.
|
|
805
|
-
*/
|
|
806
|
-
poll<T>(description: string, fn: () => T | null | undefined | false | Promise<T | null | undefined | false>, opts?: {
|
|
807
|
-
timeoutMs?: number;
|
|
808
|
-
intervalMs?: number;
|
|
809
|
-
}): Promise<Wrapped<T>>;
|
|
810
|
-
/**
|
|
811
|
-
* Register a DNS name at runtime so the rest of this test (and anything
|
|
812
|
-
* downstream of it) can reach it. `{ ingress: true }` points the name at
|
|
813
|
-
* the daemon (a fake / TLS proxy); `{ service }` points it at a
|
|
814
|
-
* container's live IP; a `*.suffix` wildcard (e.g. `"*.example.com"`)
|
|
815
|
-
* routes a whole domain — the natural fit for k3s Ingress hosts a test
|
|
816
|
-
* applies on the fly.
|
|
817
|
-
*
|
|
818
|
-
* Answered by spectest-resolver, so it works for VM-host/test code,
|
|
819
|
-
* `ctx.browser()`, and peer containers (Docker forwards unknown names to
|
|
820
|
-
* the host resolver). It does NOT land in any container's `/etc/hosts`.
|
|
821
|
-
* The registration mutates in-daemon state, so it's isolated to this
|
|
822
|
-
* test's fork — like fake state.
|
|
823
|
-
*
|
|
824
|
-
* ```ts
|
|
825
|
-
* await ctx.svc.k8s.apply(ingressFor("foo.example.com"));
|
|
826
|
-
* await ctx.dnsName("foo.example.com", { service: "k8s" });
|
|
827
|
-
* const res = await ctx.fetch("http://foo.example.com");
|
|
828
|
-
* ```
|
|
829
|
-
*/
|
|
830
|
-
dnsName(hostname: string, target: DnsTarget): Promise<void>;
|
|
831
|
-
/**
|
|
832
|
-
* Mint a leaf certificate from the in-VM root CA and return the PEMs.
|
|
833
|
-
*
|
|
834
|
-
* The value-returning counterpart to the `certificates` service field
|
|
835
|
-
* (see {@link ServiceConfig.certificates}): that one hands a
|
|
836
|
-
* certificate to a container before it boots, this one hands it to
|
|
837
|
-
* *you* — for loading into a Kubernetes `kubernetes.io/tls` Secret,
|
|
838
|
-
* posting to a control-plane API that provisions TLS endpoints, or
|
|
839
|
-
* driving a client-certificate handshake.
|
|
840
|
-
*
|
|
841
|
-
* The returned `ca` is the same root the whole environment already
|
|
842
|
-
* trusts (`ctx.fetch`, `ctx.browser()`, every service container), so a
|
|
843
|
-
* server configured with these PEMs verifies cleanly — no
|
|
844
|
-
* `rejectUnauthorized: false`, no `sslmode=require` downgrade.
|
|
845
|
-
* Wildcards (`*.example.com`) are allowed in `hostnames`.
|
|
846
|
-
*
|
|
847
|
-
* ```ts
|
|
848
|
-
* const { cert, key } = await ctx.certificate(["*.apps.test"]);
|
|
849
|
-
* await ctx.svc.k8s.apply(`
|
|
850
|
-
* apiVersion: v1
|
|
851
|
-
* kind: Secret
|
|
852
|
-
* metadata: { name: apps-tls, namespace: default }
|
|
853
|
-
* type: kubernetes.io/tls
|
|
854
|
-
* stringData:
|
|
855
|
-
* tls.crt: |
|
|
856
|
-
* ${cert.replace(/^/gm, " ")}
|
|
857
|
-
* tls.key: |
|
|
858
|
-
* ${key.replace(/^/gm, " ")}
|
|
859
|
-
* `);
|
|
860
|
-
* ```
|
|
861
|
-
*/
|
|
862
|
-
certificate(hostnames: readonly string[]): Promise<CertificateMaterial>;
|
|
863
|
-
/**
|
|
864
|
-
* Start a real container on `spectest-net` at runtime — a peer machine
|
|
865
|
-
* with its own IP, reachable like any boot service. Returns once the
|
|
866
|
-
* container is up and its `readyCheck` (if any) has passed.
|
|
867
|
-
*
|
|
868
|
-
* The new container is part of this test's post-state snapshot, so a
|
|
869
|
-
* `dependsOn` child inherits it (same PID, same data) while siblings,
|
|
870
|
-
* which fork from the parent's earlier snapshot, never see it — the same
|
|
871
|
-
* isolation fake `state` and {@link dnsName} get. Reach it by `name`
|
|
872
|
-
* (single-label, via the resolver) or by any `hostnames` you pass; map a
|
|
873
|
-
* multi-label name onto it with `ctx.dnsName(host, { service: name })`.
|
|
874
|
-
*
|
|
875
|
-
* The image is pulled on first use (fast through the host cache). See
|
|
876
|
-
* {@link RuntimeServiceSpec}.
|
|
877
|
-
*
|
|
878
|
-
* ```ts
|
|
879
|
-
* const { name } = await ctx.startService({
|
|
880
|
-
* name: `db-${crypto.randomUUID().slice(0, 8)}`,
|
|
881
|
-
* image: { type: "registry", reference: "postgres:16-alpine" },
|
|
882
|
-
* env: { POSTGRES_PASSWORD: "secret" },
|
|
883
|
-
* readyCheck: { type: "exec", command: "pg_isready -h 127.0.0.1 -p 5432" },
|
|
884
|
-
* });
|
|
885
|
-
* const sql = new Bun.SQL(`postgres://postgres:secret@${name}:5432/postgres`);
|
|
886
|
-
* ```
|
|
887
|
-
*/
|
|
888
|
-
startService(spec: RuntimeServiceSpec): Promise<RuntimeServiceHandle>;
|
|
889
|
-
/** Stop and remove a runtime service started via {@link startService}
|
|
890
|
-
* (no-op if it's already gone). */
|
|
891
|
-
stopService(name: string): Promise<void>;
|
|
892
928
|
}
|
|
893
929
|
export interface ExecResult {
|
|
894
930
|
stdout: string;
|
|
@@ -1103,42 +1139,16 @@ export interface Project<S extends ServicesMap = ServicesMap, F extends FakesMap
|
|
|
1103
1139
|
tests?: TestSuite<S, F>;
|
|
1104
1140
|
}
|
|
1105
1141
|
/**
|
|
1106
|
-
*
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1142
|
+
* Context handed to the project-level `setup` hook: the full
|
|
1143
|
+
* {@link SpectestContext}, minus only what a timeline gives a test
|
|
1144
|
+
* (testName, parent, browser/mobile/terminal). Everything a test can do
|
|
1145
|
+
* to the environment — exec, fetch, `svc`/`fakes` helpers, `poll`,
|
|
1146
|
+
* `dnsName`, `certificate`, `startService`, and project-file access via
|
|
1147
|
+
* `projectRoot`/`readProjectFile` — is available here too, and whatever it
|
|
1148
|
+
* produces is captured into the warm-template snapshot, so every test
|
|
1149
|
+
* inherits it without re-running.
|
|
1111
1150
|
*/
|
|
1112
|
-
export interface ProjectSetupContext<S extends ServicesMap = ServicesMap, F extends FakesMap = FakesMap> {
|
|
1113
|
-
fetch: SpectestFetch;
|
|
1114
|
-
exec(service: string, command: string, opts?: ExecOpts): Promise<Wrapped<ExecResult>>;
|
|
1115
|
-
readonly svc: ServiceHandlesFor<S>;
|
|
1116
|
-
/** Same surface tests see — fakes are already up by setup time. Typed
|
|
1117
|
-
* against the declared fakes map (see {@link TestContext.fakes}). */
|
|
1118
|
-
readonly fakes: FakeHandlesFor<F>;
|
|
1119
|
-
/**
|
|
1120
|
-
* Register a DNS name (see {@link TestContext.dnsName}). Useful here to
|
|
1121
|
-
* wire a wildcard like `"*.example.com"` to a k3s cluster once, before
|
|
1122
|
-
* any test runs — it's captured into the warm-template snapshot, so every
|
|
1123
|
-
* test inherits the route without re-registering.
|
|
1124
|
-
*/
|
|
1125
|
-
dnsName(hostname: string, target: DnsTarget): Promise<void>;
|
|
1126
|
-
/**
|
|
1127
|
-
* Mint a leaf certificate from the in-VM root CA (see
|
|
1128
|
-
* {@link TestContext.certificate}). Minted here it's captured into the
|
|
1129
|
-
* warm-template snapshot, so every test inherits whatever you seeded it
|
|
1130
|
-
* into — the fit for a cluster-wide TLS Secret applied once at setup.
|
|
1131
|
-
*/
|
|
1132
|
-
certificate(hostnames: readonly string[]): Promise<CertificateMaterial>;
|
|
1133
|
-
/**
|
|
1134
|
-
* Start a runtime service (see {@link TestContext.startService}). Started
|
|
1135
|
-
* here, it's captured into the warm-template snapshot and inherited by
|
|
1136
|
-
* every test — use it for backing instances that should exist before any
|
|
1137
|
-
* test runs but aren't worth a boot-time `services` entry.
|
|
1138
|
-
*/
|
|
1139
|
-
startService(spec: RuntimeServiceSpec): Promise<RuntimeServiceHandle>;
|
|
1140
|
-
/** Stop a runtime service (see {@link TestContext.stopService}). */
|
|
1141
|
-
stopService(name: string): Promise<void>;
|
|
1151
|
+
export interface ProjectSetupContext<S extends ServicesMap = ServicesMap, F extends FakesMap = FakesMap> extends SpectestContext<S, F> {
|
|
1142
1152
|
}
|
|
1143
1153
|
export type ProjectSetupFn<S extends ServicesMap = ServicesMap, F extends FakesMap = FakesMap> = (ctx: ProjectSetupContext<S, F>) => void | Promise<void>;
|
|
1144
1154
|
/**
|
|
@@ -1240,6 +1250,30 @@ export interface DefinedEnvironment<S extends ServicesMap, F extends FakesMap =
|
|
|
1240
1250
|
* matchers like `.toBe(...)` disappear.
|
|
1241
1251
|
*/
|
|
1242
1252
|
export type Ctx<E> = E extends DefinedEnvironment<infer S, infer F> ? TestContext<unknown, S, F> : never;
|
|
1253
|
+
/**
|
|
1254
|
+
* The `ctx` type a `setup` hook receives for an environment — the
|
|
1255
|
+
* {@link Ctx} counterpart for bring-up code. Use it to type the deploy /
|
|
1256
|
+
* seed helpers a project's `setup` calls, instead of hand-rolling a
|
|
1257
|
+
* structural `{ exec, certificate }` interface:
|
|
1258
|
+
*
|
|
1259
|
+
* ```ts
|
|
1260
|
+
* const env = defineEnvironment({ ... });
|
|
1261
|
+
* export type AppSetupCtx = SetupCtx<typeof env>;
|
|
1262
|
+
*
|
|
1263
|
+
* async function deployPlatform(ctx: AppSetupCtx) {
|
|
1264
|
+
* const manifests = await ctx.readProjectFile("deploy/platform.yaml");
|
|
1265
|
+
* const { cert, key } = await ctx.certificate(["*.apps.test"]);
|
|
1266
|
+
* await ctx.svc.k8s.apply(manifests);
|
|
1267
|
+
* }
|
|
1268
|
+
* ```
|
|
1269
|
+
*
|
|
1270
|
+
* A service-level `setup`/`helpers` hook gets the same capabilities plus
|
|
1271
|
+
* its own `name`/`helpers` — see {@link ServiceSetupContext}. Since
|
|
1272
|
+
* {@link ProjectSetupContext} is a {@link SpectestContext}, a helper typed
|
|
1273
|
+
* with `SetupCtx` also accepts a service `setup` ctx, so one function can
|
|
1274
|
+
* serve both.
|
|
1275
|
+
*/
|
|
1276
|
+
export type SetupCtx<E> = E extends DefinedEnvironment<infer S, infer F> ? ProjectSetupContext<S, F> : never;
|
|
1243
1277
|
/**
|
|
1244
1278
|
* Define an environment and get back a builder you can hang tests off.
|
|
1245
1279
|
* The builder's `.test(...)` returns test cases typed against the
|