@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.
Files changed (78) hide show
  1. package/README.md +95 -14
  2. package/dist/a2a/card.d.ts +35 -0
  3. package/dist/a2a/card.d.ts.map +1 -1
  4. package/dist/a2a/card.js +41 -3
  5. package/dist/a2a/card.js.map +1 -1
  6. package/dist/a2a/context.d.ts +9 -3
  7. package/dist/a2a/context.d.ts.map +1 -1
  8. package/dist/a2a/context.js +11 -4
  9. package/dist/a2a/context.js.map +1 -1
  10. package/dist/a2a/index.d.ts +1 -1
  11. package/dist/a2a/index.d.ts.map +1 -1
  12. package/dist/a2a/index.js +1 -1
  13. package/dist/a2a/index.js.map +1 -1
  14. package/dist/a2a/notify.d.ts +11 -2
  15. package/dist/a2a/notify.d.ts.map +1 -1
  16. package/dist/a2a/notify.js +12 -3
  17. package/dist/a2a/notify.js.map +1 -1
  18. package/dist/a2a/verify.d.ts +23 -2
  19. package/dist/a2a/verify.d.ts.map +1 -1
  20. package/dist/a2a/verify.js +26 -3
  21. package/dist/a2a/verify.js.map +1 -1
  22. package/dist/agent/history.d.ts +15 -0
  23. package/dist/agent/history.d.ts.map +1 -1
  24. package/dist/agent/history.js +17 -0
  25. package/dist/agent/history.js.map +1 -1
  26. package/dist/agent/index.d.ts +1 -1
  27. package/dist/agent/index.d.ts.map +1 -1
  28. package/dist/agent/index.js +1 -1
  29. package/dist/agent/index.js.map +1 -1
  30. package/dist/env.d.ts +10 -2
  31. package/dist/env.d.ts.map +1 -1
  32. package/dist/env.js +10 -2
  33. package/dist/env.js.map +1 -1
  34. package/dist/runtime/index.d.ts +8 -1
  35. package/dist/runtime/index.d.ts.map +1 -1
  36. package/dist/runtime/index.js +2 -1
  37. package/dist/runtime/index.js.map +1 -1
  38. package/dist/testing/auth.d.ts +23 -0
  39. package/dist/testing/auth.d.ts.map +1 -1
  40. package/dist/testing/auth.js +11 -3
  41. package/dist/testing/auth.js.map +1 -1
  42. package/dist/testing/fixtures.d.ts +21 -0
  43. package/dist/testing/fixtures.d.ts.map +1 -1
  44. package/dist/testing/fixtures.js +21 -0
  45. package/dist/testing/fixtures.js.map +1 -1
  46. package/dist/testing/index.d.ts +6 -6
  47. package/dist/testing/index.d.ts.map +1 -1
  48. package/dist/testing/index.js +5 -5
  49. package/dist/testing/index.js.map +1 -1
  50. package/dist/testing/node.d.ts +39 -29
  51. package/dist/testing/node.d.ts.map +1 -1
  52. package/dist/testing/node.js +39 -29
  53. package/dist/testing/node.js.map +1 -1
  54. package/dist/testing/vcr-global-setup.d.ts +12 -0
  55. package/dist/testing/vcr-global-setup.d.ts.map +1 -1
  56. package/dist/testing/vcr-global-setup.js +12 -3
  57. package/dist/testing/vcr-global-setup.js.map +1 -1
  58. package/dist/testing/vcr-shared.d.ts +24 -6
  59. package/dist/testing/vcr-shared.d.ts.map +1 -1
  60. package/dist/testing/vcr-shared.js +19 -6
  61. package/dist/testing/vcr-shared.js.map +1 -1
  62. package/dist/testing/vcr-spec.d.ts +10 -1
  63. package/dist/testing/vcr-spec.d.ts.map +1 -1
  64. package/dist/testing/vcr-spec.js +40 -8
  65. package/dist/testing/vcr-spec.js.map +1 -1
  66. package/dist/testing/vcr-store.d.ts +91 -0
  67. package/dist/testing/vcr-store.d.ts.map +1 -0
  68. package/dist/testing/vcr-store.js +192 -0
  69. package/dist/testing/vcr-store.js.map +1 -0
  70. package/dist/testing/vcr.d.ts +103 -66
  71. package/dist/testing/vcr.d.ts.map +1 -1
  72. package/dist/testing/vcr.js +228 -163
  73. package/dist/testing/vcr.js.map +1 -1
  74. package/dist/worker/index.d.ts +142 -3
  75. package/dist/worker/index.d.ts.map +1 -1
  76. package/dist/worker/index.js +152 -29
  77. package/dist/worker/index.js.map +1 -1
  78. package/package.json +8 -9
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Cassette load / match / write. **Node realm** (`node:fs`, `node:crypto`), no
3
+ * network and no `undici` — the recorder that drives this lives in `vcr.ts`.
4
+ *
5
+ * ## Why this replaced undici's `SnapshotAgent`
6
+ *
7
+ * `SnapshotAgent` keys every entry on a hash of method + URL + *every
8
+ * non-excluded request header*. In a Worker test those headers are runtime
9
+ * artifacts: a cassette recorded through the pool carries `user-agent: undici`,
10
+ * `cf-worker: vitest-pool-workers-runner-.example.com`, `sec-fetch-mode` and
11
+ * `accept-encoding`. Bump miniflare or workerd, any one of them changes, and
12
+ * every entry in every committed cassette misses at once — a whole suite going
13
+ * red with `No snapshot found` and nothing to point at.
14
+ *
15
+ * So the key here is **method + URL + request body**, and nothing else. Headers
16
+ * are still stored (minus secrets, see {@link CassetteOptions.excludeHeaders})
17
+ * because they are useful to read in a diff, but they cannot break a replay.
18
+ *
19
+ * The on-disk format is deliberately unchanged from `SnapshotAgent`'s, so
20
+ * cassettes recorded before this rewrite replay untouched — see {@link load}.
21
+ */
22
+ import { createHash } from "node:crypto";
23
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
24
+ import path from "node:path";
25
+ /**
26
+ * Headers that describe how a body was framed on the wire, which is not how it
27
+ * is framed on replay: bodies are stored decoded and are handed back whole, so
28
+ * replaying `transfer-encoding: chunked` or a stale `content-length` mis-frames
29
+ * the response, and `content-encoding: gzip` makes the consumer try to inflate
30
+ * plaintext. Stripped on the way out, kept in the file.
31
+ */
32
+ const HOP_BY_HOP = new Set([
33
+ "connection",
34
+ "content-encoding",
35
+ "content-length",
36
+ "keep-alive",
37
+ "proxy-authenticate",
38
+ "proxy-authorization",
39
+ "te",
40
+ "trailer",
41
+ "transfer-encoding",
42
+ "upgrade"
43
+ ]);
44
+ /** `${METHOD}\n${url}\n${sha256(body)}` — the whole matching rule. */
45
+ export function requestKey(method, url, body) {
46
+ const digest = createHash("sha256").update(body, "utf8").digest("base64url");
47
+ return `${method.toUpperCase()}\n${url}\n${digest}`;
48
+ }
49
+ function filterHeaders(headers, exclude) {
50
+ const kept = {};
51
+ for (const [name, value] of Object.entries(headers)) {
52
+ if (!exclude.has(name.toLowerCase()))
53
+ kept[name.toLowerCase()] = value;
54
+ }
55
+ return kept;
56
+ }
57
+ /**
58
+ * Turn a stored response into the pieces a `Response` is built from: body
59
+ * decoded from base64, headers flattened (a repeated header such as
60
+ * `set-cookie` becomes one entry per value) and {@link HOP_BY_HOP} dropped.
61
+ *
62
+ * Used on both paths, so a response served straight after recording is framed
63
+ * exactly as the same response will be on replay.
64
+ */
65
+ export function replayable(response) {
66
+ const headers = [];
67
+ for (const [name, value] of Object.entries(response.headers)) {
68
+ if (HOP_BY_HOP.has(name.toLowerCase()))
69
+ continue;
70
+ for (const one of Array.isArray(value) ? value : [value]) {
71
+ headers.push([name, one]);
72
+ }
73
+ }
74
+ return {
75
+ status: response.statusCode,
76
+ headers,
77
+ body: Buffer.from(response.body, "base64")
78
+ };
79
+ }
80
+ /**
81
+ * One cassette file, loaded lazily and written only when it has changed.
82
+ *
83
+ * Playback is **read-only**: nothing here mutates on a replay, which is what
84
+ * stops a plain `npm test` from dirtying committed cassettes. `SnapshotAgent`
85
+ * persisted its `callCount` and re-saved on close, so every run left the files
86
+ * modified; the equivalent counter here ({@link #calls}) is in memory and dies
87
+ * with the process.
88
+ */
89
+ export class Cassette {
90
+ #file;
91
+ #exclude;
92
+ /** Insertion-ordered, keyed by {@link requestKey}. */
93
+ #entries = new Map();
94
+ /** How many times each key has been replayed *this run*. Never persisted. */
95
+ #calls = new Map();
96
+ #dirty = false;
97
+ constructor(file, options = {}) {
98
+ this.#file = file;
99
+ this.#exclude = new Set((options.excludeHeaders ?? []).map((h) => h.toLowerCase()));
100
+ }
101
+ get file() {
102
+ return this.#file;
103
+ }
104
+ /** Every request in the file, as `METHOD url`, for error messages. */
105
+ get recorded() {
106
+ return [...this.#entries.values()].map((e) => `${e.request.method} ${e.request.url}`);
107
+ }
108
+ /**
109
+ * Read the file into memory. Tolerates both shapes: `SnapshotAgent` wrapped
110
+ * each entry in `{ hash, snapshot: { request, responses } }`, this writes the
111
+ * same wrapper (so a re-record diffs cleanly against an old cassette) but
112
+ * ignores `hash`, `callCount` and `timestamp` — all three were volatile
113
+ * bookkeeping and none is needed to replay.
114
+ *
115
+ * Entries are re-keyed on load rather than trusted, which is the point of the
116
+ * rewrite. Two legacy entries that differed *only* by a request header now
117
+ * collapse onto one key; their responses are concatenated in file order,
118
+ * which is exactly the sequential-response semantics {@link match} walks.
119
+ */
120
+ load() {
121
+ if (!existsSync(this.#file))
122
+ return;
123
+ const raw = JSON.parse(readFileSync(this.#file, "utf8"));
124
+ if (!Array.isArray(raw)) {
125
+ throw new Error(`VCR cassette ${this.#file} is not a JSON array`);
126
+ }
127
+ for (const item of raw) {
128
+ const entry = (item.snapshot ??
129
+ item);
130
+ const { request, responses } = entry;
131
+ const key = requestKey(request.method, request.url, request.body ?? "");
132
+ const existing = this.#entries.get(key);
133
+ if (existing)
134
+ existing.responses.push(...responses);
135
+ else
136
+ this.#entries.set(key, { request, responses: [...responses] });
137
+ }
138
+ }
139
+ /**
140
+ * The next recorded response for this request, or `null` if it was never
141
+ * recorded.
142
+ *
143
+ * Repeated identical requests walk `responses` in order and then **hold on
144
+ * the last one**, matching what `SnapshotAgent` did
145
+ * (`responses[min(callCount, length - 1)]`). A recording that captured one
146
+ * response therefore replays it for every call, which is what a poll loop
147
+ * needs.
148
+ */
149
+ match(method, url, body) {
150
+ const key = requestKey(method, url, body);
151
+ const entry = this.#entries.get(key);
152
+ if (!entry)
153
+ return null;
154
+ const call = this.#calls.get(key) ?? 0;
155
+ this.#calls.set(key, call + 1);
156
+ return replayable(entry.responses[Math.min(call, entry.responses.length - 1)]);
157
+ }
158
+ /** Append a live response. Repeats of a request extend its `responses`. */
159
+ record(request, response) {
160
+ const key = requestKey(request.method, request.url, request.body);
161
+ const stored = {
162
+ ...response,
163
+ headers: filterHeaders(response.headers, this.#exclude)
164
+ };
165
+ const entry = this.#entries.get(key);
166
+ if (entry) {
167
+ entry.responses.push(stored);
168
+ }
169
+ else {
170
+ this.#entries.set(key, {
171
+ request: {
172
+ ...request,
173
+ headers: filterHeaders(request.headers, this.#exclude)
174
+ },
175
+ responses: [stored]
176
+ });
177
+ }
178
+ this.#dirty = true;
179
+ }
180
+ /** Write if anything was recorded since the last flush. A no-op in playback. */
181
+ flush() {
182
+ if (!this.#dirty)
183
+ return;
184
+ mkdirSync(path.dirname(this.#file), { recursive: true });
185
+ const entries = [...this.#entries.values()].map((snapshot) => ({
186
+ snapshot
187
+ }));
188
+ writeFileSync(this.#file, `${JSON.stringify(entries, null, 2)}\n`);
189
+ this.#dirty = false;
190
+ }
191
+ }
192
+ //# sourceMappingURL=vcr-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vcr-store.js","sourceRoot":"","sources":["../../src/testing/vcr-store.ts"],"names":[],"mappings":"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"}
@@ -1,81 +1,118 @@
1
- import { MockAgent, type Dispatcher } from "undici";
2
- export interface CreateVcrAgentOptions {
3
- /** Absolute path of the cassettes directory (test/snapshots/). */
1
+ /**
2
+ * The **Node realm** half of the VCR harness: a Miniflare `outboundService`.
3
+ *
4
+ * ## Why `outboundService` and not `fetchMock`
5
+ *
6
+ * Miniflare 4 implemented `fetchMock` as one line of sugar over this same hook —
7
+ * `outboundService = (req) => fetch(req, { dispatcher: fetchMock })` — and
8
+ * Miniflare 5 dropped `fetchMock` entirely, keeping only `outboundService`.
9
+ * `@cloudflare/vitest-pool-workers` 0.20 removed it from its override options to
10
+ * match, saying so in as many words: *"`fetchMock`: you should use
11
+ * `outboundService` instead"*. Targeting the hook rather than the sugar is what
12
+ * lets one harness serve pool 0.18 through 0.20+.
13
+ *
14
+ * It also drops two constraints that had nothing to do with recording:
15
+ *
16
+ * - **No `undici` peer.** `fetchMock` was validated with
17
+ * `z.instanceof(MockAgent)` against *Miniflare's own* undici, so a second copy
18
+ * in `node_modules` failed with `Input not instance of MockAgent` and the peer
19
+ * range had to track Miniflare's exact pin. `outboundService` has no
20
+ * `instanceof` check in either direction — a foreign `Response` is re-wrapped.
21
+ * - **No silent no-op.** An unknown key in the `miniflare` options is ignored,
22
+ * which is how a harness wired to `fetchMock` on pool 0.20 failed: every
23
+ * request escaped to the real network and died as `internal error;
24
+ * reference = …`, naming nothing. `setupRecording()` now proves the recorder
25
+ * answered before a test runs — see {@link VCR_MARKER_HEADER}.
26
+ *
27
+ * ## How a request is routed
28
+ *
29
+ * Every outbound `fetch()` from workerd arrives here as a standard `Request`,
30
+ * with the true target URL already restored by Miniflare, and is dispatched on
31
+ * host alone:
32
+ *
33
+ * - **The control channel** ({@link VCR_CONTROL_ORIGIN}) — specs run in workerd
34
+ * and have no filesystem, so an in-band `fetch` is the only way to say which
35
+ * cassette the current test is using. See `setupRecording` (vcr-spec.ts).
36
+ * - **A {@link VcrOptions.handlers} host** — a consumer-supplied stub, for
37
+ * things that are fixtures rather than recordings (a gateway JWKS).
38
+ * - **An {@link VcrOptions.allowNetworkHosts} host** — the real network, always.
39
+ * - **Anything else, cassette active** — recorded or replayed against it.
40
+ * - **Anything else, no cassette** — blocked. A test that was never wired for
41
+ * recording cannot reach the network by accident.
42
+ */
43
+ /** Minimal shape of the `Request` Miniflare hands an outbound service. */
44
+ export interface VcrRequestHeaders {
45
+ forEach(callback: (value: string, name: string) => void): void;
46
+ get(name: string): string | null;
47
+ }
48
+ /** Minimal shape of the `Request` Miniflare hands an outbound service. */
49
+ export interface VcrRequest {
50
+ readonly url: string;
51
+ readonly method: string;
52
+ readonly headers: VcrRequestHeaders;
53
+ arrayBuffer(): Promise<ArrayBuffer>;
54
+ }
55
+ /**
56
+ * Structural on purpose: core does not depend on `miniflare`, and this type
57
+ * assigns to `WorkerOptions["outboundService"]` under both Miniflare 4 and 5.
58
+ */
59
+ export type VcrOutboundService = (request: VcrRequest) => Promise<Response>;
60
+ export interface VcrOptions {
61
+ /** Absolute path of the cassettes directory. */
4
62
  snapshotsDir: string;
5
63
  /**
6
- * true → every activated cassette records real traffic, unconditionally
7
- * overwriting whatever was there (undici's `"record"` mode never loads or
8
- * matches existing entries, unlike `"update"` see {@link VcrAgent.agentFor}).
9
- * false → replay.
64
+ * true → every activated cassette captures live traffic, replacing whatever
65
+ * was there. Recording never loads the existing file first, so a recording run
66
+ * always reflects the live API rather than half of a stale one.
67
+ * false → replay, and no request ever leaves the machine.
10
68
  */
11
69
  record: boolean;
12
- /** Hosts served by ordinary MockAgent interceptors, never VCR'd (the gateway JWKS). */
13
- passthroughHosts: string[];
14
- /** Request headers never written to a cassette (secrets: API keys, auth). */
70
+ /** Hosts answered by a stub instead of a cassette (e.g. a gateway JWKS). */
71
+ handlers?: Record<string, (request: VcrRequest) => Response | Promise<Response>>;
72
+ /** Hosts that always reach the real network, recorded or not. */
73
+ allowNetworkHosts?: string[];
74
+ /** Request *and* response headers never written to a cassette (API keys, auth). */
15
75
  excludeHeaders?: string[];
16
- /** Headers stored but excluded from request matching (e.g. session cookies). */
17
- ignoreHeaders?: string[];
76
+ }
77
+ export interface Vcr {
78
+ /** Hand this to `cloudflareTest({ miniflare: { outboundService } })`. */
79
+ readonly outboundService: VcrOutboundService;
80
+ /** Flush every cassette. Safe to call more than once. */
81
+ close(): Promise<void>;
18
82
  }
19
83
  /**
20
- * The single agent Miniflare accepts as `fetchMock` (it allows only one), fanning
21
- * outbound worker requests out by role:
84
+ * Build the recorder and register it for {@link closeVcr}.
22
85
  *
23
- * - **Control channel** ({@link VCR_CONTROL_ORIGIN}) → ordinary MockAgent
24
- * interceptors whose reply callbacks flip the *active cassette*. This is how a
25
- * spec running in workerd (no filesystem) tells this Node-side agent which
26
- * test is running — see `setupRecording` (vcr-spec.ts).
27
- * - **Passthrough hosts** (the gateway JWKS) → ordinary MockAgent interceptors,
28
- * unchanged from before VCR existed.
29
- * - **Everything else, while a cassette is active** → that cassette's undici
30
- * {@link SnapshotAgent} record/replay. In playback a request with no matching
31
- * recording throws `No snapshot found` — offline, never the network.
32
- * - **Everything else, with no active cassette** → ordinary MockAgent behavior
33
- * (`disableNetConnect()` → offline error). A non-recorded test never reaches
34
- * the network.
86
+ * ```ts
87
+ * // vitest.config.ts
88
+ * const vcr = createVcr({
89
+ * snapshotsDir: path.resolve(import.meta.dirname, "test/snapshots"),
90
+ * record: recordFromEnv(),
91
+ * excludeHeaders: ["x-api-key", "authorization", "cookie", "set-cookie"]
92
+ * });
35
93
  *
36
- * There is **no registry**: cassettes are keyed per test (file + describe + test
37
- * name) by the spec helper, activated one at a time over the control channel. A
38
- * new recorded spec touches only its own `.spec` file.
94
+ * export default defineConfig({
95
+ * plugins: [cloudflareTest({ miniflare: { outboundService: vcr.outboundService } })],
96
+ * test: { globalSetup: ["@loopingai/core/testing/vcr-global-setup"] }
97
+ * });
98
+ * ```
99
+ */
100
+ export declare function createVcr(options: VcrOptions): Vcr;
101
+ /**
102
+ * Whether this run records, from `RECORD`.
39
103
  *
40
- * Why `VcrAgent extends MockAgent` rather than `SnapshotAgent`: a `SnapshotAgent`'s
41
- * `dispatch` fully overrides `MockAgent`'s (it never falls back to the interceptor
42
- * path), so one `SnapshotAgent` cannot also serve the control/interceptor fixtures
43
- * and cannot serve more than one cassette. Keeping one internal `SnapshotAgent`
44
- * per cassette and dispatching to the active one, with an ordinary
45
- * `MockAgent.dispatch` fallback for control/passthrough/offline, leaves the
46
- * interceptor path exactly as it always worked. Miniflare validates `fetchMock`
47
- * with `instanceof MockAgent`, which `VcrAgent` satisfies directly.
104
+ * Compared against `"1"` rather than tested for truthiness, because every
105
+ * non-empty string is truthy: `RECORD=0` and `RECORD=false` the two things
106
+ * someone reaches for to turn recording *off* would otherwise turn it on and
107
+ * overwrite committed cassettes with live traffic. The opposite mistake
108
+ * (`RECORD=true` not recording) fails loudly on the next assertion and costs
109
+ * nothing.
48
110
  */
49
- export declare class VcrAgent extends MockAgent {
50
- #private;
51
- constructor(options: CreateVcrAgentOptions);
52
- dispatch(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean;
53
- /**
54
- * Closes every cassette's `SnapshotAgent`, then this agent.
55
- *
56
- * Record mode: `SnapshotAgent.close()` saves each cassette, stops its
57
- * recorder's auto-flush timer, and closes its real sockets — all required (see
58
- * {@link closeVcr}). We then {@link stripVolatileFields} from each saved file
59
- * so the committed cassette carries no `callCount`/`timestamp`.
60
- *
61
- * Playback mode: undici's `SnapshotAgent.close()` *unconditionally* re-saves
62
- * the cassette, writing back the `callCount` its recorder mutates on every
63
- * replay — the side effect that dirtied cassettes on a plain `npm test`. There
64
- * is nothing to save and no real socket in playback, so we skip that save
65
- * entirely and only stop each recorder's timers.
66
- */
67
- close(): Promise<void>;
68
- }
69
- export declare function createVcrAgent(options: CreateVcrAgentOptions): VcrAgent;
111
+ export declare function recordFromEnv(): boolean;
70
112
  /**
71
- * Flush and close the active VCR agent (all its cassettes). In record mode,
72
- * undici's recorder arms a self-refreshing, non-`unref`ed auto-flush timer and
73
- * the real `SnapshotAgent` keeps sockets open both keep the process alive
74
- * after tests finish and trip Vitest's "close timed out" at teardown. Calling
75
- * this (from a Vitest `globalSetup` teardown, which runs before Vite closes its
76
- * own server) stops them. A no-op if no agent was created, and in playback only
77
- * stops recorder timers — {@link VcrAgent.close} deliberately does not re-save
78
- * the cassette there (see its doc).
113
+ * Flush every cassette. Cassettes are already written when each one is
114
+ * released, so this is a safety net for a run that ended without releasing —
115
+ * and a no-op if no recorder was created.
79
116
  */
80
117
  export declare function closeVcr(): Promise<void>;
81
118
  //# sourceMappingURL=vcr.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vcr.d.ts","sourceRoot":"","sources":["../../src/testing/vcr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAiB,KAAK,UAAU,EAAE,MAAM,QAAQ,CAAC;AAmCnE,MAAM,WAAW,qBAAqB;IACpC,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB,uFAAuF;IACvF,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,QAAS,SAAQ,SAAS;;gBAWzB,OAAO,EAAE,qBAAqB;IA6E1C,QAAQ,CACN,IAAI,EAAE,UAAU,CAAC,eAAe,EAChC,OAAO,EAAE,UAAU,CAAC,eAAe,GAClC,OAAO;IAWV;;;;;;;;;;;;;OAaG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAW7B;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,QAAQ,CAIvE;AAOD;;;;;;;;;GASG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAM9C"}
1
+ {"version":3,"file":"vcr.d.ts","sourceRoot":"","sources":["../../src/testing/vcr.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,iBAAiB;IAChC,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/D,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAClC;AAED,0EAA0E;AAC1E,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;IACpC,WAAW,IAAI,OAAO,CAAC,WAAW,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,EAAE,UAAU,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE5E,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,MAAM,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CACf,MAAM,EACN,CAAC,OAAO,EAAE,UAAU,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CACtD,CAAC;IACF,iEAAiE;IACjE,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,GAAG;IAClB,yEAAyE;IACzE,QAAQ,CAAC,eAAe,EAAE,kBAAkB,CAAC;IAC7C,yDAAyD;IACzD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAgPD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,GAAG,CAQlD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAOD;;;;GAIG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAM9C"}