@loopingai/core 0.1.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/LICENSE +674 -0
- package/README.md +279 -0
- package/dist/a2a/agent-stub.d.ts +61 -0
- package/dist/a2a/agent-stub.d.ts.map +1 -0
- package/dist/a2a/agent-stub.js +2 -0
- package/dist/a2a/agent-stub.js.map +1 -0
- package/dist/a2a/card.d.ts +119 -0
- package/dist/a2a/card.d.ts.map +1 -0
- package/dist/a2a/card.js +115 -0
- package/dist/a2a/card.js.map +1 -0
- package/dist/a2a/context.d.ts +14 -0
- package/dist/a2a/context.d.ts.map +1 -0
- package/dist/a2a/context.js +55 -0
- package/dist/a2a/context.js.map +1 -0
- package/dist/a2a/executor.d.ts +85 -0
- package/dist/a2a/executor.d.ts.map +1 -0
- package/dist/a2a/executor.js +107 -0
- package/dist/a2a/executor.js.map +1 -0
- package/dist/a2a/index.d.ts +18 -0
- package/dist/a2a/index.d.ts.map +1 -0
- package/dist/a2a/index.js +17 -0
- package/dist/a2a/index.js.map +1 -0
- package/dist/a2a/notify.d.ts +75 -0
- package/dist/a2a/notify.d.ts.map +1 -0
- package/dist/a2a/notify.js +161 -0
- package/dist/a2a/notify.js.map +1 -0
- package/dist/a2a/parts.d.ts +46 -0
- package/dist/a2a/parts.d.ts.map +1 -0
- package/dist/a2a/parts.js +95 -0
- package/dist/a2a/parts.js.map +1 -0
- package/dist/a2a/task-store.d.ts +45 -0
- package/dist/a2a/task-store.d.ts.map +1 -0
- package/dist/a2a/task-store.js +100 -0
- package/dist/a2a/task-store.js.map +1 -0
- package/dist/a2a/task.d.ts +86 -0
- package/dist/a2a/task.d.ts.map +1 -0
- package/dist/a2a/task.js +16 -0
- package/dist/a2a/task.js.map +1 -0
- package/dist/a2a/verify.d.ts +64 -0
- package/dist/a2a/verify.d.ts.map +1 -0
- package/dist/a2a/verify.js +122 -0
- package/dist/a2a/verify.js.map +1 -0
- package/dist/agent/budget.d.ts +47 -0
- package/dist/agent/budget.d.ts.map +1 -0
- package/dist/agent/budget.js +43 -0
- package/dist/agent/budget.js.map +1 -0
- package/dist/agent/control.d.ts +110 -0
- package/dist/agent/control.d.ts.map +1 -0
- package/dist/agent/control.js +115 -0
- package/dist/agent/control.js.map +1 -0
- package/dist/agent/final-reply.d.ts +49 -0
- package/dist/agent/final-reply.d.ts.map +1 -0
- package/dist/agent/final-reply.js +52 -0
- package/dist/agent/final-reply.js.map +1 -0
- package/dist/agent/history.d.ts +83 -0
- package/dist/agent/history.d.ts.map +1 -0
- package/dist/agent/history.js +117 -0
- package/dist/agent/history.js.map +1 -0
- package/dist/agent/index.d.ts +17 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +17 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/inference.d.ts +52 -0
- package/dist/agent/inference.d.ts.map +1 -0
- package/dist/agent/inference.js +79 -0
- package/dist/agent/inference.js.map +1 -0
- package/dist/agent/model.d.ts +75 -0
- package/dist/agent/model.d.ts.map +1 -0
- package/dist/agent/model.js +62 -0
- package/dist/agent/model.js.map +1 -0
- package/dist/agent/session.d.ts +92 -0
- package/dist/agent/session.d.ts.map +1 -0
- package/dist/agent/session.js +80 -0
- package/dist/agent/session.js.map +1 -0
- package/dist/config.d.ts +159 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +92 -0
- package/dist/config.js.map +1 -0
- package/dist/contract/index.d.ts +10 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +9 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/plugin.d.ts +163 -0
- package/dist/contract/plugin.d.ts.map +1 -0
- package/dist/contract/plugin.js +39 -0
- package/dist/contract/plugin.js.map +1 -0
- package/dist/contract/recipe.d.ts +170 -0
- package/dist/contract/recipe.d.ts.map +1 -0
- package/dist/contract/recipe.js +2 -0
- package/dist/contract/recipe.js.map +1 -0
- package/dist/contract/validation.d.ts +86 -0
- package/dist/contract/validation.d.ts.map +1 -0
- package/dist/contract/validation.js +76 -0
- package/dist/contract/validation.js.map +1 -0
- package/dist/db/db.d.ts +131 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +91 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +9 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +9 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrations/index.d.ts +21 -0
- package/dist/db/migrations/index.d.ts.map +1 -0
- package/dist/db/migrations/index.js +54 -0
- package/dist/db/migrations/index.js.map +1 -0
- package/dist/db/models/subtasks.d.ts +102 -0
- package/dist/db/models/subtasks.d.ts.map +1 -0
- package/dist/db/models/subtasks.js +285 -0
- package/dist/db/models/subtasks.js.map +1 -0
- package/dist/db/models/tasks.d.ts +92 -0
- package/dist/db/models/tasks.d.ts.map +1 -0
- package/dist/db/models/tasks.js +212 -0
- package/dist/db/models/tasks.js.map +1 -0
- package/dist/db/schema.d.ts +488 -0
- package/dist/db/schema.d.ts.map +1 -0
- package/dist/db/schema.js +91 -0
- package/dist/db/schema.js.map +1 -0
- package/dist/env.d.ts +46 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +40 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +47 -0
- package/dist/platform.d.ts.map +1 -0
- package/dist/platform.js +47 -0
- package/dist/platform.js.map +1 -0
- package/dist/runtime/index.d.ts +86 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +123 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/tool-families.d.ts +33 -0
- package/dist/runtime/tool-families.d.ts.map +1 -0
- package/dist/runtime/tool-families.js +62 -0
- package/dist/runtime/tool-families.js.map +1 -0
- package/dist/subagent/fingerprint.d.ts +38 -0
- package/dist/subagent/fingerprint.d.ts.map +1 -0
- package/dist/subagent/fingerprint.js +97 -0
- package/dist/subagent/fingerprint.js.map +1 -0
- package/dist/subagent/index.d.ts +147 -0
- package/dist/subagent/index.d.ts.map +1 -0
- package/dist/subagent/index.js +299 -0
- package/dist/subagent/index.js.map +1 -0
- package/dist/subagent/prompt.d.ts +34 -0
- package/dist/subagent/prompt.d.ts.map +1 -0
- package/dist/subagent/prompt.js +46 -0
- package/dist/subagent/prompt.js.map +1 -0
- package/dist/subagent/run.d.ts +151 -0
- package/dist/subagent/run.d.ts.map +1 -0
- package/dist/subagent/run.js +491 -0
- package/dist/subagent/run.js.map +1 -0
- package/dist/subagent/workspace.d.ts +65 -0
- package/dist/subagent/workspace.d.ts.map +1 -0
- package/dist/subagent/workspace.js +53 -0
- package/dist/subagent/workspace.js.map +1 -0
- package/dist/subtasks/catalog.d.ts +40 -0
- package/dist/subtasks/catalog.d.ts.map +1 -0
- package/dist/subtasks/catalog.js +36 -0
- package/dist/subtasks/catalog.js.map +1 -0
- package/dist/subtasks/decomposition.d.ts +90 -0
- package/dist/subtasks/decomposition.d.ts.map +1 -0
- package/dist/subtasks/decomposition.js +205 -0
- package/dist/subtasks/decomposition.js.map +1 -0
- package/dist/subtasks/delegate.d.ts +77 -0
- package/dist/subtasks/delegate.d.ts.map +1 -0
- package/dist/subtasks/delegate.js +102 -0
- package/dist/subtasks/delegate.js.map +1 -0
- package/dist/subtasks/index.d.ts +11 -0
- package/dist/subtasks/index.d.ts.map +1 -0
- package/dist/subtasks/index.js +11 -0
- package/dist/subtasks/index.js.map +1 -0
- package/dist/subtasks/scheduler.d.ts +49 -0
- package/dist/subtasks/scheduler.d.ts.map +1 -0
- package/dist/subtasks/scheduler.js +48 -0
- package/dist/subtasks/scheduler.js.map +1 -0
- package/dist/subtasks/subtask-types.d.ts +100 -0
- package/dist/subtasks/subtask-types.d.ts.map +1 -0
- package/dist/subtasks/subtask-types.js +111 -0
- package/dist/subtasks/subtask-types.js.map +1 -0
- package/dist/subtasks/types.d.ts +321 -0
- package/dist/subtasks/types.d.ts.map +1 -0
- package/dist/subtasks/types.js +16 -0
- package/dist/subtasks/types.js.map +1 -0
- package/dist/testing/auth.d.ts +12 -0
- package/dist/testing/auth.d.ts.map +1 -0
- package/dist/testing/auth.js +25 -0
- package/dist/testing/auth.js.map +1 -0
- package/dist/testing/do.d.ts +30 -0
- package/dist/testing/do.d.ts.map +1 -0
- package/dist/testing/do.js +26 -0
- package/dist/testing/do.js.map +1 -0
- package/dist/testing/fake-session.d.ts +27 -0
- package/dist/testing/fake-session.d.ts.map +1 -0
- package/dist/testing/fake-session.js +38 -0
- package/dist/testing/fake-session.js.map +1 -0
- package/dist/testing/fixtures.d.ts +27 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +67 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +31 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +31 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-model.d.ts +43 -0
- package/dist/testing/mock-model.d.ts.map +1 -0
- package/dist/testing/mock-model.js +62 -0
- package/dist/testing/mock-model.js.map +1 -0
- package/dist/testing/node.d.ts +24 -0
- package/dist/testing/node.d.ts.map +1 -0
- package/dist/testing/node.js +24 -0
- package/dist/testing/node.js.map +1 -0
- package/dist/testing/vcr-global-setup.d.ts +3 -0
- package/dist/testing/vcr-global-setup.d.ts.map +1 -0
- package/dist/testing/vcr-global-setup.js +9 -0
- package/dist/testing/vcr-global-setup.js.map +1 -0
- package/dist/testing/vcr-shared.d.ts +21 -0
- package/dist/testing/vcr-shared.d.ts.map +1 -0
- package/dist/testing/vcr-shared.js +21 -0
- package/dist/testing/vcr-shared.js.map +1 -0
- package/dist/testing/vcr-spec.d.ts +17 -0
- package/dist/testing/vcr-spec.d.ts.map +1 -0
- package/dist/testing/vcr-spec.js +72 -0
- package/dist/testing/vcr-spec.js.map +1 -0
- package/dist/testing/vcr.d.ts +81 -0
- package/dist/testing/vcr.d.ts.map +1 -0
- package/dist/testing/vcr.js +211 -0
- package/dist/testing/vcr.js.map +1 -0
- package/dist/worker/index.d.ts +62 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +194 -0
- package/dist/worker/index.js.map +1 -0
- package/eslint-rules/index.js +31 -0
- package/eslint-rules/no-deprecated-object-properties.js +81 -0
- package/package.json +147 -0
- package/scripts/generate-keys.mjs +53 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@loopingai/core/testing` — the harness both predecessor agents grew, shipped so
|
|
3
|
+
* a consumer does not grow it a third time.
|
|
4
|
+
*
|
|
5
|
+
* Never import this from runtime code. It is a separate subpath precisely so it
|
|
6
|
+
* cannot reach a production bundle, and it pulls in `cloudflare:test`, which
|
|
7
|
+
* does not exist in a deployed Worker.
|
|
8
|
+
*
|
|
9
|
+
* **This barrel is the workerd half** — safe to import from a spec. The VCR
|
|
10
|
+
* recorder needs `undici` and `node:fs`, so it lives behind
|
|
11
|
+
* `@loopingai/core/testing/node` and must not be re-exported here: pulling it into
|
|
12
|
+
* this graph would drag Node builtins into every spec that wanted a fixture.
|
|
13
|
+
* `vcr-shared.ts` is the seam both realms may load.
|
|
14
|
+
*
|
|
15
|
+
* Three things live here:
|
|
16
|
+
*
|
|
17
|
+
* - **VCR (spec side)** — `setupRecording()`, which names a cassette per test and
|
|
18
|
+
* talks to the Node-side recorder over the in-band control channel.
|
|
19
|
+
* - **Fakes** — a `SessionLike` reference implementation and a scripted
|
|
20
|
+
* `LanguageModel`, so a loop can be driven with no model call at all.
|
|
21
|
+
* - **Fixtures** — Ed25519 keypairs and a gateway-JWT signer, so the zero-trust
|
|
22
|
+
* path can be exercised end to end without a real gateway.
|
|
23
|
+
*/
|
|
24
|
+
export { setupRecording, cassetteNameFor } from "./vcr-spec.js";
|
|
25
|
+
export { VCR_CONTROL_ORIGIN, CASSETTE_NAME_RE } from "./vcr-shared.js";
|
|
26
|
+
export { FakeSession } from "./fake-session.js";
|
|
27
|
+
export { mockModel, finalReply, type MockStep } from "./mock-model.js";
|
|
28
|
+
export { makeGatewayToken, type GatewayTokenOptions } from "./auth.js";
|
|
29
|
+
export { AGENT_ORIGIN, GATEWAY_ORIGIN, TEST_AGENT_PRIVATE_JWK, TEST_GATEWAY_PRIVATE_JWK, gatewayPublicJwks, testAgentMessage, testStatus, testTask } from "./fixtures.js";
|
|
30
|
+
export { doStorage, makeDoHelpers, type DoTestHelpers } from "./do.js";
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAAE,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EACL,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,wBAAwB,EACxB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACT,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@loopingai/core/testing` — the harness both predecessor agents grew, shipped so
|
|
3
|
+
* a consumer does not grow it a third time.
|
|
4
|
+
*
|
|
5
|
+
* Never import this from runtime code. It is a separate subpath precisely so it
|
|
6
|
+
* cannot reach a production bundle, and it pulls in `cloudflare:test`, which
|
|
7
|
+
* does not exist in a deployed Worker.
|
|
8
|
+
*
|
|
9
|
+
* **This barrel is the workerd half** — safe to import from a spec. The VCR
|
|
10
|
+
* recorder needs `undici` and `node:fs`, so it lives behind
|
|
11
|
+
* `@loopingai/core/testing/node` and must not be re-exported here: pulling it into
|
|
12
|
+
* this graph would drag Node builtins into every spec that wanted a fixture.
|
|
13
|
+
* `vcr-shared.ts` is the seam both realms may load.
|
|
14
|
+
*
|
|
15
|
+
* Three things live here:
|
|
16
|
+
*
|
|
17
|
+
* - **VCR (spec side)** — `setupRecording()`, which names a cassette per test and
|
|
18
|
+
* talks to the Node-side recorder over the in-band control channel.
|
|
19
|
+
* - **Fakes** — a `SessionLike` reference implementation and a scripted
|
|
20
|
+
* `LanguageModel`, so a loop can be driven with no model call at all.
|
|
21
|
+
* - **Fixtures** — Ed25519 keypairs and a gateway-JWT signer, so the zero-trust
|
|
22
|
+
* path can be exercised end to end without a real gateway.
|
|
23
|
+
*/
|
|
24
|
+
export { setupRecording, cassetteNameFor } from "./vcr-spec.js";
|
|
25
|
+
export { VCR_CONTROL_ORIGIN, CASSETTE_NAME_RE } from "./vcr-shared.js";
|
|
26
|
+
export { FakeSession } from "./fake-session.js";
|
|
27
|
+
export { mockModel, finalReply } from "./mock-model.js";
|
|
28
|
+
export { makeGatewayToken } from "./auth.js";
|
|
29
|
+
export { AGENT_ORIGIN, GATEWAY_ORIGIN, TEST_AGENT_PRIVATE_JWK, TEST_GATEWAY_PRIVATE_JWK, gatewayPublicJwks, testAgentMessage, testStatus, testTask } from "./fixtures.js";
|
|
30
|
+
export { doStorage, makeDoHelpers } from "./do.js";
|
|
31
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAiB,MAAM,iBAAiB,CAAC;AAEvE,OAAO,EAAE,gBAAgB,EAA4B,MAAM,WAAW,CAAC;AACvE,OAAO,EACL,YAAY,EACZ,cAAc,EACd,sBAAsB,EACtB,wBAAwB,EACxB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACV,QAAQ,EACT,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,SAAS,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MockLanguageModelV3 } from "ai/test";
|
|
2
|
+
export interface MockStep {
|
|
3
|
+
/**
|
|
4
|
+
* Assistant text for this step. On its own → finishReason "stop", which for the
|
|
5
|
+
* main-agent round is a *failed* attempt: prose is not an ending there, so use
|
|
6
|
+
* {@link finalReply} to script an answer. Alongside `toolCall` → the
|
|
7
|
+
* intermediate content emitted before a tool call.
|
|
8
|
+
*/
|
|
9
|
+
text?: string;
|
|
10
|
+
/** Emit a tool call (finishReason "tool-calls"); may accompany `text`. */
|
|
11
|
+
toolCall?: {
|
|
12
|
+
toolName: string;
|
|
13
|
+
input?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Emit several tool calls in **one** step, which a real model does whenever it
|
|
17
|
+
* decides two things at once. That is the only way to script the cases the round
|
|
18
|
+
* has to arbitrate: two different endings in one step (precedence decides), or
|
|
19
|
+
* the same ending twice (the tool's own `parse` decides).
|
|
20
|
+
*/
|
|
21
|
+
toolCalls?: {
|
|
22
|
+
toolName: string;
|
|
23
|
+
input?: unknown;
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A step where the main agent answers the user — the `final_reply` control call.
|
|
28
|
+
*
|
|
29
|
+
* Scripting a bare `{ text }` instead reproduces the bug this tool exists to catch
|
|
30
|
+
* (a model narrating rather than acting) and fails the attempt, which is what the
|
|
31
|
+
* error-path specs use it for.
|
|
32
|
+
*/
|
|
33
|
+
export declare function finalReply(text: string, opts?: {
|
|
34
|
+
text?: string;
|
|
35
|
+
}): MockStep;
|
|
36
|
+
/**
|
|
37
|
+
* A mock model that returns each step in sequence — one per `generateText` call.
|
|
38
|
+
* Uses the function form (with our own counter) rather than the array form, whose
|
|
39
|
+
* call-count indexing is off by one in this SDK version. Extra calls repeat the
|
|
40
|
+
* last step.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mockModel(...steps: MockStep[]): MockLanguageModelV3;
|
|
43
|
+
//# sourceMappingURL=mock-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-model.d.ts","sourceRoot":"","sources":["../../src/testing/mock-model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAc9C,MAAM,WAAW,QAAQ;IACvB;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,QAAQ,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACjD;;;;;OAKG;IACH,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;CACrD;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC3B,QAAQ,CAKV;AA+BD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,GAAG,KAAK,EAAE,QAAQ,EAAE,GAAG,mBAAmB,CAKnE"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { MockLanguageModelV3 } from "ai/test";
|
|
2
|
+
/**
|
|
3
|
+
* Test doubles for the LLM. Lets the tool-loop / executor specs run the real
|
|
4
|
+
* `generateText` machinery (tool execution, multi-step, fallback) against a
|
|
5
|
+
* scripted model with no network or `AI` binding.
|
|
6
|
+
*/
|
|
7
|
+
/** Zeroed usage block satisfying the LanguageModelV3 result shape. */
|
|
8
|
+
const USAGE = {
|
|
9
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
|
|
10
|
+
outputTokens: { total: 0, text: 0, reasoning: 0 }
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A step where the main agent answers the user — the `final_reply` control call.
|
|
14
|
+
*
|
|
15
|
+
* Scripting a bare `{ text }` instead reproduces the bug this tool exists to catch
|
|
16
|
+
* (a model narrating rather than acting) and fails the attempt, which is what the
|
|
17
|
+
* error-path specs use it for.
|
|
18
|
+
*/
|
|
19
|
+
export function finalReply(text, opts = {}) {
|
|
20
|
+
return {
|
|
21
|
+
...(opts.text !== undefined ? { text: opts.text } : {}),
|
|
22
|
+
toolCall: { toolName: "final_reply", input: { text } }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function stepResult(step) {
|
|
26
|
+
const content = [];
|
|
27
|
+
// Keep the empty-string text part so a `{ text: "" }` step still yields "".
|
|
28
|
+
if (step.text !== undefined)
|
|
29
|
+
content.push({ type: "text", text: step.text });
|
|
30
|
+
const calls = [
|
|
31
|
+
...(step.toolCall ? [step.toolCall] : []),
|
|
32
|
+
...(step.toolCalls ?? [])
|
|
33
|
+
];
|
|
34
|
+
for (const call of calls) {
|
|
35
|
+
content.push({
|
|
36
|
+
type: "tool-call",
|
|
37
|
+
toolCallId: crypto.randomUUID(),
|
|
38
|
+
toolName: call.toolName,
|
|
39
|
+
input: JSON.stringify(call.input ?? {})
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const unified = calls.length > 0 ? "tool-calls" : "stop";
|
|
43
|
+
return {
|
|
44
|
+
content,
|
|
45
|
+
finishReason: { unified, raw: undefined },
|
|
46
|
+
usage: USAGE,
|
|
47
|
+
warnings: []
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A mock model that returns each step in sequence — one per `generateText` call.
|
|
52
|
+
* Uses the function form (with our own counter) rather than the array form, whose
|
|
53
|
+
* call-count indexing is off by one in this SDK version. Extra calls repeat the
|
|
54
|
+
* last step.
|
|
55
|
+
*/
|
|
56
|
+
export function mockModel(...steps) {
|
|
57
|
+
let i = 0;
|
|
58
|
+
return new MockLanguageModelV3({
|
|
59
|
+
doGenerate: async () => stepResult(steps[Math.min(i++, steps.length - 1)])
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=mock-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-model.js","sourceRoot":"","sources":["../../src/testing/mock-model.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C;;;;GAIG;AAEH,sEAAsE;AACtE,MAAM,KAAK,GAAG;IACZ,WAAW,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE;IAClE,YAAY,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE;CAClD,CAAC;AAqBF;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,OAA0B,EAAE;IAE5B,OAAO;QACL,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvD,QAAQ,EAAE,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,EAAE;KACvD,CAAC;AACJ,CAAC;AAED,SAAS,UAAU,CAAC,IAAc;IAChC,MAAM,OAAO,GAGT,EAAE,CAAC;IACP,4EAA4E;IAC5E,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG;QACZ,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACzC,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;KAC1B,CAAC;IACF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,OAAO,CAAC,IAAI,CAAC;YACX,IAAI,EAAE,WAAW;YACjB,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE;YAC/B,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;SACxC,CAAC,CAAC;IACL,CAAC;IACD,MAAM,OAAO,GACX,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAE,YAAsB,CAAC,CAAC,CAAE,MAAgB,CAAC;IACjE,OAAO;QACL,OAAO;QACP,YAAY,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE;QACzC,KAAK,EAAE,KAAK;QACZ,QAAQ,EAAE,EAAE;KACb,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,SAAS,CAAC,GAAG,KAAiB;IAC5C,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,IAAI,mBAAmB,CAAC;QAC7B,UAAU,EAAE,KAAK,IAAI,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;KAC3E,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@loopingai/core/testing/node` — the **Node-realm** half of the VCR harness.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `@loopingai/core/testing` because the two halves cannot share a
|
|
5
|
+
* module graph. This side imports `undici` and `node:fs`; the other side runs
|
|
6
|
+
* inside workerd, which has neither. `vcr-shared.ts` is the only thing both may
|
|
7
|
+
* touch, and it is deliberately dependency-free so it can load in either realm.
|
|
8
|
+
*
|
|
9
|
+
* Import this from a Vitest **config** (or anything else running in Node), never
|
|
10
|
+
* from a spec:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // vitest.config.ts
|
|
14
|
+
* import { createVcrAgent } from "@loopingai/core/testing/node";
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* For the common case — wiring cassette flush/teardown — you do not need this
|
|
18
|
+
* subpath at all. Point `globalSetup` at
|
|
19
|
+
* `@loopingai/core/testing/vcr-global-setup`, which is this module's `setup`/
|
|
20
|
+
* `teardown` pair already packaged for Vitest.
|
|
21
|
+
*/
|
|
22
|
+
export { createVcrAgent, closeVcr, VcrAgent, type CreateVcrAgentOptions } from "./vcr.js";
|
|
23
|
+
export { VCR_CONTROL_ORIGIN, CASSETTE_NAME_RE } from "./vcr-shared.js";
|
|
24
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/testing/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACL,cAAc,EACd,QAAQ,EACR,QAAQ,EACR,KAAK,qBAAqB,EAC3B,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@loopingai/core/testing/node` — the **Node-realm** half of the VCR harness.
|
|
3
|
+
*
|
|
4
|
+
* Separate from `@loopingai/core/testing` because the two halves cannot share a
|
|
5
|
+
* module graph. This side imports `undici` and `node:fs`; the other side runs
|
|
6
|
+
* inside workerd, which has neither. `vcr-shared.ts` is the only thing both may
|
|
7
|
+
* touch, and it is deliberately dependency-free so it can load in either realm.
|
|
8
|
+
*
|
|
9
|
+
* Import this from a Vitest **config** (or anything else running in Node), never
|
|
10
|
+
* from a spec:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* // vitest.config.ts
|
|
14
|
+
* import { createVcrAgent } from "@loopingai/core/testing/node";
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* For the common case — wiring cassette flush/teardown — you do not need this
|
|
18
|
+
* subpath at all. Point `globalSetup` at
|
|
19
|
+
* `@loopingai/core/testing/vcr-global-setup`, which is this module's `setup`/
|
|
20
|
+
* `teardown` pair already packaged for Vitest.
|
|
21
|
+
*/
|
|
22
|
+
export { createVcrAgent, closeVcr, VcrAgent } from "./vcr.js";
|
|
23
|
+
export { VCR_CONTROL_ORIGIN, CASSETTE_NAME_RE } from "./vcr-shared.js";
|
|
24
|
+
//# sourceMappingURL=node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../../src/testing/node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EACL,cAAc,EACd,QAAQ,EACR,QAAQ,EAET,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vcr-global-setup.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-global-setup.ts"],"names":[],"mappings":"AAKA,wBAAgB,KAAK,IAAI,IAAI,CAAG;AAEhC,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAE9C"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { closeVcr } from "./vcr.js";
|
|
2
|
+
// Vitest globalSetup: the exported teardown runs after all tests but before Vite
|
|
3
|
+
// tears down its server, so closing the VCR agent here (flush cassette, stop the
|
|
4
|
+
// recorder timer, close real sockets) prevents the record-run hang.
|
|
5
|
+
export function setup() { }
|
|
6
|
+
export async function teardown() {
|
|
7
|
+
await closeVcr();
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=vcr-global-setup.js.map
|
|
@@ -0,0 +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,iFAAiF;AACjF,iFAAiF;AACjF,oEAAoE;AACpE,MAAM,UAAU,KAAK,KAAU,CAAC;AAEhC,MAAM,CAAC,KAAK,UAAU,QAAQ;IAC5B,MAAM,QAAQ,EAAE,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants shared between the two sides of the VCR harness that must NOT import
|
|
3
|
+
* each other: the Node-side agent (`vcr.ts`, imports `undici`) and the
|
|
4
|
+
* worker-side spec helper (`vcr-spec.ts`, runs in workerd). Keep this file
|
|
5
|
+
* dependency-free so both realms can load it.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Reserved origin for the in-band control channel. A recorded spec `fetch`es
|
|
9
|
+
* here (via the single Miniflare `fetchMock`) to tell the Node-side agent which
|
|
10
|
+
* cassette is active for the current test — see `setupRecording` (vcr-spec.ts)
|
|
11
|
+
* and `VcrAgent` (vcr.ts). Never a real network host.
|
|
12
|
+
*/
|
|
13
|
+
export declare const VCR_CONTROL_ORIGIN = "https://vcr.internal";
|
|
14
|
+
/**
|
|
15
|
+
* Cassette filenames are derived from kebab-cased file + describe + test names
|
|
16
|
+
* (see `cassetteNameFor`), so they are always lowercase `a-z0-9-` plus the
|
|
17
|
+
* `.snapshot.json` suffix. Enforced on the control channel as a path-traversal
|
|
18
|
+
* guard (no `/`, no `..`) before the name is joined to the snapshots dir.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CASSETTE_NAME_RE: RegExp;
|
|
21
|
+
//# sourceMappingURL=vcr-shared.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vcr-shared.d.ts","sourceRoot":"","sources":["../../src/testing/vcr-shared.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,QAAiC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants shared between the two sides of the VCR harness that must NOT import
|
|
3
|
+
* each other: the Node-side agent (`vcr.ts`, imports `undici`) and the
|
|
4
|
+
* worker-side spec helper (`vcr-spec.ts`, runs in workerd). Keep this file
|
|
5
|
+
* dependency-free so both realms can load it.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Reserved origin for the in-band control channel. A recorded spec `fetch`es
|
|
9
|
+
* here (via the single Miniflare `fetchMock`) to tell the Node-side agent which
|
|
10
|
+
* cassette is active for the current test — see `setupRecording` (vcr-spec.ts)
|
|
11
|
+
* and `VcrAgent` (vcr.ts). Never a real network host.
|
|
12
|
+
*/
|
|
13
|
+
export const VCR_CONTROL_ORIGIN = "https://vcr.internal";
|
|
14
|
+
/**
|
|
15
|
+
* Cassette filenames are derived from kebab-cased file + describe + test names
|
|
16
|
+
* (see `cassetteNameFor`), so they are always lowercase `a-z0-9-` plus the
|
|
17
|
+
* `.snapshot.json` suffix. Enforced on the control channel as a path-traversal
|
|
18
|
+
* guard (no `/`, no `..`) before the name is joined to the snapshots dir.
|
|
19
|
+
*/
|
|
20
|
+
export const CASSETTE_NAME_RE = /^[a-z0-9-]+\.snapshot\.json$/;
|
|
21
|
+
//# sourceMappingURL=vcr-shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vcr-shared.js","sourceRoot":"","sources":["../../src/testing/vcr-shared.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,sBAAsB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { RunnerTestCase } from "vitest";
|
|
2
|
+
/**
|
|
3
|
+
* Cassette filename for a test: `kebab(<file rel to test/, minus .spec.ts>)`
|
|
4
|
+
* then each describe level then the test name, all kebab-cased and joined by
|
|
5
|
+
* `--`, plus `.snapshot.json`. Example:
|
|
6
|
+
* `recipes-arc-game-recorded--arc-game-recorded-real-api--starts-a-real-game-and-closes-the-scorecard-on-abort.snapshot.json`.
|
|
7
|
+
* Exported for debugging / the cassette-rename step.
|
|
8
|
+
*/
|
|
9
|
+
export declare function cassetteNameFor(task: RunnerTestCase): string;
|
|
10
|
+
/**
|
|
11
|
+
* Call once at the top of a recorded spec file (outside any `describe`). Adds
|
|
12
|
+
* per-test hooks that activate the test's cassette before it runs and release
|
|
13
|
+
* it after. A missing cassette **fails** the test with instructions to record
|
|
14
|
+
* (never skips — CI must go red so the gap is visible).
|
|
15
|
+
*/
|
|
16
|
+
export declare function setupRecording(): void;
|
|
17
|
+
//# sourceMappingURL=vcr-spec.d.ts.map
|
|
@@ -0,0 +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;AA0B9D;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAc5D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,IAAI,IAAI,CA8BrC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { VCR_CONTROL_ORIGIN } from "./vcr-shared.js";
|
|
3
|
+
/**
|
|
4
|
+
* Worker-side half of the VCR harness. `setupRecording()` is the *only* thing a
|
|
5
|
+
* recorded spec wires up: one call at the top of the file gives every `it` its
|
|
6
|
+
* own cassette, auto-named from the file + describe + test names, recorded under
|
|
7
|
+
* `RECORD=1` and replayed otherwise. No per-recipe config, no registry.
|
|
8
|
+
*
|
|
9
|
+
* How it reaches the Node-side agent: specs run in workerd (no filesystem), so
|
|
10
|
+
* the active cassette is announced over an in-band control channel — a `fetch`
|
|
11
|
+
* to {@link VCR_CONTROL_ORIGIN} that `VcrAgent` (test/helpers/vcr.ts) answers
|
|
12
|
+
* from the same Miniflare `fetchMock` every worker fetch already flows through.
|
|
13
|
+
*/
|
|
14
|
+
const kebab = (s) => s
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
17
|
+
.replace(/^-+|-+$/g, "");
|
|
18
|
+
/** The File task (root of the tree) carries `filepath`; describe suites don't. */
|
|
19
|
+
function isFileTask(suite) {
|
|
20
|
+
return typeof suite.filepath === "string";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Cassette filename for a test: `kebab(<file rel to test/, minus .spec.ts>)`
|
|
24
|
+
* then each describe level then the test name, all kebab-cased and joined by
|
|
25
|
+
* `--`, plus `.snapshot.json`. Example:
|
|
26
|
+
* `recipes-arc-game-recorded--arc-game-recorded-real-api--starts-a-real-game-and-closes-the-scorecard-on-abort.snapshot.json`.
|
|
27
|
+
* Exported for debugging / the cassette-rename step.
|
|
28
|
+
*/
|
|
29
|
+
export function cassetteNameFor(task) {
|
|
30
|
+
const rel = task.file.filepath
|
|
31
|
+
.split(/[\\/]test[\\/]/)
|
|
32
|
+
.pop()
|
|
33
|
+
.replace(/\.spec\.ts$/, "");
|
|
34
|
+
const suites = [];
|
|
35
|
+
let suite = task.suite;
|
|
36
|
+
while (suite && !isFileTask(suite)) {
|
|
37
|
+
suites.unshift(suite.name);
|
|
38
|
+
suite = suite.suite;
|
|
39
|
+
}
|
|
40
|
+
return [rel, ...suites, task.name].map(kebab).join("--") + ".snapshot.json";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Call once at the top of a recorded spec file (outside any `describe`). Adds
|
|
44
|
+
* per-test hooks that activate the test's cassette before it runs and release
|
|
45
|
+
* it after. A missing cassette **fails** the test with instructions to record
|
|
46
|
+
* (never skips — CI must go red so the gap is visible).
|
|
47
|
+
*/
|
|
48
|
+
export function setupRecording() {
|
|
49
|
+
beforeEach(async (ctx) => {
|
|
50
|
+
const cassette = cassetteNameFor(ctx.task);
|
|
51
|
+
const res = await fetch(`${VCR_CONTROL_ORIGIN}/use?cassette=${cassette}`, {
|
|
52
|
+
method: "POST"
|
|
53
|
+
});
|
|
54
|
+
if (res.status === 404) {
|
|
55
|
+
throw new Error(`No VCR cassette "${cassette}". Record it with \`npm run test:record\` ` +
|
|
56
|
+
`(add \`-- -t "${ctx.task.name}"\` to record only this test), which ` +
|
|
57
|
+
`needs a real ARC_API_KEY in .env.test.`);
|
|
58
|
+
}
|
|
59
|
+
if (res.status === 409) {
|
|
60
|
+
throw new Error(`VCR cassette "${cassette}" could not activate: another recorded test is ` +
|
|
61
|
+
`already using the shared agent. Recorded specs must not run in ` +
|
|
62
|
+
`parallel — keep them sequential.`);
|
|
63
|
+
}
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
throw new Error(`VCR control channel error (HTTP ${res.status}) activating "${cassette}".`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
afterEach(async () => {
|
|
69
|
+
await fetch(`${VCR_CONTROL_ORIGIN}/release`, { method: "POST" });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=vcr-spec.js.map
|
|
@@ -0,0 +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,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;;;;;;GAUG;AAEH,MAAM,KAAK,GAAG,CAAC,CAAS,EAAU,EAAE,CAClC,CAAC;KACE,WAAW,EAAE;KACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;KAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;AAE7B,kFAAkF;AAClF,SAAS,UAAU,CAAC,KAAsB;IACxC,OAAO,OAAQ,KAA+B,CAAC,QAAQ,KAAK,QAAQ,CAAC;AACvE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAoB;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ;SAC3B,KAAK,CAAC,gBAAgB,CAAC;SACvB,GAAG,EAAG;SACN,OAAO,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IAE9B,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,GAAgC,IAAI,CAAC,KAAK,CAAC;IACpD,OAAO,KAAK,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;QACnC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC3B,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,gBAAgB,CAAC;AAC9E,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc;IAC5B,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,kBAAkB,iBAAiB,QAAQ,EAAE,EAAE;YACxE,MAAM,EAAE,MAAM;SACf,CAAC,CAAC;QACH,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,oBAAoB,QAAQ,4CAA4C;gBACtE,iBAAiB,GAAG,CAAC,IAAI,CAAC,IAAI,uCAAuC;gBACrE,wCAAwC,CAC3C,CAAC;QACJ,CAAC;QACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CACb,iBAAiB,QAAQ,iDAAiD;gBACxE,iEAAiE;gBACjE,kCAAkC,CACrC,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CACb,mCAAmC,GAAG,CAAC,MAAM,iBAAiB,QAAQ,IAAI,CAC3E,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,MAAM,KAAK,CAAC,GAAG,kBAAkB,UAAU,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { MockAgent, type Dispatcher } from "undici";
|
|
2
|
+
export interface CreateVcrAgentOptions {
|
|
3
|
+
/** Absolute path of the cassettes directory (test/snapshots/). */
|
|
4
|
+
snapshotsDir: string;
|
|
5
|
+
/**
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
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). */
|
|
15
|
+
excludeHeaders?: string[];
|
|
16
|
+
/** Headers stored but excluded from request matching (e.g. session cookies). */
|
|
17
|
+
ignoreHeaders?: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The single agent Miniflare accepts as `fetchMock` (it allows only one), fanning
|
|
21
|
+
* outbound worker requests out by role:
|
|
22
|
+
*
|
|
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.
|
|
35
|
+
*
|
|
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.
|
|
39
|
+
*
|
|
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.
|
|
48
|
+
*/
|
|
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;
|
|
70
|
+
/**
|
|
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).
|
|
79
|
+
*/
|
|
80
|
+
export declare function closeVcr(): Promise<void>;
|
|
81
|
+
//# sourceMappingURL=vcr.d.ts.map
|
|
@@ -0,0 +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"}
|