@copilotkit/aimock 1.44.0 → 1.45.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 (272) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +31 -0
  4. package/README.md +6 -0
  5. package/dist/agui-types.d.ts.map +1 -1
  6. package/dist/aimock-cli.cjs +31 -5
  7. package/dist/aimock-cli.cjs.map +1 -1
  8. package/dist/aimock-cli.d.cts.map +1 -1
  9. package/dist/aimock-cli.d.ts.map +1 -1
  10. package/dist/aimock-cli.js +31 -5
  11. package/dist/aimock-cli.js.map +1 -1
  12. package/dist/bedrock-converse.cjs +312 -39
  13. package/dist/bedrock-converse.cjs.map +1 -1
  14. package/dist/bedrock-converse.d.cts.map +1 -1
  15. package/dist/bedrock-converse.d.ts.map +1 -1
  16. package/dist/bedrock-converse.js +313 -40
  17. package/dist/bedrock-converse.js.map +1 -1
  18. package/dist/bedrock.cjs +195 -42
  19. package/dist/bedrock.cjs.map +1 -1
  20. package/dist/bedrock.d.cts.map +1 -1
  21. package/dist/bedrock.d.ts.map +1 -1
  22. package/dist/bedrock.js +196 -43
  23. package/dist/bedrock.js.map +1 -1
  24. package/dist/chaos.cjs +1 -0
  25. package/dist/chaos.js +1 -1
  26. package/dist/cli.cjs +152 -5
  27. package/dist/cli.cjs.map +1 -1
  28. package/dist/cli.js +155 -8
  29. package/dist/cli.js.map +1 -1
  30. package/dist/cohere.cjs +132 -71
  31. package/dist/cohere.cjs.map +1 -1
  32. package/dist/cohere.d.cts.map +1 -1
  33. package/dist/cohere.d.ts.map +1 -1
  34. package/dist/cohere.js +133 -72
  35. package/dist/cohere.js.map +1 -1
  36. package/dist/config-loader.cjs +62 -4
  37. package/dist/config-loader.cjs.map +1 -1
  38. package/dist/config-loader.d.cts +37 -4
  39. package/dist/config-loader.d.cts.map +1 -1
  40. package/dist/config-loader.d.ts +37 -4
  41. package/dist/config-loader.d.ts.map +1 -1
  42. package/dist/config-loader.js +62 -4
  43. package/dist/config-loader.js.map +1 -1
  44. package/dist/fakes-plugin.cjs +131 -0
  45. package/dist/fakes-plugin.cjs.map +1 -0
  46. package/dist/fakes-plugin.d.cts +23 -0
  47. package/dist/fakes-plugin.d.cts.map +1 -0
  48. package/dist/fakes-plugin.d.ts +23 -0
  49. package/dist/fakes-plugin.d.ts.map +1 -0
  50. package/dist/fakes-plugin.js +127 -0
  51. package/dist/fakes-plugin.js.map +1 -0
  52. package/dist/fixture-loader-services.cjs +1 -1
  53. package/dist/fixture-loader-services.cjs.map +1 -1
  54. package/dist/fixture-loader-services.js +1 -1
  55. package/dist/fixture-loader-services.js.map +1 -1
  56. package/dist/fixture-loader.cjs +389 -27
  57. package/dist/fixture-loader.cjs.map +1 -1
  58. package/dist/fixture-loader.d.cts +47 -3
  59. package/dist/fixture-loader.d.cts.map +1 -1
  60. package/dist/fixture-loader.d.ts +47 -3
  61. package/dist/fixture-loader.d.ts.map +1 -1
  62. package/dist/fixture-loader.js +384 -29
  63. package/dist/fixture-loader.js.map +1 -1
  64. package/dist/gemini-interactions.cjs +250 -52
  65. package/dist/gemini-interactions.cjs.map +1 -1
  66. package/dist/gemini-interactions.d.cts.map +1 -1
  67. package/dist/gemini-interactions.d.ts.map +1 -1
  68. package/dist/gemini-interactions.js +251 -53
  69. package/dist/gemini-interactions.js.map +1 -1
  70. package/dist/gemini.cjs +226 -37
  71. package/dist/gemini.cjs.map +1 -1
  72. package/dist/gemini.d.cts.map +1 -1
  73. package/dist/gemini.d.ts.map +1 -1
  74. package/dist/gemini.js +227 -38
  75. package/dist/gemini.js.map +1 -1
  76. package/dist/helpers.cjs +881 -7
  77. package/dist/helpers.cjs.map +1 -1
  78. package/dist/helpers.d.cts +85 -4
  79. package/dist/helpers.d.cts.map +1 -1
  80. package/dist/helpers.d.ts +85 -4
  81. package/dist/helpers.d.ts.map +1 -1
  82. package/dist/helpers.js +839 -8
  83. package/dist/helpers.js.map +1 -1
  84. package/dist/index.cjs +9 -0
  85. package/dist/index.d.cts +7 -5
  86. package/dist/index.d.ts +7 -5
  87. package/dist/index.js +4 -3
  88. package/dist/jest.cjs +31 -7
  89. package/dist/jest.cjs.map +1 -1
  90. package/dist/jest.d.cts +43 -3
  91. package/dist/jest.d.cts.map +1 -1
  92. package/dist/jest.d.ts +43 -3
  93. package/dist/jest.d.ts.map +1 -1
  94. package/dist/jest.js +33 -9
  95. package/dist/jest.js.map +1 -1
  96. package/dist/journal.cjs +135 -13
  97. package/dist/journal.cjs.map +1 -1
  98. package/dist/journal.d.cts +38 -4
  99. package/dist/journal.d.cts.map +1 -1
  100. package/dist/journal.d.ts +38 -4
  101. package/dist/journal.d.ts.map +1 -1
  102. package/dist/journal.js +135 -13
  103. package/dist/journal.js.map +1 -1
  104. package/dist/live-sanitize.cjs +4 -32
  105. package/dist/live-sanitize.cjs.map +1 -1
  106. package/dist/live-sanitize.js +4 -32
  107. package/dist/live-sanitize.js.map +1 -1
  108. package/dist/llmock.cjs +123 -30
  109. package/dist/llmock.cjs.map +1 -1
  110. package/dist/llmock.d.cts +16 -1
  111. package/dist/llmock.d.cts.map +1 -1
  112. package/dist/llmock.d.ts +16 -1
  113. package/dist/llmock.d.ts.map +1 -1
  114. package/dist/llmock.js +125 -32
  115. package/dist/llmock.js.map +1 -1
  116. package/dist/mcp-fakes-mount.cjs +1 -0
  117. package/dist/mcp-fakes-mount.cjs.map +1 -1
  118. package/dist/mcp-fakes-mount.js +1 -0
  119. package/dist/mcp-fakes-mount.js.map +1 -1
  120. package/dist/mcp-fakes-report.cjs +111 -0
  121. package/dist/mcp-fakes-report.cjs.map +1 -0
  122. package/dist/mcp-fakes-report.d.cts +40 -0
  123. package/dist/mcp-fakes-report.d.cts.map +1 -0
  124. package/dist/mcp-fakes-report.d.ts +40 -0
  125. package/dist/mcp-fakes-report.d.ts.map +1 -0
  126. package/dist/mcp-fakes-report.js +107 -0
  127. package/dist/mcp-fakes-report.js.map +1 -0
  128. package/dist/mcp-fakes.cjs +331 -8
  129. package/dist/mcp-fakes.cjs.map +1 -1
  130. package/dist/mcp-fakes.d.cts +406 -3
  131. package/dist/mcp-fakes.d.cts.map +1 -1
  132. package/dist/mcp-fakes.d.ts +406 -3
  133. package/dist/mcp-fakes.d.ts.map +1 -1
  134. package/dist/mcp-fakes.js +331 -8
  135. package/dist/mcp-fakes.js.map +1 -1
  136. package/dist/mcp-handler.cjs +209 -16
  137. package/dist/mcp-handler.cjs.map +1 -1
  138. package/dist/mcp-handler.d.cts +49 -0
  139. package/dist/mcp-handler.d.cts.map +1 -0
  140. package/dist/mcp-handler.d.ts +49 -0
  141. package/dist/mcp-handler.d.ts.map +1 -0
  142. package/dist/mcp-handler.js +207 -17
  143. package/dist/mcp-handler.js.map +1 -1
  144. package/dist/mcp-mock.cjs +112 -1
  145. package/dist/mcp-mock.cjs.map +1 -1
  146. package/dist/mcp-mock.d.cts +33 -2
  147. package/dist/mcp-mock.d.cts.map +1 -1
  148. package/dist/mcp-mock.d.ts +33 -2
  149. package/dist/mcp-mock.d.ts.map +1 -1
  150. package/dist/mcp-mock.js +112 -1
  151. package/dist/mcp-mock.js.map +1 -1
  152. package/dist/mcp-recorder.cjs +1224 -0
  153. package/dist/mcp-recorder.cjs.map +1 -0
  154. package/dist/mcp-recorder.d.cts +108 -0
  155. package/dist/mcp-recorder.d.cts.map +1 -0
  156. package/dist/mcp-recorder.d.ts +108 -0
  157. package/dist/mcp-recorder.d.ts.map +1 -0
  158. package/dist/mcp-recorder.js +1215 -0
  159. package/dist/mcp-recorder.js.map +1 -0
  160. package/dist/messages.cjs +362 -56
  161. package/dist/messages.cjs.map +1 -1
  162. package/dist/messages.d.cts.map +1 -1
  163. package/dist/messages.d.ts.map +1 -1
  164. package/dist/messages.js +363 -58
  165. package/dist/messages.js.map +1 -1
  166. package/dist/misbehavior.cjs +1523 -0
  167. package/dist/misbehavior.cjs.map +1 -0
  168. package/dist/misbehavior.d.cts +43 -0
  169. package/dist/misbehavior.d.cts.map +1 -0
  170. package/dist/misbehavior.d.ts +43 -0
  171. package/dist/misbehavior.d.ts.map +1 -0
  172. package/dist/misbehavior.js +1511 -0
  173. package/dist/misbehavior.js.map +1 -0
  174. package/dist/ollama.cjs +310 -112
  175. package/dist/ollama.cjs.map +1 -1
  176. package/dist/ollama.d.cts.map +1 -1
  177. package/dist/ollama.d.ts.map +1 -1
  178. package/dist/ollama.js +311 -113
  179. package/dist/ollama.js.map +1 -1
  180. package/dist/record-sanitize.cjs +231 -0
  181. package/dist/record-sanitize.cjs.map +1 -0
  182. package/dist/record-sanitize.js +224 -0
  183. package/dist/record-sanitize.js.map +1 -0
  184. package/dist/recorder.cjs +106 -5
  185. package/dist/recorder.cjs.map +1 -1
  186. package/dist/recorder.d.cts +2 -1
  187. package/dist/recorder.d.cts.map +1 -1
  188. package/dist/recorder.d.ts +2 -1
  189. package/dist/recorder.d.ts.map +1 -1
  190. package/dist/recorder.js +107 -7
  191. package/dist/recorder.js.map +1 -1
  192. package/dist/responses.cjs +667 -171
  193. package/dist/responses.cjs.map +1 -1
  194. package/dist/responses.d.cts +5 -13
  195. package/dist/responses.d.cts.map +1 -1
  196. package/dist/responses.d.ts +5 -13
  197. package/dist/responses.d.ts.map +1 -1
  198. package/dist/responses.js +665 -174
  199. package/dist/responses.js.map +1 -1
  200. package/dist/router.cjs +24 -2
  201. package/dist/router.cjs.map +1 -1
  202. package/dist/router.d.cts.map +1 -1
  203. package/dist/router.d.ts.map +1 -1
  204. package/dist/router.js +25 -4
  205. package/dist/router.js.map +1 -1
  206. package/dist/server.cjs +572 -76
  207. package/dist/server.cjs.map +1 -1
  208. package/dist/server.d.cts.map +1 -1
  209. package/dist/server.d.ts.map +1 -1
  210. package/dist/server.js +574 -78
  211. package/dist/server.js.map +1 -1
  212. package/dist/stream-collapse.cjs +236 -34
  213. package/dist/stream-collapse.cjs.map +1 -1
  214. package/dist/stream-collapse.d.cts +39 -4
  215. package/dist/stream-collapse.d.cts.map +1 -1
  216. package/dist/stream-collapse.d.ts +39 -4
  217. package/dist/stream-collapse.d.ts.map +1 -1
  218. package/dist/stream-collapse.js +235 -35
  219. package/dist/stream-collapse.js.map +1 -1
  220. package/dist/types.cjs.map +1 -1
  221. package/dist/types.d.cts +315 -4
  222. package/dist/types.d.cts.map +1 -1
  223. package/dist/types.d.ts +315 -4
  224. package/dist/types.d.ts.map +1 -1
  225. package/dist/types.js.map +1 -1
  226. package/dist/validate-cli.cjs +36 -4
  227. package/dist/validate-cli.cjs.map +1 -1
  228. package/dist/validate-cli.js +37 -5
  229. package/dist/validate-cli.js.map +1 -1
  230. package/dist/vector-types.d.ts.map +1 -1
  231. package/dist/version.cjs +45 -0
  232. package/dist/version.cjs.map +1 -0
  233. package/dist/version.js +44 -0
  234. package/dist/version.js.map +1 -0
  235. package/dist/vitest.cjs +66 -10
  236. package/dist/vitest.cjs.map +1 -1
  237. package/dist/vitest.d.cts +35 -4
  238. package/dist/vitest.d.cts.map +1 -1
  239. package/dist/vitest.d.ts +35 -4
  240. package/dist/vitest.d.ts.map +1 -1
  241. package/dist/vitest.js +67 -11
  242. package/dist/vitest.js.map +1 -1
  243. package/dist/watcher.cjs +6 -2
  244. package/dist/watcher.cjs.map +1 -1
  245. package/dist/watcher.js +6 -2
  246. package/dist/watcher.js.map +1 -1
  247. package/dist/ws-gemini-live.cjs +210 -101
  248. package/dist/ws-gemini-live.cjs.map +1 -1
  249. package/dist/ws-gemini-live.d.cts +9 -3
  250. package/dist/ws-gemini-live.d.cts.map +1 -1
  251. package/dist/ws-gemini-live.d.ts +9 -3
  252. package/dist/ws-gemini-live.d.ts.map +1 -1
  253. package/dist/ws-gemini-live.js +211 -102
  254. package/dist/ws-gemini-live.js.map +1 -1
  255. package/dist/ws-realtime.cjs +175 -62
  256. package/dist/ws-realtime.cjs.map +1 -1
  257. package/dist/ws-realtime.d.cts +8 -3
  258. package/dist/ws-realtime.d.cts.map +1 -1
  259. package/dist/ws-realtime.d.ts +8 -3
  260. package/dist/ws-realtime.d.ts.map +1 -1
  261. package/dist/ws-realtime.js +176 -63
  262. package/dist/ws-realtime.js.map +1 -1
  263. package/dist/ws-responses.cjs +109 -16
  264. package/dist/ws-responses.cjs.map +1 -1
  265. package/dist/ws-responses.d.cts +8 -12
  266. package/dist/ws-responses.d.cts.map +1 -1
  267. package/dist/ws-responses.d.ts +8 -12
  268. package/dist/ws-responses.d.ts.map +1 -1
  269. package/dist/ws-responses.js +111 -18
  270. package/dist/ws-responses.js.map +1 -1
  271. package/package.json +10 -2
  272. package/skills/write-fixtures/SKILL.md +179 -21
package/dist/vitest.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
3
3
  const require_fixture_loader = require('./fixture-loader.cjs');
4
+ const require_mcp_fakes_report = require('./mcp-fakes-report.cjs');
4
5
  const require_llmock = require('./llmock.cjs');
6
+ const require_fakes_plugin = require('./fakes-plugin.cjs');
5
7
  let node_fs = require("node:fs");
6
8
  let node_path = require("node:path");
7
9
  let vitest = require("vitest");
@@ -19,14 +21,26 @@ let vitest = require("vitest");
19
21
  * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });
20
22
  * });
21
23
  */
24
+ /** TI1: `<file> › <name1> › … › <nameN>`; unnamed suites add no segment. */
25
+ function defaultVitestId(task) {
26
+ const names = [];
27
+ for (let suite = task.suite; suite !== void 0 && suite !== task.file; suite = suite.suite) if (suite.name !== "") names.unshift(suite.name);
28
+ return [
29
+ task.file.name,
30
+ ...names,
31
+ task.name
32
+ ].join(require_fakes_plugin.TEST_ID_SEPARATOR);
33
+ }
22
34
  /**
23
35
  * Start an aimock server for the duration of the test suite.
24
36
  *
25
37
  * - `beforeAll`: starts the server and optionally loads fixtures (LLM fixtures and
26
38
  * `mcpFakes` blocks; a bad `mcpFakes` block fails `beforeAll`)
27
39
  * - `beforeEach`: closes Live sessions and resets fixture match counts and MCP fake
28
- * consumption (not fixtures or fakes)
29
- * - `afterEach`: closes Live sessions owned by this helper's server
40
+ * consumption (not fixtures or fakes); under `fakesReport: "warn" | "fail"` it
41
+ * throws in a concurrent test, which the per-test report does not support
42
+ * - `afterEach`: closes Live sessions owned by this helper's server, then checks
43
+ * the MCP fake report under `fakesReport: "warn" | "fail"`
30
44
  * - `afterAll`: stops the server
31
45
  *
32
46
  * Returns a getter function — call it inside tests to access the handle.
@@ -35,8 +49,32 @@ function useAimock(options = {}) {
35
49
  let handle = null;
36
50
  let origOpenaiUrl;
37
51
  let origAnthropicUrl;
52
+ let state = new require_fakes_plugin.FakesPluginState("off");
53
+ /** TI5: default id -> the `task.id` that first used it; fresh per `beforeAll`. */
54
+ let idMap = /* @__PURE__ */ new Map();
55
+ let current = null;
56
+ function resolveTestId(testId) {
57
+ if (testId !== void 0) return testId;
58
+ if (current === null) throw new require_mcp_fakes_report.AimockTestIdCollisionError("fakesFor() needs a running test; pass an explicit testId");
59
+ if (current.concurrent === true) throw new require_mcp_fakes_report.AimockTestIdCollisionError("default test ids are not supported in concurrent tests; pass an explicit testId");
60
+ const id = defaultVitestId(current);
61
+ const owner = idMap.get(id);
62
+ if (owner !== void 0 && owner !== current.id) throw new require_mcp_fakes_report.AimockTestIdCollisionError(`two tests share the default test id ${JSON.stringify(id)}; pass an explicit testId`);
63
+ idMap.set(id, current.id);
64
+ return id;
65
+ }
66
+ /**
67
+ * The report keeps one state per `useAimock`, so concurrent tests would
68
+ * clear and read each other's identities: under `"warn"` or `"fail"`, a
69
+ * concurrent test is refused in `beforeEach` (and not reported on).
70
+ */
71
+ function refusesConcurrent(task) {
72
+ return state.mode !== "off" && task.concurrent === true;
73
+ }
38
74
  (0, vitest.beforeAll)(async () => {
39
- const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;
75
+ const { fixtures: fixturePath, patchEnv, fakesReport, ...serverOpts } = options;
76
+ state = new require_fakes_plugin.FakesPluginState(require_fakes_plugin.fakesReportMode(fakesReport));
77
+ idMap = /* @__PURE__ */ new Map();
40
78
  const llm = new require_llmock.LLMock(serverOpts);
41
79
  if (fixturePath) loadFixtures(llm, (0, node_path.resolve)(fixturePath));
42
80
  const url = await llm.start();
@@ -48,17 +86,33 @@ function useAimock(options = {}) {
48
86
  }
49
87
  handle = {
50
88
  llm,
51
- url
89
+ url,
90
+ fakesFor(testId, opts = {}) {
91
+ const id = resolveTestId(testId);
92
+ state.register(id, opts.context ?? null);
93
+ return require_fakes_plugin.fakesTarget(url, id, opts);
94
+ },
95
+ fakesReport(testId, opts = {}) {
96
+ return require_fakes_plugin.fetchFakesReport(url, resolveTestId(testId), opts.context ?? null);
97
+ }
52
98
  };
53
99
  });
54
- (0, vitest.beforeEach)(() => {
100
+ (0, vitest.beforeEach)((ctx) => {
101
+ current = ctx.task;
102
+ if (refusesConcurrent(ctx.task)) throw new Error("useAimock(): fakesReport is not supported in concurrent tests; set fakesReport: \"off\" for this suite or run it sequentially");
55
103
  if (handle) {
56
104
  handle.llm.closeLiveSessions();
57
105
  handle.llm.resetMatchCounts();
106
+ state.beforeEach(handle.llm);
58
107
  }
59
108
  });
60
- (0, vitest.afterEach)(() => {
61
- handle?.llm.closeLiveSessions();
109
+ (0, vitest.afterEach)(async (ctx) => {
110
+ try {
111
+ handle?.llm.closeLiveSessions();
112
+ if (handle && !refusesConcurrent(ctx.task)) await state.afterEach(handle.llm, handle.url);
113
+ } finally {
114
+ current = null;
115
+ }
62
116
  });
63
117
  (0, vitest.afterAll)(async () => {
64
118
  if (handle) {
@@ -80,15 +134,17 @@ function useAimock(options = {}) {
80
134
  /**
81
135
  * Load a fixture file or directory into `llm`: its LLM fixtures and its
82
136
  * `mcpFakes` blocks, which the LLMock buffers and auto-mounts at start (F12).
83
- * A `FixtureLoadError` (a bad `mcpFakes` block) propagates and fails
84
- * `beforeAll`; any other load failure is only warned about, as before.
137
+ * A `FixtureLoadError` (a bad `mcpFakes` block) or a `MisbehaviorConfigError`
138
+ * (a bad fixture `misbehavior` key, thrown only with `enableMisbehavior: true`)
139
+ * propagates and fails `beforeAll`; any other load failure is only warned
140
+ * about, as before.
85
141
  */
86
142
  function loadFixtures(llm, fixturePath) {
87
143
  try {
88
144
  if ((0, node_fs.statSync)(fixturePath).isDirectory()) llm.loadFixtureDir(fixturePath);
89
145
  else llm.loadFixtureFile(fixturePath);
90
146
  } catch (err) {
91
- if (err instanceof require_fixture_loader.FixtureLoadError) throw err;
147
+ if (err instanceof require_fixture_loader.FixtureLoadError || err instanceof require_fixture_loader.MisbehaviorConfigError) throw err;
92
148
  console.warn(`[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`);
93
149
  }
94
150
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.cjs","names":["LLMock","FixtureLoadError"],"sources":["../src/vitest.ts"],"sourcesContent":["/**\n * Vitest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/vitest\";\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\nimport { beforeAll, afterAll, beforeEach, afterEach } from \"vitest\";\nimport { LLMock } from \"./llmock.js\";\nimport { FixtureLoadError } from \"./fixture-loader.js\";\nimport type { 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 (LLM fixtures and\n * `mcpFakes` blocks; a bad `mcpFakes` block fails `beforeAll`)\n * - `beforeEach`: closes Live sessions and resets fixture match counts and MCP fake\n * consumption (not fixtures or fakes)\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 */\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 loadFixtures(llm, resolve(fixturePath));\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\n/**\n * Load a fixture file or directory into `llm`: its LLM fixtures and its\n * `mcpFakes` blocks, which the LLMock buffers and auto-mounts at start (F12).\n * A `FixtureLoadError` (a bad `mcpFakes` block) propagates and fails\n * `beforeAll`; any other load failure is only warned about, as before.\n */\nfunction loadFixtures(llm: LLMock, fixturePath: string): void {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n llm.loadFixtureDir(fixturePath);\n } else {\n llm.loadFixtureFile(fixturePath);\n }\n } catch (err) {\n if (err instanceof FixtureLoadError) throw err;\n console.warn(\n `[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,SAAgB,UAAU,UAA4B,EAAE,EAAsB;CAC5E,IAAI,SAA8B;CAClC,IAAI;CACJ,IAAI;AAEJ,uBAAU,YAAY;EACpB,MAAM,EAAE,UAAU,aAAa,UAAU,GAAG,eAAe;EAC3D,MAAM,MAAM,IAAIA,sBAAO,WAAW;AAElC,MAAI,YACF,cAAa,4BAAa,YAAY,CAAC;EAGzC,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,8BAAiB;AACf,MAAI,QAAQ;AACV,UAAO,IAAI,mBAAmB;AAC9B,UAAO,IAAI,kBAAkB;;GAE/B;AAEF,6BAAgB;AACd,UAAQ,IAAI,mBAAmB;GAC/B;AAEF,sBAAS,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;;;;;;;;;AAUX,SAAS,aAAa,KAAa,aAA2B;AAC5D,KAAI;AAEF,4BADsB,YAAY,CACzB,aAAa,CACpB,KAAI,eAAe,YAAY;MAE/B,KAAI,gBAAgB,YAAY;UAE3B,KAAK;AACZ,MAAI,eAAeC,wCAAkB,OAAM;AAC3C,UAAQ,KACN,yCAAyC,YAAY,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC1G"}
1
+ {"version":3,"file":"vitest.cjs","names":["TEST_ID_SEPARATOR","FakesPluginState","AimockTestIdCollisionError","fakesReportMode","LLMock","fakesTarget","fetchFakesReport","FixtureLoadError","MisbehaviorConfigError"],"sources":["../src/vitest.ts"],"sourcesContent":["/**\n * Vitest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/vitest\";\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\nimport { beforeAll, afterAll, beforeEach, afterEach } from \"vitest\";\nimport type { RunnerTestCase, RunnerTestSuite } from \"vitest\";\nimport { LLMock } from \"./llmock.js\";\nimport { FixtureLoadError, MisbehaviorConfigError } from \"./fixture-loader.js\";\nimport type { MockServerOptions } from \"./types.js\";\nimport {\n FakesPluginState,\n TEST_ID_SEPARATOR,\n fakesReportMode,\n fakesTarget,\n fetchFakesReport,\n type FakesForOptions,\n type FakesReportMode,\n type FakesTarget,\n} from \"./fakes-plugin.js\";\nimport { AimockTestIdCollisionError, type McpFakesReport } from \"./mcp-fakes-report.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 * The per-test MCP fake report, read in `afterEach` (RP7): `\"off\"` (the\n * default), `\"warn\"` (print a failing report) or `\"fail\"` (throw\n * `AimockFakesReportError`). Not supported in concurrent tests: under\n * `\"warn\"` or `\"fail\"`, a concurrent test fails in `beforeEach`.\n */\n fakesReport?: FakesReportMode;\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 * The handle `useAimock` returns: an `AimockHandle` plus the MCP fakes\n * helpers (RP7). A separate type, so that `AimockHandle` stays as it was and\n * code that builds its own `AimockHandle` keeps compiling.\n */\nexport interface AimockFakesHandle extends AimockHandle {\n /**\n * The MCP URL and headers for the fakes scoped to `testId` (RP7), and\n * registers that identity for this test's report. With no `testId`, the\n * default is `<file> › <describe…> › <test>` (TI1); it is used only here and\n * in `fakesReport`, never on LLM traffic (TI4). Throws\n * `AimockTestIdCollisionError` when two tests share a default id, in a\n * concurrent test, or outside a test.\n */\n fakesFor(testId?: string, opts?: FakesForOptions): FakesTarget;\n /** The MCP fake report for `testId` (default as in `fakesFor`). */\n fakesReport(testId?: string, opts?: { context?: string }): Promise<McpFakesReport>;\n}\n\n/** TI1: `<file> › <name1> › … › <nameN>`; unnamed suites add no segment. */\nfunction defaultVitestId(task: Readonly<RunnerTestCase>): string {\n const names: string[] = [];\n for (\n let suite: RunnerTestSuite | undefined = task.suite;\n suite !== undefined && suite !== task.file;\n suite = suite.suite\n ) {\n if (suite.name !== \"\") names.unshift(suite.name);\n }\n return [task.file.name, ...names, task.name].join(TEST_ID_SEPARATOR);\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 (LLM fixtures and\n * `mcpFakes` blocks; a bad `mcpFakes` block fails `beforeAll`)\n * - `beforeEach`: closes Live sessions and resets fixture match counts and MCP fake\n * consumption (not fixtures or fakes); under `fakesReport: \"warn\" | \"fail\"` it\n * throws in a concurrent test, which the per-test report does not support\n * - `afterEach`: closes Live sessions owned by this helper's server, then checks\n * the MCP fake report under `fakesReport: \"warn\" | \"fail\"`\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockFakesHandle {\n let handle: AimockFakesHandle | null = null;\n let origOpenaiUrl: string | undefined;\n let origAnthropicUrl: string | undefined;\n let state = new FakesPluginState(\"off\");\n /** TI5: default id -> the `task.id` that first used it; fresh per `beforeAll`. */\n let idMap = new Map<string, string>();\n let current: Readonly<RunnerTestCase> | null = null;\n\n function resolveTestId(testId: string | undefined): string {\n if (testId !== undefined) return testId; // TI4: an explicit id wins\n if (current === null) {\n throw new AimockTestIdCollisionError(\n \"fakesFor() needs a running test; pass an explicit testId\",\n );\n }\n if (current.concurrent === true) {\n throw new AimockTestIdCollisionError(\n \"default test ids are not supported in concurrent tests; pass an explicit testId\",\n );\n }\n const id = defaultVitestId(current);\n const owner = idMap.get(id);\n if (owner !== undefined && owner !== current.id) {\n throw new AimockTestIdCollisionError(\n `two tests share the default test id ${JSON.stringify(id)}; pass an explicit testId`,\n );\n }\n idMap.set(id, current.id);\n return id;\n }\n\n /**\n * The report keeps one state per `useAimock`, so concurrent tests would\n * clear and read each other's identities: under `\"warn\"` or `\"fail\"`, a\n * concurrent test is refused in `beforeEach` (and not reported on).\n */\n function refusesConcurrent(task: Readonly<RunnerTestCase>): boolean {\n return state.mode !== \"off\" && task.concurrent === true;\n }\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, fakesReport, ...serverOpts } = options;\n state = new FakesPluginState(fakesReportMode(fakesReport));\n idMap = new Map();\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n loadFixtures(llm, resolve(fixturePath));\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 = {\n llm,\n url,\n fakesFor(testId?: string, opts: FakesForOptions = {}): FakesTarget {\n const id = resolveTestId(testId);\n state.register(id, opts.context ?? null);\n return fakesTarget(url, id, opts);\n },\n fakesReport(testId?: string, opts: { context?: string } = {}): Promise<McpFakesReport> {\n return fetchFakesReport(url, resolveTestId(testId), opts.context ?? null);\n },\n };\n });\n\n beforeEach((ctx) => {\n current = ctx.task;\n if (refusesConcurrent(ctx.task)) {\n throw new Error(\n 'useAimock(): fakesReport is not supported in concurrent tests; set fakesReport: \"off\" for this suite or run it sequentially',\n );\n }\n if (handle) {\n handle.llm.closeLiveSessions();\n handle.llm.resetMatchCounts();\n state.beforeEach(handle.llm);\n }\n });\n\n afterEach(async (ctx) => {\n try {\n handle?.llm.closeLiveSessions();\n if (handle && !refusesConcurrent(ctx.task)) await state.afterEach(handle.llm, handle.url);\n } finally {\n current = null;\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\n/**\n * Load a fixture file or directory into `llm`: its LLM fixtures and its\n * `mcpFakes` blocks, which the LLMock buffers and auto-mounts at start (F12).\n * A `FixtureLoadError` (a bad `mcpFakes` block) or a `MisbehaviorConfigError`\n * (a bad fixture `misbehavior` key, thrown only with `enableMisbehavior: true`)\n * propagates and fails `beforeAll`; any other load failure is only warned\n * about, as before.\n */\nfunction loadFixtures(llm: LLMock, fixturePath: string): void {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n llm.loadFixtureDir(fixturePath);\n } else {\n llm.loadFixtureFile(fixturePath);\n }\n } catch (err) {\n if (err instanceof FixtureLoadError || err instanceof MisbehaviorConfigError) throw err;\n console.warn(\n `[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\nexport type { FakesForOptions, FakesReportMode, FakesTarget } from \"./fakes-plugin.js\";\nexport type { McpFakesReport } from \"./mcp-fakes-report.js\";\n"],"mappings":"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"}
package/dist/vitest.d.cts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Fixture, MockServerOptions } from "./types.cjs";
2
2
  import { LLMock } from "./llmock.cjs";
3
+ import { McpFakesReport } from "./mcp-fakes-report.cjs";
4
+ import { FakesForOptions, FakesReportMode, FakesTarget } from "./fakes-plugin.cjs";
3
5
 
4
6
  //#region src/vitest.d.ts
5
7
 
@@ -8,6 +10,13 @@ interface UseAimockOptions extends MockServerOptions {
8
10
  fixtures?: string;
9
11
  /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */
10
12
  patchEnv?: boolean;
13
+ /**
14
+ * The per-test MCP fake report, read in `afterEach` (RP7): `"off"` (the
15
+ * default), `"warn"` (print a failing report) or `"fail"` (throw
16
+ * `AimockFakesReportError`). Not supported in concurrent tests: under
17
+ * `"warn"` or `"fail"`, a concurrent test fails in `beforeEach`.
18
+ */
19
+ fakesReport?: FakesReportMode;
11
20
  }
12
21
  interface AimockHandle {
13
22
  /** The LLMock instance. */
@@ -15,19 +24,41 @@ interface AimockHandle {
15
24
  /** The server URL (e.g., http://127.0.0.1:4010). */
16
25
  readonly url: string;
17
26
  }
27
+ /**
28
+ * The handle `useAimock` returns: an `AimockHandle` plus the MCP fakes
29
+ * helpers (RP7). A separate type, so that `AimockHandle` stays as it was and
30
+ * code that builds its own `AimockHandle` keeps compiling.
31
+ */
32
+ interface AimockFakesHandle extends AimockHandle {
33
+ /**
34
+ * The MCP URL and headers for the fakes scoped to `testId` (RP7), and
35
+ * registers that identity for this test's report. With no `testId`, the
36
+ * default is `<file> › <describe…> › <test>` (TI1); it is used only here and
37
+ * in `fakesReport`, never on LLM traffic (TI4). Throws
38
+ * `AimockTestIdCollisionError` when two tests share a default id, in a
39
+ * concurrent test, or outside a test.
40
+ */
41
+ fakesFor(testId?: string, opts?: FakesForOptions): FakesTarget;
42
+ /** The MCP fake report for `testId` (default as in `fakesFor`). */
43
+ fakesReport(testId?: string, opts?: {
44
+ context?: string;
45
+ }): Promise<McpFakesReport>;
46
+ }
18
47
  /**
19
48
  * Start an aimock server for the duration of the test suite.
20
49
  *
21
50
  * - `beforeAll`: starts the server and optionally loads fixtures (LLM fixtures and
22
51
  * `mcpFakes` blocks; a bad `mcpFakes` block fails `beforeAll`)
23
52
  * - `beforeEach`: closes Live sessions and resets fixture match counts and MCP fake
24
- * consumption (not fixtures or fakes)
25
- * - `afterEach`: closes Live sessions owned by this helper's server
53
+ * consumption (not fixtures or fakes); under `fakesReport: "warn" | "fail"` it
54
+ * throws in a concurrent test, which the per-test report does not support
55
+ * - `afterEach`: closes Live sessions owned by this helper's server, then checks
56
+ * the MCP fake report under `fakesReport: "warn" | "fail"`
26
57
  * - `afterAll`: stops the server
27
58
  *
28
59
  * Returns a getter function — call it inside tests to access the handle.
29
60
  */
30
- declare function useAimock(options?: UseAimockOptions): () => AimockHandle;
61
+ declare function useAimock(options?: UseAimockOptions): () => AimockFakesHandle;
31
62
  //#endregion
32
- export { AimockHandle, type Fixture, LLMock, type MockServerOptions, UseAimockOptions, useAimock };
63
+ export { AimockFakesHandle, AimockHandle, type FakesForOptions, type FakesReportMode, type FakesTarget, type Fixture, LLMock, type McpFakesReport, type MockServerOptions, UseAimockOptions, useAimock };
33
64
  //# sourceMappingURL=vitest.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.d.cts","names":[],"sources":["../src/vitest.ts"],"sourcesContent":[],"mappings":";;;;;UAoBiB,gBAAA,SAAyB;;;;;;UAOzB,YAAA;;gBAED;;;;;;;;;;;;;;;;iBAiBA,SAAA,WAAmB,yBAA8B"}
1
+ {"version":3,"file":"vitest.d.cts","names":[],"sources":["../src/vitest.ts"],"sourcesContent":[],"mappings":";;;;;;;AAqEqE,UArCpD,gBAAA,SAAyB,iBAqC2B,CAAA;;UAX1B,CAAA,EAAA,MAAA;EAAY;EAyCvC,QAAA,CAAA,EAAA,OAAS;EAAA;;;;;;gBAxDT;;UAGC,YAAA;;gBAED;;;;;;;;;UAUC,iBAAA,SAA0B;;;;;;;;;mCASR,kBAAkB;;;;MAEQ,QAAQ;;;;;;;;;;;;;;;;iBA8BrD,SAAA,WAAmB,yBAA8B"}
package/dist/vitest.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Fixture, MockServerOptions } from "./types.js";
2
2
  import { LLMock } from "./llmock.js";
3
+ import { McpFakesReport } from "./mcp-fakes-report.js";
4
+ import { FakesForOptions, FakesReportMode, FakesTarget } from "./fakes-plugin.js";
3
5
 
4
6
  //#region src/vitest.d.ts
5
7
 
@@ -8,6 +10,13 @@ interface UseAimockOptions extends MockServerOptions {
8
10
  fixtures?: string;
9
11
  /** If true, sets process.env.OPENAI_BASE_URL to the mock URL + /v1. */
10
12
  patchEnv?: boolean;
13
+ /**
14
+ * The per-test MCP fake report, read in `afterEach` (RP7): `"off"` (the
15
+ * default), `"warn"` (print a failing report) or `"fail"` (throw
16
+ * `AimockFakesReportError`). Not supported in concurrent tests: under
17
+ * `"warn"` or `"fail"`, a concurrent test fails in `beforeEach`.
18
+ */
19
+ fakesReport?: FakesReportMode;
11
20
  }
12
21
  interface AimockHandle {
13
22
  /** The LLMock instance. */
@@ -15,19 +24,41 @@ interface AimockHandle {
15
24
  /** The server URL (e.g., http://127.0.0.1:4010). */
16
25
  readonly url: string;
17
26
  }
27
+ /**
28
+ * The handle `useAimock` returns: an `AimockHandle` plus the MCP fakes
29
+ * helpers (RP7). A separate type, so that `AimockHandle` stays as it was and
30
+ * code that builds its own `AimockHandle` keeps compiling.
31
+ */
32
+ interface AimockFakesHandle extends AimockHandle {
33
+ /**
34
+ * The MCP URL and headers for the fakes scoped to `testId` (RP7), and
35
+ * registers that identity for this test's report. With no `testId`, the
36
+ * default is `<file> › <describe…> › <test>` (TI1); it is used only here and
37
+ * in `fakesReport`, never on LLM traffic (TI4). Throws
38
+ * `AimockTestIdCollisionError` when two tests share a default id, in a
39
+ * concurrent test, or outside a test.
40
+ */
41
+ fakesFor(testId?: string, opts?: FakesForOptions): FakesTarget;
42
+ /** The MCP fake report for `testId` (default as in `fakesFor`). */
43
+ fakesReport(testId?: string, opts?: {
44
+ context?: string;
45
+ }): Promise<McpFakesReport>;
46
+ }
18
47
  /**
19
48
  * Start an aimock server for the duration of the test suite.
20
49
  *
21
50
  * - `beforeAll`: starts the server and optionally loads fixtures (LLM fixtures and
22
51
  * `mcpFakes` blocks; a bad `mcpFakes` block fails `beforeAll`)
23
52
  * - `beforeEach`: closes Live sessions and resets fixture match counts and MCP fake
24
- * consumption (not fixtures or fakes)
25
- * - `afterEach`: closes Live sessions owned by this helper's server
53
+ * consumption (not fixtures or fakes); under `fakesReport: "warn" | "fail"` it
54
+ * throws in a concurrent test, which the per-test report does not support
55
+ * - `afterEach`: closes Live sessions owned by this helper's server, then checks
56
+ * the MCP fake report under `fakesReport: "warn" | "fail"`
26
57
  * - `afterAll`: stops the server
27
58
  *
28
59
  * Returns a getter function — call it inside tests to access the handle.
29
60
  */
30
- declare function useAimock(options?: UseAimockOptions): () => AimockHandle;
61
+ declare function useAimock(options?: UseAimockOptions): () => AimockFakesHandle;
31
62
  //#endregion
32
- export { AimockHandle, type Fixture, LLMock, type MockServerOptions, UseAimockOptions, useAimock };
63
+ export { AimockFakesHandle, AimockHandle, type FakesForOptions, type FakesReportMode, type FakesTarget, type Fixture, LLMock, type McpFakesReport, type MockServerOptions, UseAimockOptions, useAimock };
33
64
  //# sourceMappingURL=vitest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.d.ts","names":[],"sources":["../src/vitest.ts"],"sourcesContent":[],"mappings":";;;;;UAoBiB,gBAAA,SAAyB;;;;;;UAOzB,YAAA;;gBAED;;;;;;;;;;;;;;;;iBAiBA,SAAA,WAAmB,yBAA8B"}
1
+ {"version":3,"file":"vitest.d.ts","names":[],"sources":["../src/vitest.ts"],"sourcesContent":[],"mappings":";;;;;;;AAqEqE,UArCpD,gBAAA,SAAyB,iBAqC2B,CAAA;;UAX1B,CAAA,EAAA,MAAA;EAAY;EAyCvC,QAAA,CAAA,EAAA,OAAS;EAAA;;;;;;gBAxDT;;UAGC,YAAA;;gBAED;;;;;;;;;UAUC,iBAAA,SAA0B;;;;;;;;;mCASR,kBAAkB;;;;MAEQ,QAAQ;;;;;;;;;;;;;;;;iBA8BrD,SAAA,WAAmB,yBAA8B"}
package/dist/vitest.js CHANGED
@@ -1,5 +1,7 @@
1
- import { FixtureLoadError } from "./fixture-loader.js";
1
+ import { FixtureLoadError, MisbehaviorConfigError } from "./fixture-loader.js";
2
+ import { AimockTestIdCollisionError } from "./mcp-fakes-report.js";
2
3
  import { LLMock } from "./llmock.js";
4
+ import { FakesPluginState, TEST_ID_SEPARATOR, fakesReportMode, fakesTarget, fetchFakesReport } from "./fakes-plugin.js";
3
5
  import { statSync } from "node:fs";
4
6
  import { resolve } from "node:path";
5
7
  import { afterAll, afterEach, beforeAll, beforeEach } from "vitest";
@@ -17,14 +19,26 @@ import { afterAll, afterEach, beforeAll, beforeEach } from "vitest";
17
19
  * const res = await fetch(`${mock().url}/v1/chat/completions`, { ... });
18
20
  * });
19
21
  */
22
+ /** TI1: `<file> › <name1> › … › <nameN>`; unnamed suites add no segment. */
23
+ function defaultVitestId(task) {
24
+ const names = [];
25
+ for (let suite = task.suite; suite !== void 0 && suite !== task.file; suite = suite.suite) if (suite.name !== "") names.unshift(suite.name);
26
+ return [
27
+ task.file.name,
28
+ ...names,
29
+ task.name
30
+ ].join(TEST_ID_SEPARATOR);
31
+ }
20
32
  /**
21
33
  * Start an aimock server for the duration of the test suite.
22
34
  *
23
35
  * - `beforeAll`: starts the server and optionally loads fixtures (LLM fixtures and
24
36
  * `mcpFakes` blocks; a bad `mcpFakes` block fails `beforeAll`)
25
37
  * - `beforeEach`: closes Live sessions and resets fixture match counts and MCP fake
26
- * consumption (not fixtures or fakes)
27
- * - `afterEach`: closes Live sessions owned by this helper's server
38
+ * consumption (not fixtures or fakes); under `fakesReport: "warn" | "fail"` it
39
+ * throws in a concurrent test, which the per-test report does not support
40
+ * - `afterEach`: closes Live sessions owned by this helper's server, then checks
41
+ * the MCP fake report under `fakesReport: "warn" | "fail"`
28
42
  * - `afterAll`: stops the server
29
43
  *
30
44
  * Returns a getter function — call it inside tests to access the handle.
@@ -33,8 +47,32 @@ function useAimock(options = {}) {
33
47
  let handle = null;
34
48
  let origOpenaiUrl;
35
49
  let origAnthropicUrl;
50
+ let state = new FakesPluginState("off");
51
+ /** TI5: default id -> the `task.id` that first used it; fresh per `beforeAll`. */
52
+ let idMap = /* @__PURE__ */ new Map();
53
+ let current = null;
54
+ function resolveTestId(testId) {
55
+ if (testId !== void 0) return testId;
56
+ if (current === null) throw new AimockTestIdCollisionError("fakesFor() needs a running test; pass an explicit testId");
57
+ if (current.concurrent === true) throw new AimockTestIdCollisionError("default test ids are not supported in concurrent tests; pass an explicit testId");
58
+ const id = defaultVitestId(current);
59
+ const owner = idMap.get(id);
60
+ if (owner !== void 0 && owner !== current.id) throw new AimockTestIdCollisionError(`two tests share the default test id ${JSON.stringify(id)}; pass an explicit testId`);
61
+ idMap.set(id, current.id);
62
+ return id;
63
+ }
64
+ /**
65
+ * The report keeps one state per `useAimock`, so concurrent tests would
66
+ * clear and read each other's identities: under `"warn"` or `"fail"`, a
67
+ * concurrent test is refused in `beforeEach` (and not reported on).
68
+ */
69
+ function refusesConcurrent(task) {
70
+ return state.mode !== "off" && task.concurrent === true;
71
+ }
36
72
  beforeAll(async () => {
37
- const { fixtures: fixturePath, patchEnv, ...serverOpts } = options;
73
+ const { fixtures: fixturePath, patchEnv, fakesReport, ...serverOpts } = options;
74
+ state = new FakesPluginState(fakesReportMode(fakesReport));
75
+ idMap = /* @__PURE__ */ new Map();
38
76
  const llm = new LLMock(serverOpts);
39
77
  if (fixturePath) loadFixtures(llm, resolve(fixturePath));
40
78
  const url = await llm.start();
@@ -46,17 +84,33 @@ function useAimock(options = {}) {
46
84
  }
47
85
  handle = {
48
86
  llm,
49
- url
87
+ url,
88
+ fakesFor(testId, opts = {}) {
89
+ const id = resolveTestId(testId);
90
+ state.register(id, opts.context ?? null);
91
+ return fakesTarget(url, id, opts);
92
+ },
93
+ fakesReport(testId, opts = {}) {
94
+ return fetchFakesReport(url, resolveTestId(testId), opts.context ?? null);
95
+ }
50
96
  };
51
97
  });
52
- beforeEach(() => {
98
+ beforeEach((ctx) => {
99
+ current = ctx.task;
100
+ if (refusesConcurrent(ctx.task)) throw new Error("useAimock(): fakesReport is not supported in concurrent tests; set fakesReport: \"off\" for this suite or run it sequentially");
53
101
  if (handle) {
54
102
  handle.llm.closeLiveSessions();
55
103
  handle.llm.resetMatchCounts();
104
+ state.beforeEach(handle.llm);
56
105
  }
57
106
  });
58
- afterEach(() => {
59
- handle?.llm.closeLiveSessions();
107
+ afterEach(async (ctx) => {
108
+ try {
109
+ handle?.llm.closeLiveSessions();
110
+ if (handle && !refusesConcurrent(ctx.task)) await state.afterEach(handle.llm, handle.url);
111
+ } finally {
112
+ current = null;
113
+ }
60
114
  });
61
115
  afterAll(async () => {
62
116
  if (handle) {
@@ -78,15 +132,17 @@ function useAimock(options = {}) {
78
132
  /**
79
133
  * Load a fixture file or directory into `llm`: its LLM fixtures and its
80
134
  * `mcpFakes` blocks, which the LLMock buffers and auto-mounts at start (F12).
81
- * A `FixtureLoadError` (a bad `mcpFakes` block) propagates and fails
82
- * `beforeAll`; any other load failure is only warned about, as before.
135
+ * A `FixtureLoadError` (a bad `mcpFakes` block) or a `MisbehaviorConfigError`
136
+ * (a bad fixture `misbehavior` key, thrown only with `enableMisbehavior: true`)
137
+ * propagates and fails `beforeAll`; any other load failure is only warned
138
+ * about, as before.
83
139
  */
84
140
  function loadFixtures(llm, fixturePath) {
85
141
  try {
86
142
  if (statSync(fixturePath).isDirectory()) llm.loadFixtureDir(fixturePath);
87
143
  else llm.loadFixtureFile(fixturePath);
88
144
  } catch (err) {
89
- if (err instanceof FixtureLoadError) throw err;
145
+ if (err instanceof FixtureLoadError || err instanceof MisbehaviorConfigError) throw err;
90
146
  console.warn(`[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`);
91
147
  }
92
148
  }
@@ -1 +1 @@
1
- {"version":3,"file":"vitest.js","names":[],"sources":["../src/vitest.ts"],"sourcesContent":["/**\n * Vitest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/vitest\";\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\nimport { beforeAll, afterAll, beforeEach, afterEach } from \"vitest\";\nimport { LLMock } from \"./llmock.js\";\nimport { FixtureLoadError } from \"./fixture-loader.js\";\nimport type { 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 (LLM fixtures and\n * `mcpFakes` blocks; a bad `mcpFakes` block fails `beforeAll`)\n * - `beforeEach`: closes Live sessions and resets fixture match counts and MCP fake\n * consumption (not fixtures or fakes)\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 */\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 loadFixtures(llm, resolve(fixturePath));\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\n/**\n * Load a fixture file or directory into `llm`: its LLM fixtures and its\n * `mcpFakes` blocks, which the LLMock buffers and auto-mounts at start (F12).\n * A `FixtureLoadError` (a bad `mcpFakes` block) propagates and fails\n * `beforeAll`; any other load failure is only warned about, as before.\n */\nfunction loadFixtures(llm: LLMock, fixturePath: string): void {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n llm.loadFixtureDir(fixturePath);\n } else {\n llm.loadFixtureFile(fixturePath);\n }\n } catch (err) {\n if (err instanceof FixtureLoadError) throw err;\n console.warn(\n `[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,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,YACF,cAAa,KAAK,QAAQ,YAAY,CAAC;EAGzC,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;;;;;;;;;AAUX,SAAS,aAAa,KAAa,aAA2B;AAC5D,KAAI;AAEF,MADa,SAAS,YAAY,CACzB,aAAa,CACpB,KAAI,eAAe,YAAY;MAE/B,KAAI,gBAAgB,YAAY;UAE3B,KAAK;AACZ,MAAI,eAAe,iBAAkB,OAAM;AAC3C,UAAQ,KACN,yCAAyC,YAAY,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC1G"}
1
+ {"version":3,"file":"vitest.js","names":[],"sources":["../src/vitest.ts"],"sourcesContent":["/**\n * Vitest integration for aimock.\n *\n * Usage:\n * import { useAimock } from \"@copilotkit/aimock/vitest\";\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\nimport { beforeAll, afterAll, beforeEach, afterEach } from \"vitest\";\nimport type { RunnerTestCase, RunnerTestSuite } from \"vitest\";\nimport { LLMock } from \"./llmock.js\";\nimport { FixtureLoadError, MisbehaviorConfigError } from \"./fixture-loader.js\";\nimport type { MockServerOptions } from \"./types.js\";\nimport {\n FakesPluginState,\n TEST_ID_SEPARATOR,\n fakesReportMode,\n fakesTarget,\n fetchFakesReport,\n type FakesForOptions,\n type FakesReportMode,\n type FakesTarget,\n} from \"./fakes-plugin.js\";\nimport { AimockTestIdCollisionError, type McpFakesReport } from \"./mcp-fakes-report.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 * The per-test MCP fake report, read in `afterEach` (RP7): `\"off\"` (the\n * default), `\"warn\"` (print a failing report) or `\"fail\"` (throw\n * `AimockFakesReportError`). Not supported in concurrent tests: under\n * `\"warn\"` or `\"fail\"`, a concurrent test fails in `beforeEach`.\n */\n fakesReport?: FakesReportMode;\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 * The handle `useAimock` returns: an `AimockHandle` plus the MCP fakes\n * helpers (RP7). A separate type, so that `AimockHandle` stays as it was and\n * code that builds its own `AimockHandle` keeps compiling.\n */\nexport interface AimockFakesHandle extends AimockHandle {\n /**\n * The MCP URL and headers for the fakes scoped to `testId` (RP7), and\n * registers that identity for this test's report. With no `testId`, the\n * default is `<file> › <describe…> › <test>` (TI1); it is used only here and\n * in `fakesReport`, never on LLM traffic (TI4). Throws\n * `AimockTestIdCollisionError` when two tests share a default id, in a\n * concurrent test, or outside a test.\n */\n fakesFor(testId?: string, opts?: FakesForOptions): FakesTarget;\n /** The MCP fake report for `testId` (default as in `fakesFor`). */\n fakesReport(testId?: string, opts?: { context?: string }): Promise<McpFakesReport>;\n}\n\n/** TI1: `<file> › <name1> › … › <nameN>`; unnamed suites add no segment. */\nfunction defaultVitestId(task: Readonly<RunnerTestCase>): string {\n const names: string[] = [];\n for (\n let suite: RunnerTestSuite | undefined = task.suite;\n suite !== undefined && suite !== task.file;\n suite = suite.suite\n ) {\n if (suite.name !== \"\") names.unshift(suite.name);\n }\n return [task.file.name, ...names, task.name].join(TEST_ID_SEPARATOR);\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 (LLM fixtures and\n * `mcpFakes` blocks; a bad `mcpFakes` block fails `beforeAll`)\n * - `beforeEach`: closes Live sessions and resets fixture match counts and MCP fake\n * consumption (not fixtures or fakes); under `fakesReport: \"warn\" | \"fail\"` it\n * throws in a concurrent test, which the per-test report does not support\n * - `afterEach`: closes Live sessions owned by this helper's server, then checks\n * the MCP fake report under `fakesReport: \"warn\" | \"fail\"`\n * - `afterAll`: stops the server\n *\n * Returns a getter function — call it inside tests to access the handle.\n */\nexport function useAimock(options: UseAimockOptions = {}): () => AimockFakesHandle {\n let handle: AimockFakesHandle | null = null;\n let origOpenaiUrl: string | undefined;\n let origAnthropicUrl: string | undefined;\n let state = new FakesPluginState(\"off\");\n /** TI5: default id -> the `task.id` that first used it; fresh per `beforeAll`. */\n let idMap = new Map<string, string>();\n let current: Readonly<RunnerTestCase> | null = null;\n\n function resolveTestId(testId: string | undefined): string {\n if (testId !== undefined) return testId; // TI4: an explicit id wins\n if (current === null) {\n throw new AimockTestIdCollisionError(\n \"fakesFor() needs a running test; pass an explicit testId\",\n );\n }\n if (current.concurrent === true) {\n throw new AimockTestIdCollisionError(\n \"default test ids are not supported in concurrent tests; pass an explicit testId\",\n );\n }\n const id = defaultVitestId(current);\n const owner = idMap.get(id);\n if (owner !== undefined && owner !== current.id) {\n throw new AimockTestIdCollisionError(\n `two tests share the default test id ${JSON.stringify(id)}; pass an explicit testId`,\n );\n }\n idMap.set(id, current.id);\n return id;\n }\n\n /**\n * The report keeps one state per `useAimock`, so concurrent tests would\n * clear and read each other's identities: under `\"warn\"` or `\"fail\"`, a\n * concurrent test is refused in `beforeEach` (and not reported on).\n */\n function refusesConcurrent(task: Readonly<RunnerTestCase>): boolean {\n return state.mode !== \"off\" && task.concurrent === true;\n }\n\n beforeAll(async () => {\n const { fixtures: fixturePath, patchEnv, fakesReport, ...serverOpts } = options;\n state = new FakesPluginState(fakesReportMode(fakesReport));\n idMap = new Map();\n const llm = new LLMock(serverOpts);\n\n if (fixturePath) {\n loadFixtures(llm, resolve(fixturePath));\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 = {\n llm,\n url,\n fakesFor(testId?: string, opts: FakesForOptions = {}): FakesTarget {\n const id = resolveTestId(testId);\n state.register(id, opts.context ?? null);\n return fakesTarget(url, id, opts);\n },\n fakesReport(testId?: string, opts: { context?: string } = {}): Promise<McpFakesReport> {\n return fetchFakesReport(url, resolveTestId(testId), opts.context ?? null);\n },\n };\n });\n\n beforeEach((ctx) => {\n current = ctx.task;\n if (refusesConcurrent(ctx.task)) {\n throw new Error(\n 'useAimock(): fakesReport is not supported in concurrent tests; set fakesReport: \"off\" for this suite or run it sequentially',\n );\n }\n if (handle) {\n handle.llm.closeLiveSessions();\n handle.llm.resetMatchCounts();\n state.beforeEach(handle.llm);\n }\n });\n\n afterEach(async (ctx) => {\n try {\n handle?.llm.closeLiveSessions();\n if (handle && !refusesConcurrent(ctx.task)) await state.afterEach(handle.llm, handle.url);\n } finally {\n current = null;\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\n/**\n * Load a fixture file or directory into `llm`: its LLM fixtures and its\n * `mcpFakes` blocks, which the LLMock buffers and auto-mounts at start (F12).\n * A `FixtureLoadError` (a bad `mcpFakes` block) or a `MisbehaviorConfigError`\n * (a bad fixture `misbehavior` key, thrown only with `enableMisbehavior: true`)\n * propagates and fails `beforeAll`; any other load failure is only warned\n * about, as before.\n */\nfunction loadFixtures(llm: LLMock, fixturePath: string): void {\n try {\n const stat = statSync(fixturePath);\n if (stat.isDirectory()) {\n llm.loadFixtureDir(fixturePath);\n } else {\n llm.loadFixtureFile(fixturePath);\n }\n } catch (err) {\n if (err instanceof FixtureLoadError || err instanceof MisbehaviorConfigError) throw err;\n console.warn(\n `[aimock] Failed to load fixtures from ${fixturePath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n}\n\nexport { LLMock } from \"./llmock.js\";\nexport type { MockServerOptions, Fixture } from \"./types.js\";\nexport type { FakesForOptions, FakesReportMode, FakesTarget } from \"./fakes-plugin.js\";\nexport type { McpFakesReport } from \"./mcp-fakes-report.js\";\n"],"mappings":"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"}
package/dist/watcher.cjs CHANGED
@@ -1,11 +1,12 @@
1
1
  const require_runtime = require('./_virtual/_rolldown/runtime.cjs');
2
2
  const require_fixture_loader = require('./fixture-loader.cjs');
3
3
  let node_fs = require("node:fs");
4
+ let node_path = require("node:path");
4
5
 
5
6
  //#region src/watcher.ts
6
7
  const DEBOUNCE_MS = 500;
7
8
  function watchFixtures(fixturePath, fixtures, loadFn, opts) {
8
- const { logger, validate, validateFn } = opts;
9
+ const { logger, validate, validateFn, ignore } = opts;
9
10
  let debounceTimer = null;
10
11
  function reload() {
11
12
  logger.info(`File changed — reloading fixtures from ${fixturePath}...`);
@@ -36,7 +37,10 @@ function watchFixtures(fixturePath, fixtures, loadFn, opts) {
36
37
  fixtures.push(...newFixtures);
37
38
  logger.info(`Reloaded ${newFixtures.length} fixture(s)`);
38
39
  }
39
- const watcher = (0, node_fs.watch)(fixturePath, { recursive: true }, () => {
40
+ const watcher = (0, node_fs.watch)(fixturePath, { recursive: true }, (_event, filename) => {
41
+ if (ignore && filename) try {
42
+ if (ignore((0, node_path.join)(fixturePath, filename))) return;
43
+ } catch {}
40
44
  if (debounceTimer) clearTimeout(debounceTimer);
41
45
  debounceTimer = setTimeout(reload, DEBOUNCE_MS);
42
46
  });
@@ -1 +1 @@
1
- {"version":3,"file":"watcher.cjs","names":[],"sources":["../src/watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from \"node:fs\";\nimport type { Fixture } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport { clearFixtureQueue, type ValidationResult } from \"./fixture-loader.js\";\n\nconst DEBOUNCE_MS = 500;\n\nexport function watchFixtures(\n fixturePath: string,\n fixtures: Fixture[],\n loadFn: () => Fixture[],\n opts: {\n logger: Logger;\n validate?: boolean;\n validateFn?: (fixtures: Fixture[]) => ValidationResult[];\n },\n): { close: () => void } {\n const { logger, validate, validateFn } = opts;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n function reload() {\n logger.info(`File changed — reloading fixtures from ${fixturePath}...`);\n\n let newFixtures: Fixture[];\n try {\n newFixtures = loadFn();\n } catch (err) {\n logger.error(\"Failed to reload fixtures:\", err);\n logger.error(\"Previous fixtures remain active. Fix the error and save again to retry.\");\n return;\n }\n\n if (newFixtures.length === 0 && fixtures.length > 0) {\n logger.warn(\n \"Reload produced 0 fixtures — keeping previous fixtures. Check fixture file for errors.\",\n );\n return;\n }\n\n if (validate && validateFn) {\n const results = validateFn(newFixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n\n if (errors.length > 0) {\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n logger.error(`${errors.length} validation error(s) — keeping previous fixtures`);\n return;\n }\n }\n\n // Replace in-place to preserve array reference identity. Same door as\n // reset: a one-shot claim parked across the reload must not re-arm into it.\n clearFixtureQueue(fixtures);\n fixtures.push(...newFixtures);\n logger.info(`Reloaded ${newFixtures.length} fixture(s)`);\n }\n\n const watcher: FSWatcher = watch(fixturePath, { recursive: true }, () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(reload, DEBOUNCE_MS);\n });\n\n watcher.on(\"error\", (err: Error) => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = null;\n try {\n watcher.close();\n } catch {\n /* already dead */\n }\n logger.error(`File watcher error on ${fixturePath}: ${err.message}`);\n logger.error(\"Fixture auto-reload is no longer active. Restart the server to resume watching.\");\n });\n\n return {\n close() {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n },\n };\n}\n"],"mappings":";;;;;AAKA,MAAM,cAAc;AAEpB,SAAgB,cACd,aACA,UACA,QACA,MAKuB;CACvB,MAAM,EAAE,QAAQ,UAAU,eAAe;CACzC,IAAI,gBAAsD;CAE1D,SAAS,SAAS;AAChB,SAAO,KAAK,0CAA0C,YAAY,KAAK;EAEvE,IAAI;AACJ,MAAI;AACF,iBAAc,QAAQ;WACf,KAAK;AACZ,UAAO,MAAM,8BAA8B,IAAI;AAC/C,UAAO,MAAM,0EAA0E;AACvF;;AAGF,MAAI,YAAY,WAAW,KAAK,SAAS,SAAS,GAAG;AACnD,UAAO,KACL,yFACD;AACD;;AAGF,MAAI,YAAY,YAAY;GAC1B,MAAM,UAAU,WAAW,YAAY;GACvC,MAAM,SAAS,QAAQ,QAAQ,MAAM,EAAE,aAAa,QAAQ;GAC5D,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,UAAU;AAEhE,QAAK,MAAM,KAAK,SACd,QAAO,KAAK,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAGxD,OAAI,OAAO,SAAS,GAAG;AACrB,SAAK,MAAM,KAAK,OACd,QAAO,MAAM,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAEzD,WAAO,MAAM,GAAG,OAAO,OAAO,kDAAkD;AAChF;;;AAMJ,2CAAkB,SAAS;AAC3B,WAAS,KAAK,GAAG,YAAY;AAC7B,SAAO,KAAK,YAAY,YAAY,OAAO,aAAa;;CAG1D,MAAM,6BAA2B,aAAa,EAAE,WAAW,MAAM,QAAQ;AACvE,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB,WAAW,QAAQ,YAAY;GAC/C;AAEF,SAAQ,GAAG,UAAU,QAAe;AAClC,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB;AAChB,MAAI;AACF,WAAQ,OAAO;UACT;AAGR,SAAO,MAAM,yBAAyB,YAAY,IAAI,IAAI,UAAU;AACpE,SAAO,MAAM,kFAAkF;GAC/F;AAEF,QAAO,EACL,QAAQ;AACN,MAAI,cAAe,cAAa,cAAc;AAC9C,UAAQ,OAAO;IAElB"}
1
+ {"version":3,"file":"watcher.cjs","names":[],"sources":["../src/watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Fixture } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport { clearFixtureQueue, type ValidationResult } from \"./fixture-loader.js\";\n\nconst DEBOUNCE_MS = 500;\n\nexport function watchFixtures(\n fixturePath: string,\n fixtures: Fixture[],\n loadFn: () => Fixture[],\n opts: {\n logger: Logger;\n validate?: boolean;\n validateFn?: (fixtures: Fixture[]) => ValidationResult[];\n /** MR13: an event for a path this returns true for does not reload. */\n ignore?: (absPath: string) => boolean;\n },\n): { close: () => void } {\n const { logger, validate, validateFn, ignore } = opts;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n function reload() {\n logger.info(`File changed — reloading fixtures from ${fixturePath}...`);\n\n let newFixtures: Fixture[];\n try {\n newFixtures = loadFn();\n } catch (err) {\n logger.error(\"Failed to reload fixtures:\", err);\n logger.error(\"Previous fixtures remain active. Fix the error and save again to retry.\");\n return;\n }\n\n if (newFixtures.length === 0 && fixtures.length > 0) {\n logger.warn(\n \"Reload produced 0 fixtures — keeping previous fixtures. Check fixture file for errors.\",\n );\n return;\n }\n\n if (validate && validateFn) {\n const results = validateFn(newFixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n\n if (errors.length > 0) {\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n logger.error(`${errors.length} validation error(s) — keeping previous fixtures`);\n return;\n }\n }\n\n // Replace in-place to preserve array reference identity. Same door as\n // reset: a one-shot claim parked across the reload must not re-arm into it.\n clearFixtureQueue(fixtures);\n fixtures.push(...newFixtures);\n logger.info(`Reloaded ${newFixtures.length} fixture(s)`);\n }\n\n const watcher: FSWatcher = watch(fixturePath, { recursive: true }, (_event, filename) => {\n if (ignore && filename) {\n try {\n if (ignore(join(fixturePath, filename))) return;\n } catch {\n /* a failing ignore never kills the watcher: reload */\n }\n }\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(reload, DEBOUNCE_MS);\n });\n\n watcher.on(\"error\", (err: Error) => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = null;\n try {\n watcher.close();\n } catch {\n /* already dead */\n }\n logger.error(`File watcher error on ${fixturePath}: ${err.message}`);\n logger.error(\"Fixture auto-reload is no longer active. Restart the server to resume watching.\");\n });\n\n return {\n close() {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n },\n };\n}\n"],"mappings":";;;;;;AAMA,MAAM,cAAc;AAEpB,SAAgB,cACd,aACA,UACA,QACA,MAOuB;CACvB,MAAM,EAAE,QAAQ,UAAU,YAAY,WAAW;CACjD,IAAI,gBAAsD;CAE1D,SAAS,SAAS;AAChB,SAAO,KAAK,0CAA0C,YAAY,KAAK;EAEvE,IAAI;AACJ,MAAI;AACF,iBAAc,QAAQ;WACf,KAAK;AACZ,UAAO,MAAM,8BAA8B,IAAI;AAC/C,UAAO,MAAM,0EAA0E;AACvF;;AAGF,MAAI,YAAY,WAAW,KAAK,SAAS,SAAS,GAAG;AACnD,UAAO,KACL,yFACD;AACD;;AAGF,MAAI,YAAY,YAAY;GAC1B,MAAM,UAAU,WAAW,YAAY;GACvC,MAAM,SAAS,QAAQ,QAAQ,MAAM,EAAE,aAAa,QAAQ;GAC5D,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,UAAU;AAEhE,QAAK,MAAM,KAAK,SACd,QAAO,KAAK,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAGxD,OAAI,OAAO,SAAS,GAAG;AACrB,SAAK,MAAM,KAAK,OACd,QAAO,MAAM,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAEzD,WAAO,MAAM,GAAG,OAAO,OAAO,kDAAkD;AAChF;;;AAMJ,2CAAkB,SAAS;AAC3B,WAAS,KAAK,GAAG,YAAY;AAC7B,SAAO,KAAK,YAAY,YAAY,OAAO,aAAa;;CAG1D,MAAM,6BAA2B,aAAa,EAAE,WAAW,MAAM,GAAG,QAAQ,aAAa;AACvF,MAAI,UAAU,SACZ,KAAI;AACF,OAAI,2BAAY,aAAa,SAAS,CAAC,CAAE;UACnC;AAIV,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB,WAAW,QAAQ,YAAY;GAC/C;AAEF,SAAQ,GAAG,UAAU,QAAe;AAClC,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB;AAChB,MAAI;AACF,WAAQ,OAAO;UACT;AAGR,SAAO,MAAM,yBAAyB,YAAY,IAAI,IAAI,UAAU;AACpE,SAAO,MAAM,kFAAkF;GAC/F;AAEF,QAAO,EACL,QAAQ;AACN,MAAI,cAAe,cAAa,cAAc;AAC9C,UAAQ,OAAO;IAElB"}
package/dist/watcher.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { clearFixtureQueue } from "./fixture-loader.js";
2
2
  import { watch } from "node:fs";
3
+ import { join } from "node:path";
3
4
 
4
5
  //#region src/watcher.ts
5
6
  const DEBOUNCE_MS = 500;
6
7
  function watchFixtures(fixturePath, fixtures, loadFn, opts) {
7
- const { logger, validate, validateFn } = opts;
8
+ const { logger, validate, validateFn, ignore } = opts;
8
9
  let debounceTimer = null;
9
10
  function reload() {
10
11
  logger.info(`File changed — reloading fixtures from ${fixturePath}...`);
@@ -35,7 +36,10 @@ function watchFixtures(fixturePath, fixtures, loadFn, opts) {
35
36
  fixtures.push(...newFixtures);
36
37
  logger.info(`Reloaded ${newFixtures.length} fixture(s)`);
37
38
  }
38
- const watcher = watch(fixturePath, { recursive: true }, () => {
39
+ const watcher = watch(fixturePath, { recursive: true }, (_event, filename) => {
40
+ if (ignore && filename) try {
41
+ if (ignore(join(fixturePath, filename))) return;
42
+ } catch {}
39
43
  if (debounceTimer) clearTimeout(debounceTimer);
40
44
  debounceTimer = setTimeout(reload, DEBOUNCE_MS);
41
45
  });
@@ -1 +1 @@
1
- {"version":3,"file":"watcher.js","names":[],"sources":["../src/watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from \"node:fs\";\nimport type { Fixture } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport { clearFixtureQueue, type ValidationResult } from \"./fixture-loader.js\";\n\nconst DEBOUNCE_MS = 500;\n\nexport function watchFixtures(\n fixturePath: string,\n fixtures: Fixture[],\n loadFn: () => Fixture[],\n opts: {\n logger: Logger;\n validate?: boolean;\n validateFn?: (fixtures: Fixture[]) => ValidationResult[];\n },\n): { close: () => void } {\n const { logger, validate, validateFn } = opts;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n function reload() {\n logger.info(`File changed — reloading fixtures from ${fixturePath}...`);\n\n let newFixtures: Fixture[];\n try {\n newFixtures = loadFn();\n } catch (err) {\n logger.error(\"Failed to reload fixtures:\", err);\n logger.error(\"Previous fixtures remain active. Fix the error and save again to retry.\");\n return;\n }\n\n if (newFixtures.length === 0 && fixtures.length > 0) {\n logger.warn(\n \"Reload produced 0 fixtures — keeping previous fixtures. Check fixture file for errors.\",\n );\n return;\n }\n\n if (validate && validateFn) {\n const results = validateFn(newFixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n\n if (errors.length > 0) {\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n logger.error(`${errors.length} validation error(s) — keeping previous fixtures`);\n return;\n }\n }\n\n // Replace in-place to preserve array reference identity. Same door as\n // reset: a one-shot claim parked across the reload must not re-arm into it.\n clearFixtureQueue(fixtures);\n fixtures.push(...newFixtures);\n logger.info(`Reloaded ${newFixtures.length} fixture(s)`);\n }\n\n const watcher: FSWatcher = watch(fixturePath, { recursive: true }, () => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(reload, DEBOUNCE_MS);\n });\n\n watcher.on(\"error\", (err: Error) => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = null;\n try {\n watcher.close();\n } catch {\n /* already dead */\n }\n logger.error(`File watcher error on ${fixturePath}: ${err.message}`);\n logger.error(\"Fixture auto-reload is no longer active. Restart the server to resume watching.\");\n });\n\n return {\n close() {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n },\n };\n}\n"],"mappings":";;;;AAKA,MAAM,cAAc;AAEpB,SAAgB,cACd,aACA,UACA,QACA,MAKuB;CACvB,MAAM,EAAE,QAAQ,UAAU,eAAe;CACzC,IAAI,gBAAsD;CAE1D,SAAS,SAAS;AAChB,SAAO,KAAK,0CAA0C,YAAY,KAAK;EAEvE,IAAI;AACJ,MAAI;AACF,iBAAc,QAAQ;WACf,KAAK;AACZ,UAAO,MAAM,8BAA8B,IAAI;AAC/C,UAAO,MAAM,0EAA0E;AACvF;;AAGF,MAAI,YAAY,WAAW,KAAK,SAAS,SAAS,GAAG;AACnD,UAAO,KACL,yFACD;AACD;;AAGF,MAAI,YAAY,YAAY;GAC1B,MAAM,UAAU,WAAW,YAAY;GACvC,MAAM,SAAS,QAAQ,QAAQ,MAAM,EAAE,aAAa,QAAQ;GAC5D,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,UAAU;AAEhE,QAAK,MAAM,KAAK,SACd,QAAO,KAAK,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAGxD,OAAI,OAAO,SAAS,GAAG;AACrB,SAAK,MAAM,KAAK,OACd,QAAO,MAAM,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAEzD,WAAO,MAAM,GAAG,OAAO,OAAO,kDAAkD;AAChF;;;AAMJ,oBAAkB,SAAS;AAC3B,WAAS,KAAK,GAAG,YAAY;AAC7B,SAAO,KAAK,YAAY,YAAY,OAAO,aAAa;;CAG1D,MAAM,UAAqB,MAAM,aAAa,EAAE,WAAW,MAAM,QAAQ;AACvE,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB,WAAW,QAAQ,YAAY;GAC/C;AAEF,SAAQ,GAAG,UAAU,QAAe;AAClC,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB;AAChB,MAAI;AACF,WAAQ,OAAO;UACT;AAGR,SAAO,MAAM,yBAAyB,YAAY,IAAI,IAAI,UAAU;AACpE,SAAO,MAAM,kFAAkF;GAC/F;AAEF,QAAO,EACL,QAAQ;AACN,MAAI,cAAe,cAAa,cAAc;AAC9C,UAAQ,OAAO;IAElB"}
1
+ {"version":3,"file":"watcher.js","names":[],"sources":["../src/watcher.ts"],"sourcesContent":["import { watch, type FSWatcher } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { Fixture } from \"./types.js\";\nimport type { Logger } from \"./logger.js\";\nimport { clearFixtureQueue, type ValidationResult } from \"./fixture-loader.js\";\n\nconst DEBOUNCE_MS = 500;\n\nexport function watchFixtures(\n fixturePath: string,\n fixtures: Fixture[],\n loadFn: () => Fixture[],\n opts: {\n logger: Logger;\n validate?: boolean;\n validateFn?: (fixtures: Fixture[]) => ValidationResult[];\n /** MR13: an event for a path this returns true for does not reload. */\n ignore?: (absPath: string) => boolean;\n },\n): { close: () => void } {\n const { logger, validate, validateFn, ignore } = opts;\n let debounceTimer: ReturnType<typeof setTimeout> | null = null;\n\n function reload() {\n logger.info(`File changed — reloading fixtures from ${fixturePath}...`);\n\n let newFixtures: Fixture[];\n try {\n newFixtures = loadFn();\n } catch (err) {\n logger.error(\"Failed to reload fixtures:\", err);\n logger.error(\"Previous fixtures remain active. Fix the error and save again to retry.\");\n return;\n }\n\n if (newFixtures.length === 0 && fixtures.length > 0) {\n logger.warn(\n \"Reload produced 0 fixtures — keeping previous fixtures. Check fixture file for errors.\",\n );\n return;\n }\n\n if (validate && validateFn) {\n const results = validateFn(newFixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n\n if (errors.length > 0) {\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n logger.error(`${errors.length} validation error(s) — keeping previous fixtures`);\n return;\n }\n }\n\n // Replace in-place to preserve array reference identity. Same door as\n // reset: a one-shot claim parked across the reload must not re-arm into it.\n clearFixtureQueue(fixtures);\n fixtures.push(...newFixtures);\n logger.info(`Reloaded ${newFixtures.length} fixture(s)`);\n }\n\n const watcher: FSWatcher = watch(fixturePath, { recursive: true }, (_event, filename) => {\n if (ignore && filename) {\n try {\n if (ignore(join(fixturePath, filename))) return;\n } catch {\n /* a failing ignore never kills the watcher: reload */\n }\n }\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = setTimeout(reload, DEBOUNCE_MS);\n });\n\n watcher.on(\"error\", (err: Error) => {\n if (debounceTimer) clearTimeout(debounceTimer);\n debounceTimer = null;\n try {\n watcher.close();\n } catch {\n /* already dead */\n }\n logger.error(`File watcher error on ${fixturePath}: ${err.message}`);\n logger.error(\"Fixture auto-reload is no longer active. Restart the server to resume watching.\");\n });\n\n return {\n close() {\n if (debounceTimer) clearTimeout(debounceTimer);\n watcher.close();\n },\n };\n}\n"],"mappings":";;;;;AAMA,MAAM,cAAc;AAEpB,SAAgB,cACd,aACA,UACA,QACA,MAOuB;CACvB,MAAM,EAAE,QAAQ,UAAU,YAAY,WAAW;CACjD,IAAI,gBAAsD;CAE1D,SAAS,SAAS;AAChB,SAAO,KAAK,0CAA0C,YAAY,KAAK;EAEvE,IAAI;AACJ,MAAI;AACF,iBAAc,QAAQ;WACf,KAAK;AACZ,UAAO,MAAM,8BAA8B,IAAI;AAC/C,UAAO,MAAM,0EAA0E;AACvF;;AAGF,MAAI,YAAY,WAAW,KAAK,SAAS,SAAS,GAAG;AACnD,UAAO,KACL,yFACD;AACD;;AAGF,MAAI,YAAY,YAAY;GAC1B,MAAM,UAAU,WAAW,YAAY;GACvC,MAAM,SAAS,QAAQ,QAAQ,MAAM,EAAE,aAAa,QAAQ;GAC5D,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,aAAa,UAAU;AAEhE,QAAK,MAAM,KAAK,SACd,QAAO,KAAK,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAGxD,OAAI,OAAO,SAAS,GAAG;AACrB,SAAK,MAAM,KAAK,OACd,QAAO,MAAM,WAAW,EAAE,aAAa,IAAI,EAAE,UAAU;AAEzD,WAAO,MAAM,GAAG,OAAO,OAAO,kDAAkD;AAChF;;;AAMJ,oBAAkB,SAAS;AAC3B,WAAS,KAAK,GAAG,YAAY;AAC7B,SAAO,KAAK,YAAY,YAAY,OAAO,aAAa;;CAG1D,MAAM,UAAqB,MAAM,aAAa,EAAE,WAAW,MAAM,GAAG,QAAQ,aAAa;AACvF,MAAI,UAAU,SACZ,KAAI;AACF,OAAI,OAAO,KAAK,aAAa,SAAS,CAAC,CAAE;UACnC;AAIV,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB,WAAW,QAAQ,YAAY;GAC/C;AAEF,SAAQ,GAAG,UAAU,QAAe;AAClC,MAAI,cAAe,cAAa,cAAc;AAC9C,kBAAgB;AAChB,MAAI;AACF,WAAQ,OAAO;UACT;AAGR,SAAO,MAAM,yBAAyB,YAAY,IAAI,IAAI,UAAU;AACpE,SAAO,MAAM,kFAAkF;GAC/F;AAEF,QAAO,EACL,QAAQ;AACN,MAAI,cAAe,cAAa,cAAc;AAC9C,UAAQ,OAAO;IAElB"}