@loopingai/core 0.2.0 → 0.3.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/README.md +22 -3
- 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 +87 -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/package.json +13 -13
package/dist/testing/vcr.js
CHANGED
|
@@ -1,211 +1,276 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import {
|
|
3
|
+
import { Cassette, replayable } from "./vcr-store.js";
|
|
4
|
+
import { VCR_CONTROL_ORIGIN, VCR_MARKER_HEADER, CASSETTE_NAME_RE } from "./vcr-shared.js";
|
|
5
|
+
const CONTROL_HOST = new URL(VCR_CONTROL_ORIGIN).host;
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* so we delete them from a freshly recorded cassette. Removing them is also what
|
|
11
|
-
* stops a plain `npm test` from dirtying cassettes: with no persisted `callCount`
|
|
12
|
-
* there is nothing for a replay to bump — and {@link VcrAgent.close} no longer
|
|
13
|
-
* writes in playback anyway.
|
|
7
|
+
* Request headers dropped before a live call. `host` and `content-length` are
|
|
8
|
+
* recomputed by the outgoing fetch and a stale value corrupts it;
|
|
9
|
+
* `accept-encoding` is dropped so the body arrives as plaintext to store; the
|
|
10
|
+
* rest are hop-by-hop or Miniflare's own loopback bookkeeping.
|
|
14
11
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
const SKIP_REQUEST_HEADERS = new Set([
|
|
13
|
+
"accept-encoding",
|
|
14
|
+
"connection",
|
|
15
|
+
"content-length",
|
|
16
|
+
"host",
|
|
17
|
+
"keep-alive",
|
|
18
|
+
"te",
|
|
19
|
+
"trailer",
|
|
20
|
+
"transfer-encoding",
|
|
21
|
+
"upgrade"
|
|
22
|
+
]);
|
|
23
|
+
function headerRecord(headers) {
|
|
24
|
+
const record = {};
|
|
25
|
+
headers.forEach((value, name) => {
|
|
26
|
+
record[name.toLowerCase()] = value;
|
|
27
|
+
});
|
|
28
|
+
return record;
|
|
22
29
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
/** `set-cookie` is the one header `forEach` folds together destructively. */
|
|
31
|
+
function responseHeaderRecord(headers) {
|
|
32
|
+
const record = {};
|
|
33
|
+
headers.forEach((value, name) => {
|
|
34
|
+
record[name.toLowerCase()] = value;
|
|
35
|
+
});
|
|
36
|
+
const cookies = headers.getSetCookie?.() ?? [];
|
|
37
|
+
if (cookies.length > 0)
|
|
38
|
+
record["set-cookie"] = cookies;
|
|
39
|
+
return record;
|
|
32
40
|
}
|
|
33
|
-
const CONTROL_HOST = new URL(VCR_CONTROL_ORIGIN).host;
|
|
34
41
|
/**
|
|
35
|
-
* The
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* - **Control channel** ({@link VCR_CONTROL_ORIGIN}) → ordinary MockAgent
|
|
39
|
-
* interceptors whose reply callbacks flip the *active cassette*. This is how a
|
|
40
|
-
* spec running in workerd (no filesystem) tells this Node-side agent which
|
|
41
|
-
* test is running — see `setupRecording` (vcr-spec.ts).
|
|
42
|
-
* - **Passthrough hosts** (the gateway JWKS) → ordinary MockAgent interceptors,
|
|
43
|
-
* unchanged from before VCR existed.
|
|
44
|
-
* - **Everything else, while a cassette is active** → that cassette's undici
|
|
45
|
-
* {@link SnapshotAgent} record/replay. In playback a request with no matching
|
|
46
|
-
* recording throws `No snapshot found` — offline, never the network.
|
|
47
|
-
* - **Everything else, with no active cassette** → ordinary MockAgent behavior
|
|
48
|
-
* (`disableNetConnect()` → offline error). A non-recorded test never reaches
|
|
49
|
-
* the network.
|
|
42
|
+
* The one place a `Response` is built, so recording and replaying a given
|
|
43
|
+
* cassette entry cannot disagree about it.
|
|
50
44
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* Why `VcrAgent extends MockAgent` rather than `SnapshotAgent`: a `SnapshotAgent`'s
|
|
56
|
-
* `dispatch` fully overrides `MockAgent`'s (it never falls back to the interceptor
|
|
57
|
-
* path), so one `SnapshotAgent` cannot also serve the control/interceptor fixtures
|
|
58
|
-
* — and cannot serve more than one cassette. Keeping one internal `SnapshotAgent`
|
|
59
|
-
* per cassette and dispatching to the active one, with an ordinary
|
|
60
|
-
* `MockAgent.dispatch` fallback for control/passthrough/offline, leaves the
|
|
61
|
-
* interceptor path exactly as it always worked. Miniflare validates `fetchMock`
|
|
62
|
-
* with `instanceof MockAgent`, which `VcrAgent` satisfies directly.
|
|
45
|
+
* `Response` rejects *any* non-null body on 204/205/304 — an empty `Uint8Array`
|
|
46
|
+
* throws just as a full one does — so a recorded 204 has to be handed back with
|
|
47
|
+
* a null body or it cannot be replayed at all.
|
|
63
48
|
*/
|
|
64
|
-
|
|
49
|
+
function toResponse({ status, headers, body }) {
|
|
50
|
+
const nullBody = status === 204 || status === 205 || status === 304;
|
|
51
|
+
return new Response(nullBody ? null : body, { status, headers });
|
|
52
|
+
}
|
|
53
|
+
function json(status, value) {
|
|
54
|
+
return new Response(JSON.stringify(value), {
|
|
55
|
+
status,
|
|
56
|
+
headers: {
|
|
57
|
+
"content-type": "application/json",
|
|
58
|
+
[VCR_MARKER_HEADER]: "1"
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
class Recorder {
|
|
65
63
|
#snapshotsDir;
|
|
66
64
|
#record;
|
|
67
|
-
#
|
|
65
|
+
#handlers;
|
|
66
|
+
#allowNetwork;
|
|
68
67
|
#excludeHeaders;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
#agents = new Map();
|
|
68
|
+
/** One cassette per name, created on first activation. */
|
|
69
|
+
#cassettes = new Map();
|
|
72
70
|
#active = null;
|
|
73
71
|
#activeName = null;
|
|
72
|
+
/** Requests that could not be served since the active cassette was set. */
|
|
73
|
+
#misses = [];
|
|
74
74
|
constructor(options) {
|
|
75
|
-
super();
|
|
76
75
|
this.#snapshotsDir = options.snapshotsDir;
|
|
77
76
|
this.#record = options.record;
|
|
78
|
-
this.#
|
|
79
|
-
this.#
|
|
80
|
-
this.#
|
|
81
|
-
this.#registerControl();
|
|
77
|
+
this.#handlers = options.handlers ?? {};
|
|
78
|
+
this.#allowNetwork = new Set(options.allowNetworkHosts ?? []);
|
|
79
|
+
this.#excludeHeaders = options.excludeHeaders ?? [];
|
|
82
80
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
81
|
+
handle = async (request) => {
|
|
82
|
+
const url = new URL(request.url);
|
|
83
|
+
if (url.host === CONTROL_HOST)
|
|
84
|
+
return this.#control(url);
|
|
85
|
+
const handler = this.#handlers[url.host];
|
|
86
|
+
if (handler)
|
|
87
|
+
return handler(request);
|
|
88
|
+
// Read once: the body is needed to key the cassette *and* to forward.
|
|
89
|
+
const bytes = Buffer.from(await request.arrayBuffer());
|
|
90
|
+
if (this.#allowNetwork.has(url.host))
|
|
91
|
+
return this.#live(request, bytes);
|
|
92
|
+
const body = bytes.toString("utf8");
|
|
93
|
+
const what = `${request.method} ${request.url}`;
|
|
94
|
+
if (this.#active === null) {
|
|
95
|
+
this.#misses.push(what);
|
|
96
|
+
return this.#blocked(`VCR blocked ${what}: no cassette is active for this test. ` +
|
|
97
|
+
`Call setupRecording() at the top of the spec, or add the host to ` +
|
|
98
|
+
`allowNetworkHosts / handlers.`);
|
|
99
|
+
}
|
|
100
|
+
if (this.#record)
|
|
101
|
+
return this.#recordOne(request, bytes);
|
|
102
|
+
const hit = this.#active.match(request.method, request.url, body);
|
|
103
|
+
if (hit)
|
|
104
|
+
return toResponse(hit);
|
|
105
|
+
this.#misses.push(what);
|
|
106
|
+
return this.#blocked(`VCR has no recording of ${what} in ${path.basename(this.#active.file)}. ` +
|
|
107
|
+
`Re-record with RECORD=1. The cassette holds: ` +
|
|
108
|
+
`${this.#active.recorded.join(", ") || "(nothing)"}.`);
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* A miss cannot be made to *reject* the Worker's `fetch()` — Miniflare catches
|
|
112
|
+
* whatever an outbound service throws and turns it into a 500 anyway. So the
|
|
113
|
+
* message goes in the body where it can be read, and the miss is also reported
|
|
114
|
+
* on `/release` so the test itself fails with it. That is the half the old
|
|
115
|
+
* harness lacked: `disableNetConnect()` rejected, and the rejection surfaced
|
|
116
|
+
* as `internal error; reference = …` naming nothing.
|
|
117
|
+
*/
|
|
118
|
+
#blocked(message) {
|
|
119
|
+
return new Response(message, {
|
|
120
|
+
status: 500,
|
|
121
|
+
headers: { [VCR_MARKER_HEADER]: "1" }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
#control(url) {
|
|
125
|
+
if (url.pathname === "/release") {
|
|
126
|
+
const misses = this.#misses;
|
|
127
|
+
// Write here rather than only at teardown, so a run that is interrupted
|
|
128
|
+
// still keeps every cassette whose test finished. A no-op in playback:
|
|
129
|
+
// `flush()` returns immediately unless something was recorded.
|
|
130
|
+
this.#active?.flush();
|
|
95
131
|
this.#active = null;
|
|
96
132
|
this.#activeName = null;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
#onUse(opts) {
|
|
104
|
-
const name = new URL(opts.path, VCR_CONTROL_ORIGIN).searchParams.get("cassette") ?? "";
|
|
133
|
+
this.#misses = [];
|
|
134
|
+
return json(200, { misses });
|
|
135
|
+
}
|
|
136
|
+
if (url.pathname !== "/use")
|
|
137
|
+
return json(404, { error: "unknown" });
|
|
138
|
+
const name = url.searchParams.get("cassette") ?? "";
|
|
105
139
|
if (!CASSETTE_NAME_RE.test(name)) {
|
|
106
|
-
return
|
|
140
|
+
return json(400, { error: `invalid cassette name: ${name}` });
|
|
107
141
|
}
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
142
|
+
// Recorded specs are expected to run sequentially: a second cassette
|
|
143
|
+
// activating while one is held means two of them are sharing this recorder,
|
|
144
|
+
// which mis-records rather than failing, so say so instead.
|
|
111
145
|
if (this.#activeName !== null && this.#activeName !== name) {
|
|
112
|
-
return {
|
|
113
|
-
statusCode: 409,
|
|
114
|
-
data: `cassette ${this.#activeName} still active`
|
|
115
|
-
};
|
|
146
|
+
return json(409, { error: `cassette ${this.#activeName} still active` });
|
|
116
147
|
}
|
|
117
148
|
const file = path.join(this.#snapshotsDir, name);
|
|
118
|
-
// Playback with no cassette → 404, which the spec turns into a
|
|
119
|
-
//
|
|
149
|
+
// Playback with no cassette on disk → 404, which the spec turns into a
|
|
150
|
+
// "record it" failure. Recording creates the file on first write.
|
|
120
151
|
if (!this.#record && !existsSync(file)) {
|
|
121
|
-
return
|
|
152
|
+
return json(404, { error: `no cassette ${name}` });
|
|
122
153
|
}
|
|
123
|
-
this.#active = this.#
|
|
154
|
+
this.#active = this.#cassetteFor(name, file);
|
|
124
155
|
this.#activeName = name;
|
|
125
|
-
|
|
156
|
+
this.#misses = [];
|
|
157
|
+
return json(200, { cassette: name });
|
|
126
158
|
}
|
|
127
|
-
#
|
|
128
|
-
let
|
|
129
|
-
if (!
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
snapshotPath: file,
|
|
138
|
-
excludeHeaders: this.#excludeHeaders,
|
|
139
|
-
ignoreHeaders: this.#ignoreHeaders,
|
|
140
|
-
autoFlush: this.#record
|
|
141
|
-
});
|
|
142
|
-
this.#agents.set(name, agent);
|
|
159
|
+
#cassetteFor(name, file) {
|
|
160
|
+
let cassette = this.#cassettes.get(name);
|
|
161
|
+
if (!cassette) {
|
|
162
|
+
cassette = new Cassette(file, { excludeHeaders: this.#excludeHeaders });
|
|
163
|
+
// Recording deliberately starts from nothing rather than topping up an
|
|
164
|
+
// existing file: a cassette that mixes today's traffic with last month's
|
|
165
|
+
// is stale in a way no assertion can see.
|
|
166
|
+
if (!this.#record)
|
|
167
|
+
cassette.load();
|
|
168
|
+
this.#cassettes.set(name, cassette);
|
|
143
169
|
}
|
|
144
|
-
return
|
|
170
|
+
return cassette;
|
|
145
171
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
!
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return
|
|
172
|
+
/** The real call, with the headers the Worker sent minus the unforwardable. */
|
|
173
|
+
#live(request, body) {
|
|
174
|
+
const headers = [];
|
|
175
|
+
request.headers.forEach((value, name) => {
|
|
176
|
+
if (!SKIP_REQUEST_HEADERS.has(name.toLowerCase())) {
|
|
177
|
+
headers.push([name, value]);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return fetch(request.url, {
|
|
181
|
+
method: request.method,
|
|
182
|
+
headers,
|
|
183
|
+
body: body.byteLength > 0 ? body : undefined,
|
|
184
|
+
// Matches what Miniflare's own `fetch(req, { dispatcher })` sugar did, so
|
|
185
|
+
// a recording captures the same final response the Worker used to see.
|
|
186
|
+
redirect: "follow"
|
|
187
|
+
});
|
|
155
188
|
}
|
|
156
189
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* so the committed cassette carries no `callCount`/`timestamp`.
|
|
163
|
-
*
|
|
164
|
-
* Playback mode: undici's `SnapshotAgent.close()` *unconditionally* re-saves
|
|
165
|
-
* the cassette, writing back the `callCount` its recorder mutates on every
|
|
166
|
-
* replay — the side effect that dirtied cassettes on a plain `npm test`. There
|
|
167
|
-
* is nothing to save and no real socket in playback, so we skip that save
|
|
168
|
-
* entirely and only stop each recorder's timers.
|
|
190
|
+
* `requestBytes` is forwarded to the live endpoint **as received**. The
|
|
191
|
+
* cassette stores the utf-8 text of it, which is what the format has always
|
|
192
|
+
* held and what keys the entry — but a payload that is not valid utf-8 must
|
|
193
|
+
* not be re-encoded on its way to a real API, so the two are kept separate
|
|
194
|
+
* rather than the string being round-tripped back into bytes.
|
|
169
195
|
*/
|
|
196
|
+
async #recordOne(request, requestBytes) {
|
|
197
|
+
const live = await this.#live(request, requestBytes);
|
|
198
|
+
const bytes = Buffer.from(await live.arrayBuffer());
|
|
199
|
+
const recordedRequest = {
|
|
200
|
+
method: request.method,
|
|
201
|
+
url: request.url,
|
|
202
|
+
headers: headerRecord(request.headers),
|
|
203
|
+
body: requestBytes.toString("utf8")
|
|
204
|
+
};
|
|
205
|
+
const recordedResponse = {
|
|
206
|
+
statusCode: live.status,
|
|
207
|
+
headers: responseHeaderRecord(live.headers),
|
|
208
|
+
body: bytes.toString("base64")
|
|
209
|
+
};
|
|
210
|
+
this.#active.record(recordedRequest, recordedResponse);
|
|
211
|
+
// Serve the response through the same path a replay takes, so a recording
|
|
212
|
+
// run and the replay of what it just wrote cannot disagree.
|
|
213
|
+
return toResponse(replayable(recordedResponse));
|
|
214
|
+
}
|
|
170
215
|
async close() {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
for (const name of this.#agents.keys()) {
|
|
174
|
-
stripVolatileFields(path.join(this.#snapshotsDir, name));
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
for (const a of this.#agents.values())
|
|
179
|
-
a.getRecorder().destroy();
|
|
180
|
-
}
|
|
181
|
-
await super.close();
|
|
216
|
+
for (const cassette of this.#cassettes.values())
|
|
217
|
+
cassette.flush();
|
|
182
218
|
}
|
|
183
219
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Build the recorder and register it for {@link closeVcr}.
|
|
222
|
+
*
|
|
223
|
+
* ```ts
|
|
224
|
+
* // vitest.config.ts
|
|
225
|
+
* const vcr = createVcr({
|
|
226
|
+
* snapshotsDir: path.resolve(import.meta.dirname, "test/snapshots"),
|
|
227
|
+
* record: recordFromEnv(),
|
|
228
|
+
* excludeHeaders: ["x-api-key", "authorization", "cookie", "set-cookie"]
|
|
229
|
+
* });
|
|
230
|
+
*
|
|
231
|
+
* export default defineConfig({
|
|
232
|
+
* plugins: [cloudflareTest({ miniflare: { outboundService: vcr.outboundService } })],
|
|
233
|
+
* test: { globalSetup: ["@loopingai/core/testing/vcr-global-setup"] }
|
|
234
|
+
* });
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
export function createVcr(options) {
|
|
238
|
+
const recorder = new Recorder(options);
|
|
239
|
+
const vcr = {
|
|
240
|
+
outboundService: recorder.handle,
|
|
241
|
+
close: () => recorder.close()
|
|
242
|
+
};
|
|
243
|
+
globalThis[VCR_KEY] = vcr;
|
|
244
|
+
return vcr;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Whether this run records, from `RECORD`.
|
|
248
|
+
*
|
|
249
|
+
* Compared against `"1"` rather than tested for truthiness, because every
|
|
250
|
+
* non-empty string is truthy: `RECORD=0` and `RECORD=false` — the two things
|
|
251
|
+
* someone reaches for to turn recording *off* — would otherwise turn it on and
|
|
252
|
+
* overwrite committed cassettes with live traffic. The opposite mistake
|
|
253
|
+
* (`RECORD=true` not recording) fails loudly on the next assertion and costs
|
|
254
|
+
* nothing.
|
|
255
|
+
*/
|
|
256
|
+
export function recordFromEnv() {
|
|
257
|
+
return process.env.RECORD === "1";
|
|
188
258
|
}
|
|
189
|
-
/** globalThis slot so the Vitest globalSetup teardown can reach the
|
|
259
|
+
/** globalThis slot so the Vitest globalSetup teardown can reach the recorder
|
|
190
260
|
* created during config evaluation (same process, possibly a different module
|
|
191
261
|
* realm — globalThis is the reliable channel). */
|
|
192
262
|
const VCR_KEY = "__VCR_AGENT__";
|
|
193
263
|
/**
|
|
194
|
-
* Flush
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* after tests finish and trip Vitest's "close timed out" at teardown. Calling
|
|
198
|
-
* this (from a Vitest `globalSetup` teardown, which runs before Vite closes its
|
|
199
|
-
* own server) stops them. A no-op if no agent was created, and in playback only
|
|
200
|
-
* stops recorder timers — {@link VcrAgent.close} deliberately does not re-save
|
|
201
|
-
* the cassette there (see its doc).
|
|
264
|
+
* Flush every cassette. Cassettes are already written when each one is
|
|
265
|
+
* released, so this is a safety net for a run that ended without releasing —
|
|
266
|
+
* and a no-op if no recorder was created.
|
|
202
267
|
*/
|
|
203
268
|
export async function closeVcr() {
|
|
204
269
|
const g = globalThis;
|
|
205
|
-
const
|
|
206
|
-
if (!
|
|
270
|
+
const vcr = g[VCR_KEY];
|
|
271
|
+
if (!vcr)
|
|
207
272
|
return;
|
|
208
273
|
g[VCR_KEY] = undefined;
|
|
209
|
-
await
|
|
274
|
+
await vcr.close();
|
|
210
275
|
}
|
|
211
276
|
//# sourceMappingURL=vcr.js.map
|
package/dist/testing/vcr.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vcr.js","sourceRoot":"","sources":["../../src/testing/vcr.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"vcr.js","sourceRoot":"","sources":["../../src/testing/vcr.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loopingai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Shared, mandatory foundation for Looping agents on Cloudflare Workers: zero-trust A2A, durable task lifecycle, delegation and subagent runtime, test harness.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"a2a",
|
|
@@ -100,16 +100,15 @@
|
|
|
100
100
|
},
|
|
101
101
|
"dependencies": {
|
|
102
102
|
"@a2a-js/sdk": "^1.0.0",
|
|
103
|
-
"@cloudflare/workers-types": "^5.
|
|
103
|
+
"@cloudflare/workers-types": "^5.20260804.1",
|
|
104
104
|
"drizzle-orm": "^0.45.2",
|
|
105
|
-
"jose": "^6.2.
|
|
105
|
+
"jose": "^6.2.8",
|
|
106
106
|
"zod": "^4.4.3"
|
|
107
107
|
},
|
|
108
108
|
"peerDependencies": {
|
|
109
|
-
"@cloudflare/vitest-pool-workers": "
|
|
109
|
+
"@cloudflare/vitest-pool-workers": ">=0.18",
|
|
110
110
|
"agents": "^0.20.0",
|
|
111
|
-
"ai": "^7.0.
|
|
112
|
-
"undici": "^7.28.0",
|
|
111
|
+
"ai": "^7.0.51",
|
|
113
112
|
"vitest": ">=4",
|
|
114
113
|
"workers-ai-provider": "^4.0.0"
|
|
115
114
|
},
|
|
@@ -117,15 +116,12 @@
|
|
|
117
116
|
"@cloudflare/vitest-pool-workers": {
|
|
118
117
|
"optional": true
|
|
119
118
|
},
|
|
120
|
-
"undici": {
|
|
121
|
-
"optional": true
|
|
122
|
-
},
|
|
123
119
|
"vitest": {
|
|
124
120
|
"optional": true
|
|
125
121
|
}
|
|
126
122
|
},
|
|
127
123
|
"devDependencies": {
|
|
128
|
-
"@cloudflare/vitest-pool-workers": "^0.
|
|
124
|
+
"@cloudflare/vitest-pool-workers": "^0.20.1",
|
|
129
125
|
"@types/node": "^26.1.1",
|
|
130
126
|
"agents": "^0.20.0",
|
|
131
127
|
"ai": "^7.0.40",
|
|
@@ -133,11 +129,10 @@
|
|
|
133
129
|
"eslint": "^10.8.0",
|
|
134
130
|
"prettier": "^3.9.6",
|
|
135
131
|
"typescript": "^6.0.3",
|
|
136
|
-
"typescript-eslint": "^8.
|
|
137
|
-
"undici": "7.28.0",
|
|
132
|
+
"typescript-eslint": "^8.66.0",
|
|
138
133
|
"vitest": "^4.1.10",
|
|
139
134
|
"workers-ai-provider": "^4.0.0",
|
|
140
|
-
"wrangler": "^4.
|
|
135
|
+
"wrangler": "^4.118.0"
|
|
141
136
|
},
|
|
142
137
|
"engines": {
|
|
143
138
|
"node": ">=24"
|
|
@@ -145,6 +140,11 @@
|
|
|
145
140
|
"publishConfig": {
|
|
146
141
|
"access": "public"
|
|
147
142
|
},
|
|
143
|
+
"overrides": {
|
|
144
|
+
"@esbuild-kit/core-utils": {
|
|
145
|
+
"esbuild": "^0.25.0"
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
148
|
"allowScripts": {
|
|
149
149
|
"esbuild": false,
|
|
150
150
|
"fsevents": false,
|