@copilotkit/aimock 1.42.1 → 1.44.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 (442) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +65 -0
  4. package/README.md +108 -16
  5. package/dist/agui-handler.d.ts.map +1 -1
  6. package/dist/agui-mock.cjs +1 -1
  7. package/dist/agui-mock.cjs.map +1 -1
  8. package/dist/agui-mock.d.cts.map +1 -1
  9. package/dist/agui-mock.d.ts.map +1 -1
  10. package/dist/agui-mock.js +1 -1
  11. package/dist/agui-mock.js.map +1 -1
  12. package/dist/agui-recorder.cjs +33 -11
  13. package/dist/agui-recorder.cjs.map +1 -1
  14. package/dist/agui-recorder.d.cts +10 -1
  15. package/dist/agui-recorder.d.cts.map +1 -1
  16. package/dist/agui-recorder.d.ts +10 -1
  17. package/dist/agui-recorder.d.ts.map +1 -1
  18. package/dist/agui-recorder.js +33 -11
  19. package/dist/agui-recorder.js.map +1 -1
  20. package/dist/agui-types.d.cts +1 -0
  21. package/dist/agui-types.d.cts.map +1 -1
  22. package/dist/agui-types.d.ts +1 -0
  23. package/dist/agui-types.d.ts.map +1 -1
  24. package/dist/aimock-cli.cjs +38 -10
  25. package/dist/aimock-cli.cjs.map +1 -1
  26. package/dist/aimock-cli.d.cts +5 -0
  27. package/dist/aimock-cli.d.cts.map +1 -1
  28. package/dist/aimock-cli.d.ts +5 -0
  29. package/dist/aimock-cli.d.ts.map +1 -1
  30. package/dist/aimock-cli.js +38 -10
  31. package/dist/aimock-cli.js.map +1 -1
  32. package/dist/batches.cjs +458 -0
  33. package/dist/batches.cjs.map +1 -0
  34. package/dist/batches.d.cts +57 -0
  35. package/dist/batches.d.cts.map +1 -0
  36. package/dist/batches.d.ts +57 -0
  37. package/dist/batches.d.ts.map +1 -0
  38. package/dist/batches.js +453 -0
  39. package/dist/batches.js.map +1 -0
  40. package/dist/bedrock-converse.cjs +44 -12
  41. package/dist/bedrock-converse.cjs.map +1 -1
  42. package/dist/bedrock-converse.d.cts.map +1 -1
  43. package/dist/bedrock-converse.d.ts.map +1 -1
  44. package/dist/bedrock-converse.js +46 -14
  45. package/dist/bedrock-converse.js.map +1 -1
  46. package/dist/bedrock.cjs +10 -8
  47. package/dist/bedrock.cjs.map +1 -1
  48. package/dist/bedrock.d.cts.map +1 -1
  49. package/dist/bedrock.d.ts.map +1 -1
  50. package/dist/bedrock.js +12 -10
  51. package/dist/bedrock.js.map +1 -1
  52. package/dist/byteplus-video.cjs +3 -3
  53. package/dist/byteplus-video.cjs.map +1 -1
  54. package/dist/byteplus-video.js +4 -4
  55. package/dist/byteplus-video.js.map +1 -1
  56. package/dist/chaos.cjs +625 -72
  57. package/dist/chaos.cjs.map +1 -1
  58. package/dist/chaos.d.cts +164 -7
  59. package/dist/chaos.d.cts.map +1 -1
  60. package/dist/chaos.d.ts +164 -7
  61. package/dist/chaos.d.ts.map +1 -1
  62. package/dist/chaos.js +616 -74
  63. package/dist/chaos.js.map +1 -1
  64. package/dist/cli.cjs +131 -30
  65. package/dist/cli.cjs.map +1 -1
  66. package/dist/cli.js +132 -31
  67. package/dist/cli.js.map +1 -1
  68. package/dist/cohere.cjs +37 -9
  69. package/dist/cohere.cjs.map +1 -1
  70. package/dist/cohere.d.cts.map +1 -1
  71. package/dist/cohere.d.ts.map +1 -1
  72. package/dist/cohere.js +39 -11
  73. package/dist/cohere.js.map +1 -1
  74. package/dist/config-loader.cjs +1 -1
  75. package/dist/config-loader.js +1 -1
  76. package/dist/constants.cjs +8 -0
  77. package/dist/constants.cjs.map +1 -1
  78. package/dist/constants.d.cts +6 -2
  79. package/dist/constants.d.ts +6 -2
  80. package/dist/constants.js +8 -1
  81. package/dist/constants.js.map +1 -1
  82. package/dist/convert.cjs +1 -1
  83. package/dist/convert.cjs.map +1 -1
  84. package/dist/convert.js +1 -1
  85. package/dist/convert.js.map +1 -1
  86. package/dist/echo-text.cjs +65 -0
  87. package/dist/echo-text.cjs.map +1 -0
  88. package/dist/echo-text.d.cts +22 -0
  89. package/dist/echo-text.d.cts.map +1 -0
  90. package/dist/echo-text.d.ts +22 -0
  91. package/dist/echo-text.d.ts.map +1 -0
  92. package/dist/echo-text.js +65 -0
  93. package/dist/echo-text.js.map +1 -0
  94. package/dist/elevenlabs-audio.cjs +3 -3
  95. package/dist/elevenlabs-audio.cjs.map +1 -1
  96. package/dist/elevenlabs-audio.js +4 -4
  97. package/dist/elevenlabs-audio.js.map +1 -1
  98. package/dist/elevenlabs-voice.cjs +1053 -0
  99. package/dist/elevenlabs-voice.cjs.map +1 -0
  100. package/dist/elevenlabs-voice.d.cts +72 -0
  101. package/dist/elevenlabs-voice.d.cts.map +1 -0
  102. package/dist/elevenlabs-voice.d.ts +72 -0
  103. package/dist/elevenlabs-voice.d.ts.map +1 -0
  104. package/dist/elevenlabs-voice.js +1048 -0
  105. package/dist/elevenlabs-voice.js.map +1 -0
  106. package/dist/embeddings.cjs +26 -5
  107. package/dist/embeddings.cjs.map +1 -1
  108. package/dist/embeddings.d.cts +1 -1
  109. package/dist/embeddings.d.cts.map +1 -1
  110. package/dist/embeddings.d.ts +1 -1
  111. package/dist/embeddings.d.ts.map +1 -1
  112. package/dist/embeddings.js +27 -6
  113. package/dist/embeddings.js.map +1 -1
  114. package/dist/fal-audio.cjs +3 -3
  115. package/dist/fal-audio.cjs.map +1 -1
  116. package/dist/fal-audio.js +4 -4
  117. package/dist/fal-audio.js.map +1 -1
  118. package/dist/fal.cjs +28 -4
  119. package/dist/fal.cjs.map +1 -1
  120. package/dist/fal.d.cts +16 -1
  121. package/dist/fal.d.cts.map +1 -1
  122. package/dist/fal.d.ts +16 -1
  123. package/dist/fal.d.ts.map +1 -1
  124. package/dist/fal.js +28 -5
  125. package/dist/fal.js.map +1 -1
  126. package/dist/files.cjs +1069 -0
  127. package/dist/files.cjs.map +1 -0
  128. package/dist/files.d.cts +68 -0
  129. package/dist/files.d.cts.map +1 -0
  130. package/dist/files.d.ts +68 -0
  131. package/dist/files.d.ts.map +1 -0
  132. package/dist/files.js +1056 -0
  133. package/dist/files.js.map +1 -0
  134. package/dist/fine-tuning.cjs +1033 -0
  135. package/dist/fine-tuning.cjs.map +1 -0
  136. package/dist/fine-tuning.d.cts +96 -0
  137. package/dist/fine-tuning.d.cts.map +1 -0
  138. package/dist/fine-tuning.d.ts +96 -0
  139. package/dist/fine-tuning.d.ts.map +1 -0
  140. package/dist/fine-tuning.js +1026 -0
  141. package/dist/fine-tuning.js.map +1 -0
  142. package/dist/fixture-loader-services.cjs +116 -0
  143. package/dist/fixture-loader-services.cjs.map +1 -0
  144. package/dist/fixture-loader-services.d.cts +48 -0
  145. package/dist/fixture-loader-services.d.cts.map +1 -0
  146. package/dist/fixture-loader-services.d.ts +48 -0
  147. package/dist/fixture-loader-services.d.ts.map +1 -0
  148. package/dist/fixture-loader-services.js +114 -0
  149. package/dist/fixture-loader-services.js.map +1 -0
  150. package/dist/fixture-loader.cjs +0 -0
  151. package/dist/fixture-loader.cjs.map +1 -1
  152. package/dist/fixture-loader.d.cts +131 -5
  153. package/dist/fixture-loader.d.cts.map +1 -1
  154. package/dist/fixture-loader.d.ts +131 -5
  155. package/dist/fixture-loader.d.ts.map +1 -1
  156. package/dist/fixture-loader.js +0 -0
  157. package/dist/fixture-loader.js.map +1 -1
  158. package/dist/gemini-embeddings.cjs +44 -4
  159. package/dist/gemini-embeddings.cjs.map +1 -1
  160. package/dist/gemini-embeddings.js +46 -6
  161. package/dist/gemini-embeddings.js.map +1 -1
  162. package/dist/gemini-interactions.cjs +51 -7
  163. package/dist/gemini-interactions.cjs.map +1 -1
  164. package/dist/gemini-interactions.d.cts.map +1 -1
  165. package/dist/gemini-interactions.d.ts.map +1 -1
  166. package/dist/gemini-interactions.js +53 -9
  167. package/dist/gemini-interactions.js.map +1 -1
  168. package/dist/gemini.cjs +19 -8
  169. package/dist/gemini.cjs.map +1 -1
  170. package/dist/gemini.d.cts.map +1 -1
  171. package/dist/gemini.d.ts.map +1 -1
  172. package/dist/gemini.js +21 -10
  173. package/dist/gemini.js.map +1 -1
  174. package/dist/grok-video.cjs +14 -14
  175. package/dist/grok-video.cjs.map +1 -1
  176. package/dist/grok-video.js +15 -15
  177. package/dist/grok-video.js.map +1 -1
  178. package/dist/helpers.cjs +497 -10
  179. package/dist/helpers.cjs.map +1 -1
  180. package/dist/helpers.d.cts +38 -2
  181. package/dist/helpers.d.cts.map +1 -1
  182. package/dist/helpers.d.ts +38 -2
  183. package/dist/helpers.d.ts.map +1 -1
  184. package/dist/helpers.js +487 -12
  185. package/dist/helpers.js.map +1 -1
  186. package/dist/images.cjs +57 -125
  187. package/dist/images.cjs.map +1 -1
  188. package/dist/images.d.cts.map +1 -1
  189. package/dist/images.d.ts.map +1 -1
  190. package/dist/images.js +58 -126
  191. package/dist/images.js.map +1 -1
  192. package/dist/index.cjs +71 -9
  193. package/dist/index.d.cts +17 -7
  194. package/dist/index.d.ts +17 -7
  195. package/dist/index.js +21 -11
  196. package/dist/jest.cjs +24 -12
  197. package/dist/jest.cjs.map +1 -1
  198. package/dist/jest.d.cts +6 -3
  199. package/dist/jest.d.cts.map +1 -1
  200. package/dist/jest.d.ts +6 -3
  201. package/dist/jest.d.ts.map +1 -1
  202. package/dist/jest.js +25 -13
  203. package/dist/jest.js.map +1 -1
  204. package/dist/journal.cjs +61 -4
  205. package/dist/journal.cjs.map +1 -1
  206. package/dist/journal.d.cts +60 -3
  207. package/dist/journal.d.cts.map +1 -1
  208. package/dist/journal.d.ts +60 -3
  209. package/dist/journal.d.ts.map +1 -1
  210. package/dist/journal.js +61 -5
  211. package/dist/journal.js.map +1 -1
  212. package/dist/live-fixture.cjs +1192 -0
  213. package/dist/live-fixture.cjs.map +1 -0
  214. package/dist/live-fixture.js +1187 -0
  215. package/dist/live-fixture.js.map +1 -0
  216. package/dist/live-recorder.cjs +256 -0
  217. package/dist/live-recorder.cjs.map +1 -0
  218. package/dist/live-recorder.js +254 -0
  219. package/dist/live-recorder.js.map +1 -0
  220. package/dist/live-replay.cjs +286 -0
  221. package/dist/live-replay.cjs.map +1 -0
  222. package/dist/live-replay.js +285 -0
  223. package/dist/live-replay.js.map +1 -0
  224. package/dist/live-sanitize.cjs +196 -0
  225. package/dist/live-sanitize.cjs.map +1 -0
  226. package/dist/live-sanitize.js +196 -0
  227. package/dist/live-sanitize.js.map +1 -0
  228. package/dist/live-types.d.cts +71 -0
  229. package/dist/live-types.d.cts.map +1 -0
  230. package/dist/live-types.d.ts +71 -0
  231. package/dist/live-types.d.ts.map +1 -0
  232. package/dist/llmock.cjs +132 -49
  233. package/dist/llmock.cjs.map +1 -1
  234. package/dist/llmock.d.cts +51 -5
  235. package/dist/llmock.d.cts.map +1 -1
  236. package/dist/llmock.d.ts +51 -5
  237. package/dist/llmock.d.ts.map +1 -1
  238. package/dist/llmock.js +133 -50
  239. package/dist/llmock.js.map +1 -1
  240. package/dist/mcp-fakes-mount.cjs +216 -0
  241. package/dist/mcp-fakes-mount.cjs.map +1 -0
  242. package/dist/mcp-fakes-mount.js +210 -0
  243. package/dist/mcp-fakes-mount.js.map +1 -0
  244. package/dist/mcp-fakes.cjs +1619 -0
  245. package/dist/mcp-fakes.cjs.map +1 -0
  246. package/dist/mcp-fakes.d.cts +101 -0
  247. package/dist/mcp-fakes.d.cts.map +1 -0
  248. package/dist/mcp-fakes.d.ts +101 -0
  249. package/dist/mcp-fakes.d.ts.map +1 -0
  250. package/dist/mcp-fakes.js +1614 -0
  251. package/dist/mcp-fakes.js.map +1 -0
  252. package/dist/mcp-handler.cjs +331 -17
  253. package/dist/mcp-handler.cjs.map +1 -1
  254. package/dist/mcp-handler.js +330 -18
  255. package/dist/mcp-handler.js.map +1 -1
  256. package/dist/mcp-mock.cjs +217 -49
  257. package/dist/mcp-mock.cjs.map +1 -1
  258. package/dist/mcp-mock.d.cts +66 -1
  259. package/dist/mcp-mock.d.cts.map +1 -1
  260. package/dist/mcp-mock.d.ts +66 -1
  261. package/dist/mcp-mock.d.ts.map +1 -1
  262. package/dist/mcp-mock.js +218 -50
  263. package/dist/mcp-mock.js.map +1 -1
  264. package/dist/mcp-stub.cjs +5 -1
  265. package/dist/mcp-stub.d.cts +3 -1
  266. package/dist/mcp-stub.d.ts +3 -1
  267. package/dist/mcp-stub.js +3 -1
  268. package/dist/mcp-types.d.cts +6 -0
  269. package/dist/mcp-types.d.ts +6 -0
  270. package/dist/message-text.cjs +192 -0
  271. package/dist/message-text.cjs.map +1 -0
  272. package/dist/message-text.d.cts +31 -0
  273. package/dist/message-text.d.cts.map +1 -0
  274. package/dist/message-text.d.ts +31 -0
  275. package/dist/message-text.d.ts.map +1 -0
  276. package/dist/message-text.js +184 -0
  277. package/dist/message-text.js.map +1 -0
  278. package/dist/messages.cjs +49 -7
  279. package/dist/messages.cjs.map +1 -1
  280. package/dist/messages.d.cts.map +1 -1
  281. package/dist/messages.d.ts.map +1 -1
  282. package/dist/messages.js +51 -9
  283. package/dist/messages.js.map +1 -1
  284. package/dist/metrics.cjs +278 -45
  285. package/dist/metrics.cjs.map +1 -1
  286. package/dist/metrics.d.cts +17 -7
  287. package/dist/metrics.d.cts.map +1 -1
  288. package/dist/metrics.d.ts +17 -7
  289. package/dist/metrics.d.ts.map +1 -1
  290. package/dist/metrics.js +270 -29
  291. package/dist/metrics.js.map +1 -1
  292. package/dist/moderation.cjs +10 -1
  293. package/dist/moderation.cjs.map +1 -1
  294. package/dist/moderation.d.cts.map +1 -1
  295. package/dist/moderation.d.ts.map +1 -1
  296. package/dist/moderation.js +10 -1
  297. package/dist/moderation.js.map +1 -1
  298. package/dist/ollama.cjs +10 -6
  299. package/dist/ollama.cjs.map +1 -1
  300. package/dist/ollama.d.cts.map +1 -1
  301. package/dist/ollama.d.ts.map +1 -1
  302. package/dist/ollama.js +11 -7
  303. package/dist/ollama.js.map +1 -1
  304. package/dist/openapi.cjs +3405 -0
  305. package/dist/openapi.cjs.map +1 -0
  306. package/dist/openapi.d.cts +30 -0
  307. package/dist/openapi.d.cts.map +1 -0
  308. package/dist/openapi.d.ts +30 -0
  309. package/dist/openapi.d.ts.map +1 -0
  310. package/dist/openapi.js +3404 -0
  311. package/dist/openapi.js.map +1 -0
  312. package/dist/openrouter-video.cjs +9 -9
  313. package/dist/openrouter-video.cjs.map +1 -1
  314. package/dist/openrouter-video.d.cts.map +1 -1
  315. package/dist/openrouter-video.d.ts.map +1 -1
  316. package/dist/openrouter-video.js +10 -10
  317. package/dist/openrouter-video.js.map +1 -1
  318. package/dist/recorder.cjs +54 -10
  319. package/dist/recorder.cjs.map +1 -1
  320. package/dist/recorder.d.cts.map +1 -1
  321. package/dist/recorder.d.ts.map +1 -1
  322. package/dist/recorder.js +52 -12
  323. package/dist/recorder.js.map +1 -1
  324. package/dist/responses.cjs +47 -7
  325. package/dist/responses.cjs.map +1 -1
  326. package/dist/responses.d.cts.map +1 -1
  327. package/dist/responses.d.ts.map +1 -1
  328. package/dist/responses.js +48 -9
  329. package/dist/responses.js.map +1 -1
  330. package/dist/route-registry.cjs +951 -0
  331. package/dist/route-registry.cjs.map +1 -0
  332. package/dist/route-registry.d.cts +33 -0
  333. package/dist/route-registry.d.cts.map +1 -0
  334. package/dist/route-registry.d.ts +33 -0
  335. package/dist/route-registry.d.ts.map +1 -0
  336. package/dist/route-registry.js +866 -0
  337. package/dist/route-registry.js.map +1 -0
  338. package/dist/router.cjs +21 -34
  339. package/dist/router.cjs.map +1 -1
  340. package/dist/router.d.cts +4 -1
  341. package/dist/router.d.cts.map +1 -1
  342. package/dist/router.d.ts +4 -1
  343. package/dist/router.d.ts.map +1 -1
  344. package/dist/router.js +21 -35
  345. package/dist/router.js.map +1 -1
  346. package/dist/server.cjs +1252 -777
  347. package/dist/server.cjs.map +1 -1
  348. package/dist/server.d.cts +5 -1
  349. package/dist/server.d.cts.map +1 -1
  350. package/dist/server.d.ts +5 -1
  351. package/dist/server.d.ts.map +1 -1
  352. package/dist/server.js +1185 -710
  353. package/dist/server.js.map +1 -1
  354. package/dist/speech.cjs +6 -4
  355. package/dist/speech.cjs.map +1 -1
  356. package/dist/speech.js +7 -5
  357. package/dist/speech.js.map +1 -1
  358. package/dist/suite.cjs +1 -1
  359. package/dist/suite.js +1 -1
  360. package/dist/transcription.cjs +6 -6
  361. package/dist/transcription.cjs.map +1 -1
  362. package/dist/transcription.d.cts.map +1 -1
  363. package/dist/transcription.d.ts.map +1 -1
  364. package/dist/transcription.js +7 -7
  365. package/dist/transcription.js.map +1 -1
  366. package/dist/types.cjs +18 -0
  367. package/dist/types.cjs.map +1 -0
  368. package/dist/types.d.cts +320 -17
  369. package/dist/types.d.cts.map +1 -1
  370. package/dist/types.d.ts +320 -17
  371. package/dist/types.d.ts.map +1 -1
  372. package/dist/types.js +17 -0
  373. package/dist/types.js.map +1 -0
  374. package/dist/validate-cli.cjs +983 -0
  375. package/dist/validate-cli.cjs.map +1 -0
  376. package/dist/validate-cli.js +982 -0
  377. package/dist/validate-cli.js.map +1 -0
  378. package/dist/vector-stores.cjs +1180 -0
  379. package/dist/vector-stores.cjs.map +1 -0
  380. package/dist/vector-stores.d.cts +106 -0
  381. package/dist/vector-stores.d.cts.map +1 -0
  382. package/dist/vector-stores.d.ts +106 -0
  383. package/dist/vector-stores.d.ts.map +1 -0
  384. package/dist/vector-stores.js +1164 -0
  385. package/dist/vector-stores.js.map +1 -0
  386. package/dist/vector-types.d.cts.map +1 -1
  387. package/dist/vector-types.d.ts.map +1 -1
  388. package/dist/veo-video.cjs +13 -13
  389. package/dist/veo-video.cjs.map +1 -1
  390. package/dist/veo-video.js +14 -14
  391. package/dist/veo-video.js.map +1 -1
  392. package/dist/video.cjs +23 -4
  393. package/dist/video.cjs.map +1 -1
  394. package/dist/video.d.cts +1 -1
  395. package/dist/video.d.cts.map +1 -1
  396. package/dist/video.d.ts +1 -1
  397. package/dist/video.d.ts.map +1 -1
  398. package/dist/video.js +24 -5
  399. package/dist/video.js.map +1 -1
  400. package/dist/vitest.cjs +23 -11
  401. package/dist/vitest.cjs.map +1 -1
  402. package/dist/vitest.d.cts +5 -2
  403. package/dist/vitest.d.cts.map +1 -1
  404. package/dist/vitest.d.ts +5 -2
  405. package/dist/vitest.d.ts.map +1 -1
  406. package/dist/vitest.js +25 -13
  407. package/dist/vitest.js.map +1 -1
  408. package/dist/watcher.cjs +2 -1
  409. package/dist/watcher.cjs.map +1 -1
  410. package/dist/watcher.js +2 -1
  411. package/dist/watcher.js.map +1 -1
  412. package/dist/ws-framing.cjs +150 -35
  413. package/dist/ws-framing.cjs.map +1 -1
  414. package/dist/ws-framing.d.cts +18 -3
  415. package/dist/ws-framing.d.cts.map +1 -1
  416. package/dist/ws-framing.d.ts +18 -3
  417. package/dist/ws-framing.d.ts.map +1 -1
  418. package/dist/ws-framing.js +151 -36
  419. package/dist/ws-framing.js.map +1 -1
  420. package/dist/ws-gemini-live.cjs +2 -2
  421. package/dist/ws-gemini-live.js +2 -2
  422. package/dist/ws-live.cjs +401 -0
  423. package/dist/ws-live.cjs.map +1 -0
  424. package/dist/ws-live.js +400 -0
  425. package/dist/ws-live.js.map +1 -0
  426. package/dist/ws-realtime.cjs +46 -2
  427. package/dist/ws-realtime.cjs.map +1 -1
  428. package/dist/ws-realtime.d.cts.map +1 -1
  429. package/dist/ws-realtime.d.ts.map +1 -1
  430. package/dist/ws-realtime.js +46 -2
  431. package/dist/ws-realtime.js.map +1 -1
  432. package/dist/ws-responses.cjs +16 -4
  433. package/dist/ws-responses.cjs.map +1 -1
  434. package/dist/ws-responses.js +16 -4
  435. package/dist/ws-responses.js.map +1 -1
  436. package/dist/ws-upstream.cjs +118 -0
  437. package/dist/ws-upstream.cjs.map +1 -0
  438. package/dist/ws-upstream.js +115 -0
  439. package/dist/ws-upstream.js.map +1 -0
  440. package/fixtures/openai-live-client.json +390 -0
  441. package/fixtures/openai-live-managed.json +709 -0
  442. package/package.json +4 -1
package/dist/chaos.js CHANGED
@@ -1,10 +1,21 @@
1
- import { resolveTestId } from "./helpers.js";
2
- import { writeErrorResponse } from "./sse-writer.js";
1
+ import { DEFAULT_TEST_ID } from "./constants.js";
2
+ import { describeMatch, parseStrictIntegerText, resolveTestId } from "./helpers.js";
3
+ import { delay, writeErrorResponse } from "./sse-writer.js";
3
4
 
4
5
  //#region src/chaos.ts
5
6
  /**
6
- * Narrow the server-defaults argument. A `ChaosScope` is distinguishable from a
7
- * `ChaosConfig` by construction: the latter only ever carries the three rates.
7
+ * Narrow the server-defaults argument. The discrimination is STRUCTURAL: a
8
+ * `ChaosConfig` only ever carries the fault fields (`dropRate`,
9
+ * `malformedRate`, `rateLimitRate`, `disconnectRate`, `latencyMs`), so the
10
+ * presence of a `base` / `byTestId` key means a `ChaosScope`.
11
+ *
12
+ * Both members are optional, so `{}` satisfies BOTH types and is reported here
13
+ * as a config, not a scope. That is not a bug to route around: an empty scope
14
+ * and an empty config resolve to exactly the same thing (no chaos, nothing to
15
+ * select), so no caller can observe which branch ran. The server's own defaults
16
+ * getter (`src/server.ts`) always sets `byTestId`, so a real server's value is
17
+ * never the ambiguous one. A THIRD variant, or a scope whose emptiness became
18
+ * observable, would need a tagged discriminant instead of this shape test.
8
19
  */
9
20
  function isChaosScope(defaults) {
10
21
  return "base" in defaults || "byTestId" in defaults;
@@ -15,75 +26,520 @@ function isChaosScope(defaults) {
15
26
  * resolved by `resolveTestId` — the same helper `getTestId` and the control API
16
27
  * use — so a harness that tags by `?testId=` lands in the same scope its chaos
17
28
  * override was stored under, and the two sides cannot disagree.
29
+ *
30
+ * SELECTION, NOT MERGE. A scoped override REPLACES the baseline wholesale; it
31
+ * is not layered field-wise over it. A server started with `--chaos-latency
32
+ * 500` whose test installs `{ dropRate: 1 }` sees drops with NO latency on that
33
+ * test's traffic — restate `latencyMs` in the override to keep it. See
34
+ * `ChaosScope` in types.ts for why replacement is the contract (it is what
35
+ * separates `POST /__aimock/chaos {}` from `DELETE`), and note that the two
36
+ * links above this one — fixture and headers, applied below — DO merge
37
+ * field-wise. `POST`/`GET /__aimock/chaos` echo the config actually in effect
38
+ * for the scope, and the control API warns when an install drops a field that
39
+ * was in effect, so nothing is lost silently.
40
+ *
41
+ * PRESENCE of the entry is what selects, not its truthiness: the lookup asks
42
+ * `has`, not `get`. A map built with the key mapped to `undefined` (or to an
43
+ * empty object) is an override that was explicitly installed and says "no
44
+ * chaos for this test" — the `POST /__aimock/chaos {}` case — so it must win
45
+ * over `base` exactly like a populated one. Testing `get(...)` truthiness would
46
+ * quietly demote that one shape back to the baseline, which is the merge
47
+ * behaviour this contract exists to rule out.
18
48
  */
19
49
  function resolveScopedDefaults(serverDefaults, rawHeaders, url) {
20
50
  if (!serverDefaults) return void 0;
21
51
  if (!isChaosScope(serverDefaults)) return serverDefaults;
22
- const scoped = serverDefaults.byTestId?.get(resolveTestId(rawHeaders ?? {}, url));
23
- if (scoped) return scoped;
52
+ const testId = resolveTestId(rawHeaders ?? {}, url);
53
+ if (serverDefaults.byTestId?.has(testId)) return serverDefaults.byTestId.get(testId) ?? {};
24
54
  return serverDefaults.base;
25
55
  }
26
56
  /**
27
- * Resolve chaos config from headers, fixture, and server defaults.
28
- * Header values override fixture values, which override server defaults
29
- * (which are themselves per-testId — see `ChaosScope`).
57
+ * Every chaos field, with its accepted upper bound and its header spelling.
58
+ * ONE table: every source — request header, fixture chaos, server default, the
59
+ * `POST /__aimock/chaos` control API and the `--chaos-*` CLI flags — resolves a
60
+ * field through this entry via {@link parseChaosField}, so no source can drift
61
+ * onto its own bound or its own idea of which fields take a whole number.
62
+ *
63
+ * Exported for the two surfaces that live in other modules (`src/server.ts`'s
64
+ * control API, `src/cli.ts`'s flag parsing); in-module callers go through
65
+ * {@link parseChaosField} too.
30
66
  */
31
- function resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, url) {
32
- const base = { ...resolveScopedDefaults(serverDefaults, rawHeaders, url) };
33
- if (fixture?.chaos) {
34
- if (fixture.chaos.dropRate !== void 0) base.dropRate = fixture.chaos.dropRate;
35
- if (fixture.chaos.malformedRate !== void 0) base.malformedRate = fixture.chaos.malformedRate;
36
- if (fixture.chaos.disconnectRate !== void 0) base.disconnectRate = fixture.chaos.disconnectRate;
67
+ const CHAOS_FIELDS = {
68
+ dropRate: {
69
+ header: "x-aimock-chaos-drop",
70
+ max: 1,
71
+ integer: false
72
+ },
73
+ malformedRate: {
74
+ header: "x-aimock-chaos-malformed",
75
+ max: 1,
76
+ integer: false
77
+ },
78
+ disconnectRate: {
79
+ header: "x-aimock-chaos-disconnect",
80
+ max: 1,
81
+ integer: false
82
+ },
83
+ rateLimitRate: {
84
+ header: "x-aimock-chaos-ratelimit",
85
+ max: 1,
86
+ integer: false
87
+ },
88
+ latencyMs: {
89
+ header: "x-aimock-chaos-latency",
90
+ max: 3e4,
91
+ integer: true
37
92
  }
38
- if (rawHeaders) {
39
- const dropHeader = rawHeaders["x-aimock-chaos-drop"];
40
- const malformedHeader = rawHeaders["x-aimock-chaos-malformed"];
41
- const disconnectHeader = rawHeaders["x-aimock-chaos-disconnect"];
42
- if (typeof dropHeader === "string") {
43
- const val = parseFloat(dropHeader);
44
- if (isNaN(val)) logger?.warn(`[chaos] x-aimock-chaos-drop: invalid value "${dropHeader}", ignoring`);
45
- else {
46
- if (val < 0 || val > 1) logger?.warn(`[chaos] x-aimock-chaos-drop: value ${val} out of range [0,1], clamping`);
47
- base.dropRate = Math.min(1, Math.max(0, val));
48
- }
49
- }
50
- if (typeof malformedHeader === "string") {
51
- const val = parseFloat(malformedHeader);
52
- if (isNaN(val)) logger?.warn(`[chaos] x-aimock-chaos-malformed: invalid value "${malformedHeader}", ignoring`);
53
- else {
54
- if (val < 0 || val > 1) logger?.warn(`[chaos] x-aimock-chaos-malformed: value ${val} out of range [0,1], clamping`);
55
- base.malformedRate = Math.min(1, Math.max(0, val));
56
- }
93
+ };
94
+ const CHAOS_FIELD_NAMES = Object.keys(CHAOS_FIELDS);
95
+ /**
96
+ * A plain decimal RATE: digits with an optional fractional part, and nothing
97
+ * else. Deliberately NOT what `Number()` accepts — `Number` is a full-string
98
+ * parse (unlike `parseFloat`, which would take the `0.5` out of `"0.5abc"`),
99
+ * but it is also a JS *literal* parse, so it happily reads `"0x1e"` as 30,
100
+ * `"0b1"` as 1, `"1e3"` as 1000 and `"Infinity"` as Infinity. None of those is
101
+ * a rate a caller meant to write, and `0x1` silently becoming `dropRate: 1`
102
+ * turns a typo into a 100 % outage. A leading `+`/`-` sign, `_` separators and
103
+ * exponents are rejected for the same reason.
104
+ */
105
+ const DECIMAL_RATE = /^(?:\d+(?:\.\d*)?|\.\d+)$/;
106
+ /**
107
+ * The ONE way a chaos number is parsed, whatever its source.
108
+ *
109
+ * Two shapes, picked per field by the `CHAOS_FIELDS` table: a RATE is a plain
110
+ * decimal in `[0, max]` (max is always 1), and an INTEGER field (`latencyMs`)
111
+ * is a whole, non-negative count of milliseconds in `[0, max]`. `integer`
112
+ * defaults to false so a caller that omits it gets the rate grammar.
113
+ *
114
+ * The VALUE rules apply to every source alike: `NaN` and non-finite values, a
115
+ * fractional value for an integer field, negative zero, and anything outside
116
+ * `[0, max]` are all rejected (returned as `undefined`) whether they arrived as
117
+ * text or as a typed number. Nothing is ever clamped — a value the caller did
118
+ * not ask for is never substituted for the one they did. Negative zero is
119
+ * rejected rather than folded to `0` so that it matches its own text form
120
+ * (`"-0"`, which the sign-less grammar already refuses) and so that no `-0`
121
+ * escapes into a config object that later gets echoed back over JSON.
122
+ *
123
+ * The GRAMMAR above — the two regexes — is a rule about TEXT, and only a string
124
+ * input passes through it: trailing garbage (`"0.5abc"`), non-numeric text
125
+ * (`"banana"`), the empty string, a sign (`"+1"`, `"-0"`) and the JS
126
+ * numeric-literal forms that are not plain decimals (`"0x1e"`, `"0b1"`,
127
+ * `"1e3"`, `"Infinity"`) are rejected as spellings. A fixture or JSON body that
128
+ * writes `latencyMs: 1e3` has ALREADY been through a JSON/JS parser and hands
129
+ * this function the number `1000`, indistinguishable from a literal `1000` — so
130
+ * it is accepted, while the header `"1e3"` is not. That asymmetry is the point,
131
+ * not a gap: the grammar exists to stop an ambiguous *spelling* on the wire
132
+ * (`0x1` silently becoming `dropRate: 1` turns a typo into a 100 % outage),
133
+ * and a value that reached us as a number has no spelling left to police.
134
+ */
135
+ function parseChaosNumber(raw, max, integer = false) {
136
+ let value;
137
+ if (typeof raw === "number") value = raw;
138
+ else if (typeof raw === "string") if (integer) {
139
+ const n = parseStrictIntegerText(raw.trim());
140
+ if (n === null) return void 0;
141
+ value = n;
142
+ } else {
143
+ const text = raw.trim();
144
+ if (!DECIMAL_RATE.test(text)) return void 0;
145
+ value = Number(text);
146
+ }
147
+ else return;
148
+ if (!Number.isFinite(value)) return void 0;
149
+ if (integer && !Number.isInteger(value)) return void 0;
150
+ if (Object.is(value, -0)) return void 0;
151
+ if (value < 0 || value > max) return void 0;
152
+ return value;
153
+ }
154
+ /**
155
+ * Parse a value for a NAMED chaos field, taking its bound and its integer-ness
156
+ * from `CHAOS_FIELDS` instead of from the call site. Every entry point should
157
+ * call this rather than `parseChaosNumber` with hand-written limits — that is
158
+ * what makes the "ONE table" claim above true instead of aspirational.
159
+ */
160
+ function parseChaosField(field, raw) {
161
+ return parseChaosNumber(raw, CHAOS_FIELDS[field].max, CHAOS_FIELDS[field].integer);
162
+ }
163
+ /**
164
+ * The single value a repeated header carries. Node models a repeat two ways —
165
+ * as a `string[]`, and (for most headers) folded into one comma-joined string —
166
+ * and ONE rule covers both shapes, so the two can never diverge: split on
167
+ * commas, trim, drop empties, and the FIRST NON-EMPTY value wins.
168
+ *
169
+ * "Non-empty" is the load-bearing word. `x-aimock-chaos-latency: ` followed by
170
+ * `x-aimock-chaos-latency: 500` reaches the handler as `",500"` or
171
+ * `["", "500"]` depending on the shape; taking element `[0]` blindly made the
172
+ * folded form warn "rejected latencyMs value \"\"" while the array form went
173
+ * silently absent — two behaviours for one wire-level request. An empty value
174
+ * carries no instruction, so it is skipped rather than rejected.
175
+ *
176
+ * A repeat whose values DISAGREE is a real misconfiguration (two different
177
+ * rates asked for on one request), so it warns once and the first wins.
178
+ * "Disagree" is judged on what the values MEAN, not how they are spelled:
179
+ * `0.5, 0.50` is one rate written twice and says nothing worth warning about,
180
+ * whereas a TEXT comparison called it a conflict. Values the parser rejects
181
+ * have no meaning to compare, so they stand for themselves — two different
182
+ * typos still disagree, and a typo still disagrees with a valid number.
183
+ */
184
+ function firstHeaderValue(raw, field, logger) {
185
+ const values = (Array.isArray(raw) ? raw : typeof raw === "string" ? [raw] : []).flatMap((part) => part.split(",")).map((part) => part.trim()).filter((part) => part !== "");
186
+ const first = values[0];
187
+ if (first === void 0) return void 0;
188
+ const firstMeaning = headerValueMeaning(first, field);
189
+ if (values.some((value) => headerValueMeaning(value, field) !== firstMeaning)) logger?.warn(`[chaos] ${CHAOS_FIELDS[field].header}: repeated header with conflicting values ${JSON.stringify(values)} — using the first (${JSON.stringify(first)})`);
190
+ return first;
191
+ }
192
+ /**
193
+ * What one spelling of a header value MEANS, as a comparable token: the parsed
194
+ * number when the grammar accepts it, else the raw text. Prefixed so a rejected
195
+ * literal `"0.5"` could never compare equal to the parsed number 0.5.
196
+ */
197
+ function headerValueMeaning(text, field) {
198
+ const { max, integer } = CHAOS_FIELDS[field];
199
+ const value = parseChaosNumber(text, max, integer);
200
+ return value === void 0 ? `raw:${text}` : `num:${value}`;
201
+ }
202
+ /** Placeholder for a level that carries no config, so nothing can be warned about. */
203
+ const NO_SOURCE = { label: "(no chaos config)" };
204
+ /**
205
+ * Static chaos config (a fixture's `chaos`, a server default) is re-read on
206
+ * EVERY request, so an invalid value there used to warn on every request for
207
+ * the life of the process — thousands of identical lines for one typo. Latch
208
+ * those: once per distinct source+field+value, PER SERVER. Per-request HEADERS
209
+ * are dynamic input and keep warning per request, because each request really
210
+ * is a new caller making a new mistake.
211
+ *
212
+ * PER SERVER, not per process. The latch hangs off the server's `Logger` — one
213
+ * is constructed per `createServer` and every handler warns through
214
+ * `defaults.logger`, so the logger IS the server instance as far as this
215
+ * module can see it. A module-global `Set` instead meant two `LLMock`s in one
216
+ * process shared one latch: the second server never reported its own bad
217
+ * value, and one server's `resetChaosWarnings` re-armed the other's. A
218
+ * `WeakMap` so a stopped server's keys go with it — nothing else holds them.
219
+ *
220
+ * Cleared by `resetChaosWarnings`, which `performFullReset` calls alongside the
221
+ * other per-server state, so a suite that resets between tests sees the warning
222
+ * again rather than inheriting the previous test's latch.
223
+ */
224
+ const warnedStaticRejections = /* @__PURE__ */ new WeakMap();
225
+ /**
226
+ * Cap on distinct latched keys per server. Runtime chaos installs (`POST
227
+ * /__aimock/chaos`, once per test in a long-lived harness) can mint unboundedly
228
+ * many distinct bad values, and a latch that only ever grows is a leak. Past
229
+ * the cap the OLDEST key is evicted (a `Set` iterates in insertion order), so
230
+ * the worst case is that a long-since-warned typo warns a second time — never
231
+ * unbounded memory.
232
+ */
233
+ const MAX_LATCHED_REJECTIONS = 64;
234
+ /**
235
+ * Clear ONE server's static-config rejection latch, so the next request reports
236
+ * a bad static value again. See `warnedStaticRejections`.
237
+ *
238
+ * Takes that server's logger because the logger is what the latch is keyed by.
239
+ * There is deliberately no "clear every server" form: `performFullReset` is an
240
+ * isolation barrier for the server being reset, and reaching into a concurrent
241
+ * server's latch is exactly the cross-talk this scoping removes.
242
+ *
243
+ * Does NOT clear the `applyChaos` deprecation notice (`warnedSyncApplyChaos`),
244
+ * which is once per process by design — see the latch there.
245
+ */
246
+ function resetChaosWarnings(logger) {
247
+ warnedStaticRejections.delete(logger);
248
+ }
249
+ function warnRejected(source, field, raw, logger) {
250
+ if (!logger) return;
251
+ if (source.latchKey !== void 0) {
252
+ const key = `${source.latchKey}\u0000${field}\u0000${JSON.stringify(raw)}`;
253
+ let latched = warnedStaticRejections.get(logger);
254
+ if (!latched) {
255
+ latched = /* @__PURE__ */ new Set();
256
+ warnedStaticRejections.set(logger, latched);
57
257
  }
58
- if (typeof disconnectHeader === "string") {
59
- const val = parseFloat(disconnectHeader);
60
- if (isNaN(val)) logger?.warn(`[chaos] x-aimock-chaos-disconnect: invalid value "${disconnectHeader}", ignoring`);
61
- else {
62
- if (val < 0 || val > 1) logger?.warn(`[chaos] x-aimock-chaos-disconnect: value ${val} out of range [0,1], clamping`);
63
- base.disconnectRate = Math.min(1, Math.max(0, val));
64
- }
258
+ if (latched.has(key)) return;
259
+ if (latched.size >= MAX_LATCHED_REJECTIONS) {
260
+ const oldest = latched.values().next();
261
+ if (!oldest.done) latched.delete(oldest.value);
65
262
  }
263
+ latched.add(key);
264
+ }
265
+ const shape = CHAOS_FIELDS[field].integer ? "a whole number of ms" : "a number";
266
+ logger.warn(`[chaos] ${source.label}: rejected ${field} value ${JSON.stringify(raw)} — must be ${shape} in [0,${CHAOS_FIELDS[field].max}]; ignoring it`);
267
+ }
268
+ /**
269
+ * Serial number per fixture OBJECT, minted on demand, so a fixture's latch key
270
+ * is its identity rather than its (possibly colliding) description. Same reason
271
+ * `router.ts` keys its relaxed-turnIndex throttle by object identity. A
272
+ * `WeakMap`, so the serial dies with the fixture.
273
+ */
274
+ const fixtureSerials = /* @__PURE__ */ new WeakMap();
275
+ let nextFixtureSerial = 0;
276
+ /** Name and latch-identify the fixture whose `chaos` block is being read. */
277
+ function fixtureSource(fixture) {
278
+ let serial = fixtureSerials.get(fixture);
279
+ if (serial === void 0) {
280
+ serial = nextFixtureSerial++;
281
+ fixtureSerials.set(fixture, serial);
282
+ }
283
+ return {
284
+ label: `fixture chaos ${describeMatch(fixture.match, -1)}`,
285
+ latchKey: `fixture#${serial}`
286
+ };
287
+ }
288
+ /**
289
+ * Name and latch-identify the server-level config this request resolved to. A
290
+ * per-testId override and the server-wide baseline are DIFFERENT sources — a
291
+ * typo in one must not latch the other silent — so the testId is part of both
292
+ * the label and the key.
293
+ *
294
+ * Mirrors the selection in `resolveScopedDefaults` (which is the authority):
295
+ * the baseline is the scope's `DEFAULT_TEST_ID` entry, so an untagged request
296
+ * reads as the plain server default rather than an override named after an
297
+ * internal sentinel.
298
+ */
299
+ function serverSource(serverDefaults, rawHeaders, url) {
300
+ if (serverDefaults && isChaosScope(serverDefaults)) {
301
+ const testId = resolveTestId(rawHeaders ?? {}, url);
302
+ if (testId !== DEFAULT_TEST_ID && serverDefaults.byTestId?.has(testId)) return {
303
+ label: `server chaos default (testId ${JSON.stringify(testId)})`,
304
+ latchKey: `server:${testId}`
305
+ };
306
+ }
307
+ return {
308
+ label: "server chaos default",
309
+ latchKey: "server"
310
+ };
311
+ }
312
+ /** Read one field from the request headers. */
313
+ function readHeaderField(rawHeaders, field, logger) {
314
+ const text = firstHeaderValue(rawHeaders?.[CHAOS_FIELDS[field].header], field, logger);
315
+ if (text === void 0) return void 0;
316
+ const value = parseChaosField(field, text);
317
+ if (value === void 0) warnRejected({ label: CHAOS_FIELDS[field].header }, field, text, logger);
318
+ return value;
319
+ }
320
+ /** Read one field from a config object — a fixture's chaos, or the server default. */
321
+ function readConfigField(config, field, source, logger) {
322
+ const raw = config?.[field];
323
+ if (raw === void 0) return void 0;
324
+ const value = parseChaosField(field, raw);
325
+ if (value === void 0) warnRejected(source, field, raw, logger);
326
+ return value;
327
+ }
328
+ /**
329
+ * Resolve chaos config from headers, fixture, and server defaults.
330
+ * Header values override fixture values, which override server defaults —
331
+ * field-wise at each step, so a level that sets only `dropRate` leaves the
332
+ * level below's `latencyMs` intact. The server defaults are themselves selected
333
+ * per-testId first, and THAT step replaces rather than merges (see
334
+ * `resolveScopedDefaults` and `ChaosScope`).
335
+ *
336
+ * Every source goes through the same parser and the same ONE out-of-range
337
+ * policy — reject and warn, never clamp (see `ChaosConfig`). A rejected value
338
+ * is simply not set, so the next level of precedence applies. All three levels
339
+ * are parsed even when a higher one wins, so a bad value is always reported
340
+ * rather than hidden by the override above it.
341
+ *
342
+ * Exported so a caller that needs BOTH the latency delay and the terminal
343
+ * action resolves once per request and threads the result through, instead of
344
+ * parsing (and re-warning about) the same headers twice on the hot path.
345
+ */
346
+ function resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, url) {
347
+ const serverConfig = resolveScopedDefaults(serverDefaults, rawHeaders, url);
348
+ const resolved = {};
349
+ const fixtureFrom = fixture ? fixtureSource(fixture) : NO_SOURCE;
350
+ const serverFrom = serverConfig ? serverSource(serverDefaults, rawHeaders, url) : NO_SOURCE;
351
+ for (const field of CHAOS_FIELD_NAMES) {
352
+ const fromHeader = readHeaderField(rawHeaders, field, logger);
353
+ const fromFixture = readConfigField(fixture?.chaos, field, fixtureFrom, logger);
354
+ const fromServer = readConfigField(serverConfig, field, serverFrom, logger);
355
+ const value = fromHeader ?? fromFixture ?? fromServer;
356
+ if (value !== void 0) resolved[field] = value;
357
+ }
358
+ return resolved;
359
+ }
360
+ /**
361
+ * Resolve the deterministic latency delay (ms) for this request.
362
+ * Precedence is header > fixture > server, same as the rates. Returns 0
363
+ * when no latency is configured. Exported so async handlers can await the
364
+ * delay BEFORE evaluating terminal chaos actions.
365
+ */
366
+ function resolveChaosLatencyMs(fixture, serverDefaults, rawHeaders, logger, url, resolved) {
367
+ return (resolved ?? resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, url)).latencyMs ?? 0;
368
+ }
369
+ /**
370
+ * Responses this module destroyed itself, through the `disconnect` chaos
371
+ * action. A `WeakSet` rather than a marker property on the response: it adds
372
+ * nothing to the object's shape (so a `ServerResponse` test double stays a
373
+ * faithful stand-in) and the entry goes away with the response.
374
+ */
375
+ const chaosDisconnectedResponses = /* @__PURE__ */ new WeakSet();
376
+ /**
377
+ * The reason this response can no longer carry a fresh status line, or `null`
378
+ * while it can. Checked most-specific first: our own teardown, then an ended
379
+ * body, then a destroyed socket, then a committed status line.
380
+ */
381
+ function responseUnwritableReason(res) {
382
+ if (chaosDisconnectedResponses.has(res)) return "chaos-disconnected";
383
+ if (res.writableEnded) return "already-ended";
384
+ if (res.destroyed) return "client-gone";
385
+ if (res.headersSent) return "headers-sent";
386
+ return null;
387
+ }
388
+ /**
389
+ * The reason this response is GONE — dead for every purpose, chaos or not —
390
+ * or `null` while anything at all can still be done with it. `"headers-sent"`
391
+ * is deliberately NOT one of these.
392
+ */
393
+ function responseGoneReason(res) {
394
+ const reason = responseUnwritableReason(res);
395
+ return reason === null || reason === "headers-sent" ? null : reason;
396
+ }
397
+ /**
398
+ * True once nothing can be delivered on this response at all: the client hung
399
+ * up, the body was already ended, or our own `disconnect` action destroyed it.
400
+ *
401
+ * This is the predicate the streaming surfaces guard their post-`await` writes
402
+ * with (`grok-video.ts`, `veo-video.ts` — which hand-rolled
403
+ * `res.destroyed || res.writableEnded`, i.e. this minus the chaos-teardown
404
+ * case), and the one the SPLIT chaos gates in `src/server.ts` — the ones that
405
+ * call `awaitChaosLatency` and `evaluateChaos` separately instead of going
406
+ * through `applyChaosAsync` — have to re-check themselves after the await.
407
+ * Without that check a client that hangs up mid-delay still gets a full
408
+ * response built, "served" into a dead socket and JOURNALLED (C13).
409
+ */
410
+ function responseGone(res) {
411
+ return responseGoneReason(res) !== null;
412
+ }
413
+ /** Human-readable gloss per reason, so a skipped chaos action logs what actually happened. */
414
+ const UNWRITABLE_DESCRIPTION = {
415
+ "chaos-disconnected": "the chaos disconnect action had already destroyed it",
416
+ "already-ended": "the response was already ended",
417
+ "client-gone": "the client disconnected",
418
+ "headers-sent": "a status line was already committed"
419
+ };
420
+ /** The gloss for one reason. Exported so the split gates in `src/server.ts` log the same words. */
421
+ function describeUnwritableReason(reason) {
422
+ return UNWRITABLE_DESCRIPTION[reason];
423
+ }
424
+ /**
425
+ * Await the deterministic latency delay configured for this request, if any.
426
+ * Fixed (never randomised or jittered) so replay stays deterministic. Returns
427
+ * immediately when no latency is configured. Handlers that roll the chaos dice
428
+ * themselves (branching on the action before dispatching) call this first;
429
+ * handlers that just need "delay, then maybe fail" use `applyChaosAsync`.
430
+ *
431
+ * Pass `res` so a client that hangs up mid-delay CANCELS the pending timer
432
+ * (via the cancellable `delay(ms, signal)` helper) instead of leaving it armed
433
+ * for up to the 30s cap. The wait resolves early on disconnect — callers must
434
+ * re-check the response is still writable afterwards, which every chaos write
435
+ * path does through `responseUnwritable`.
436
+ *
437
+ * The disconnect signal is taken from the RESPONSE, not the request: an
438
+ * `IncomingMessage` is auto-destroyed as soon as its body has been fully read,
439
+ * so `req.destroyed` / `req`'s `close` event fire on every healthy request and
440
+ * would cancel the delay for everyone. `res`'s `close` before a write can only
441
+ * mean the connection was terminated prematurely.
442
+ */
443
+ async function awaitChaosLatency(fixture, serverDefaults, rawHeaders, logger, url, res, resolved) {
444
+ const delayMs = resolveChaosLatencyMs(fixture, serverDefaults, rawHeaders, logger, url, resolved);
445
+ if (delayMs <= 0) return;
446
+ if (!res) {
447
+ await delay(delayMs);
448
+ return;
449
+ }
450
+ const controller = new AbortController();
451
+ const onClose = () => controller.abort();
452
+ let unsubscribe;
453
+ if (responseGone(res)) controller.abort();
454
+ else if (typeof res.once === "function") {
455
+ res.once("close", onClose);
456
+ if (typeof res.off === "function") unsubscribe = () => res.off("close", onClose);
457
+ else if (typeof res.removeListener === "function") unsubscribe = () => res.removeListener("close", onClose);
458
+ }
459
+ try {
460
+ await delay(delayMs, controller.signal);
461
+ } finally {
462
+ unsubscribe?.();
66
463
  }
67
- if (base.dropRate !== void 0) base.dropRate = Math.min(1, Math.max(0, base.dropRate));
68
- if (base.malformedRate !== void 0) base.malformedRate = Math.min(1, Math.max(0, base.malformedRate));
69
- if (base.disconnectRate !== void 0) base.disconnectRate = Math.min(1, Math.max(0, base.disconnectRate));
70
- return base;
71
464
  }
72
465
  /**
73
466
  * Evaluate chaos config and return the triggered action, or null if none.
74
- * Checks in order: drop, malformed, disconnect — first hit wins.
467
+ *
468
+ * THE RATES DO NOT ROLL INDEPENDENTLY. Each is a separate `Math.random()` draw
469
+ * taken in a fixed order — drop → malformed → rateLimit → disconnect — and the
470
+ * first draw that hits returns immediately, so the ones after it are never
471
+ * rolled at all. A rate is therefore the probability that ITS draw hits GIVEN
472
+ * that no earlier rate fired, not the share of requests that end in that fault.
473
+ * Only `dropRate`, first in the chain, is unconditional.
474
+ *
475
+ * Worked example: `{ dropRate: 0.5, rateLimitRate: 0.5 }` yields ~50 % drops
476
+ * and ~25 % rate limits (0.5 x 0.5 — half the requests never reach the second
477
+ * draw), not 50 % of each. To exercise two faults at their nominal frequency,
478
+ * configure them on separate requests rather than in one config.
75
479
  */
76
- function evaluateChaos(fixture, serverDefaults, rawHeaders, logger, url) {
77
- const config = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, url);
480
+ function evaluateChaos(fixture, serverDefaults, rawHeaders, logger, url, resolved) {
481
+ const config = resolved ?? resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, url);
78
482
  if (config.dropRate !== void 0 && config.dropRate > 0 && Math.random() < config.dropRate) return "drop";
79
483
  if (config.malformedRate !== void 0 && config.malformedRate > 0 && Math.random() < config.malformedRate) return "malformed";
484
+ if (config.rateLimitRate !== void 0 && config.rateLimitRate > 0 && Math.random() < config.rateLimitRate) return "rateLimit";
80
485
  if (config.disconnectRate !== void 0 && config.disconnectRate > 0 && Math.random() < config.disconnectRate) return "disconnect";
81
486
  return null;
82
487
  }
83
488
  /**
489
+ * Async chaos entrypoint: awaits the deterministic latency delay (when
490
+ * configured) BEFORE rolling terminal actions. Returns true when a terminal
491
+ * action fired (caller returns early), false to proceed.
492
+ *
493
+ * This is the ONLY entrypoint that honours `latencyMs`, so it is the one every
494
+ * route handler must use: `src/__tests__/chaos-async-call-sites.test.ts` fails
495
+ * the build if any module outside this file rolls chaos without an awaited
496
+ * latency dominating the roll. That guard skips this file wholesale — it
497
+ * defines the primitives — so the `applyChaos(...)` delegation below is neither
498
+ * checked by it nor a control for it. The guard proves it is not vacuous its
499
+ * own way: synthetic fixtures for each evasion shape, a floor on the number of
500
+ * files scanned and chaos imports resolved, and a reconciliation of its name
501
+ * sets against this file's real exports.
502
+ *
503
+ * The delay is tied to the response socket, so a client that disconnects during
504
+ * it cancels the timer. When that happens (or the response went away for any
505
+ * other reason while we waited) the dice are never rolled: returns
506
+ * `"unwritable"` so the caller returns early, with nothing written and nothing
507
+ * journalled — a response that was never sent must not appear in the journal.
508
+ *
509
+ * A response that merely has its HEADERS SENT is NOT gone, and is not
510
+ * short-circuited: the delay is still awaited and the dice are still rolled.
511
+ * `disconnect` needs no status line and is applied; the three body actions
512
+ * are skipped and the call reports `false`, because the caller still has a
513
+ * healthy response to finish and `if (outcome) return;` would abandon it.
514
+ *
515
+ * The return value is a {@link ChaosAsyncOutcome}, not a boolean, so callers
516
+ * can tell "chaos fired" from "the response was already dead" — two outcomes
517
+ * the old `true` conflated. Both are truthy, so the standard call shape
518
+ * `if (await applyChaosAsync(...)) return;` keeps working unchanged; a caller
519
+ * that cares compares against `"handled"` / `"unwritable"`.
520
+ */
521
+ async function applyChaosAsync(res, fixture, serverDefaults, rawHeaders, requestUrl, journal, context, source, registry, logger) {
522
+ const resolved = resolveChaosConfig(fixture, serverDefaults, rawHeaders, logger, requestUrl);
523
+ await awaitChaosLatency(fixture, serverDefaults, rawHeaders, logger, requestUrl, res, resolved);
524
+ const gone = responseGoneReason(res);
525
+ if (gone !== null) {
526
+ logger?.debug(`[chaos] not applying chaos to ${context.method} ${context.path}: ${UNWRITABLE_DESCRIPTION[gone]} — nothing written, nothing journalled`);
527
+ return "unwritable";
528
+ }
529
+ return applyChaos(res, fixture, serverDefaults, rawHeaders, requestUrl, journal, context, source, registry, logger, resolved) ? "handled" : false;
530
+ }
531
+ /**
84
532
  * Apply chaos to a request. Returns true if chaos was applied (caller should
85
533
  * return early), false if the request should proceed normally.
86
534
  *
535
+ * Synchronous, and therefore SKIPS the configured `latencyMs` delay — it cannot
536
+ * await. It IS exported (the async wrapper and this module's tests call it by
537
+ * name), but it is not the public surface: no handler may call it —
538
+ * `src/__tests__/chaos-async-call-sites.test.ts` fails the build if one does —
539
+ * and the barrel deliberately publishes `applyChaosDeprecated` under the name
540
+ * `applyChaos` instead, so library consumers who still call it are told about
541
+ * the latency gap. See both below.
542
+ *
87
543
  * `requestUrl` is the RAW `req.url` (query string included) — chaos scoping
88
544
  * resolves the testId from it exactly as `getTestId` does, so `?testId=` tagged
89
545
  * traffic is not silently unscoped. It is required, not optional, so a new
@@ -93,11 +549,48 @@ function evaluateChaos(fixture, serverDefaults, rawHeaders, logger, url) {
93
549
  * the <X> phase" is enforced at the type level. A future handler that grows
94
550
  * a proxy path MUST pass `"proxy"` explicitly; the default can't drift silently.
95
551
  */
96
- function applyChaos(res, fixture, serverDefaults, rawHeaders, requestUrl, journal, context, source, registry, logger) {
97
- const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger, requestUrl);
552
+ function applyChaos(res, fixture, serverDefaults, rawHeaders, requestUrl, journal, context, source, registry, logger, resolved) {
553
+ const action = evaluateChaos(fixture, serverDefaults, rawHeaders, logger, requestUrl, resolved);
98
554
  if (!action) return false;
99
- applyChaosAction(action, res, fixture, journal, context, source, registry);
100
- return true;
555
+ return applyChaosAction(action, res, fixture, journal, context, source, registry, logger) === "applied";
556
+ }
557
+ /**
558
+ * One-shot latch for the deprecation notice below: warn once per process, not
559
+ * per request.
560
+ *
561
+ * PER PROCESS, and deliberately not cleared by `resetChaosWarnings` /
562
+ * `performFullReset` — unlike `warnedStaticRejections`, which IS per server and
563
+ * IS cleared. The two latches guard different kinds of fact. A rejected static
564
+ * chaos value describes THIS server's configuration, which a reset may change,
565
+ * so a reset suite must hear about it again. This notice describes the
566
+ * EMBEDDER'S CODE — a call site that still imports the sync `applyChaos` — and
567
+ * a `reset()` cannot change that: re-emitting it per test would print the same
568
+ * unchanged migration instruction once per test with nothing new to say. The
569
+ * deprecation policy (`docs/deprecation-policy/index.html`) is about which
570
+ * upstream endpoints aimock keeps mocking and says nothing about notice
571
+ * frequency, so the choice is ours; the message states it ("Warned once per
572
+ * process.") so nobody reads the single line as "it only happened once".
573
+ */
574
+ let warnedSyncApplyChaos = false;
575
+ /**
576
+ * Public, deprecated wrapper around the synchronous {@link applyChaos}. The
577
+ * barrel (`src/index.ts`) exports this under the name `applyChaos`, which has
578
+ * been part of the published API since v1.10.0 — removing it outright would
579
+ * break embedders silently, so it stays and says so instead.
580
+ *
581
+ * @deprecated Use {@link applyChaosAsync}. The synchronous form cannot await,
582
+ * so it silently ignores the configured chaos latency (`latencyMs`,
583
+ * `--chaos-latency`, `x-aimock-chaos-latency`): the fault rates still roll, but
584
+ * no delay is injected. Every fault behaviour is otherwise identical.
585
+ */
586
+ function applyChaosDeprecated(res, fixture, serverDefaults, rawHeaders, requestUrl, journal, context, source, registry, logger) {
587
+ if (!warnedSyncApplyChaos) {
588
+ warnedSyncApplyChaos = true;
589
+ const notice = "applyChaos() is deprecated and ignores the configured chaos latency (latencyMs / --chaos-latency / x-aimock-chaos-latency): it is synchronous and cannot await the delay. Use `await applyChaosAsync(...)` instead — same arguments, same fault behaviour, plus the latency. (Warned once per process.)";
590
+ if (logger) logger.warn(notice);
591
+ else console.warn(`[aimock] ${notice}`);
592
+ }
593
+ return applyChaos(res, fixture, serverDefaults, rawHeaders, requestUrl, journal, context, source, registry, logger);
101
594
  }
102
595
  /**
103
596
  * Apply a specific (already-rolled) chaos action. Exposed so callers that roll
@@ -107,16 +600,46 @@ function applyChaos(res, fixture, serverDefaults, rawHeaders, requestUrl, journa
107
600
  *
108
601
  * `source` is required (not optional) so callers can't silently omit it on
109
602
  * one branch and journal an ambiguous entry. Pass `"fixture"` when a fixture
110
- * matched (or would have) and `"proxy"` when the request was headed for the
111
- * proxy path.
603
+ * matched (or would have), `"proxy"` when the request was headed for the proxy
604
+ * path, and `"internal"` — the value most call sites pass — for aimock's own
605
+ * built-in endpoints, which are neither.
606
+ *
607
+ * A chaos action can be dispatched an arbitrarily long time after the request
608
+ * arrived (up to the 30s latency cap), by which point the client may be gone.
609
+ * Writing to a destroyed socket is silently lost, so counting the action and
610
+ * journalling a status the client never received would be a lie. When the
611
+ * response cannot take THIS action this is a no-op: no metric, no journal
612
+ * entry, no write, and the return value says `"skipped"`. The journal
613
+ * vocabulary has no "aborted" chaos status — the entry is omitted rather than
614
+ * invented.
615
+ *
616
+ * "Cannot take this action" is per-action, not one bit. A GONE response
617
+ * (client hung up, body ended, already chaos-destroyed) forecloses everything.
618
+ * A response whose HEADERS ARE SENT forecloses only the three actions that
619
+ * write a status line of their own — `disconnect` is a bare `res.destroy()`
620
+ * and is applied normally.
621
+ *
622
+ * `logger` is REQUIRED (not optional, not defaulted) because every branch
623
+ * above can decline to act, and a skip nobody can see is the bug this
624
+ * parameter exists to prevent: one call site had quietly omitted it. It is
625
+ * typed `Logger | undefined` rather than `Logger` because the public,
626
+ * deprecated `applyChaos` wrapper's own logger is optional and threads through
627
+ * here — arity is what the compiler enforces, and that is what catches the
628
+ * omission.
112
629
  */
113
- function applyChaosAction(action, res, fixture, journal, context, source, registry) {
114
- if (registry) registry.incrementCounter("aimock_chaos_triggered_total", {
115
- action,
116
- source
117
- });
630
+ function applyChaosAction(action, res, fixture, journal, context, source, registry, logger) {
631
+ const unwritable = responseUnwritableReason(res);
632
+ if (unwritable !== null && (unwritable !== "headers-sent" || action !== "disconnect")) {
633
+ logger?.debug(`[chaos] skipping ${action} on ${context.method} ${context.path}: ${UNWRITABLE_DESCRIPTION[unwritable]} — no metric, no journal entry, no write`);
634
+ return "skipped";
635
+ }
118
636
  switch (action) {
119
637
  case "drop":
638
+ writeErrorResponse(res, 500, JSON.stringify({ error: {
639
+ message: "Chaos: request dropped",
640
+ type: "server_error",
641
+ code: "chaos_drop"
642
+ } }));
120
643
  journal.add({
121
644
  ...context,
122
645
  response: {
@@ -126,13 +649,10 @@ function applyChaosAction(action, res, fixture, journal, context, source, regist
126
649
  source
127
650
  }
128
651
  });
129
- writeErrorResponse(res, 500, JSON.stringify({ error: {
130
- message: "Chaos: request dropped",
131
- type: "server_error",
132
- code: "chaos_drop"
133
- } }));
134
- return;
652
+ break;
135
653
  case "malformed":
654
+ res.writeHead(200, { "Content-Type": "application/json" });
655
+ res.end("{malformed json: <<<chaos>>>");
136
656
  journal.add({
137
657
  ...context,
138
658
  response: {
@@ -142,25 +662,47 @@ function applyChaosAction(action, res, fixture, journal, context, source, regist
142
662
  source
143
663
  }
144
664
  });
145
- res.writeHead(200, { "Content-Type": "application/json" });
146
- res.end("{malformed json: <<<chaos>>>");
147
- return;
148
- case "disconnect":
665
+ break;
666
+ case "rateLimit":
667
+ writeErrorResponse(res, 429, JSON.stringify({ error: {
668
+ message: "Chaos: rate limit exceeded",
669
+ type: "rate_limit_error",
670
+ code: "chaos_ratelimit"
671
+ } }));
149
672
  journal.add({
150
673
  ...context,
151
674
  response: {
152
- status: 0,
675
+ status: 429,
153
676
  fixture,
154
- chaosAction: "disconnect",
677
+ chaosAction: "rateLimit",
155
678
  source
156
679
  }
157
680
  });
681
+ break;
682
+ case "disconnect": {
683
+ const status = res.headersSent ? res.statusCode : 0;
684
+ chaosDisconnectedResponses.add(res);
158
685
  res.destroy();
159
- return;
160
- default: return;
686
+ journal.add({
687
+ ...context,
688
+ response: {
689
+ status,
690
+ fixture,
691
+ chaosAction: "disconnect",
692
+ source
693
+ }
694
+ });
695
+ break;
696
+ }
697
+ default: return "skipped";
161
698
  }
699
+ registry?.incrementCounter("aimock_chaos_triggered_total", {
700
+ action,
701
+ source
702
+ });
703
+ return "applied";
162
704
  }
163
705
 
164
706
  //#endregion
165
- export { applyChaos, applyChaosAction, evaluateChaos, isChaosScope };
707
+ export { CHAOS_FIELDS, CHAOS_FIELD_NAMES, applyChaosAction, applyChaosAsync, applyChaosDeprecated, awaitChaosLatency, describeUnwritableReason, evaluateChaos, isChaosScope, parseChaosField, resetChaosWarnings, resolveChaosConfig, resolveChaosLatencyMs, responseGone, responseGoneReason };
166
708
  //# sourceMappingURL=chaos.js.map