@copilotkit/aimock 1.42.0 → 1.43.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 (341) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +56 -0
  4. package/README.md +102 -16
  5. package/dist/agui-mock.cjs +1 -1
  6. package/dist/agui-mock.cjs.map +1 -1
  7. package/dist/agui-mock.d.cts.map +1 -1
  8. package/dist/agui-mock.d.ts.map +1 -1
  9. package/dist/agui-mock.js +1 -1
  10. package/dist/agui-mock.js.map +1 -1
  11. package/dist/agui-recorder.cjs +33 -11
  12. package/dist/agui-recorder.cjs.map +1 -1
  13. package/dist/agui-recorder.d.cts +10 -1
  14. package/dist/agui-recorder.d.cts.map +1 -1
  15. package/dist/agui-recorder.d.ts +10 -1
  16. package/dist/agui-recorder.d.ts.map +1 -1
  17. package/dist/agui-recorder.js +33 -11
  18. package/dist/agui-recorder.js.map +1 -1
  19. package/dist/agui-types.d.cts +1 -0
  20. package/dist/agui-types.d.cts.map +1 -1
  21. package/dist/agui-types.d.ts +1 -0
  22. package/dist/agui-types.d.ts.map +1 -1
  23. package/dist/aimock-cli.cjs +38 -10
  24. package/dist/aimock-cli.cjs.map +1 -1
  25. package/dist/aimock-cli.d.cts +5 -0
  26. package/dist/aimock-cli.d.cts.map +1 -1
  27. package/dist/aimock-cli.d.ts +5 -0
  28. package/dist/aimock-cli.d.ts.map +1 -1
  29. package/dist/aimock-cli.js +38 -10
  30. package/dist/aimock-cli.js.map +1 -1
  31. package/dist/batches.cjs +458 -0
  32. package/dist/batches.cjs.map +1 -0
  33. package/dist/batches.d.cts +57 -0
  34. package/dist/batches.d.cts.map +1 -0
  35. package/dist/batches.d.ts +57 -0
  36. package/dist/batches.d.ts.map +1 -0
  37. package/dist/batches.js +453 -0
  38. package/dist/batches.js.map +1 -0
  39. package/dist/bedrock-converse.cjs +12 -8
  40. package/dist/bedrock-converse.cjs.map +1 -1
  41. package/dist/bedrock-converse.d.cts.map +1 -1
  42. package/dist/bedrock-converse.d.ts.map +1 -1
  43. package/dist/bedrock-converse.js +14 -10
  44. package/dist/bedrock-converse.js.map +1 -1
  45. package/dist/bedrock.cjs +10 -8
  46. package/dist/bedrock.cjs.map +1 -1
  47. package/dist/bedrock.d.cts.map +1 -1
  48. package/dist/bedrock.d.ts.map +1 -1
  49. package/dist/bedrock.js +12 -10
  50. package/dist/bedrock.js.map +1 -1
  51. package/dist/byteplus-video.cjs +3 -3
  52. package/dist/byteplus-video.cjs.map +1 -1
  53. package/dist/byteplus-video.js +4 -4
  54. package/dist/byteplus-video.js.map +1 -1
  55. package/dist/chaos.cjs +622 -72
  56. package/dist/chaos.cjs.map +1 -1
  57. package/dist/chaos.d.cts +164 -7
  58. package/dist/chaos.d.cts.map +1 -1
  59. package/dist/chaos.d.ts +164 -7
  60. package/dist/chaos.d.ts.map +1 -1
  61. package/dist/chaos.js +613 -74
  62. package/dist/chaos.js.map +1 -1
  63. package/dist/cli.cjs +42 -23
  64. package/dist/cli.cjs.map +1 -1
  65. package/dist/cli.js +42 -23
  66. package/dist/cli.js.map +1 -1
  67. package/dist/cohere.cjs +10 -8
  68. package/dist/cohere.cjs.map +1 -1
  69. package/dist/cohere.d.cts.map +1 -1
  70. package/dist/cohere.d.ts.map +1 -1
  71. package/dist/cohere.js +12 -10
  72. package/dist/cohere.js.map +1 -1
  73. package/dist/config-loader.d.cts.map +1 -1
  74. package/dist/config-loader.d.ts.map +1 -1
  75. package/dist/convert.cjs +1 -1
  76. package/dist/convert.cjs.map +1 -1
  77. package/dist/convert.js +1 -1
  78. package/dist/convert.js.map +1 -1
  79. package/dist/elevenlabs-audio.cjs +3 -3
  80. package/dist/elevenlabs-audio.cjs.map +1 -1
  81. package/dist/elevenlabs-audio.js +4 -4
  82. package/dist/elevenlabs-audio.js.map +1 -1
  83. package/dist/elevenlabs-voice.cjs +1053 -0
  84. package/dist/elevenlabs-voice.cjs.map +1 -0
  85. package/dist/elevenlabs-voice.d.cts +72 -0
  86. package/dist/elevenlabs-voice.d.cts.map +1 -0
  87. package/dist/elevenlabs-voice.d.ts +72 -0
  88. package/dist/elevenlabs-voice.d.ts.map +1 -0
  89. package/dist/elevenlabs-voice.js +1048 -0
  90. package/dist/elevenlabs-voice.js.map +1 -0
  91. package/dist/embeddings.cjs +8 -4
  92. package/dist/embeddings.cjs.map +1 -1
  93. package/dist/embeddings.d.cts.map +1 -1
  94. package/dist/embeddings.d.ts.map +1 -1
  95. package/dist/embeddings.js +9 -5
  96. package/dist/embeddings.js.map +1 -1
  97. package/dist/fal-audio.cjs +3 -3
  98. package/dist/fal-audio.cjs.map +1 -1
  99. package/dist/fal-audio.js +4 -4
  100. package/dist/fal-audio.js.map +1 -1
  101. package/dist/fal.cjs +28 -4
  102. package/dist/fal.cjs.map +1 -1
  103. package/dist/fal.d.cts +16 -1
  104. package/dist/fal.d.cts.map +1 -1
  105. package/dist/fal.d.ts +16 -1
  106. package/dist/fal.d.ts.map +1 -1
  107. package/dist/fal.js +28 -5
  108. package/dist/fal.js.map +1 -1
  109. package/dist/files.cjs +1071 -0
  110. package/dist/files.cjs.map +1 -0
  111. package/dist/files.d.cts +68 -0
  112. package/dist/files.d.cts.map +1 -0
  113. package/dist/files.d.ts +68 -0
  114. package/dist/files.d.ts.map +1 -0
  115. package/dist/files.js +1059 -0
  116. package/dist/files.js.map +1 -0
  117. package/dist/fine-tuning.cjs +1051 -0
  118. package/dist/fine-tuning.cjs.map +1 -0
  119. package/dist/fine-tuning.d.cts +96 -0
  120. package/dist/fine-tuning.d.cts.map +1 -0
  121. package/dist/fine-tuning.d.ts +96 -0
  122. package/dist/fine-tuning.d.ts.map +1 -0
  123. package/dist/fine-tuning.js +1044 -0
  124. package/dist/fine-tuning.js.map +1 -0
  125. package/dist/fixture-loader.cjs +0 -0
  126. package/dist/fixture-loader.cjs.map +1 -1
  127. package/dist/fixture-loader.d.cts +37 -5
  128. package/dist/fixture-loader.d.cts.map +1 -1
  129. package/dist/fixture-loader.d.ts +37 -5
  130. package/dist/fixture-loader.d.ts.map +1 -1
  131. package/dist/fixture-loader.js +0 -0
  132. package/dist/fixture-loader.js.map +1 -1
  133. package/dist/gemini-embeddings.cjs +2 -2
  134. package/dist/gemini-embeddings.cjs.map +1 -1
  135. package/dist/gemini-embeddings.js +3 -3
  136. package/dist/gemini-embeddings.js.map +1 -1
  137. package/dist/gemini-interactions.cjs +13 -7
  138. package/dist/gemini-interactions.cjs.map +1 -1
  139. package/dist/gemini-interactions.d.cts.map +1 -1
  140. package/dist/gemini-interactions.d.ts.map +1 -1
  141. package/dist/gemini-interactions.js +15 -9
  142. package/dist/gemini-interactions.js.map +1 -1
  143. package/dist/gemini.cjs +11 -7
  144. package/dist/gemini.cjs.map +1 -1
  145. package/dist/gemini.d.cts.map +1 -1
  146. package/dist/gemini.d.ts.map +1 -1
  147. package/dist/gemini.js +13 -9
  148. package/dist/gemini.js.map +1 -1
  149. package/dist/grok-video.cjs +14 -14
  150. package/dist/grok-video.cjs.map +1 -1
  151. package/dist/grok-video.js +15 -15
  152. package/dist/grok-video.js.map +1 -1
  153. package/dist/helpers.cjs +229 -7
  154. package/dist/helpers.cjs.map +1 -1
  155. package/dist/helpers.d.cts +38 -2
  156. package/dist/helpers.d.cts.map +1 -1
  157. package/dist/helpers.d.ts +38 -2
  158. package/dist/helpers.d.ts.map +1 -1
  159. package/dist/helpers.js +221 -8
  160. package/dist/helpers.js.map +1 -1
  161. package/dist/images.cjs +54 -123
  162. package/dist/images.cjs.map +1 -1
  163. package/dist/images.d.cts.map +1 -1
  164. package/dist/images.d.ts.map +1 -1
  165. package/dist/images.js +55 -124
  166. package/dist/images.js.map +1 -1
  167. package/dist/index.cjs +41 -8
  168. package/dist/index.d.cts +12 -7
  169. package/dist/index.d.ts +12 -7
  170. package/dist/index.js +14 -10
  171. package/dist/jest.cjs +14 -7
  172. package/dist/jest.cjs.map +1 -1
  173. package/dist/jest.d.cts +3 -2
  174. package/dist/jest.d.cts.map +1 -1
  175. package/dist/jest.d.ts +3 -2
  176. package/dist/jest.d.ts.map +1 -1
  177. package/dist/jest.js +14 -7
  178. package/dist/jest.js.map +1 -1
  179. package/dist/journal.cjs +41 -4
  180. package/dist/journal.cjs.map +1 -1
  181. package/dist/journal.d.cts +44 -3
  182. package/dist/journal.d.cts.map +1 -1
  183. package/dist/journal.d.ts +44 -3
  184. package/dist/journal.d.ts.map +1 -1
  185. package/dist/journal.js +41 -5
  186. package/dist/journal.js.map +1 -1
  187. package/dist/live-fixture.cjs +1192 -0
  188. package/dist/live-fixture.cjs.map +1 -0
  189. package/dist/live-fixture.js +1187 -0
  190. package/dist/live-fixture.js.map +1 -0
  191. package/dist/live-recorder.cjs +256 -0
  192. package/dist/live-recorder.cjs.map +1 -0
  193. package/dist/live-recorder.js +254 -0
  194. package/dist/live-recorder.js.map +1 -0
  195. package/dist/live-replay.cjs +286 -0
  196. package/dist/live-replay.cjs.map +1 -0
  197. package/dist/live-replay.js +285 -0
  198. package/dist/live-replay.js.map +1 -0
  199. package/dist/live-sanitize.cjs +196 -0
  200. package/dist/live-sanitize.cjs.map +1 -0
  201. package/dist/live-sanitize.js +196 -0
  202. package/dist/live-sanitize.js.map +1 -0
  203. package/dist/live-types.d.cts +71 -0
  204. package/dist/live-types.d.cts.map +1 -0
  205. package/dist/live-types.d.ts +71 -0
  206. package/dist/live-types.d.ts.map +1 -0
  207. package/dist/llmock.cjs +52 -41
  208. package/dist/llmock.cjs.map +1 -1
  209. package/dist/llmock.d.cts +16 -5
  210. package/dist/llmock.d.cts.map +1 -1
  211. package/dist/llmock.d.ts +16 -5
  212. package/dist/llmock.d.ts.map +1 -1
  213. package/dist/llmock.js +53 -42
  214. package/dist/llmock.js.map +1 -1
  215. package/dist/messages.cjs +11 -7
  216. package/dist/messages.cjs.map +1 -1
  217. package/dist/messages.d.cts.map +1 -1
  218. package/dist/messages.d.ts.map +1 -1
  219. package/dist/messages.js +13 -9
  220. package/dist/messages.js.map +1 -1
  221. package/dist/metrics.cjs +258 -5
  222. package/dist/metrics.cjs.map +1 -1
  223. package/dist/metrics.d.cts +3 -1
  224. package/dist/metrics.d.cts.map +1 -1
  225. package/dist/metrics.d.ts +3 -1
  226. package/dist/metrics.d.ts.map +1 -1
  227. package/dist/metrics.js +255 -6
  228. package/dist/metrics.js.map +1 -1
  229. package/dist/moderation.cjs +10 -1
  230. package/dist/moderation.cjs.map +1 -1
  231. package/dist/moderation.d.cts.map +1 -1
  232. package/dist/moderation.d.ts.map +1 -1
  233. package/dist/moderation.js +10 -1
  234. package/dist/moderation.js.map +1 -1
  235. package/dist/ollama.cjs +5 -5
  236. package/dist/ollama.cjs.map +1 -1
  237. package/dist/ollama.js +6 -6
  238. package/dist/ollama.js.map +1 -1
  239. package/dist/openrouter-video.cjs +5 -5
  240. package/dist/openrouter-video.cjs.map +1 -1
  241. package/dist/openrouter-video.js +6 -6
  242. package/dist/openrouter-video.js.map +1 -1
  243. package/dist/recorder.cjs +45 -9
  244. package/dist/recorder.cjs.map +1 -1
  245. package/dist/recorder.d.cts.map +1 -1
  246. package/dist/recorder.d.ts.map +1 -1
  247. package/dist/recorder.js +43 -11
  248. package/dist/recorder.js.map +1 -1
  249. package/dist/responses.cjs +4 -3
  250. package/dist/responses.cjs.map +1 -1
  251. package/dist/responses.js +5 -5
  252. package/dist/responses.js.map +1 -1
  253. package/dist/router.cjs +20 -33
  254. package/dist/router.cjs.map +1 -1
  255. package/dist/router.d.cts +4 -1
  256. package/dist/router.d.cts.map +1 -1
  257. package/dist/router.d.ts +4 -1
  258. package/dist/router.d.ts.map +1 -1
  259. package/dist/router.js +20 -34
  260. package/dist/router.js.map +1 -1
  261. package/dist/server.cjs +701 -625
  262. package/dist/server.cjs.map +1 -1
  263. package/dist/server.d.cts +1 -0
  264. package/dist/server.d.cts.map +1 -1
  265. package/dist/server.d.ts +1 -0
  266. package/dist/server.d.ts.map +1 -1
  267. package/dist/server.js +704 -628
  268. package/dist/server.js.map +1 -1
  269. package/dist/speech.cjs +2 -2
  270. package/dist/speech.cjs.map +1 -1
  271. package/dist/speech.js +3 -3
  272. package/dist/speech.js.map +1 -1
  273. package/dist/transcription.cjs +4 -4
  274. package/dist/transcription.cjs.map +1 -1
  275. package/dist/transcription.js +5 -5
  276. package/dist/transcription.js.map +1 -1
  277. package/dist/types.d.cts +119 -16
  278. package/dist/types.d.cts.map +1 -1
  279. package/dist/types.d.ts +119 -16
  280. package/dist/types.d.ts.map +1 -1
  281. package/dist/validate-cli.cjs +925 -0
  282. package/dist/validate-cli.cjs.map +1 -0
  283. package/dist/validate-cli.js +924 -0
  284. package/dist/validate-cli.js.map +1 -0
  285. package/dist/vector-types.d.cts.map +1 -1
  286. package/dist/vector-types.d.ts.map +1 -1
  287. package/dist/veo-video.cjs +13 -13
  288. package/dist/veo-video.cjs.map +1 -1
  289. package/dist/veo-video.js +14 -14
  290. package/dist/veo-video.js.map +1 -1
  291. package/dist/video.cjs +4 -4
  292. package/dist/video.cjs.map +1 -1
  293. package/dist/video.d.cts +1 -1
  294. package/dist/video.d.cts.map +1 -1
  295. package/dist/video.d.ts +1 -1
  296. package/dist/video.d.ts.map +1 -1
  297. package/dist/video.js +5 -5
  298. package/dist/video.js.map +1 -1
  299. package/dist/vitest.cjs +13 -6
  300. package/dist/vitest.cjs.map +1 -1
  301. package/dist/vitest.d.cts +2 -1
  302. package/dist/vitest.d.cts.map +1 -1
  303. package/dist/vitest.d.ts +2 -1
  304. package/dist/vitest.d.ts.map +1 -1
  305. package/dist/vitest.js +14 -7
  306. package/dist/vitest.js.map +1 -1
  307. package/dist/watcher.cjs +2 -1
  308. package/dist/watcher.cjs.map +1 -1
  309. package/dist/watcher.js +2 -1
  310. package/dist/watcher.js.map +1 -1
  311. package/dist/ws-framing.cjs +150 -35
  312. package/dist/ws-framing.cjs.map +1 -1
  313. package/dist/ws-framing.d.cts +18 -3
  314. package/dist/ws-framing.d.cts.map +1 -1
  315. package/dist/ws-framing.d.ts +18 -3
  316. package/dist/ws-framing.d.ts.map +1 -1
  317. package/dist/ws-framing.js +151 -36
  318. package/dist/ws-framing.js.map +1 -1
  319. package/dist/ws-gemini-live.cjs +2 -2
  320. package/dist/ws-gemini-live.d.ts +2 -2
  321. package/dist/ws-gemini-live.js +2 -2
  322. package/dist/ws-live.cjs +401 -0
  323. package/dist/ws-live.cjs.map +1 -0
  324. package/dist/ws-live.js +400 -0
  325. package/dist/ws-live.js.map +1 -0
  326. package/dist/ws-realtime.cjs +46 -2
  327. package/dist/ws-realtime.cjs.map +1 -1
  328. package/dist/ws-realtime.d.cts.map +1 -1
  329. package/dist/ws-realtime.d.ts +2 -2
  330. package/dist/ws-realtime.d.ts.map +1 -1
  331. package/dist/ws-realtime.js +46 -2
  332. package/dist/ws-realtime.js.map +1 -1
  333. package/dist/ws-responses.cjs +2 -2
  334. package/dist/ws-responses.js +2 -2
  335. package/dist/ws-upstream.cjs +118 -0
  336. package/dist/ws-upstream.cjs.map +1 -0
  337. package/dist/ws-upstream.js +115 -0
  338. package/dist/ws-upstream.js.map +1 -0
  339. package/fixtures/openai-live-client.json +390 -0
  340. package/fixtures/openai-live-managed.json +709 -0
  341. package/package.json +1 -1
package/dist/jest.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jest.cjs","names":["LLMock","loadFixturesFromDir","loadFixtureFile"],"sources":["../src/jest.ts"],"sourcesContent":["/**\n * Jest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/jest\";\n *\n * const mock = useAimock({ fixtures: \"./fixtures\" });\n *\n * it(\"responds\", async () => {\n * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });\n * });\n */\n\n/* eslint-disable no-var */\n// Jest globals — available at runtime in jest test files\ndeclare var beforeAll: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var afterAll: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var beforeEach: (fn: () => Promise<void> | void, timeout?: number) => void;\n/* eslint-enable no-var */\n\nimport { LLMock } from \"./llmock.js\";\nimport { loadFixtureFile, loadFixturesFromDir } from \"./fixture-loader.js\";\nimport type { Fixture, MockServerOptions } from \"./types.js\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface UseAimockOptions extends MockServerOptions {\n /** Path to fixture file or directory. Loaded automatically on start. */\n fixtures?: string;\n /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */\n patchEnv?: boolean;\n}\n\nexport interface AimockHandle {\n /** The LLMock instance. */\n readonly llm: LLMock;\n /** The server URL (e.g., http://127.0.0.1:4010). */\n readonly url: string;\n}\n\n/**\n * Start an aimock server for the duration of the test suite.\n *\n * - `beforeAll`: starts the server and optionally loads fixtures\n * - `beforeEach`: resets fixture match counts (not fixtures themselves)\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n *\n * NOTE: Jest globals (beforeAll, afterAll, beforeEach) must be available\n * in the test environment. This works with the default jest configuration.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockHandle {\n let handle: AimockHandle | null = null;\n let origOpenaiUrl: string | undefined;\n let origAnthropicUrl: string | undefined;\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n const resolved = resolve(fixturePath);\n const loadedFixtures = loadFixtures(resolved);\n for (const f of loadedFixtures) {\n llm.addFixture(f);\n }\n }\n\n const url = await llm.start();\n\n if (patchEnv !== false) {\n origOpenaiUrl = process.env.OPENAI_BASE_URL;\n origAnthropicUrl = process.env.ANTHROPIC_BASE_URL;\n process.env.OPENAI_BASE_URL = `${url}/v1`;\n process.env.ANTHROPIC_BASE_URL = `${url}/v1`;\n }\n\n handle = { llm, url };\n });\n\n beforeEach(() => {\n if (handle) {\n handle.llm.resetMatchCounts();\n }\n });\n\n afterAll(async () => {\n if (handle) {\n if (options.patchEnv !== false) {\n if (origOpenaiUrl !== undefined) process.env.OPENAI_BASE_URL = origOpenaiUrl;\n else delete process.env.OPENAI_BASE_URL;\n if (origAnthropicUrl !== undefined) process.env.ANTHROPIC_BASE_URL = origAnthropicUrl;\n else delete process.env.ANTHROPIC_BASE_URL;\n }\n await handle.llm.stop();\n handle = null;\n }\n });\n\n return () => {\n if (!handle) {\n throw new Error(\"useAimock(): server not started — are you calling this inside a test?\");\n }\n return handle;\n };\n}\n\nfunction loadFixtures(fixturePath: string): Fixture[] {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n return loadFixturesFromDir(fixturePath);\n }\n return loadFixtureFile(fixturePath);\n } catch (err) {\n console.warn(\n `[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return [];\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;CAClC,IAAI;CACJ,IAAI;AAEJ,WAAU,YAAY;EACpB,MAAM,EAAE,UAAU,aAAa,UAAU,GAAG,eAAe;EAC3D,MAAM,MAAM,IAAIA,sBAAO,WAAW;AAElC,MAAI,aAAa;GAEf,MAAM,iBAAiB,oCADE,YAAY,CACQ;AAC7C,QAAK,MAAM,KAAK,eACd,KAAI,WAAW,EAAE;;EAIrB,MAAM,MAAM,MAAM,IAAI,OAAO;AAE7B,MAAI,aAAa,OAAO;AACtB,mBAAgB,QAAQ,IAAI;AAC5B,sBAAmB,QAAQ,IAAI;AAC/B,WAAQ,IAAI,kBAAkB,GAAG,IAAI;AACrC,WAAQ,IAAI,qBAAqB,GAAG,IAAI;;AAG1C,WAAS;GAAE;GAAK;GAAK;GACrB;AAEF,kBAAiB;AACf,MAAI,OACF,QAAO,IAAI,kBAAkB;GAE/B;AAEF,UAAS,YAAY;AACnB,MAAI,QAAQ;AACV,OAAI,QAAQ,aAAa,OAAO;AAC9B,QAAI,kBAAkB,OAAW,SAAQ,IAAI,kBAAkB;QAC1D,QAAO,QAAQ,IAAI;AACxB,QAAI,qBAAqB,OAAW,SAAQ,IAAI,qBAAqB;QAChE,QAAO,QAAQ,IAAI;;AAE1B,SAAM,OAAO,IAAI,MAAM;AACvB,YAAS;;GAEX;AAEF,cAAa;AACX,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,wEAAwE;AAE1F,SAAO;;;AAIX,SAAS,aAAa,aAAgC;AACpD,KAAI;AAEF,4BADsB,YAAY,CACzB,aAAa,CACpB,QAAOC,2CAAoB,YAAY;AAEzC,SAAOC,uCAAgB,YAAY;UAC5B,KAAK;AACZ,UAAQ,KACN,yCAAyC,YAAY,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC1G;AACD,SAAO,EAAE"}
1
+ {"version":3,"file":"jest.cjs","names":["LLMock","loadFixturesFromDir","loadFixtureFile"],"sources":["../src/jest.ts"],"sourcesContent":["/**\n * Jest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/jest\";\n *\n * const mock = useAimock({ fixtures: \"./fixtures\" });\n *\n * it(\"responds\", async () => {\n * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });\n * });\n */\n\n/* eslint-disable no-var */\n// Jest globals — available at runtime in jest test files\ndeclare var beforeAll: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var afterAll: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var beforeEach: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var afterEach: (fn: () => Promise<void> | void, timeout?: number) => void;\n/* eslint-enable no-var */\n\nimport { LLMock } from \"./llmock.js\";\nimport { loadFixtureFile, loadFixturesFromDir } from \"./fixture-loader.js\";\nimport type { LiveOptions } from \"./live-types.js\";\nimport type { Fixture, MockServerOptions } from \"./types.js\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface UseAimockOptions extends MockServerOptions {\n /** Path to fixture file or directory. Loaded automatically on start. */\n fixtures?: string;\n /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */\n patchEnv?: boolean;\n}\n\nexport interface AimockHandle {\n /** The LLMock instance. */\n readonly llm: LLMock;\n /** The server URL (e.g., http://127.0.0.1:4010). */\n readonly url: string;\n}\n\n/**\n * Start an aimock server for the duration of the test suite.\n *\n * - `beforeAll`: starts the server and optionally loads fixtures\n * - `beforeEach`: closes Live sessions and resets fixture match counts (not fixtures)\n * - `afterEach`: closes Live sessions owned by this helper's server\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n *\n * NOTE: Jest globals (beforeAll, afterAll, beforeEach, afterEach) must be available\n * in the test environment. This works with the default jest configuration.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockHandle {\n let handle: AimockHandle | null = null;\n let origOpenaiUrl: string | undefined;\n let origAnthropicUrl: string | undefined;\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n const resolved = resolve(fixturePath);\n const loadedFixtures = loadFixtures(resolved, options.live);\n for (const f of loadedFixtures) {\n llm.addFixture(f);\n }\n }\n\n const url = await llm.start();\n\n if (patchEnv !== false) {\n origOpenaiUrl = process.env.OPENAI_BASE_URL;\n origAnthropicUrl = process.env.ANTHROPIC_BASE_URL;\n process.env.OPENAI_BASE_URL = `${url}/v1`;\n process.env.ANTHROPIC_BASE_URL = `${url}/v1`;\n }\n\n handle = { llm, url };\n });\n\n beforeEach(() => {\n if (handle) {\n handle.llm.closeLiveSessions();\n handle.llm.resetMatchCounts();\n }\n });\n\n afterEach(() => {\n handle?.llm.closeLiveSessions();\n });\n\n afterAll(async () => {\n if (handle) {\n if (options.patchEnv !== false) {\n if (origOpenaiUrl !== undefined) process.env.OPENAI_BASE_URL = origOpenaiUrl;\n else delete process.env.OPENAI_BASE_URL;\n if (origAnthropicUrl !== undefined) process.env.ANTHROPIC_BASE_URL = origAnthropicUrl;\n else delete process.env.ANTHROPIC_BASE_URL;\n }\n await handle.llm.stop();\n handle = null;\n }\n });\n\n return () => {\n if (!handle) {\n throw new Error(\"useAimock(): server not started — are you calling this inside a test?\");\n }\n return handle;\n };\n}\n\nfunction loadFixtures(fixturePath: string, liveOptions?: LiveOptions): Fixture[] {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n return loadFixturesFromDir(fixturePath, undefined, liveOptions);\n }\n return loadFixtureFile(fixturePath, undefined, liveOptions);\n } catch (err) {\n console.warn(\n `[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return [];\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;CAClC,IAAI;CACJ,IAAI;AAEJ,WAAU,YAAY;EACpB,MAAM,EAAE,UAAU,aAAa,UAAU,GAAG,eAAe;EAC3D,MAAM,MAAM,IAAIA,sBAAO,WAAW;AAElC,MAAI,aAAa;GAEf,MAAM,iBAAiB,oCADE,YAAY,EACS,QAAQ,KAAK;AAC3D,QAAK,MAAM,KAAK,eACd,KAAI,WAAW,EAAE;;EAIrB,MAAM,MAAM,MAAM,IAAI,OAAO;AAE7B,MAAI,aAAa,OAAO;AACtB,mBAAgB,QAAQ,IAAI;AAC5B,sBAAmB,QAAQ,IAAI;AAC/B,WAAQ,IAAI,kBAAkB,GAAG,IAAI;AACrC,WAAQ,IAAI,qBAAqB,GAAG,IAAI;;AAG1C,WAAS;GAAE;GAAK;GAAK;GACrB;AAEF,kBAAiB;AACf,MAAI,QAAQ;AACV,UAAO,IAAI,mBAAmB;AAC9B,UAAO,IAAI,kBAAkB;;GAE/B;AAEF,iBAAgB;AACd,UAAQ,IAAI,mBAAmB;GAC/B;AAEF,UAAS,YAAY;AACnB,MAAI,QAAQ;AACV,OAAI,QAAQ,aAAa,OAAO;AAC9B,QAAI,kBAAkB,OAAW,SAAQ,IAAI,kBAAkB;QAC1D,QAAO,QAAQ,IAAI;AACxB,QAAI,qBAAqB,OAAW,SAAQ,IAAI,qBAAqB;QAChE,QAAO,QAAQ,IAAI;;AAE1B,SAAM,OAAO,IAAI,MAAM;AACvB,YAAS;;GAEX;AAEF,cAAa;AACX,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,wEAAwE;AAE1F,SAAO;;;AAIX,SAAS,aAAa,aAAqB,aAAsC;AAC/E,KAAI;AAEF,4BADsB,YAAY,CACzB,aAAa,CACpB,QAAOC,2CAAoB,aAAa,QAAW,YAAY;AAEjE,SAAOC,uCAAgB,aAAa,QAAW,YAAY;UACpD,KAAK;AACZ,UAAQ,KACN,yCAAyC,YAAY,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC1G;AACD,SAAO,EAAE"}
package/dist/jest.d.cts CHANGED
@@ -19,12 +19,13 @@ interface AimockHandle {
19
19
  * Start an aimock server for the duration of the test suite.
20
20
  *
21
21
  * - `beforeAll`: starts the server and optionally loads fixtures
22
- * - `beforeEach`: resets fixture match counts (not fixtures themselves)
22
+ * - `beforeEach`: closes Live sessions and resets fixture match counts (not fixtures)
23
+ * - `afterEach`: closes Live sessions owned by this helper's server
23
24
  * - `afterAll`: stops the server
24
25
  *
25
26
  * Returns a getter function — call it inside tests to access the handle.
26
27
  *
27
- * NOTE: Jest globals (beforeAll, afterAll, beforeEach) must be available
28
+ * NOTE: Jest globals (beforeAll, afterAll, beforeEach, afterEach) must be available
28
29
  * in the test environment. This works with the default jest configuration.
29
30
  */
30
31
  declare function useAimock(options?: UseAimockOptions): () => AimockHandle;
@@ -1 +1 @@
1
- {"version":3,"file":"jest.d.cts","names":[],"sources":["../src/jest.ts"],"sourcesContent":[],"mappings":";;;;;UA0BiB,gBAAA,SAAyB;;;;;;UAOzB,YAAA;;gBAED;;;;;;;;;;;;;;;;iBAiBA,SAAA,WAAmB,yBAA8B"}
1
+ {"version":3,"file":"jest.d.cts","names":[],"sources":["../src/jest.ts"],"sourcesContent":[],"mappings":";;;;;UA4BiB,gBAAA,SAAyB;;;;;;UAOzB,YAAA;;gBAED;;;;;;;;;;;;;;;;;iBAkBA,SAAA,WAAmB,yBAA8B"}
package/dist/jest.d.ts CHANGED
@@ -19,12 +19,13 @@ interface AimockHandle {
19
19
  * Start an aimock server for the duration of the test suite.
20
20
  *
21
21
  * - `beforeAll`: starts the server and optionally loads fixtures
22
- * - `beforeEach`: resets fixture match counts (not fixtures themselves)
22
+ * - `beforeEach`: closes Live sessions and resets fixture match counts (not fixtures)
23
+ * - `afterEach`: closes Live sessions owned by this helper's server
23
24
  * - `afterAll`: stops the server
24
25
  *
25
26
  * Returns a getter function — call it inside tests to access the handle.
26
27
  *
27
- * NOTE: Jest globals (beforeAll, afterAll, beforeEach) must be available
28
+ * NOTE: Jest globals (beforeAll, afterAll, beforeEach, afterEach) must be available
28
29
  * in the test environment. This works with the default jest configuration.
29
30
  */
30
31
  declare function useAimock(options?: UseAimockOptions): () => AimockHandle;
@@ -1 +1 @@
1
- {"version":3,"file":"jest.d.ts","names":[],"sources":["../src/jest.ts"],"sourcesContent":[],"mappings":";;;;;UA0BiB,gBAAA,SAAyB;;;;;;UAOzB,YAAA;;gBAED;;;;;;;;;;;;;;;;iBAiBA,SAAA,WAAmB,yBAA8B"}
1
+ {"version":3,"file":"jest.d.ts","names":[],"sources":["../src/jest.ts"],"sourcesContent":[],"mappings":";;;;;UA4BiB,gBAAA,SAAyB;;;;;;UAOzB,YAAA;;gBAED;;;;;;;;;;;;;;;;;iBAkBA,SAAA,WAAmB,yBAA8B"}
package/dist/jest.js CHANGED
@@ -8,12 +8,13 @@ import { resolve } from "node:path";
8
8
  * Start an aimock server for the duration of the test suite.
9
9
  *
10
10
  * - `beforeAll`: starts the server and optionally loads fixtures
11
- * - `beforeEach`: resets fixture match counts (not fixtures themselves)
11
+ * - `beforeEach`: closes Live sessions and resets fixture match counts (not fixtures)
12
+ * - `afterEach`: closes Live sessions owned by this helper's server
12
13
  * - `afterAll`: stops the server
13
14
  *
14
15
  * Returns a getter function — call it inside tests to access the handle.
15
16
  *
16
- * NOTE: Jest globals (beforeAll, afterAll, beforeEach) must be available
17
+ * NOTE: Jest globals (beforeAll, afterAll, beforeEach, afterEach) must be available
17
18
  * in the test environment. This works with the default jest configuration.
18
19
  */
19
20
  function useAimock(options = {}) {
@@ -24,7 +25,7 @@ function useAimock(options = {}) {
24
25
  const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;
25
26
  const llm = new LLMock(serverOpts);
26
27
  if (fixturePath) {
27
- const loadedFixtures = loadFixtures(resolve(fixturePath));
28
+ const loadedFixtures = loadFixtures(resolve(fixturePath), options.live);
28
29
  for (const f of loadedFixtures) llm.addFixture(f);
29
30
  }
30
31
  const url = await llm.start();
@@ -40,7 +41,13 @@ function useAimock(options = {}) {
40
41
  };
41
42
  });
42
43
  beforeEach(() => {
43
- if (handle) handle.llm.resetMatchCounts();
44
+ if (handle) {
45
+ handle.llm.closeLiveSessions();
46
+ handle.llm.resetMatchCounts();
47
+ }
48
+ });
49
+ afterEach(() => {
50
+ handle?.llm.closeLiveSessions();
44
51
  });
45
52
  afterAll(async () => {
46
53
  if (handle) {
@@ -59,10 +66,10 @@ function useAimock(options = {}) {
59
66
  return handle;
60
67
  };
61
68
  }
62
- function loadFixtures(fixturePath) {
69
+ function loadFixtures(fixturePath, liveOptions) {
63
70
  try {
64
- if (statSync(fixturePath).isDirectory()) return loadFixturesFromDir(fixturePath);
65
- return loadFixtureFile(fixturePath);
71
+ if (statSync(fixturePath).isDirectory()) return loadFixturesFromDir(fixturePath, void 0, liveOptions);
72
+ return loadFixtureFile(fixturePath, void 0, liveOptions);
66
73
  } catch (err) {
67
74
  console.warn(`[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`);
68
75
  return [];
package/dist/jest.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jest.js","names":[],"sources":["../src/jest.ts"],"sourcesContent":["/**\n * Jest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/jest\";\n *\n * const mock = useAimock({ fixtures: \"./fixtures\" });\n *\n * it(\"responds\", async () => {\n * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });\n * });\n */\n\n/* eslint-disable no-var */\n// Jest globals — available at runtime in jest test files\ndeclare var beforeAll: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var afterAll: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var beforeEach: (fn: () => Promise<void> | void, timeout?: number) => void;\n/* eslint-enable no-var */\n\nimport { LLMock } from \"./llmock.js\";\nimport { loadFixtureFile, loadFixturesFromDir } from \"./fixture-loader.js\";\nimport type { Fixture, MockServerOptions } from \"./types.js\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface UseAimockOptions extends MockServerOptions {\n /** Path to fixture file or directory. Loaded automatically on start. */\n fixtures?: string;\n /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */\n patchEnv?: boolean;\n}\n\nexport interface AimockHandle {\n /** The LLMock instance. */\n readonly llm: LLMock;\n /** The server URL (e.g., http://127.0.0.1:4010). */\n readonly url: string;\n}\n\n/**\n * Start an aimock server for the duration of the test suite.\n *\n * - `beforeAll`: starts the server and optionally loads fixtures\n * - `beforeEach`: resets fixture match counts (not fixtures themselves)\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n *\n * NOTE: Jest globals (beforeAll, afterAll, beforeEach) must be available\n * in the test environment. This works with the default jest configuration.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockHandle {\n let handle: AimockHandle | null = null;\n let origOpenaiUrl: string | undefined;\n let origAnthropicUrl: string | undefined;\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n const resolved = resolve(fixturePath);\n const loadedFixtures = loadFixtures(resolved);\n for (const f of loadedFixtures) {\n llm.addFixture(f);\n }\n }\n\n const url = await llm.start();\n\n if (patchEnv !== false) {\n origOpenaiUrl = process.env.OPENAI_BASE_URL;\n origAnthropicUrl = process.env.ANTHROPIC_BASE_URL;\n process.env.OPENAI_BASE_URL = `${url}/v1`;\n process.env.ANTHROPIC_BASE_URL = `${url}/v1`;\n }\n\n handle = { llm, url };\n });\n\n beforeEach(() => {\n if (handle) {\n handle.llm.resetMatchCounts();\n }\n });\n\n afterAll(async () => {\n if (handle) {\n if (options.patchEnv !== false) {\n if (origOpenaiUrl !== undefined) process.env.OPENAI_BASE_URL = origOpenaiUrl;\n else delete process.env.OPENAI_BASE_URL;\n if (origAnthropicUrl !== undefined) process.env.ANTHROPIC_BASE_URL = origAnthropicUrl;\n else delete process.env.ANTHROPIC_BASE_URL;\n }\n await handle.llm.stop();\n handle = null;\n }\n });\n\n return () => {\n if (!handle) {\n throw new Error(\"useAimock(): server not started — are you calling this inside a test?\");\n }\n return handle;\n };\n}\n\nfunction loadFixtures(fixturePath: string): Fixture[] {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n return loadFixturesFromDir(fixturePath);\n }\n return loadFixtureFile(fixturePath);\n } catch (err) {\n console.warn(\n `[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return [];\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;AAoDA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;CAClC,IAAI;CACJ,IAAI;AAEJ,WAAU,YAAY;EACpB,MAAM,EAAE,UAAU,aAAa,UAAU,GAAG,eAAe;EAC3D,MAAM,MAAM,IAAI,OAAO,WAAW;AAElC,MAAI,aAAa;GAEf,MAAM,iBAAiB,aADN,QAAQ,YAAY,CACQ;AAC7C,QAAK,MAAM,KAAK,eACd,KAAI,WAAW,EAAE;;EAIrB,MAAM,MAAM,MAAM,IAAI,OAAO;AAE7B,MAAI,aAAa,OAAO;AACtB,mBAAgB,QAAQ,IAAI;AAC5B,sBAAmB,QAAQ,IAAI;AAC/B,WAAQ,IAAI,kBAAkB,GAAG,IAAI;AACrC,WAAQ,IAAI,qBAAqB,GAAG,IAAI;;AAG1C,WAAS;GAAE;GAAK;GAAK;GACrB;AAEF,kBAAiB;AACf,MAAI,OACF,QAAO,IAAI,kBAAkB;GAE/B;AAEF,UAAS,YAAY;AACnB,MAAI,QAAQ;AACV,OAAI,QAAQ,aAAa,OAAO;AAC9B,QAAI,kBAAkB,OAAW,SAAQ,IAAI,kBAAkB;QAC1D,QAAO,QAAQ,IAAI;AACxB,QAAI,qBAAqB,OAAW,SAAQ,IAAI,qBAAqB;QAChE,QAAO,QAAQ,IAAI;;AAE1B,SAAM,OAAO,IAAI,MAAM;AACvB,YAAS;;GAEX;AAEF,cAAa;AACX,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,wEAAwE;AAE1F,SAAO;;;AAIX,SAAS,aAAa,aAAgC;AACpD,KAAI;AAEF,MADa,SAAS,YAAY,CACzB,aAAa,CACpB,QAAO,oBAAoB,YAAY;AAEzC,SAAO,gBAAgB,YAAY;UAC5B,KAAK;AACZ,UAAQ,KACN,yCAAyC,YAAY,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC1G;AACD,SAAO,EAAE"}
1
+ {"version":3,"file":"jest.js","names":[],"sources":["../src/jest.ts"],"sourcesContent":["/**\n * Jest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/jest\";\n *\n * const mock = useAimock({ fixtures: \"./fixtures\" });\n *\n * it(\"responds\", async () => {\n * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });\n * });\n */\n\n/* eslint-disable no-var */\n// Jest globals — available at runtime in jest test files\ndeclare var beforeAll: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var afterAll: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var beforeEach: (fn: () => Promise<void> | void, timeout?: number) => void;\ndeclare var afterEach: (fn: () => Promise<void> | void, timeout?: number) => void;\n/* eslint-enable no-var */\n\nimport { LLMock } from \"./llmock.js\";\nimport { loadFixtureFile, loadFixturesFromDir } from \"./fixture-loader.js\";\nimport type { LiveOptions } from \"./live-types.js\";\nimport type { Fixture, MockServerOptions } from \"./types.js\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\n\nexport interface UseAimockOptions extends MockServerOptions {\n /** Path to fixture file or directory. Loaded automatically on start. */\n fixtures?: string;\n /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */\n patchEnv?: boolean;\n}\n\nexport interface AimockHandle {\n /** The LLMock instance. */\n readonly llm: LLMock;\n /** The server URL (e.g., http://127.0.0.1:4010). */\n readonly url: string;\n}\n\n/**\n * Start an aimock server for the duration of the test suite.\n *\n * - `beforeAll`: starts the server and optionally loads fixtures\n * - `beforeEach`: closes Live sessions and resets fixture match counts (not fixtures)\n * - `afterEach`: closes Live sessions owned by this helper's server\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n *\n * NOTE: Jest globals (beforeAll, afterAll, beforeEach, afterEach) must be available\n * in the test environment. This works with the default jest configuration.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockHandle {\n let handle: AimockHandle | null = null;\n let origOpenaiUrl: string | undefined;\n let origAnthropicUrl: string | undefined;\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n const resolved = resolve(fixturePath);\n const loadedFixtures = loadFixtures(resolved, options.live);\n for (const f of loadedFixtures) {\n llm.addFixture(f);\n }\n }\n\n const url = await llm.start();\n\n if (patchEnv !== false) {\n origOpenaiUrl = process.env.OPENAI_BASE_URL;\n origAnthropicUrl = process.env.ANTHROPIC_BASE_URL;\n process.env.OPENAI_BASE_URL = `${url}/v1`;\n process.env.ANTHROPIC_BASE_URL = `${url}/v1`;\n }\n\n handle = { llm, url };\n });\n\n beforeEach(() => {\n if (handle) {\n handle.llm.closeLiveSessions();\n handle.llm.resetMatchCounts();\n }\n });\n\n afterEach(() => {\n handle?.llm.closeLiveSessions();\n });\n\n afterAll(async () => {\n if (handle) {\n if (options.patchEnv !== false) {\n if (origOpenaiUrl !== undefined) process.env.OPENAI_BASE_URL = origOpenaiUrl;\n else delete process.env.OPENAI_BASE_URL;\n if (origAnthropicUrl !== undefined) process.env.ANTHROPIC_BASE_URL = origAnthropicUrl;\n else delete process.env.ANTHROPIC_BASE_URL;\n }\n await handle.llm.stop();\n handle = null;\n }\n });\n\n return () => {\n if (!handle) {\n throw new Error(\"useAimock(): server not started — are you calling this inside a test?\");\n }\n return handle;\n };\n}\n\nfunction loadFixtures(fixturePath: string, liveOptions?: LiveOptions): Fixture[] {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n return loadFixturesFromDir(fixturePath, undefined, liveOptions);\n }\n return loadFixtureFile(fixturePath, undefined, liveOptions);\n } catch (err) {\n console.warn(\n `[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n return [];\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAuDA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;CAClC,IAAI;CACJ,IAAI;AAEJ,WAAU,YAAY;EACpB,MAAM,EAAE,UAAU,aAAa,UAAU,GAAG,eAAe;EAC3D,MAAM,MAAM,IAAI,OAAO,WAAW;AAElC,MAAI,aAAa;GAEf,MAAM,iBAAiB,aADN,QAAQ,YAAY,EACS,QAAQ,KAAK;AAC3D,QAAK,MAAM,KAAK,eACd,KAAI,WAAW,EAAE;;EAIrB,MAAM,MAAM,MAAM,IAAI,OAAO;AAE7B,MAAI,aAAa,OAAO;AACtB,mBAAgB,QAAQ,IAAI;AAC5B,sBAAmB,QAAQ,IAAI;AAC/B,WAAQ,IAAI,kBAAkB,GAAG,IAAI;AACrC,WAAQ,IAAI,qBAAqB,GAAG,IAAI;;AAG1C,WAAS;GAAE;GAAK;GAAK;GACrB;AAEF,kBAAiB;AACf,MAAI,QAAQ;AACV,UAAO,IAAI,mBAAmB;AAC9B,UAAO,IAAI,kBAAkB;;GAE/B;AAEF,iBAAgB;AACd,UAAQ,IAAI,mBAAmB;GAC/B;AAEF,UAAS,YAAY;AACnB,MAAI,QAAQ;AACV,OAAI,QAAQ,aAAa,OAAO;AAC9B,QAAI,kBAAkB,OAAW,SAAQ,IAAI,kBAAkB;QAC1D,QAAO,QAAQ,IAAI;AACxB,QAAI,qBAAqB,OAAW,SAAQ,IAAI,qBAAqB;QAChE,QAAO,QAAQ,IAAI;;AAE1B,SAAM,OAAO,IAAI,MAAM;AACvB,YAAS;;GAEX;AAEF,cAAa;AACX,MAAI,CAAC,OACH,OAAM,IAAI,MAAM,wEAAwE;AAE1F,SAAO;;;AAIX,SAAS,aAAa,aAAqB,aAAsC;AAC/E,KAAI;AAEF,MADa,SAAS,YAAY,CACzB,aAAa,CACpB,QAAO,oBAAoB,aAAa,QAAW,YAAY;AAEjE,SAAO,gBAAgB,aAAa,QAAW,YAAY;UACpD,KAAK;AACZ,UAAQ,KACN,yCAAyC,YAAY,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC1G;AACD,SAAO,EAAE"}
package/dist/journal.cjs CHANGED
@@ -36,6 +36,23 @@ function capBody(body) {
36
36
  };
37
37
  }
38
38
  /**
39
+ * Narrow a journaled body to a chat-completion request.
40
+ *
41
+ * `JournalEntry.body` is a union (see {@link JournalBody}): most entries hold a
42
+ * chat request, but the fine-tuning create payload and this module's own
43
+ * truncation marker do not. Anything reading `messages`, `model` or the rest of
44
+ * the chat shape off a journal entry — inside aimock or in a consumer's test
45
+ * — goes through here, so an entry that is not a chat request fails as a
46
+ * `false` it can assert on rather than as an undefined property three accesses
47
+ * later.
48
+ *
49
+ * `messages` is the discriminator because it is the one required field no
50
+ * other journaled shape carries.
51
+ */
52
+ function isChatCompletionBody(body) {
53
+ return body !== null && body !== void 0 && Array.isArray(body.messages) && typeof body.model === "string";
54
+ }
55
+ /**
39
56
  * Compare two field values, handling RegExp by source+flags rather than reference.
40
57
  */
41
58
  function fieldEqual(a, b) {
@@ -59,14 +76,17 @@ function systemMessageEqual(a, b) {
59
76
  * (ignoring sequenceIndex). Used to group sequenced fixtures.
60
77
  */
61
78
  function matchCriteriaEqual(a, b) {
62
- return fieldEqual(a.userMessage, b.userMessage) && systemMessageEqual(a.systemMessage, b.systemMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint) && fieldEqual(a.turnIndex, b.turnIndex) && fieldEqual(a.hasToolResult, b.hasToolResult);
79
+ return fieldEqual(a.userMessage, b.userMessage) && systemMessageEqual(a.systemMessage, b.systemMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint) && fieldEqual(a.turnIndex, b.turnIndex) && fieldEqual(a.hasToolResult, b.hasToolResult) && fieldEqual(a.toolResultContains, b.toolResultContains) && fieldEqual(a.context, b.context);
63
80
  }
64
81
  var Journal = class {
65
82
  entries = [];
83
+ matchedFixtures = /* @__PURE__ */ new WeakMap();
66
84
  fixtureMatchCountsByTestId = /* @__PURE__ */ new Map();
67
85
  maxEntries;
68
86
  fixtureCountsMaxTestIds;
87
+ onAdd;
69
88
  constructor(options = {}) {
89
+ this.onAdd = options.onAdd;
70
90
  const cap = options.maxEntries;
71
91
  this.maxEntries = cap !== void 0 && cap > 0 ? cap : 0;
72
92
  const testIdCap = options.fixtureCountsMaxTestIds;
@@ -76,26 +96,42 @@ var Journal = class {
76
96
  get fixtureMatchCounts() {
77
97
  return this.getFixtureMatchCountsForTest(require_constants.DEFAULT_TEST_ID);
78
98
  }
79
- add(entry) {
99
+ /** @internal matchedFixture preserves identity when Live supplies safe diagnostics. */
100
+ add(entry, matchedFixture) {
80
101
  const full = {
81
102
  id: require_helpers.generateId("req"),
82
103
  timestamp: Date.now(),
83
104
  ...entry,
84
105
  body: capBody(entry.body)
85
106
  };
107
+ if (matchedFixture) this.matchedFixtures.set(full, matchedFixture);
86
108
  this.entries.push(full);
87
109
  if (this.maxEntries > 0 && this.entries.length > this.maxEntries) this.entries.shift();
110
+ this.onAdd?.(full);
88
111
  return full;
89
112
  }
113
+ /**
114
+ * Return every entry, or with `limit` the most recent `limit` entries.
115
+ * `0` returns `[]` — exactly what `GET /__aimock/journal?limit=0` answers.
116
+ * `slice(-0)` is `slice(0)`, so a `0` used to return the ENTIRE journal; it
117
+ * is special-cased so the library and the HTTP route agree. This is a public
118
+ * library API, so odd limits are tolerated rather than thrown on: a
119
+ * non-finite limit (`NaN`, `Infinity`) means "no limit", a fractional limit
120
+ * floors, and any limit at or below `0` asks for nothing.
121
+ */
90
122
  getAll(opts) {
91
- if (opts?.limit !== void 0) return this.entries.slice(-opts.limit);
123
+ if (opts?.limit !== void 0 && Number.isFinite(opts.limit)) {
124
+ const limit = Math.floor(opts.limit);
125
+ if (limit <= 0) return [];
126
+ return this.entries.slice(-limit);
127
+ }
92
128
  return this.entries.slice();
93
129
  }
94
130
  getLast() {
95
131
  return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;
96
132
  }
97
133
  findByFixture(fixture) {
98
- return this.entries.filter((e) => e.response.fixture === fixture);
134
+ return this.entries.filter((e) => (this.matchedFixtures.get(e) ?? e.response.fixture) === fixture);
99
135
  }
100
136
  /**
101
137
  * READ-ONLY accessor. Returns the existing count map for `testId`, or an
@@ -162,4 +198,5 @@ var Journal = class {
162
198
 
163
199
  //#endregion
164
200
  exports.Journal = Journal;
201
+ exports.isChatCompletionBody = isChatCompletionBody;
165
202
  //# sourceMappingURL=journal.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"journal.cjs","names":["DEFAULT_TEST_ID","generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { ChatCompletionRequest, Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Maximum UTF-8 byte length of a serialized request body retained in a\n * journal entry. Bodies whose JSON serialization exceeds this byte size are\n * replaced with a truncation marker.\n *\n * Only the request `body` field is capped here. `headers` and `path` are\n * retained uncapped, but they are naturally bounded by Node's default ~16 KB\n * maximum HTTP header size, so the per-entry overhead beyond the body cap\n * remains small. Combined with the journal's maxEntries limit (default 1000),\n * the total `JSON.stringify(journal.getAll())` output stays well under V8's\n * ~512 MB string limit even at maximum capacity.\n */\nconst JOURNAL_BODY_CAP_BYTES = 64 * 1024; // 64 KB\n\n/**\n * If `body` serializes to more than JOURNAL_BODY_CAP_BYTES (in UTF-8 bytes),\n * replace it with a truncation marker. Returns the original body when within\n * the cap, or null when `body` is null.\n *\n * The gate uses Buffer.byteLength (UTF-8 bytes) rather than .length (UTF-16\n * code units) to match the constant name and to correctly cap multibyte\n * content (e.g. CJK/emoji) whose code-unit count falls under the threshold\n * but whose byte size does not.\n */\nfunction capBody(body: ChatCompletionRequest | null): ChatCompletionRequest | null {\n if (body === null) return null;\n const serialized = JSON.stringify(body);\n if (Buffer.byteLength(serialized, \"utf8\") <= JOURNAL_BODY_CAP_BYTES) return body;\n // Cast: the marker is not a real ChatCompletionRequest, but the field type\n // is already nullable-union and downstream consumers (e.g. GET /journal)\n // treat the body as opaque JSON — the truncation marker is safe to store.\n return {\n __aimock_truncated: true,\n originalByteSize: Buffer.byteLength(serialized, \"utf8\"),\n note: \"body truncated by aimock journal cap (64 KB limit)\",\n } as unknown as ChatCompletionRequest;\n}\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n a: string | string[] | RegExp | undefined,\n b: string | string[] | RegExp | undefined,\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => v === b[i]);\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n systemMessageEqual(a.systemMessage, b.systemMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n body: capBody(entry.body),\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n /**\n * Clear ONLY the request journal entries, preserving fixture match-counts.\n * Match-counts are fixture-matching/sequencing state, not journal data, so\n * clearing the journal must not silently rewind sequenced fixtures. Used by\n * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),\n * use `clear()` instead.\n */\n clearEntries(): void {\n this.entries = [];\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"journal.cjs","names":["DEFAULT_TEST_ID","generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type {\n ChatCompletionRequest,\n Fixture,\n FixtureMatch,\n JournalBody,\n JournalEntry,\n} from \"./types.js\";\nimport { DEFAULT_TEST_ID } from \"./constants.js\";\nexport { DEFAULT_TEST_ID } from \"./constants.js\";\n\n/**\n * Maximum UTF-8 byte length of a serialized request body retained in a\n * journal entry. Bodies whose JSON serialization exceeds this byte size are\n * replaced with a truncation marker.\n *\n * Only the request `body` field is capped here. `headers` and `path` are\n * retained uncapped, but they are naturally bounded by Node's default ~16 KB\n * maximum HTTP header size, so the per-entry overhead beyond the body cap\n * remains small. Combined with the journal's maxEntries limit (default 1000),\n * the total `JSON.stringify(journal.getAll())` output stays well under V8's\n * ~512 MB string limit even at maximum capacity.\n */\nconst JOURNAL_BODY_CAP_BYTES = 64 * 1024; // 64 KB\n\n/**\n * If `body` serializes to more than JOURNAL_BODY_CAP_BYTES (in UTF-8 bytes),\n * replace it with a truncation marker. Returns the original body when within\n * the cap, or null when `body` is null.\n *\n * The gate uses Buffer.byteLength (UTF-8 bytes) rather than .length (UTF-16\n * code units) to match the constant name and to correctly cap multibyte\n * content (e.g. CJK/emoji) whose code-unit count falls under the threshold\n * but whose byte size does not.\n */\nfunction capBody(body: JournalBody | null): JournalBody | null {\n if (body === null) return null;\n const serialized = JSON.stringify(body);\n if (Buffer.byteLength(serialized, \"utf8\") <= JOURNAL_BODY_CAP_BYTES) return body;\n // The marker is not a chat request, and `JournalBody` no longer claims it is\n // — downstream consumers (e.g. GET /journal) treat the body as opaque JSON.\n return {\n __aimock_truncated: true,\n originalByteSize: Buffer.byteLength(serialized, \"utf8\"),\n note: \"body truncated by aimock journal cap (64 KB limit)\",\n };\n}\n\n/**\n * Narrow a journaled body to a chat-completion request.\n *\n * `JournalEntry.body` is a union (see {@link JournalBody}): most entries hold a\n * chat request, but the fine-tuning create payload and this module's own\n * truncation marker do not. Anything reading `messages`, `model` or the rest of\n * the chat shape off a journal entry — inside aimock or in a consumer's test\n * — goes through here, so an entry that is not a chat request fails as a\n * `false` it can assert on rather than as an undefined property three accesses\n * later.\n *\n * `messages` is the discriminator because it is the one required field no\n * other journaled shape carries.\n */\nexport function isChatCompletionBody(\n body: JournalBody | null | undefined,\n): body is ChatCompletionRequest {\n return (\n body !== null &&\n body !== undefined &&\n Array.isArray((body as { messages?: unknown }).messages) &&\n typeof (body as { model?: unknown }).model === \"string\"\n );\n}\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Compare two systemMessage values. Handles string, string[], and RegExp.\n * Both-undefined is treated as equal.\n */\nfunction systemMessageEqual(\n a: string | string[] | RegExp | undefined,\n b: string | string[] | RegExp | undefined,\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n if (typeof a === \"string\" && typeof b === \"string\") return a === b;\n if (Array.isArray(a) && Array.isArray(b))\n return a.length === b.length && a.every((v, i) => v === b[i]);\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return false;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n systemMessageEqual(a.systemMessage, b.systemMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate) &&\n fieldEqual(a.endpoint, b.endpoint) &&\n fieldEqual(a.turnIndex, b.turnIndex) &&\n fieldEqual(a.hasToolResult, b.hasToolResult) &&\n fieldEqual(a.toolResultContains, b.toolResultContains) &&\n fieldEqual(a.context, b.context)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n /**\n * Called with every entry {@link Journal.add} creates, right after it is\n * recorded. The hook exists so an embedder can attribute an entry to the\n * request that produced it WITHOUT touching each of the ~100 `journal.add`\n * call sites spread across the handler modules: the server binds the entry\n * to the in-flight `IncomingMessage` here, and its error arm then amends\n * that exact object rather than guessing from a caller-supplied header.\n *\n * The entry is already recorded when the hook runs, so a hook that throws\n * does not lose the write — but it does propagate out of `add` into the\n * caller's error path, so a hook must not throw.\n */\n onAdd?: (entry: JournalEntry) => void;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly matchedFixtures = new WeakMap<JournalEntry, Fixture>();\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n private readonly onAdd?: (entry: JournalEntry) => void;\n\n constructor(options: JournalOptions = {}) {\n this.onAdd = options.onAdd;\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n /** @internal matchedFixture preserves identity when Live supplies safe diagnostics. */\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">, matchedFixture?: Fixture): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n body: capBody(entry.body),\n };\n if (matchedFixture) this.matchedFixtures.set(full, matchedFixture);\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n this.onAdd?.(full);\n return full;\n }\n\n /**\n * Return every entry, or with `limit` the most recent `limit` entries.\n * `0` returns `[]` — exactly what `GET /__aimock/journal?limit=0` answers.\n * `slice(-0)` is `slice(0)`, so a `0` used to return the ENTIRE journal; it\n * is special-cased so the library and the HTTP route agree. This is a public\n * library API, so odd limits are tolerated rather than thrown on: a\n * non-finite limit (`NaN`, `Infinity`) means \"no limit\", a fractional limit\n * floors, and any limit at or below `0` asks for nothing.\n */\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined && Number.isFinite(opts.limit)) {\n const limit = Math.floor(opts.limit);\n if (limit <= 0) return [];\n return this.entries.slice(-limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter(\n (e) => (this.matchedFixtures.get(e) ?? e.response.fixture) === fixture,\n );\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n /**\n * Clear ONLY the request journal entries, preserving fixture match-counts.\n * Match-counts are fixture-matching/sequencing state, not journal data, so\n * clearing the journal must not silently rewind sequenced fixtures. Used by\n * `POST /__aimock/reset/journal`. For a full reset (entries + match-counts),\n * use `clear()` instead.\n */\n clearEntries(): void {\n this.entries = [];\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,23 @@
1
1
  import { DEFAULT_TEST_ID } from "./constants.cjs";
2
- import { Fixture, JournalEntry } from "./types.cjs";
2
+ import { ChatCompletionRequest, Fixture, JournalBody, JournalEntry } from "./types.cjs";
3
3
 
4
4
  //#region src/journal.d.ts
5
+
6
+ /**
7
+ * Narrow a journaled body to a chat-completion request.
8
+ *
9
+ * `JournalEntry.body` is a union (see {@link JournalBody}): most entries hold a
10
+ * chat request, but the fine-tuning create payload and this module's own
11
+ * truncation marker do not. Anything reading `messages`, `model` or the rest of
12
+ * the chat shape off a journal entry — inside aimock or in a consumer's test
13
+ * — goes through here, so an entry that is not a chat request fails as a
14
+ * `false` it can assert on rather than as an undefined property three accesses
15
+ * later.
16
+ *
17
+ * `messages` is the discriminator because it is the one required field no
18
+ * other journaled shape carries.
19
+ */
20
+ declare function isChatCompletionBody(body: JournalBody | null | undefined): body is ChatCompletionRequest;
5
21
  interface JournalOptions {
6
22
  /**
7
23
  * Maximum number of entries to retain. When exceeded, oldest entries are
@@ -26,16 +42,41 @@ interface JournalOptions {
26
42
  * servers that see many unique testIds.
27
43
  */
28
44
  fixtureCountsMaxTestIds?: number;
45
+ /**
46
+ * Called with every entry {@link Journal.add} creates, right after it is
47
+ * recorded. The hook exists so an embedder can attribute an entry to the
48
+ * request that produced it WITHOUT touching each of the ~100 `journal.add`
49
+ * call sites spread across the handler modules: the server binds the entry
50
+ * to the in-flight `IncomingMessage` here, and its error arm then amends
51
+ * that exact object rather than guessing from a caller-supplied header.
52
+ *
53
+ * The entry is already recorded when the hook runs, so a hook that throws
54
+ * does not lose the write — but it does propagate out of `add` into the
55
+ * caller's error path, so a hook must not throw.
56
+ */
57
+ onAdd?: (entry: JournalEntry) => void;
29
58
  }
30
59
  declare class Journal {
31
60
  private entries;
61
+ private readonly matchedFixtures;
32
62
  private readonly fixtureMatchCountsByTestId;
33
63
  private readonly maxEntries;
34
64
  private readonly fixtureCountsMaxTestIds;
65
+ private readonly onAdd?;
35
66
  constructor(options?: JournalOptions);
36
67
  /** Backwards-compatible accessor — returns the default (no testId) count map. */
37
68
  get fixtureMatchCounts(): Map<Fixture, number>;
38
- add(entry: Omit<JournalEntry, "id" | "timestamp">): JournalEntry;
69
+ /** @internal matchedFixture preserves identity when Live supplies safe diagnostics. */
70
+ add(entry: Omit<JournalEntry, "id" | "timestamp">, matchedFixture?: Fixture): JournalEntry;
71
+ /**
72
+ * Return every entry, or with `limit` the most recent `limit` entries.
73
+ * `0` returns `[]` — exactly what `GET /__aimock/journal?limit=0` answers.
74
+ * `slice(-0)` is `slice(0)`, so a `0` used to return the ENTIRE journal; it
75
+ * is special-cased so the library and the HTTP route agree. This is a public
76
+ * library API, so odd limits are tolerated rather than thrown on: a
77
+ * non-finite limit (`NaN`, `Infinity`) means "no limit", a fractional limit
78
+ * floors, and any limit at or below `0` asks for nothing.
79
+ */
39
80
  getAll(opts?: {
40
81
  limit?: number;
41
82
  }): JournalEntry[];
@@ -72,5 +113,5 @@ declare class Journal {
72
113
  }
73
114
  //# sourceMappingURL=journal.d.ts.map
74
115
  //#endregion
75
- export { Journal };
116
+ export { Journal, isChatCompletionBody };
76
117
  //# sourceMappingURL=journal.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"journal.d.cts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;UA0FiB,cAAA;;AAAjB;AA0BA;;;;;;;;;;YAkDyB,CAAA,EAAA,MAAA;;;;;;;;;;;;cAlDZ,OAAA;;;;;wBAMU;;4BAUK,IAAI;aAInB,KAAK,oCAAoC;;;MAmBjB;aAOxB;yBAIY,UAAU;;;;;;;;gDAWa,IAAI;;;;;;;;gCA+BpB;sCAKnB,gCACc"}
1
+ {"version":3,"file":"journal.d.cts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;;;;AA8DA;;;;;AA4DA;AAuCA;;;;;;AAwBa,iBA3HG,oBAAA,CA2HH,IAAA,EA1HL,WA0HK,GAAA,IAAA,GAAA,SAAA,CAAA,EAAA,IAAA,IAzHF,qBAyHE;AAAyD,UA/DrD,cAAA,CA+DqD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA3BpD;;cAGL,OAAA;;;;;;;wBAQU;;4BAWK,IAAI;;aAKnB,KAAK,oDAAoD,UAAU;;;;;;;;;;;;MA8B3C;aASxB;yBAIY,UAAU;;;;;;;;gDAaa,IAAI;;;;;;;;gCA+BpB;sCAKnB,gCACc"}
package/dist/journal.d.ts CHANGED
@@ -1,7 +1,23 @@
1
1
  import { DEFAULT_TEST_ID } from "./constants.js";
2
- import { Fixture, JournalEntry } from "./types.js";
2
+ import { ChatCompletionRequest, Fixture, JournalBody, JournalEntry } from "./types.js";
3
3
 
4
4
  //#region src/journal.d.ts
5
+
6
+ /**
7
+ * Narrow a journaled body to a chat-completion request.
8
+ *
9
+ * `JournalEntry.body` is a union (see {@link JournalBody}): most entries hold a
10
+ * chat request, but the fine-tuning create payload and this module's own
11
+ * truncation marker do not. Anything reading `messages`, `model` or the rest of
12
+ * the chat shape off a journal entry — inside aimock or in a consumer's test
13
+ * — goes through here, so an entry that is not a chat request fails as a
14
+ * `false` it can assert on rather than as an undefined property three accesses
15
+ * later.
16
+ *
17
+ * `messages` is the discriminator because it is the one required field no
18
+ * other journaled shape carries.
19
+ */
20
+ declare function isChatCompletionBody(body: JournalBody | null | undefined): body is ChatCompletionRequest;
5
21
  interface JournalOptions {
6
22
  /**
7
23
  * Maximum number of entries to retain. When exceeded, oldest entries are
@@ -26,16 +42,41 @@ interface JournalOptions {
26
42
  * servers that see many unique testIds.
27
43
  */
28
44
  fixtureCountsMaxTestIds?: number;
45
+ /**
46
+ * Called with every entry {@link Journal.add} creates, right after it is
47
+ * recorded. The hook exists so an embedder can attribute an entry to the
48
+ * request that produced it WITHOUT touching each of the ~100 `journal.add`
49
+ * call sites spread across the handler modules: the server binds the entry
50
+ * to the in-flight `IncomingMessage` here, and its error arm then amends
51
+ * that exact object rather than guessing from a caller-supplied header.
52
+ *
53
+ * The entry is already recorded when the hook runs, so a hook that throws
54
+ * does not lose the write — but it does propagate out of `add` into the
55
+ * caller's error path, so a hook must not throw.
56
+ */
57
+ onAdd?: (entry: JournalEntry) => void;
29
58
  }
30
59
  declare class Journal {
31
60
  private entries;
61
+ private readonly matchedFixtures;
32
62
  private readonly fixtureMatchCountsByTestId;
33
63
  private readonly maxEntries;
34
64
  private readonly fixtureCountsMaxTestIds;
65
+ private readonly onAdd?;
35
66
  constructor(options?: JournalOptions);
36
67
  /** Backwards-compatible accessor — returns the default (no testId) count map. */
37
68
  get fixtureMatchCounts(): Map<Fixture, number>;
38
- add(entry: Omit<JournalEntry, "id" | "timestamp">): JournalEntry;
69
+ /** @internal matchedFixture preserves identity when Live supplies safe diagnostics. */
70
+ add(entry: Omit<JournalEntry, "id" | "timestamp">, matchedFixture?: Fixture): JournalEntry;
71
+ /**
72
+ * Return every entry, or with `limit` the most recent `limit` entries.
73
+ * `0` returns `[]` — exactly what `GET /__aimock/journal?limit=0` answers.
74
+ * `slice(-0)` is `slice(0)`, so a `0` used to return the ENTIRE journal; it
75
+ * is special-cased so the library and the HTTP route agree. This is a public
76
+ * library API, so odd limits are tolerated rather than thrown on: a
77
+ * non-finite limit (`NaN`, `Infinity`) means "no limit", a fractional limit
78
+ * floors, and any limit at or below `0` asks for nothing.
79
+ */
39
80
  getAll(opts?: {
40
81
  limit?: number;
41
82
  }): JournalEntry[];
@@ -72,5 +113,5 @@ declare class Journal {
72
113
  }
73
114
  //# sourceMappingURL=journal.d.ts.map
74
115
  //#endregion
75
- export { Journal };
116
+ export { Journal, isChatCompletionBody };
76
117
  //# sourceMappingURL=journal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"journal.d.ts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;UA0FiB,cAAA;;AAAjB;AA0BA;;;;;;;;;;YAkDyB,CAAA,EAAA,MAAA;;;;;;;;;;;;cAlDZ,OAAA;;;;;wBAMU;;4BAUK,IAAI;aAInB,KAAK,oCAAoC;;;MAmBjB;aAOxB;yBAIY,UAAU;;;;;;;;gDAWa,IAAI;;;;;;;;gCA+BpB;sCAKnB,gCACc"}
1
+ {"version":3,"file":"journal.d.ts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;;;;AA8DA;;;;;AA4DA;AAuCA;;;;;;AAwBa,iBA3HG,oBAAA,CA2HH,IAAA,EA1HL,WA0HK,GAAA,IAAA,GAAA,SAAA,CAAA,EAAA,IAAA,IAzHF,qBAyHE;AAAyD,UA/DrD,cAAA,CA+DqD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBA3BpD;;cAGL,OAAA;;;;;;;wBAQU;;4BAWK,IAAI;;aAKnB,KAAK,oDAAoD,UAAU;;;;;;;;;;;;MA8B3C;aASxB;yBAIY,UAAU;;;;;;;;gDAaa,IAAI;;;;;;;;gCA+BpB;sCAKnB,gCACc"}
package/dist/journal.js CHANGED
@@ -36,6 +36,23 @@ function capBody(body) {
36
36
  };
37
37
  }
38
38
  /**
39
+ * Narrow a journaled body to a chat-completion request.
40
+ *
41
+ * `JournalEntry.body` is a union (see {@link JournalBody}): most entries hold a
42
+ * chat request, but the fine-tuning create payload and this module's own
43
+ * truncation marker do not. Anything reading `messages`, `model` or the rest of
44
+ * the chat shape off a journal entry — inside aimock or in a consumer's test
45
+ * — goes through here, so an entry that is not a chat request fails as a
46
+ * `false` it can assert on rather than as an undefined property three accesses
47
+ * later.
48
+ *
49
+ * `messages` is the discriminator because it is the one required field no
50
+ * other journaled shape carries.
51
+ */
52
+ function isChatCompletionBody(body) {
53
+ return body !== null && body !== void 0 && Array.isArray(body.messages) && typeof body.model === "string";
54
+ }
55
+ /**
39
56
  * Compare two field values, handling RegExp by source+flags rather than reference.
40
57
  */
41
58
  function fieldEqual(a, b) {
@@ -59,14 +76,17 @@ function systemMessageEqual(a, b) {
59
76
  * (ignoring sequenceIndex). Used to group sequenced fixtures.
60
77
  */
61
78
  function matchCriteriaEqual(a, b) {
62
- return fieldEqual(a.userMessage, b.userMessage) && systemMessageEqual(a.systemMessage, b.systemMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint) && fieldEqual(a.turnIndex, b.turnIndex) && fieldEqual(a.hasToolResult, b.hasToolResult);
79
+ return fieldEqual(a.userMessage, b.userMessage) && systemMessageEqual(a.systemMessage, b.systemMessage) && fieldEqual(a.inputText, b.inputText) && fieldEqual(a.toolCallId, b.toolCallId) && fieldEqual(a.toolName, b.toolName) && fieldEqual(a.model, b.model) && fieldEqual(a.responseFormat, b.responseFormat) && fieldEqual(a.predicate, b.predicate) && fieldEqual(a.endpoint, b.endpoint) && fieldEqual(a.turnIndex, b.turnIndex) && fieldEqual(a.hasToolResult, b.hasToolResult) && fieldEqual(a.toolResultContains, b.toolResultContains) && fieldEqual(a.context, b.context);
63
80
  }
64
81
  var Journal = class {
65
82
  entries = [];
83
+ matchedFixtures = /* @__PURE__ */ new WeakMap();
66
84
  fixtureMatchCountsByTestId = /* @__PURE__ */ new Map();
67
85
  maxEntries;
68
86
  fixtureCountsMaxTestIds;
87
+ onAdd;
69
88
  constructor(options = {}) {
89
+ this.onAdd = options.onAdd;
70
90
  const cap = options.maxEntries;
71
91
  this.maxEntries = cap !== void 0 && cap > 0 ? cap : 0;
72
92
  const testIdCap = options.fixtureCountsMaxTestIds;
@@ -76,26 +96,42 @@ var Journal = class {
76
96
  get fixtureMatchCounts() {
77
97
  return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);
78
98
  }
79
- add(entry) {
99
+ /** @internal matchedFixture preserves identity when Live supplies safe diagnostics. */
100
+ add(entry, matchedFixture) {
80
101
  const full = {
81
102
  id: generateId("req"),
82
103
  timestamp: Date.now(),
83
104
  ...entry,
84
105
  body: capBody(entry.body)
85
106
  };
107
+ if (matchedFixture) this.matchedFixtures.set(full, matchedFixture);
86
108
  this.entries.push(full);
87
109
  if (this.maxEntries > 0 && this.entries.length > this.maxEntries) this.entries.shift();
110
+ this.onAdd?.(full);
88
111
  return full;
89
112
  }
113
+ /**
114
+ * Return every entry, or with `limit` the most recent `limit` entries.
115
+ * `0` returns `[]` — exactly what `GET /__aimock/journal?limit=0` answers.
116
+ * `slice(-0)` is `slice(0)`, so a `0` used to return the ENTIRE journal; it
117
+ * is special-cased so the library and the HTTP route agree. This is a public
118
+ * library API, so odd limits are tolerated rather than thrown on: a
119
+ * non-finite limit (`NaN`, `Infinity`) means "no limit", a fractional limit
120
+ * floors, and any limit at or below `0` asks for nothing.
121
+ */
90
122
  getAll(opts) {
91
- if (opts?.limit !== void 0) return this.entries.slice(-opts.limit);
123
+ if (opts?.limit !== void 0 && Number.isFinite(opts.limit)) {
124
+ const limit = Math.floor(opts.limit);
125
+ if (limit <= 0) return [];
126
+ return this.entries.slice(-limit);
127
+ }
92
128
  return this.entries.slice();
93
129
  }
94
130
  getLast() {
95
131
  return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;
96
132
  }
97
133
  findByFixture(fixture) {
98
- return this.entries.filter((e) => e.response.fixture === fixture);
134
+ return this.entries.filter((e) => (this.matchedFixtures.get(e) ?? e.response.fixture) === fixture);
99
135
  }
100
136
  /**
101
137
  * READ-ONLY accessor. Returns the existing count map for `testId`, or an
@@ -161,5 +197,5 @@ var Journal = class {
161
197
  };
162
198
 
163
199
  //#endregion
164
- export { Journal };
200
+ export { Journal, isChatCompletionBody };
165
201
  //# sourceMappingURL=journal.js.map