@specific.dev/spectest 0.34.0 → 0.35.1
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/aws.d.ts +7 -0
- package/dist/components/aws.js +54 -7
- package/dist/index.d.ts +37 -10
- package/dist/index.js +37 -10
- package/dist/inspect.d.ts +8 -2
- package/package.json +1 -1
package/dist/components/aws.d.ts
CHANGED
|
@@ -101,7 +101,14 @@ export declare function aws(opts?: AwsOptions): {
|
|
|
101
101
|
};
|
|
102
102
|
env: {
|
|
103
103
|
AWS_DEFAULT_REGION: string;
|
|
104
|
+
LAMBDA_EXECUTOR: string;
|
|
105
|
+
LAMBDA_STRICT: string;
|
|
106
|
+
LAMBDA_DOCKER_NETWORK: string;
|
|
104
107
|
};
|
|
108
|
+
volumes: {
|
|
109
|
+
source: string;
|
|
110
|
+
target: string;
|
|
111
|
+
}[];
|
|
105
112
|
ports: number[];
|
|
106
113
|
readyCheck: {
|
|
107
114
|
type: "http";
|
package/dist/components/aws.js
CHANGED
|
@@ -19,9 +19,37 @@
|
|
|
19
19
|
// pinned rather than floated. It replaced LocalStack, which folded its
|
|
20
20
|
// Apache-2.0 Community edition into one paid product in March 2026 (current
|
|
21
21
|
// images demand a LOCALSTACK_AUTH_TOKEN). MiniStack also suits fork-per-test
|
|
22
|
-
// far better: ~270 MB against ~1 GB
|
|
23
|
-
//
|
|
24
|
-
//
|
|
22
|
+
// far better: ~270 MB against ~1 GB and a ~2 s boot.
|
|
23
|
+
//
|
|
24
|
+
// Lambda runs in MiniStack's **docker** executor, never its default `local`
|
|
25
|
+
// one, because `local` has no runtime fidelity at all: it maps `python*` to
|
|
26
|
+
// its own `sys.executable` and `nodejs*` to its own `node`, so the declared
|
|
27
|
+
// `Runtime` selects only the language. A function declared `python3.11` runs
|
|
28
|
+
// on the emulator image's CPython 3.13/musl, and one declared `nodejs20.x`
|
|
29
|
+
// runs on Node 24 — which is why a native wheel built for the real target
|
|
30
|
+
// fails to import ("No module named 'pydantic_core._pydantic_core'"). The
|
|
31
|
+
// docker executor runs the handler under AWS's own Runtime Interface
|
|
32
|
+
// Emulator in `public.ecr.aws/lambda/<runtime>`, giving the real
|
|
33
|
+
// interpreter, glibc, `/var/task` and `AWS_EXECUTION_ENV`. Measured on
|
|
34
|
+
// aarch64: it is also ~7x FASTER per warm invoke (7 ms against 53 ms) at
|
|
35
|
+
// ~25-40 MiB per function container.
|
|
36
|
+
//
|
|
37
|
+
// None of this is configurable, on purpose — a knob here is a knob between
|
|
38
|
+
// "behaves like AWS" and "does not".
|
|
39
|
+
//
|
|
40
|
+
// The cost is a bind-mount of the VM's own docker socket, which the daemon
|
|
41
|
+
// already supports (`ensureVolumes` treats an existing non-directory source
|
|
42
|
+
// as a thing to mount as-is). The RIE containers are ordinary VM containers
|
|
43
|
+
// on `spectest-net`, so they ride snapshots and forks like any service: a
|
|
44
|
+
// per-test fork inherits its parent's already-warm function.
|
|
45
|
+
//
|
|
46
|
+
// Landmine: the runtime images are pulled from `public.ecr.aws`, which the
|
|
47
|
+
// in-VM dockerd reaches DIRECTLY. Its `registry-mirrors` only ever applies
|
|
48
|
+
// to Docker Hub, so the zot mirror (which does carry public.ecr.aws, on
|
|
49
|
+
// :5004) is NOT consulted. MiniStack's own `MINISTACK_IMAGE_PREFIX` cannot
|
|
50
|
+
// close that gap either: it prepends rather than replacing the registry
|
|
51
|
+
// host. Routing these pulls through zot needs a pre-pull of
|
|
52
|
+
// `spectest-host:5004/lambda/<rt>` retagged to the `public.ecr.aws` name.
|
|
25
53
|
//
|
|
26
54
|
// Like `email()`'s mail server, the product name stays out of every
|
|
27
55
|
// user-facing surface (docs, examples, error messages). Users get "the AWS
|
|
@@ -73,6 +101,13 @@ const AWS_HOST_PATTERNS = [
|
|
|
73
101
|
"*.amazonaws.com.cn",
|
|
74
102
|
...AWS_CN_REGIONS.map((r) => `*.${r}.amazonaws.com.cn`),
|
|
75
103
|
];
|
|
104
|
+
/** The VM's own docker socket, bind-mounted so the emulator can start the
|
|
105
|
+
* official AWS runtime containers. This is the VM's dockerd, inside the
|
|
106
|
+
* hermetic environment — the same daemon that runs the services. */
|
|
107
|
+
const DOCKER_SOCKET = "/var/run/docker.sock";
|
|
108
|
+
/** The network the function containers join, so a handler reaches the other
|
|
109
|
+
* services and the AWS endpoints by the same names a test uses. */
|
|
110
|
+
const SPECTEST_NETWORK = "spectest-net";
|
|
76
111
|
/** Region the emulator's own tools default to — the AWS CLI refuses to run
|
|
77
112
|
* without one. Only the *container's* default; it constrains nothing about
|
|
78
113
|
* the app, and every region is served regardless. A `setup` hook that drives
|
|
@@ -144,7 +179,18 @@ export function aws(opts = {}) {
|
|
|
144
179
|
const lambdas = opts.lambdas ?? {};
|
|
145
180
|
const service = {
|
|
146
181
|
image: { type: "registry", reference: AWS_IMAGE },
|
|
147
|
-
env: {
|
|
182
|
+
env: {
|
|
183
|
+
AWS_DEFAULT_REGION: CONTAINER_DEFAULT_REGION,
|
|
184
|
+
// Run every function in the official AWS runtime image rather than in
|
|
185
|
+
// the emulator's own interpreter — see the note at the top of the file.
|
|
186
|
+
LAMBDA_EXECUTOR: "docker",
|
|
187
|
+
// No silent fall-back to the low-fidelity executor. If the runtime
|
|
188
|
+
// container cannot start, say so instead of running the handler on the
|
|
189
|
+
// wrong interpreter and failing later, somewhere else.
|
|
190
|
+
LAMBDA_STRICT: "1",
|
|
191
|
+
LAMBDA_DOCKER_NETWORK: SPECTEST_NETWORK,
|
|
192
|
+
},
|
|
193
|
+
volumes: [{ source: DOCKER_SOCKET, target: DOCKER_SOCKET }],
|
|
148
194
|
ports: [AWS_PORT],
|
|
149
195
|
readyCheck: {
|
|
150
196
|
type: "http",
|
|
@@ -226,9 +272,10 @@ async function deployLambda(service, projectRoot, name, spec) {
|
|
|
226
272
|
Handler: spec.handler ?? "index.handler",
|
|
227
273
|
Role: spec.role ?? DEFAULT_ROLE_ARN,
|
|
228
274
|
Timeout: spec.timeoutSecs ?? 30,
|
|
229
|
-
// Fixed, and not an option: the emulated cloud
|
|
230
|
-
//
|
|
231
|
-
// nothing about how a function
|
|
275
|
+
// Fixed, and not an option: the emulated cloud reports this number back
|
|
276
|
+
// as `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` but never caps the runtime
|
|
277
|
+
// container at it, so the number changes nothing about how a function
|
|
278
|
+
// behaves here.
|
|
232
279
|
MemorySize: MEMORY_MB,
|
|
233
280
|
...(spec.env ? { Environment: { Variables: spec.env } } : {}),
|
|
234
281
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1390,16 +1390,43 @@ export declare function expect(actual: Locator, message?: string): LocatorAssert
|
|
|
1390
1390
|
export declare function expect(actual: Browser, message?: string): BrowserAssertion;
|
|
1391
1391
|
export declare function expect(actual: Provenanced, message?: string): Expectation;
|
|
1392
1392
|
/**
|
|
1393
|
-
* Assert on a value with **no provenance** — a computed number, a
|
|
1394
|
-
* WebSocket
|
|
1395
|
-
* required (it's the second argument) and reads as
|
|
1396
|
-
* "assert …" (e.g.
|
|
1397
|
-
*
|
|
1398
|
-
*
|
|
1399
|
-
*
|
|
1400
|
-
*
|
|
1401
|
-
*
|
|
1402
|
-
*
|
|
1393
|
+
* Assert on a value with **no provenance** — a computed number, a frame read
|
|
1394
|
+
* off a raw `WebSocket` you opened yourself, anything that never flowed from a
|
|
1395
|
+
* recorded op. `message` is required (it's the second argument) and reads as
|
|
1396
|
+
* the natural follow-on to "assert …" (e.g.
|
|
1397
|
+
* `expectRaw(id, "id matches the generated value")`); it renders as the
|
|
1398
|
+
* assertion's label in the CLI/dashboard ("ASSERT <message>") since a raw
|
|
1399
|
+
* assertion has no op to nest under. (`expect`'s own `message` is optional;
|
|
1400
|
+
* here it is mandatory, since the label is the only human-meaningful summary a
|
|
1401
|
+
* raw assertion has.)
|
|
1402
|
+
*
|
|
1403
|
+
* **This is an escape hatch, and reaching for it is almost always a mistake.**
|
|
1404
|
+
* A raw assertion is *deliberately* unlinked: it renders as a disconnected
|
|
1405
|
+
* top-level row with no op above it, so whoever reads the failure can't see the
|
|
1406
|
+
* request, query, or command the value came from. Using it to get past
|
|
1407
|
+
* `expect`'s compile-time provenance gate is an anti-pattern — the gate rejects
|
|
1408
|
+
* a value precisely because its provenance was destroyed on the way in, and
|
|
1409
|
+
* this doesn't restore it, it just accepts the loss.
|
|
1410
|
+
*
|
|
1411
|
+
* Nearly every real use is a *wrapped* value that got flattened by `.unwrap()`,
|
|
1412
|
+
* `String(x)`, `JSON.parse(x)`, `x.length`, or a home-grown coercion helper.
|
|
1413
|
+
* Keep it wrapped instead:
|
|
1414
|
+
*
|
|
1415
|
+
* ```ts
|
|
1416
|
+
* // ✗ flattened, then asserted raw — the link to the op is gone
|
|
1417
|
+
* expectRaw(JSON.parse(res.body.unwrap()).tier, "tier is pro").toBe("pro");
|
|
1418
|
+
* expectRaw(rows.length, "one row").toBe(1);
|
|
1419
|
+
*
|
|
1420
|
+
* // ✓ same checks, still nested under the http / db step
|
|
1421
|
+
* expect(res.body.transform<{ tier: string }>("json", (s) => JSON.parse(s)).tier).toBe("pro");
|
|
1422
|
+
* expect(rows).toHaveLength(1);
|
|
1423
|
+
* ```
|
|
1424
|
+
*
|
|
1425
|
+
* `.transform(label, fn)` carries the source op's tag through a decode,
|
|
1426
|
+
* `toHaveLength` reads a length without severing it, and a nullish leaf read
|
|
1427
|
+
* inline in `expect(...)` (or via `field`) recovers its own tag. If the value
|
|
1428
|
+
* came from a `fetch`/db/exec/browser/fake/kube op at any point, there is a
|
|
1429
|
+
* wrapped way to assert on it — see the `/tests` docs page.
|
|
1403
1430
|
*/
|
|
1404
1431
|
export declare function expectRaw(actual: unknown, message: string): Expectation;
|
|
1405
1432
|
/** `node:assert/strict` re-exported for users who prefer Node's built-in API. */
|
package/dist/index.js
CHANGED
|
@@ -657,16 +657,43 @@ function buildBrowserMatchers(session, negated, message) {
|
|
|
657
657
|
};
|
|
658
658
|
}
|
|
659
659
|
/**
|
|
660
|
-
* Assert on a value with **no provenance** — a computed number, a
|
|
661
|
-
* WebSocket
|
|
662
|
-
* required (it's the second argument) and reads as
|
|
663
|
-
* "assert …" (e.g.
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
660
|
+
* Assert on a value with **no provenance** — a computed number, a frame read
|
|
661
|
+
* off a raw `WebSocket` you opened yourself, anything that never flowed from a
|
|
662
|
+
* recorded op. `message` is required (it's the second argument) and reads as
|
|
663
|
+
* the natural follow-on to "assert …" (e.g.
|
|
664
|
+
* `expectRaw(id, "id matches the generated value")`); it renders as the
|
|
665
|
+
* assertion's label in the CLI/dashboard ("ASSERT <message>") since a raw
|
|
666
|
+
* assertion has no op to nest under. (`expect`'s own `message` is optional;
|
|
667
|
+
* here it is mandatory, since the label is the only human-meaningful summary a
|
|
668
|
+
* raw assertion has.)
|
|
669
|
+
*
|
|
670
|
+
* **This is an escape hatch, and reaching for it is almost always a mistake.**
|
|
671
|
+
* A raw assertion is *deliberately* unlinked: it renders as a disconnected
|
|
672
|
+
* top-level row with no op above it, so whoever reads the failure can't see the
|
|
673
|
+
* request, query, or command the value came from. Using it to get past
|
|
674
|
+
* `expect`'s compile-time provenance gate is an anti-pattern — the gate rejects
|
|
675
|
+
* a value precisely because its provenance was destroyed on the way in, and
|
|
676
|
+
* this doesn't restore it, it just accepts the loss.
|
|
677
|
+
*
|
|
678
|
+
* Nearly every real use is a *wrapped* value that got flattened by `.unwrap()`,
|
|
679
|
+
* `String(x)`, `JSON.parse(x)`, `x.length`, or a home-grown coercion helper.
|
|
680
|
+
* Keep it wrapped instead:
|
|
681
|
+
*
|
|
682
|
+
* ```ts
|
|
683
|
+
* // ✗ flattened, then asserted raw — the link to the op is gone
|
|
684
|
+
* expectRaw(JSON.parse(res.body.unwrap()).tier, "tier is pro").toBe("pro");
|
|
685
|
+
* expectRaw(rows.length, "one row").toBe(1);
|
|
686
|
+
*
|
|
687
|
+
* // ✓ same checks, still nested under the http / db step
|
|
688
|
+
* expect(res.body.transform<{ tier: string }>("json", (s) => JSON.parse(s)).tier).toBe("pro");
|
|
689
|
+
* expect(rows).toHaveLength(1);
|
|
690
|
+
* ```
|
|
691
|
+
*
|
|
692
|
+
* `.transform(label, fn)` carries the source op's tag through a decode,
|
|
693
|
+
* `toHaveLength` reads a length without severing it, and a nullish leaf read
|
|
694
|
+
* inline in `expect(...)` (or via `field`) recovers its own tag. If the value
|
|
695
|
+
* came from a `fetch`/db/exec/browser/fake/kube op at any point, there is a
|
|
696
|
+
* wrapped way to assert on it — see the `/tests` docs page.
|
|
670
697
|
*/
|
|
671
698
|
export function expectRaw(actual, message) {
|
|
672
699
|
// Force the raw form so no stray tag is read even if a wrapped value is
|
package/dist/inspect.d.ts
CHANGED
|
@@ -106,8 +106,14 @@ export interface Carrier<T> {
|
|
|
106
106
|
* `null`/`undefined` are also admitted: a nullish leaf can't carry the symbol
|
|
107
107
|
* tag, but `adoptNullishTag` recovers its provenance at runtime, so
|
|
108
108
|
* `expect(rows[0]?.text)` and `expect(dep.status.readyReplicas)` stay on
|
|
109
|
-
* `expect`.
|
|
110
|
-
*
|
|
109
|
+
* `expect`.
|
|
110
|
+
*
|
|
111
|
+
* When this gate rejects a value, the fix is usually to stop flattening it —
|
|
112
|
+
* `.transform(label, fn)` instead of a decode-then-`.unwrap()`, `toHaveLength`
|
|
113
|
+
* instead of `.length` — not to switch to `expectRaw`, which accepts anything
|
|
114
|
+
* but renders the assertion unlinked. Reach for `expectRaw(value, message)`
|
|
115
|
+
* only when the value never flowed from a recorded op at all (a computed
|
|
116
|
+
* number, a frame off a raw WebSocket); see its own doc comment.
|
|
111
117
|
*/
|
|
112
118
|
export type Provenanced = {
|
|
113
119
|
unwrap(): unknown;
|