@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.
Files changed (238) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +279 -0
  3. package/dist/a2a/agent-stub.d.ts +61 -0
  4. package/dist/a2a/agent-stub.d.ts.map +1 -0
  5. package/dist/a2a/agent-stub.js +2 -0
  6. package/dist/a2a/agent-stub.js.map +1 -0
  7. package/dist/a2a/card.d.ts +119 -0
  8. package/dist/a2a/card.d.ts.map +1 -0
  9. package/dist/a2a/card.js +115 -0
  10. package/dist/a2a/card.js.map +1 -0
  11. package/dist/a2a/context.d.ts +14 -0
  12. package/dist/a2a/context.d.ts.map +1 -0
  13. package/dist/a2a/context.js +55 -0
  14. package/dist/a2a/context.js.map +1 -0
  15. package/dist/a2a/executor.d.ts +85 -0
  16. package/dist/a2a/executor.d.ts.map +1 -0
  17. package/dist/a2a/executor.js +107 -0
  18. package/dist/a2a/executor.js.map +1 -0
  19. package/dist/a2a/index.d.ts +18 -0
  20. package/dist/a2a/index.d.ts.map +1 -0
  21. package/dist/a2a/index.js +17 -0
  22. package/dist/a2a/index.js.map +1 -0
  23. package/dist/a2a/notify.d.ts +75 -0
  24. package/dist/a2a/notify.d.ts.map +1 -0
  25. package/dist/a2a/notify.js +161 -0
  26. package/dist/a2a/notify.js.map +1 -0
  27. package/dist/a2a/parts.d.ts +46 -0
  28. package/dist/a2a/parts.d.ts.map +1 -0
  29. package/dist/a2a/parts.js +95 -0
  30. package/dist/a2a/parts.js.map +1 -0
  31. package/dist/a2a/task-store.d.ts +45 -0
  32. package/dist/a2a/task-store.d.ts.map +1 -0
  33. package/dist/a2a/task-store.js +100 -0
  34. package/dist/a2a/task-store.js.map +1 -0
  35. package/dist/a2a/task.d.ts +86 -0
  36. package/dist/a2a/task.d.ts.map +1 -0
  37. package/dist/a2a/task.js +16 -0
  38. package/dist/a2a/task.js.map +1 -0
  39. package/dist/a2a/verify.d.ts +64 -0
  40. package/dist/a2a/verify.d.ts.map +1 -0
  41. package/dist/a2a/verify.js +122 -0
  42. package/dist/a2a/verify.js.map +1 -0
  43. package/dist/agent/budget.d.ts +47 -0
  44. package/dist/agent/budget.d.ts.map +1 -0
  45. package/dist/agent/budget.js +43 -0
  46. package/dist/agent/budget.js.map +1 -0
  47. package/dist/agent/control.d.ts +110 -0
  48. package/dist/agent/control.d.ts.map +1 -0
  49. package/dist/agent/control.js +115 -0
  50. package/dist/agent/control.js.map +1 -0
  51. package/dist/agent/final-reply.d.ts +49 -0
  52. package/dist/agent/final-reply.d.ts.map +1 -0
  53. package/dist/agent/final-reply.js +52 -0
  54. package/dist/agent/final-reply.js.map +1 -0
  55. package/dist/agent/history.d.ts +83 -0
  56. package/dist/agent/history.d.ts.map +1 -0
  57. package/dist/agent/history.js +117 -0
  58. package/dist/agent/history.js.map +1 -0
  59. package/dist/agent/index.d.ts +17 -0
  60. package/dist/agent/index.d.ts.map +1 -0
  61. package/dist/agent/index.js +17 -0
  62. package/dist/agent/index.js.map +1 -0
  63. package/dist/agent/inference.d.ts +52 -0
  64. package/dist/agent/inference.d.ts.map +1 -0
  65. package/dist/agent/inference.js +79 -0
  66. package/dist/agent/inference.js.map +1 -0
  67. package/dist/agent/model.d.ts +75 -0
  68. package/dist/agent/model.d.ts.map +1 -0
  69. package/dist/agent/model.js +62 -0
  70. package/dist/agent/model.js.map +1 -0
  71. package/dist/agent/session.d.ts +92 -0
  72. package/dist/agent/session.d.ts.map +1 -0
  73. package/dist/agent/session.js +80 -0
  74. package/dist/agent/session.js.map +1 -0
  75. package/dist/config.d.ts +159 -0
  76. package/dist/config.d.ts.map +1 -0
  77. package/dist/config.js +92 -0
  78. package/dist/config.js.map +1 -0
  79. package/dist/contract/index.d.ts +10 -0
  80. package/dist/contract/index.d.ts.map +1 -0
  81. package/dist/contract/index.js +9 -0
  82. package/dist/contract/index.js.map +1 -0
  83. package/dist/contract/plugin.d.ts +163 -0
  84. package/dist/contract/plugin.d.ts.map +1 -0
  85. package/dist/contract/plugin.js +39 -0
  86. package/dist/contract/plugin.js.map +1 -0
  87. package/dist/contract/recipe.d.ts +170 -0
  88. package/dist/contract/recipe.d.ts.map +1 -0
  89. package/dist/contract/recipe.js +2 -0
  90. package/dist/contract/recipe.js.map +1 -0
  91. package/dist/contract/validation.d.ts +86 -0
  92. package/dist/contract/validation.d.ts.map +1 -0
  93. package/dist/contract/validation.js +76 -0
  94. package/dist/contract/validation.js.map +1 -0
  95. package/dist/db/db.d.ts +131 -0
  96. package/dist/db/db.d.ts.map +1 -0
  97. package/dist/db/db.js +91 -0
  98. package/dist/db/db.js.map +1 -0
  99. package/dist/db/index.d.ts +9 -0
  100. package/dist/db/index.d.ts.map +1 -0
  101. package/dist/db/index.js +9 -0
  102. package/dist/db/index.js.map +1 -0
  103. package/dist/db/migrations/index.d.ts +21 -0
  104. package/dist/db/migrations/index.d.ts.map +1 -0
  105. package/dist/db/migrations/index.js +54 -0
  106. package/dist/db/migrations/index.js.map +1 -0
  107. package/dist/db/models/subtasks.d.ts +102 -0
  108. package/dist/db/models/subtasks.d.ts.map +1 -0
  109. package/dist/db/models/subtasks.js +285 -0
  110. package/dist/db/models/subtasks.js.map +1 -0
  111. package/dist/db/models/tasks.d.ts +92 -0
  112. package/dist/db/models/tasks.d.ts.map +1 -0
  113. package/dist/db/models/tasks.js +212 -0
  114. package/dist/db/models/tasks.js.map +1 -0
  115. package/dist/db/schema.d.ts +488 -0
  116. package/dist/db/schema.d.ts.map +1 -0
  117. package/dist/db/schema.js +91 -0
  118. package/dist/db/schema.js.map +1 -0
  119. package/dist/env.d.ts +46 -0
  120. package/dist/env.d.ts.map +1 -0
  121. package/dist/env.js +40 -0
  122. package/dist/env.js.map +1 -0
  123. package/dist/index.d.ts +20 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +18 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/platform.d.ts +47 -0
  128. package/dist/platform.d.ts.map +1 -0
  129. package/dist/platform.js +47 -0
  130. package/dist/platform.js.map +1 -0
  131. package/dist/runtime/index.d.ts +86 -0
  132. package/dist/runtime/index.d.ts.map +1 -0
  133. package/dist/runtime/index.js +123 -0
  134. package/dist/runtime/index.js.map +1 -0
  135. package/dist/runtime/tool-families.d.ts +33 -0
  136. package/dist/runtime/tool-families.d.ts.map +1 -0
  137. package/dist/runtime/tool-families.js +62 -0
  138. package/dist/runtime/tool-families.js.map +1 -0
  139. package/dist/subagent/fingerprint.d.ts +38 -0
  140. package/dist/subagent/fingerprint.d.ts.map +1 -0
  141. package/dist/subagent/fingerprint.js +97 -0
  142. package/dist/subagent/fingerprint.js.map +1 -0
  143. package/dist/subagent/index.d.ts +147 -0
  144. package/dist/subagent/index.d.ts.map +1 -0
  145. package/dist/subagent/index.js +299 -0
  146. package/dist/subagent/index.js.map +1 -0
  147. package/dist/subagent/prompt.d.ts +34 -0
  148. package/dist/subagent/prompt.d.ts.map +1 -0
  149. package/dist/subagent/prompt.js +46 -0
  150. package/dist/subagent/prompt.js.map +1 -0
  151. package/dist/subagent/run.d.ts +151 -0
  152. package/dist/subagent/run.d.ts.map +1 -0
  153. package/dist/subagent/run.js +491 -0
  154. package/dist/subagent/run.js.map +1 -0
  155. package/dist/subagent/workspace.d.ts +65 -0
  156. package/dist/subagent/workspace.d.ts.map +1 -0
  157. package/dist/subagent/workspace.js +53 -0
  158. package/dist/subagent/workspace.js.map +1 -0
  159. package/dist/subtasks/catalog.d.ts +40 -0
  160. package/dist/subtasks/catalog.d.ts.map +1 -0
  161. package/dist/subtasks/catalog.js +36 -0
  162. package/dist/subtasks/catalog.js.map +1 -0
  163. package/dist/subtasks/decomposition.d.ts +90 -0
  164. package/dist/subtasks/decomposition.d.ts.map +1 -0
  165. package/dist/subtasks/decomposition.js +205 -0
  166. package/dist/subtasks/decomposition.js.map +1 -0
  167. package/dist/subtasks/delegate.d.ts +77 -0
  168. package/dist/subtasks/delegate.d.ts.map +1 -0
  169. package/dist/subtasks/delegate.js +102 -0
  170. package/dist/subtasks/delegate.js.map +1 -0
  171. package/dist/subtasks/index.d.ts +11 -0
  172. package/dist/subtasks/index.d.ts.map +1 -0
  173. package/dist/subtasks/index.js +11 -0
  174. package/dist/subtasks/index.js.map +1 -0
  175. package/dist/subtasks/scheduler.d.ts +49 -0
  176. package/dist/subtasks/scheduler.d.ts.map +1 -0
  177. package/dist/subtasks/scheduler.js +48 -0
  178. package/dist/subtasks/scheduler.js.map +1 -0
  179. package/dist/subtasks/subtask-types.d.ts +100 -0
  180. package/dist/subtasks/subtask-types.d.ts.map +1 -0
  181. package/dist/subtasks/subtask-types.js +111 -0
  182. package/dist/subtasks/subtask-types.js.map +1 -0
  183. package/dist/subtasks/types.d.ts +321 -0
  184. package/dist/subtasks/types.d.ts.map +1 -0
  185. package/dist/subtasks/types.js +16 -0
  186. package/dist/subtasks/types.js.map +1 -0
  187. package/dist/testing/auth.d.ts +12 -0
  188. package/dist/testing/auth.d.ts.map +1 -0
  189. package/dist/testing/auth.js +25 -0
  190. package/dist/testing/auth.js.map +1 -0
  191. package/dist/testing/do.d.ts +30 -0
  192. package/dist/testing/do.d.ts.map +1 -0
  193. package/dist/testing/do.js +26 -0
  194. package/dist/testing/do.js.map +1 -0
  195. package/dist/testing/fake-session.d.ts +27 -0
  196. package/dist/testing/fake-session.d.ts.map +1 -0
  197. package/dist/testing/fake-session.js +38 -0
  198. package/dist/testing/fake-session.js.map +1 -0
  199. package/dist/testing/fixtures.d.ts +27 -0
  200. package/dist/testing/fixtures.d.ts.map +1 -0
  201. package/dist/testing/fixtures.js +67 -0
  202. package/dist/testing/fixtures.js.map +1 -0
  203. package/dist/testing/index.d.ts +31 -0
  204. package/dist/testing/index.d.ts.map +1 -0
  205. package/dist/testing/index.js +31 -0
  206. package/dist/testing/index.js.map +1 -0
  207. package/dist/testing/mock-model.d.ts +43 -0
  208. package/dist/testing/mock-model.d.ts.map +1 -0
  209. package/dist/testing/mock-model.js +62 -0
  210. package/dist/testing/mock-model.js.map +1 -0
  211. package/dist/testing/node.d.ts +24 -0
  212. package/dist/testing/node.d.ts.map +1 -0
  213. package/dist/testing/node.js +24 -0
  214. package/dist/testing/node.js.map +1 -0
  215. package/dist/testing/vcr-global-setup.d.ts +3 -0
  216. package/dist/testing/vcr-global-setup.d.ts.map +1 -0
  217. package/dist/testing/vcr-global-setup.js +9 -0
  218. package/dist/testing/vcr-global-setup.js.map +1 -0
  219. package/dist/testing/vcr-shared.d.ts +21 -0
  220. package/dist/testing/vcr-shared.d.ts.map +1 -0
  221. package/dist/testing/vcr-shared.js +21 -0
  222. package/dist/testing/vcr-shared.js.map +1 -0
  223. package/dist/testing/vcr-spec.d.ts +17 -0
  224. package/dist/testing/vcr-spec.d.ts.map +1 -0
  225. package/dist/testing/vcr-spec.js +72 -0
  226. package/dist/testing/vcr-spec.js.map +1 -0
  227. package/dist/testing/vcr.d.ts +81 -0
  228. package/dist/testing/vcr.d.ts.map +1 -0
  229. package/dist/testing/vcr.js +211 -0
  230. package/dist/testing/vcr.js.map +1 -0
  231. package/dist/worker/index.d.ts +62 -0
  232. package/dist/worker/index.d.ts.map +1 -0
  233. package/dist/worker/index.js +194 -0
  234. package/dist/worker/index.js.map +1 -0
  235. package/eslint-rules/index.js +31 -0
  236. package/eslint-rules/no-deprecated-object-properties.js +81 -0
  237. package/package.json +147 -0
  238. 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,3 @@
1
+ export declare function setup(): void;
2
+ export declare function teardown(): Promise<void>;
3
+ //# sourceMappingURL=vcr-global-setup.d.ts.map
@@ -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"}