@loopingai/core 0.1.2 → 0.3.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/README.md +95 -14
- package/dist/a2a/card.d.ts +35 -0
- package/dist/a2a/card.d.ts.map +1 -1
- package/dist/a2a/card.js +41 -3
- package/dist/a2a/card.js.map +1 -1
- package/dist/a2a/context.d.ts +9 -3
- package/dist/a2a/context.d.ts.map +1 -1
- package/dist/a2a/context.js +11 -4
- package/dist/a2a/context.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/notify.d.ts +11 -2
- package/dist/a2a/notify.d.ts.map +1 -1
- package/dist/a2a/notify.js +12 -3
- package/dist/a2a/notify.js.map +1 -1
- package/dist/a2a/verify.d.ts +23 -2
- package/dist/a2a/verify.d.ts.map +1 -1
- package/dist/a2a/verify.js +26 -3
- package/dist/a2a/verify.js.map +1 -1
- package/dist/agent/history.d.ts +15 -0
- package/dist/agent/history.d.ts.map +1 -1
- package/dist/agent/history.js +17 -0
- package/dist/agent/history.js.map +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +1 -1
- package/dist/agent/index.js.map +1 -1
- package/dist/env.d.ts +10 -2
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +10 -2
- package/dist/env.js.map +1 -1
- package/dist/runtime/index.d.ts +8 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/testing/auth.d.ts +23 -0
- package/dist/testing/auth.d.ts.map +1 -1
- package/dist/testing/auth.js +11 -3
- package/dist/testing/auth.js.map +1 -1
- package/dist/testing/fixtures.d.ts +21 -0
- package/dist/testing/fixtures.d.ts.map +1 -1
- package/dist/testing/fixtures.js +21 -0
- package/dist/testing/fixtures.js.map +1 -1
- package/dist/testing/index.d.ts +6 -6
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +5 -5
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/node.d.ts +39 -29
- package/dist/testing/node.d.ts.map +1 -1
- package/dist/testing/node.js +39 -29
- package/dist/testing/node.js.map +1 -1
- package/dist/testing/vcr-global-setup.d.ts +12 -0
- package/dist/testing/vcr-global-setup.d.ts.map +1 -1
- package/dist/testing/vcr-global-setup.js +12 -3
- package/dist/testing/vcr-global-setup.js.map +1 -1
- package/dist/testing/vcr-shared.d.ts +24 -6
- package/dist/testing/vcr-shared.d.ts.map +1 -1
- package/dist/testing/vcr-shared.js +19 -6
- package/dist/testing/vcr-shared.js.map +1 -1
- package/dist/testing/vcr-spec.d.ts +10 -1
- package/dist/testing/vcr-spec.d.ts.map +1 -1
- package/dist/testing/vcr-spec.js +40 -8
- package/dist/testing/vcr-spec.js.map +1 -1
- package/dist/testing/vcr-store.d.ts +91 -0
- package/dist/testing/vcr-store.d.ts.map +1 -0
- package/dist/testing/vcr-store.js +192 -0
- package/dist/testing/vcr-store.js.map +1 -0
- package/dist/testing/vcr.d.ts +103 -66
- package/dist/testing/vcr.d.ts.map +1 -1
- package/dist/testing/vcr.js +228 -163
- package/dist/testing/vcr.js.map +1 -1
- package/dist/worker/index.d.ts +142 -3
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +152 -29
- package/dist/worker/index.js.map +1 -1
- package/package.json +8 -9
package/dist/testing/node.js
CHANGED
|
@@ -2,46 +2,56 @@
|
|
|
2
2
|
* `@loopingai/core/testing/node` — the **Node-realm** half of the VCR harness.
|
|
3
3
|
*
|
|
4
4
|
* Separate from `@loopingai/core/testing` because the two halves cannot share a
|
|
5
|
-
* module graph. This side
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* module graph. This side reaches `node:fs`; the other side runs inside workerd,
|
|
6
|
+
* which has no filesystem. `vcr-shared.ts` is the only thing both may touch, and
|
|
7
|
+
* it is deliberately dependency-free so it can load in either realm.
|
|
8
8
|
*
|
|
9
9
|
* Import this from a Vitest **config** (or anything else running in Node), never
|
|
10
10
|
* from a spec:
|
|
11
11
|
*
|
|
12
12
|
* ```ts
|
|
13
13
|
* // vitest.config.ts
|
|
14
|
-
* import {
|
|
14
|
+
* import { defineConfig } from "vitest/config";
|
|
15
|
+
* import { cloudflareTest } from "@cloudflare/vitest-pool-workers";
|
|
16
|
+
* import path from "node:path";
|
|
17
|
+
* import { createVcr, recordFromEnv } from "@loopingai/core/testing/node";
|
|
18
|
+
*
|
|
19
|
+
* const vcr = createVcr({
|
|
20
|
+
* snapshotsDir: path.resolve(import.meta.dirname, "test/snapshots"),
|
|
21
|
+
* record: recordFromEnv(),
|
|
22
|
+
* // Why a committed cassette is safe, and why playback needs no credentials.
|
|
23
|
+
* excludeHeaders: ["x-api-key", "authorization", "cookie", "set-cookie"]
|
|
24
|
+
* });
|
|
15
25
|
*
|
|
16
26
|
* export default defineConfig({
|
|
17
|
-
* plugins: [
|
|
27
|
+
* plugins: [
|
|
28
|
+
* cloudflareTest({
|
|
29
|
+
* wrangler: { configPath: "./wrangler.jsonc" },
|
|
30
|
+
* miniflare: { outboundService: vcr.outboundService }
|
|
31
|
+
* })
|
|
32
|
+
* ],
|
|
18
33
|
* test: { globalSetup: ["@loopingai/core/testing/vcr-global-setup"] }
|
|
19
34
|
* });
|
|
20
35
|
* ```
|
|
21
36
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* `undici`
|
|
37
|
-
* `
|
|
38
|
-
* second copy, and the fix is to align yours rather than to debug the message.
|
|
39
|
-
*
|
|
40
|
-
* For the common case — wiring cassette flush/teardown — you do not need this
|
|
41
|
-
* subpath at all. Point `globalSetup` at
|
|
42
|
-
* `@loopingai/core/testing/vcr-global-setup`, which is this module's `setup`/
|
|
43
|
-
* `teardown` pair already packaged for Vitest.
|
|
37
|
+
* `RECORD=1` captures live traffic into every cassette a run activates; anything
|
|
38
|
+
* else replays. A host with no active cassette is **blocked**, so a test that
|
|
39
|
+
* was never wired for recording cannot reach the network by accident.
|
|
40
|
+
*
|
|
41
|
+
* ## Version constraints: there are none
|
|
42
|
+
*
|
|
43
|
+
* This used to carry two, both of which failed unreadably, and both of which
|
|
44
|
+
* were consequences of installing the recorder as Miniflare's `fetchMock`:
|
|
45
|
+
* the pool had to be `^0.18` (Miniflare 5 dropped the option, and pool 0.20
|
|
46
|
+
* removed it from its overrides), and your `undici` had to be byte-identical to
|
|
47
|
+
* Miniflare's (`fetchMock` was validated with `instanceof MockAgent`).
|
|
48
|
+
*
|
|
49
|
+
* The recorder is now a plain `outboundService` function — the hook `fetchMock`
|
|
50
|
+
* was sugar over in the first place. It is present and identical in Miniflare 4
|
|
51
|
+
* and 5, it type-checks against both, and it involves no `undici` at all. Core
|
|
52
|
+
* declares no `undici` peer and the pool peer is open.
|
|
44
53
|
*/
|
|
45
|
-
export {
|
|
46
|
-
export {
|
|
54
|
+
export { createVcr, recordFromEnv, closeVcr } from "./vcr.js";
|
|
55
|
+
export { Cassette, requestKey, replayable } from "./vcr-store.js";
|
|
56
|
+
export { VCR_CONTROL_ORIGIN, VCR_MARKER_HEADER, CASSETTE_NAME_RE } from "./vcr-shared.js";
|
|
47
57
|
//# sourceMappingURL=node.js.map
|
package/dist/testing/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/testing/node.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/testing/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,EACL,SAAS,EACT,aAAa,EACb,QAAQ,EAMT,MAAM,UAAU,CAAC;AAElB,OAAO,EACL,QAAQ,EACR,UAAU,EACV,UAAU,EAMX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,gBAAgB,EAEjB,MAAM,iBAAiB,CAAC"}
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest `globalSetup`: the exported teardown runs after all tests but before
|
|
3
|
+
* Vite tears down its server, so it is the last point at which a cassette can
|
|
4
|
+
* still be written.
|
|
5
|
+
*
|
|
6
|
+
* Belt and braces rather than load-bearing — each cassette is already flushed
|
|
7
|
+
* when its test releases it, so a run that ends normally has nothing left to do
|
|
8
|
+
* here, and a run that dies mid-test keeps everything recorded up to that point.
|
|
9
|
+
* (Under the old undici recorder this step was mandatory: it held real sockets
|
|
10
|
+
* and a self-refreshing auto-flush timer open, and a `RECORD=1` run hung on
|
|
11
|
+
* "close timed out" without it. Neither exists now.)
|
|
12
|
+
*/
|
|
1
13
|
export declare function setup(): void;
|
|
2
14
|
export declare function teardown(): Promise<void>;
|
|
3
15
|
//# sourceMappingURL=vcr-global-setup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vcr-global-setup.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-global-setup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vcr-global-setup.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-global-setup.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,IAAI,IAAI,CAAG;AAEhC,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAE9C"}
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { closeVcr } from "./vcr.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Vitest `globalSetup`: the exported teardown runs after all tests but before
|
|
4
|
+
* Vite tears down its server, so it is the last point at which a cassette can
|
|
5
|
+
* still be written.
|
|
6
|
+
*
|
|
7
|
+
* Belt and braces rather than load-bearing — each cassette is already flushed
|
|
8
|
+
* when its test releases it, so a run that ends normally has nothing left to do
|
|
9
|
+
* here, and a run that dies mid-test keeps everything recorded up to that point.
|
|
10
|
+
* (Under the old undici recorder this step was mandatory: it held real sockets
|
|
11
|
+
* and a self-refreshing auto-flush timer open, and a `RECORD=1` run hung on
|
|
12
|
+
* "close timed out" without it. Neither exists now.)
|
|
13
|
+
*/
|
|
5
14
|
export function setup() { }
|
|
6
15
|
export async function teardown() {
|
|
7
16
|
await closeVcr();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vcr-global-setup.js","sourceRoot":"","sources":["../../src/testing/vcr-global-setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC
|
|
1
|
+
{"version":3,"file":"vcr-global-setup.js","sourceRoot":"","sources":["../../src/testing/vcr-global-setup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,KAAK,KAAU,CAAC;AAEhC,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,MAAM,QAAQ,EAAE,CAAC;AACnB,CAAC"}
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Constants shared between the two sides of the VCR harness that must NOT import
|
|
3
|
-
* each other: the Node-side
|
|
4
|
-
* worker-side spec helper (`vcr-spec.ts`, runs in
|
|
5
|
-
* dependency-free so both realms can load it.
|
|
3
|
+
* each other: the Node-side recorder (`vcr.ts` / `vcr-store.ts`, which reach
|
|
4
|
+
* `node:fs`) and the worker-side spec helper (`vcr-spec.ts`, which runs in
|
|
5
|
+
* workerd). Keep this file dependency-free so both realms can load it.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* Reserved origin for the in-band control channel. A recorded spec `fetch`es
|
|
9
|
-
* here
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* here — through the same Miniflare `outboundService` every worker fetch already
|
|
10
|
+
* flows through — to tell the Node-side recorder which cassette is active for
|
|
11
|
+
* the current test. See `setupRecording` (vcr-spec.ts) and `createVcr` (vcr.ts).
|
|
12
|
+
* Never a real network host.
|
|
12
13
|
*/
|
|
13
14
|
export declare const VCR_CONTROL_ORIGIN = "https://vcr.internal";
|
|
15
|
+
/**
|
|
16
|
+
* Stamped on every response the recorder produces.
|
|
17
|
+
*
|
|
18
|
+
* The point is to prove the recorder is *installed*. A Miniflare option that no
|
|
19
|
+
* longer exists is silently ignored rather than rejected — which is exactly how
|
|
20
|
+
* a harness still wired to `fetchMock` on pool 0.20 failed: nothing intercepted
|
|
21
|
+
* anything, every request escaped to the real network, and each one died as
|
|
22
|
+
* `internal error; reference = …` naming nothing. Checking for this header on
|
|
23
|
+
* the control response turns that whole class of failure into one named error
|
|
24
|
+
* before any test runs.
|
|
25
|
+
*/
|
|
26
|
+
export declare const VCR_MARKER_HEADER = "x-vcr";
|
|
27
|
+
/** Body of a successful `POST /release`: what the recorder could not serve. */
|
|
28
|
+
export interface VcrReleaseResult {
|
|
29
|
+
/** `"<METHOD> <url>"` for each blocked or unmatched request in the test. */
|
|
30
|
+
misses: string[];
|
|
31
|
+
}
|
|
14
32
|
/**
|
|
15
33
|
* Cassette filenames are derived from kebab-cased file + describe + test names
|
|
16
34
|
* (see `cassetteNameFor`), so they are always lowercase `a-z0-9-` plus the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vcr-shared.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-shared.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH
|
|
1
|
+
{"version":3,"file":"vcr-shared.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-shared.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,UAAU,CAAC;AAEzC,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAC/B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAiC,CAAC"}
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Constants shared between the two sides of the VCR harness that must NOT import
|
|
3
|
-
* each other: the Node-side
|
|
4
|
-
* worker-side spec helper (`vcr-spec.ts`, runs in
|
|
5
|
-
* dependency-free so both realms can load it.
|
|
3
|
+
* each other: the Node-side recorder (`vcr.ts` / `vcr-store.ts`, which reach
|
|
4
|
+
* `node:fs`) and the worker-side spec helper (`vcr-spec.ts`, which runs in
|
|
5
|
+
* workerd). Keep this file dependency-free so both realms can load it.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
8
|
* Reserved origin for the in-band control channel. A recorded spec `fetch`es
|
|
9
|
-
* here
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* here — through the same Miniflare `outboundService` every worker fetch already
|
|
10
|
+
* flows through — to tell the Node-side recorder which cassette is active for
|
|
11
|
+
* the current test. See `setupRecording` (vcr-spec.ts) and `createVcr` (vcr.ts).
|
|
12
|
+
* Never a real network host.
|
|
12
13
|
*/
|
|
13
14
|
export const VCR_CONTROL_ORIGIN = "https://vcr.internal";
|
|
15
|
+
/**
|
|
16
|
+
* Stamped on every response the recorder produces.
|
|
17
|
+
*
|
|
18
|
+
* The point is to prove the recorder is *installed*. A Miniflare option that no
|
|
19
|
+
* longer exists is silently ignored rather than rejected — which is exactly how
|
|
20
|
+
* a harness still wired to `fetchMock` on pool 0.20 failed: nothing intercepted
|
|
21
|
+
* anything, every request escaped to the real network, and each one died as
|
|
22
|
+
* `internal error; reference = …` naming nothing. Checking for this header on
|
|
23
|
+
* the control response turns that whole class of failure into one named error
|
|
24
|
+
* before any test runs.
|
|
25
|
+
*/
|
|
26
|
+
export const VCR_MARKER_HEADER = "x-vcr";
|
|
14
27
|
/**
|
|
15
28
|
* Cassette filenames are derived from kebab-cased file + describe + test names
|
|
16
29
|
* (see `cassetteNameFor`), so they are always lowercase `a-z0-9-` plus the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vcr-shared.js","sourceRoot":"","sources":["../../src/testing/vcr-shared.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH
|
|
1
|
+
{"version":3,"file":"vcr-shared.js","sourceRoot":"","sources":["../../src/testing/vcr-shared.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,OAAO,CAAC;AAQzC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,8BAA8B,CAAC"}
|
|
@@ -7,11 +7,20 @@ import type { RunnerTestCase } from "vitest";
|
|
|
7
7
|
* Exported for debugging / the cassette-rename step.
|
|
8
8
|
*/
|
|
9
9
|
export declare function cassetteNameFor(task: RunnerTestCase): string;
|
|
10
|
+
export interface SetupRecordingOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Let a test finish even though the recorder could not serve one of its
|
|
13
|
+
* requests. Only for a spec that deliberately exercises a failing `fetch`;
|
|
14
|
+
* the default is to fail, because a silent miss reads as a broken assertion
|
|
15
|
+
* several frames from its cause.
|
|
16
|
+
*/
|
|
17
|
+
allowMisses?: boolean;
|
|
18
|
+
}
|
|
10
19
|
/**
|
|
11
20
|
* Call once at the top of a recorded spec file (outside any `describe`). Adds
|
|
12
21
|
* per-test hooks that activate the test's cassette before it runs and release
|
|
13
22
|
* it after. A missing cassette **fails** the test with instructions to record
|
|
14
23
|
* (never skips — CI must go red so the gap is visible).
|
|
15
24
|
*/
|
|
16
|
-
export declare function setupRecording(): void;
|
|
25
|
+
export declare function setupRecording(options?: SetupRecordingOptions): void;
|
|
17
26
|
//# sourceMappingURL=vcr-spec.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vcr-spec.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-spec.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"vcr-spec.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-spec.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAmB,MAAM,QAAQ,CAAC;AAsD9D;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAW5D;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAgBD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,IAAI,CAkDxE"}
|
package/dist/testing/vcr-spec.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { VCR_CONTROL_ORIGIN } from "./vcr-shared.js";
|
|
2
|
+
import { VCR_CONTROL_ORIGIN, VCR_MARKER_HEADER } from "./vcr-shared.js";
|
|
3
3
|
/**
|
|
4
4
|
* Worker-side half of the VCR harness. `setupRecording()` is the *only* thing a
|
|
5
5
|
* recorded spec wires up: one call at the top of the file gives every `it` its
|
|
6
6
|
* own cassette, auto-named from the file + describe + test names, recorded under
|
|
7
7
|
* `RECORD=1` and replayed otherwise. No per-recipe config, no registry.
|
|
8
8
|
*
|
|
9
|
-
* How it reaches the Node-side
|
|
10
|
-
* the active cassette is announced over an in-band control channel — a
|
|
11
|
-
* to {@link VCR_CONTROL_ORIGIN} that `
|
|
12
|
-
*
|
|
9
|
+
* How it reaches the Node-side recorder: specs run in workerd (no filesystem),
|
|
10
|
+
* so the active cassette is announced over an in-band control channel — a
|
|
11
|
+
* `fetch` to {@link VCR_CONTROL_ORIGIN} that `createVcr` (vcr.ts) answers from
|
|
12
|
+
* the same Miniflare `outboundService` every worker fetch already flows through.
|
|
13
13
|
*/
|
|
14
14
|
const kebab = (s) => s
|
|
15
15
|
.toLowerCase()
|
|
@@ -60,18 +60,35 @@ export function cassetteNameFor(task) {
|
|
|
60
60
|
}
|
|
61
61
|
return [rel, ...suites, task.name].map(kebab).join("--") + ".snapshot.json";
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The recorder answers every control request with {@link VCR_MARKER_HEADER}, so
|
|
65
|
+
* its absence — or a `fetch` that rejects outright, which is what a request to
|
|
66
|
+
* a host that does not resolve does — means nothing is intercepting outbound
|
|
67
|
+
* traffic at all.
|
|
68
|
+
*/
|
|
69
|
+
const NOT_INSTALLED = `The VCR recorder is not installed, so ${VCR_CONTROL_ORIGIN} went to the ` +
|
|
70
|
+
`real network. Wire it into your vitest config:\n\n` +
|
|
71
|
+
` const vcr = createVcr({ snapshotsDir, record: recordFromEnv() });\n` +
|
|
72
|
+
` cloudflareTest({ miniflare: { outboundService: vcr.outboundService } })\n\n` +
|
|
73
|
+
`(\`fetchMock\` is not it: pool 0.20 removed the option, and an unknown key ` +
|
|
74
|
+
`in \`miniflare\` is ignored rather than rejected.)`;
|
|
63
75
|
/**
|
|
64
76
|
* Call once at the top of a recorded spec file (outside any `describe`). Adds
|
|
65
77
|
* per-test hooks that activate the test's cassette before it runs and release
|
|
66
78
|
* it after. A missing cassette **fails** the test with instructions to record
|
|
67
79
|
* (never skips — CI must go red so the gap is visible).
|
|
68
80
|
*/
|
|
69
|
-
export function setupRecording() {
|
|
81
|
+
export function setupRecording(options = {}) {
|
|
70
82
|
beforeEach(async (ctx) => {
|
|
71
83
|
const cassette = cassetteNameFor(ctx.task);
|
|
72
84
|
const res = await fetch(`${VCR_CONTROL_ORIGIN}/use?cassette=${cassette}`, {
|
|
73
85
|
method: "POST"
|
|
86
|
+
}).catch(() => {
|
|
87
|
+
throw new Error(NOT_INSTALLED);
|
|
74
88
|
});
|
|
89
|
+
if (res.headers.get(VCR_MARKER_HEADER) === null) {
|
|
90
|
+
throw new Error(NOT_INSTALLED);
|
|
91
|
+
}
|
|
75
92
|
if (res.status === 404) {
|
|
76
93
|
throw new Error(`No VCR cassette "${cassette}". Record it with \`RECORD=1\` ` +
|
|
77
94
|
`(add \`-t "${ctx.task.name}"\` to record only this test), which needs ` +
|
|
@@ -79,7 +96,7 @@ export function setupRecording() {
|
|
|
79
96
|
}
|
|
80
97
|
if (res.status === 409) {
|
|
81
98
|
throw new Error(`VCR cassette "${cassette}" could not activate: another recorded test is ` +
|
|
82
|
-
`already using the shared
|
|
99
|
+
`already using the shared recorder. Recorded specs must not run in ` +
|
|
83
100
|
`parallel — keep them sequential.`);
|
|
84
101
|
}
|
|
85
102
|
if (!res.ok) {
|
|
@@ -87,7 +104,22 @@ export function setupRecording() {
|
|
|
87
104
|
}
|
|
88
105
|
});
|
|
89
106
|
afterEach(async () => {
|
|
90
|
-
await fetch(`${VCR_CONTROL_ORIGIN}/release`, {
|
|
107
|
+
const res = await fetch(`${VCR_CONTROL_ORIGIN}/release`, {
|
|
108
|
+
method: "POST"
|
|
109
|
+
});
|
|
110
|
+
if (options.allowMisses === true || !res.ok)
|
|
111
|
+
return;
|
|
112
|
+
const { misses } = (await res.json());
|
|
113
|
+
if (misses.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
// A miss cannot reject the Worker's own `fetch()` — Miniflare turns anything
|
|
116
|
+
// an outbound service throws into a 500 — so the request under test sees a
|
|
117
|
+
// 500 body and fails somewhere downstream, if at all. Reporting it here is
|
|
118
|
+
// what makes the *cause* the failure.
|
|
119
|
+
throw new Error(`VCR could not serve ${misses.length} request(s) in this test:\n` +
|
|
120
|
+
misses.map((m) => ` • ${m}`).join("\n") +
|
|
121
|
+
`\nRe-record with \`RECORD=1\`, or pass ` +
|
|
122
|
+
`\`setupRecording({ allowMisses: true })\` if the failure is the point.`);
|
|
91
123
|
});
|
|
92
124
|
}
|
|
93
125
|
//# sourceMappingURL=vcr-spec.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vcr-spec.js","sourceRoot":"","sources":["../../src/testing/vcr-spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAE/C,OAAO,
|
|
1
|
+
{"version":3,"file":"vcr-spec.js","sourceRoot":"","sources":["../../src/testing/vcr-spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** A stored request. `body` is utf-8 text; binary request bodies are not supported. */
|
|
2
|
+
export interface RecordedRequest {
|
|
3
|
+
method: string;
|
|
4
|
+
url: string;
|
|
5
|
+
headers: Record<string, string | string[]>;
|
|
6
|
+
body: string;
|
|
7
|
+
}
|
|
8
|
+
/** A stored response. `body` is base64, as `SnapshotAgent` wrote it. */
|
|
9
|
+
export interface RecordedResponse {
|
|
10
|
+
statusCode: number;
|
|
11
|
+
headers: Record<string, string | string[]>;
|
|
12
|
+
body: string;
|
|
13
|
+
trailers?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One cassette entry: a request and every response recorded for it, in order.
|
|
17
|
+
*
|
|
18
|
+
* The array is what makes a replayed sequence work — two identical requests
|
|
19
|
+
* that returned different things (an ARC frame after a move, say) are one entry
|
|
20
|
+
* with two responses, walked in order. See {@link Cassette.match}.
|
|
21
|
+
*/
|
|
22
|
+
export interface CassetteEntry {
|
|
23
|
+
request: RecordedRequest;
|
|
24
|
+
responses: RecordedResponse[];
|
|
25
|
+
}
|
|
26
|
+
/** What {@link Cassette.match} hands back: everything needed to build a Response. */
|
|
27
|
+
export interface ReplayableResponse {
|
|
28
|
+
status: number;
|
|
29
|
+
headers: [string, string][];
|
|
30
|
+
body: Uint8Array;
|
|
31
|
+
}
|
|
32
|
+
export interface CassetteOptions {
|
|
33
|
+
/** Request *and* response headers never written to the file (API keys, auth). */
|
|
34
|
+
excludeHeaders?: string[];
|
|
35
|
+
}
|
|
36
|
+
/** `${METHOD}\n${url}\n${sha256(body)}` — the whole matching rule. */
|
|
37
|
+
export declare function requestKey(method: string, url: string, body: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Turn a stored response into the pieces a `Response` is built from: body
|
|
40
|
+
* decoded from base64, headers flattened (a repeated header such as
|
|
41
|
+
* `set-cookie` becomes one entry per value) and {@link HOP_BY_HOP} dropped.
|
|
42
|
+
*
|
|
43
|
+
* Used on both paths, so a response served straight after recording is framed
|
|
44
|
+
* exactly as the same response will be on replay.
|
|
45
|
+
*/
|
|
46
|
+
export declare function replayable(response: RecordedResponse): ReplayableResponse;
|
|
47
|
+
/**
|
|
48
|
+
* One cassette file, loaded lazily and written only when it has changed.
|
|
49
|
+
*
|
|
50
|
+
* Playback is **read-only**: nothing here mutates on a replay, which is what
|
|
51
|
+
* stops a plain `npm test` from dirtying committed cassettes. `SnapshotAgent`
|
|
52
|
+
* persisted its `callCount` and re-saved on close, so every run left the files
|
|
53
|
+
* modified; the equivalent counter here ({@link #calls}) is in memory and dies
|
|
54
|
+
* with the process.
|
|
55
|
+
*/
|
|
56
|
+
export declare class Cassette {
|
|
57
|
+
#private;
|
|
58
|
+
constructor(file: string, options?: CassetteOptions);
|
|
59
|
+
get file(): string;
|
|
60
|
+
/** Every request in the file, as `METHOD url`, for error messages. */
|
|
61
|
+
get recorded(): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Read the file into memory. Tolerates both shapes: `SnapshotAgent` wrapped
|
|
64
|
+
* each entry in `{ hash, snapshot: { request, responses } }`, this writes the
|
|
65
|
+
* same wrapper (so a re-record diffs cleanly against an old cassette) but
|
|
66
|
+
* ignores `hash`, `callCount` and `timestamp` — all three were volatile
|
|
67
|
+
* bookkeeping and none is needed to replay.
|
|
68
|
+
*
|
|
69
|
+
* Entries are re-keyed on load rather than trusted, which is the point of the
|
|
70
|
+
* rewrite. Two legacy entries that differed *only* by a request header now
|
|
71
|
+
* collapse onto one key; their responses are concatenated in file order,
|
|
72
|
+
* which is exactly the sequential-response semantics {@link match} walks.
|
|
73
|
+
*/
|
|
74
|
+
load(): void;
|
|
75
|
+
/**
|
|
76
|
+
* The next recorded response for this request, or `null` if it was never
|
|
77
|
+
* recorded.
|
|
78
|
+
*
|
|
79
|
+
* Repeated identical requests walk `responses` in order and then **hold on
|
|
80
|
+
* the last one**, matching what `SnapshotAgent` did
|
|
81
|
+
* (`responses[min(callCount, length - 1)]`). A recording that captured one
|
|
82
|
+
* response therefore replays it for every call, which is what a poll loop
|
|
83
|
+
* needs.
|
|
84
|
+
*/
|
|
85
|
+
match(method: string, url: string, body: string): ReplayableResponse | null;
|
|
86
|
+
/** Append a live response. Repeats of a request extend its `responses`. */
|
|
87
|
+
record(request: RecordedRequest, response: RecordedResponse): void;
|
|
88
|
+
/** Write if anything was recorded since the last flush. A no-op in playback. */
|
|
89
|
+
flush(): void;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=vcr-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vcr-store.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-store.ts"],"names":[],"mappings":"AAyBA,uFAAuF;AACvF,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wEAAwE;AACxE,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,gBAAgB,EAAE,CAAC;CAC/B;AAsBD,qFAAqF;AACrF,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC5B,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG5E;AAaD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,gBAAgB,GAAG,kBAAkB,CAazE;AAED;;;;;;;;GAQG;AACH,qBAAa,QAAQ;;gBASP,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB;IAOvD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,sEAAsE;IACtE,IAAI,QAAQ,IAAI,MAAM,EAAE,CAIvB;IAED;;;;;;;;;;;OAWG;IACH,IAAI,IAAI,IAAI;IAiBZ;;;;;;;;;OASG;IACH,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;IAW3E,2EAA2E;IAC3E,MAAM,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,gBAAgB,GAAG,IAAI;IAqBlE,gFAAgF;IAChF,KAAK,IAAI,IAAI;CASd"}
|