llm-mock-server 1.0.6 → 1.0.8

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 (251) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/cli.d.ts +3 -0
  3. package/dist/cli/cli.d.ts.map +1 -0
  4. package/dist/cli/cli.js +103 -0
  5. package/dist/cli/cli.js.map +1 -0
  6. package/dist/cli/validators.d.ts +7 -0
  7. package/dist/cli/validators.d.ts.map +1 -0
  8. package/dist/cli/validators.js +53 -0
  9. package/dist/cli/validators.js.map +1 -0
  10. package/dist/formats/anthropic/index.d.ts +1 -1
  11. package/dist/formats/anthropic/index.d.ts.map +1 -1
  12. package/dist/formats/anthropic/index.js +1 -1
  13. package/dist/formats/anthropic/index.js.map +1 -1
  14. package/dist/formats/anthropic/parse.d.ts +2 -2
  15. package/dist/formats/anthropic/parse.d.ts.map +1 -1
  16. package/dist/formats/anthropic/parse.js +4 -2
  17. package/dist/formats/anthropic/parse.js.map +1 -1
  18. package/dist/formats/anthropic/schema.d.ts +1 -1
  19. package/dist/formats/anthropic/schema.d.ts.map +1 -1
  20. package/dist/formats/anthropic/schema.js +9 -4
  21. package/dist/formats/anthropic/schema.js.map +1 -1
  22. package/dist/formats/anthropic/serialize.d.ts +2 -2
  23. package/dist/formats/anthropic/serialize.d.ts.map +1 -1
  24. package/dist/formats/anthropic/serialize.js +76 -19
  25. package/dist/formats/anthropic/serialize.js.map +1 -1
  26. package/dist/formats/openai/chat-completions/index.d.ts +3 -0
  27. package/dist/formats/openai/chat-completions/index.d.ts.map +1 -0
  28. package/dist/formats/openai/chat-completions/index.js +13 -0
  29. package/dist/formats/openai/chat-completions/index.js.map +1 -0
  30. package/dist/formats/openai/chat-completions/parse.d.ts +4 -0
  31. package/dist/formats/openai/chat-completions/parse.d.ts.map +1 -0
  32. package/dist/formats/openai/chat-completions/parse.js +33 -0
  33. package/dist/formats/openai/chat-completions/parse.js.map +1 -0
  34. package/dist/formats/openai/chat-completions/schema.d.ts +93 -0
  35. package/dist/formats/openai/chat-completions/schema.d.ts.map +1 -0
  36. package/dist/formats/openai/chat-completions/schema.js +74 -0
  37. package/dist/formats/openai/chat-completions/schema.js.map +1 -0
  38. package/dist/formats/openai/chat-completions/serialize.d.ts +10 -0
  39. package/dist/formats/openai/chat-completions/serialize.d.ts.map +1 -0
  40. package/dist/formats/openai/chat-completions/serialize.js +99 -0
  41. package/dist/formats/openai/chat-completions/serialize.js.map +1 -0
  42. package/dist/formats/openai/responses/index.d.ts +3 -0
  43. package/dist/formats/openai/responses/index.d.ts.map +1 -0
  44. package/dist/formats/openai/responses/index.js +13 -0
  45. package/dist/formats/openai/responses/index.js.map +1 -0
  46. package/dist/formats/openai/responses/parse.d.ts +4 -0
  47. package/dist/formats/openai/responses/parse.d.ts.map +1 -0
  48. package/dist/formats/openai/responses/parse.js +51 -0
  49. package/dist/formats/openai/responses/parse.js.map +1 -0
  50. package/dist/formats/openai/responses/schema.d.ts +103 -0
  51. package/dist/formats/openai/responses/schema.d.ts.map +1 -0
  52. package/dist/formats/openai/responses/schema.js +71 -0
  53. package/dist/formats/openai/responses/schema.js.map +1 -0
  54. package/dist/formats/openai/responses/serialize.d.ts +10 -0
  55. package/dist/formats/openai/responses/serialize.d.ts.map +1 -0
  56. package/dist/formats/openai/responses/serialize.js +273 -0
  57. package/dist/formats/openai/responses/serialize.js.map +1 -0
  58. package/dist/formats/request-helpers.d.ts +1 -1
  59. package/dist/formats/request-helpers.d.ts.map +1 -1
  60. package/dist/formats/request-helpers.js.map +1 -1
  61. package/dist/formats/serialize-helpers.d.ts +1 -1
  62. package/dist/formats/serialize-helpers.d.ts.map +1 -1
  63. package/dist/formats/serialize-helpers.js +6 -3
  64. package/dist/formats/serialize-helpers.js.map +1 -1
  65. package/dist/formats/types.d.ts +2 -1
  66. package/dist/formats/types.d.ts.map +1 -1
  67. package/dist/history.d.ts +6 -2
  68. package/dist/history.d.ts.map +1 -1
  69. package/dist/history.js +2 -0
  70. package/dist/history.js.map +1 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/loader.d.ts +1 -1
  74. package/dist/loader.d.ts.map +1 -1
  75. package/dist/loader.js +26 -9
  76. package/dist/loader.js.map +1 -1
  77. package/dist/logger.d.ts.map +1 -1
  78. package/dist/logger.js +12 -4
  79. package/dist/logger.js.map +1 -1
  80. package/dist/mock-server.d.ts +44 -48
  81. package/dist/mock-server.d.ts.map +1 -1
  82. package/dist/mock-server.js +37 -85
  83. package/dist/mock-server.js.map +1 -1
  84. package/dist/route-handler.d.ts +1 -1
  85. package/dist/route-handler.d.ts.map +1 -1
  86. package/dist/route-handler.js +19 -7
  87. package/dist/route-handler.js.map +1 -1
  88. package/dist/rule-builder.d.ts +21 -0
  89. package/dist/rule-builder.d.ts.map +1 -0
  90. package/dist/rule-builder.js +58 -0
  91. package/dist/rule-builder.js.map +1 -0
  92. package/dist/rule-engine.d.ts +3 -1
  93. package/dist/rule-engine.d.ts.map +1 -1
  94. package/dist/rule-engine.js +7 -2
  95. package/dist/rule-engine.js.map +1 -1
  96. package/dist/sse-writer.d.ts +1 -1
  97. package/dist/sse-writer.d.ts.map +1 -1
  98. package/dist/types/reply.d.ts +51 -8
  99. package/dist/types/reply.d.ts.map +1 -1
  100. package/dist/types/request.d.ts +21 -6
  101. package/dist/types/request.d.ts.map +1 -1
  102. package/dist/types/rule.d.ts +65 -7
  103. package/dist/types/rule.d.ts.map +1 -1
  104. package/dist/types.d.ts +3 -3
  105. package/dist/types.d.ts.map +1 -1
  106. package/package.json +15 -9
  107. package/.claude/skills/desloppify/SKILL.md +0 -308
  108. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +0 -242
  109. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +0 -248
  110. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +0 -248
  111. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +0 -311
  112. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +0 -17
  113. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +0 -255
  114. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +0 -22
  115. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +0 -20
  116. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +0 -20
  117. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050000.json +0 -286
  118. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050028.json +0 -303
  119. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/claude_launch_prompt.md +0 -17
  120. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json +0 -297
  121. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json +0 -22
  122. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/reviewer_instructions.md +0 -20
  123. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/session.json +0 -20
  124. package/.desloppify/query.json +0 -1312
  125. package/.desloppify/review_packet_blind.json +0 -1249
  126. package/.desloppify/review_packets/holistic_packet_20260315_000339.json +0 -1471
  127. package/.desloppify/review_packets/holistic_packet_20260315_045546.json +0 -1480
  128. package/.desloppify/review_packets/holistic_packet_20260315_185401.json +0 -1407
  129. package/.desloppify/review_packets/holistic_packet_20260315_185613.json +0 -1407
  130. package/.desloppify/state-typescript.json +0 -8438
  131. package/.desloppify/state-typescript.json.bak +0 -8432
  132. package/.desloppify/subagents/runs/20260315_185401/logs/batch-1.log +0 -384
  133. package/.desloppify/subagents/runs/20260315_185401/logs/batch-10.log +0 -484
  134. package/.desloppify/subagents/runs/20260315_185401/logs/batch-2.log +0 -408
  135. package/.desloppify/subagents/runs/20260315_185401/logs/batch-3.log +0 -416
  136. package/.desloppify/subagents/runs/20260315_185401/logs/batch-4.log +0 -360
  137. package/.desloppify/subagents/runs/20260315_185401/logs/batch-5.log +0 -360
  138. package/.desloppify/subagents/runs/20260315_185401/logs/batch-6.log +0 -364
  139. package/.desloppify/subagents/runs/20260315_185401/logs/batch-7.log +0 -428
  140. package/.desloppify/subagents/runs/20260315_185401/logs/batch-8.log +0 -388
  141. package/.desloppify/subagents/runs/20260315_185401/logs/batch-9.log +0 -500
  142. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-1.md +0 -83
  143. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-10.md +0 -108
  144. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-2.md +0 -89
  145. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-3.md +0 -91
  146. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-4.md +0 -77
  147. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-5.md +0 -77
  148. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-6.md +0 -78
  149. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-7.md +0 -94
  150. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-8.md +0 -84
  151. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-9.md +0 -112
  152. package/.desloppify/subagents/runs/20260315_185401/results/batch-1.raw.txt +0 -0
  153. package/.desloppify/subagents/runs/20260315_185401/results/batch-10.raw.txt +0 -0
  154. package/.desloppify/subagents/runs/20260315_185401/results/batch-2.raw.txt +0 -0
  155. package/.desloppify/subagents/runs/20260315_185401/results/batch-3.raw.txt +0 -0
  156. package/.desloppify/subagents/runs/20260315_185401/results/batch-4.raw.txt +0 -0
  157. package/.desloppify/subagents/runs/20260315_185401/results/batch-5.raw.txt +0 -0
  158. package/.desloppify/subagents/runs/20260315_185401/results/batch-6.raw.txt +0 -0
  159. package/.desloppify/subagents/runs/20260315_185401/results/batch-7.raw.txt +0 -0
  160. package/.desloppify/subagents/runs/20260315_185401/results/batch-8.raw.txt +0 -0
  161. package/.desloppify/subagents/runs/20260315_185401/results/batch-9.raw.txt +0 -0
  162. package/.desloppify/subagents/runs/20260315_185401/run.log +0 -36
  163. package/.desloppify/subagents/runs/20260315_185401/run_summary.json +0 -156
  164. package/.desloppify/subagents/runs/20260315_185613/holistic_findings_merged.json +0 -741
  165. package/.desloppify/subagents/runs/20260315_185613/logs/batch-1.log +0 -579
  166. package/.desloppify/subagents/runs/20260315_185613/logs/batch-10.log +0 -1537
  167. package/.desloppify/subagents/runs/20260315_185613/logs/batch-2.log +0 -829
  168. package/.desloppify/subagents/runs/20260315_185613/logs/batch-3.log +0 -927
  169. package/.desloppify/subagents/runs/20260315_185613/logs/batch-4.log +0 -429
  170. package/.desloppify/subagents/runs/20260315_185613/logs/batch-5.log +0 -276
  171. package/.desloppify/subagents/runs/20260315_185613/logs/batch-6.log +0 -450
  172. package/.desloppify/subagents/runs/20260315_185613/logs/batch-7.log +0 -730
  173. package/.desloppify/subagents/runs/20260315_185613/logs/batch-8.log +0 -698
  174. package/.desloppify/subagents/runs/20260315_185613/logs/batch-9.log +0 -938
  175. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-1.md +0 -83
  176. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-10.md +0 -108
  177. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-2.md +0 -89
  178. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-3.md +0 -91
  179. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-4.md +0 -77
  180. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-5.md +0 -77
  181. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-6.md +0 -78
  182. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-7.md +0 -94
  183. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-8.md +0 -84
  184. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-9.md +0 -112
  185. package/.desloppify/subagents/runs/20260315_185613/results/batch-1.raw.txt +0 -78
  186. package/.desloppify/subagents/runs/20260315_185613/results/batch-10.raw.txt +0 -242
  187. package/.desloppify/subagents/runs/20260315_185613/results/batch-2.raw.txt +0 -102
  188. package/.desloppify/subagents/runs/20260315_185613/results/batch-3.raw.txt +0 -94
  189. package/.desloppify/subagents/runs/20260315_185613/results/batch-4.raw.txt +0 -86
  190. package/.desloppify/subagents/runs/20260315_185613/results/batch-5.raw.txt +0 -1
  191. package/.desloppify/subagents/runs/20260315_185613/results/batch-6.raw.txt +0 -87
  192. package/.desloppify/subagents/runs/20260315_185613/results/batch-7.raw.txt +0 -1
  193. package/.desloppify/subagents/runs/20260315_185613/results/batch-8.raw.txt +0 -107
  194. package/.desloppify/subagents/runs/20260315_185613/results/batch-9.raw.txt +0 -67
  195. package/.desloppify/subagents/runs/20260315_185613/run.log +0 -96
  196. package/.desloppify/subagents/runs/20260315_185613/run_summary.json +0 -156
  197. package/.editorconfig +0 -12
  198. package/.github/dependabot.yml +0 -11
  199. package/.github/workflows/docs.yml +0 -46
  200. package/.github/workflows/test.yml +0 -40
  201. package/.markdownlint.jsonc +0 -11
  202. package/.node-version +0 -1
  203. package/.oxfmtrc.json +0 -9
  204. package/.oxlintrc.json +0 -35
  205. package/docs/ARCHITECTURE.md +0 -125
  206. package/scorecard.png +0 -0
  207. package/src/cli/cli.ts +0 -141
  208. package/src/cli/validators.ts +0 -68
  209. package/src/formats/anthropic/index.ts +0 -14
  210. package/src/formats/anthropic/parse.ts +0 -70
  211. package/src/formats/anthropic/schema.ts +0 -74
  212. package/src/formats/anthropic/serialize.ts +0 -179
  213. package/src/formats/openai/chat-completions/index.ts +0 -14
  214. package/src/formats/openai/chat-completions/parse.ts +0 -47
  215. package/src/formats/openai/chat-completions/schema.ts +0 -92
  216. package/src/formats/openai/chat-completions/serialize.ts +0 -146
  217. package/src/formats/openai/responses/index.ts +0 -14
  218. package/src/formats/openai/responses/parse.ts +0 -73
  219. package/src/formats/openai/responses/schema.ts +0 -86
  220. package/src/formats/openai/responses/serialize.ts +0 -328
  221. package/src/formats/request-helpers.ts +0 -56
  222. package/src/formats/serialize-helpers.ts +0 -43
  223. package/src/formats/types.ts +0 -26
  224. package/src/history.ts +0 -70
  225. package/src/index.ts +0 -46
  226. package/src/loader.ts +0 -246
  227. package/src/logger.ts +0 -70
  228. package/src/mock-server.ts +0 -203
  229. package/src/route-handler.ts +0 -144
  230. package/src/rule-builder.ts +0 -73
  231. package/src/rule-engine.ts +0 -165
  232. package/src/sse-writer.ts +0 -35
  233. package/src/types/reply.ts +0 -92
  234. package/src/types/request.ts +0 -56
  235. package/src/types/rule.ts +0 -125
  236. package/src/types.ts +0 -24
  237. package/test/cli-validators.test.ts +0 -151
  238. package/test/formats/anthropic.test.ts +0 -336
  239. package/test/formats/openai.test.ts +0 -316
  240. package/test/formats/parse-helpers.test.ts +0 -315
  241. package/test/formats/responses.test.ts +0 -380
  242. package/test/helpers/make-req.ts +0 -18
  243. package/test/history.test.ts +0 -361
  244. package/test/loader.test.ts +0 -333
  245. package/test/logger.test.ts +0 -344
  246. package/test/mock-server.test.ts +0 -619
  247. package/test/rule-engine.test.ts +0 -229
  248. package/tsconfig.json +0 -24
  249. package/tsconfig.test.json +0 -11
  250. package/typedoc.json +0 -9
  251. package/vitest.config.ts +0 -18
@@ -1,361 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "vitest";
2
- import { RequestHistory, type RecordedRequest } from "#/history.js";
3
- import { makeReq } from "./helpers/make-req.js";
4
-
5
- describe("RequestHistory", () => {
6
- let history: RequestHistory;
7
-
8
- beforeEach(() => {
9
- history = new RequestHistory();
10
- });
11
-
12
- describe("record()", () => {
13
- it("adds an entry", () => {
14
- history.record(makeReq(), "rule-1");
15
- expect(history.count()).toBe(1);
16
- });
17
-
18
- it("adds multiple entries in order", () => {
19
- history.record(makeReq({ lastMessage: "first" }), "r1");
20
- history.record(makeReq({ lastMessage: "second" }), "r2");
21
- history.record(makeReq({ lastMessage: "third" }), undefined);
22
-
23
- expect(history.count()).toBe(3);
24
- expect(history.first()?.request.lastMessage).toBe("first");
25
- expect(history.last()?.request.lastMessage).toBe("third");
26
- });
27
-
28
- it("stores the matched rule name", () => {
29
- history.record(makeReq(), "my-rule");
30
- expect(history.first()?.rule).toBe("my-rule");
31
- });
32
-
33
- it("stores undefined rule when fallback was used", () => {
34
- history.record(makeReq(), undefined);
35
- expect(history.first()?.rule).toBeUndefined();
36
- });
37
-
38
- it("sets a numeric timestamp", () => {
39
- const before = Date.now();
40
- history.record(makeReq(), "r");
41
- const after = Date.now();
42
-
43
- const ts = history.first()!.timestamp;
44
- expect(ts).toBeGreaterThanOrEqual(before);
45
- expect(ts).toBeLessThanOrEqual(after);
46
- });
47
- });
48
-
49
- describe("count()", () => {
50
- it("returns 0 for empty history", () => {
51
- expect(history.count()).toBe(0);
52
- });
53
-
54
- it("returns the correct count after multiple records", () => {
55
- history.record(makeReq(), "a");
56
- history.record(makeReq(), "b");
57
- history.record(makeReq(), "c");
58
- expect(history.count()).toBe(3);
59
- });
60
-
61
- it("returns 0 after clear", () => {
62
- history.record(makeReq(), "a");
63
- history.clear();
64
- expect(history.count()).toBe(0);
65
- });
66
- });
67
-
68
- describe("first()", () => {
69
- it("returns undefined when history is empty", () => {
70
- expect(history.first()).toBeUndefined();
71
- });
72
-
73
- it("returns the first recorded entry", () => {
74
- history.record(makeReq({ lastMessage: "alpha" }), "r1");
75
- history.record(makeReq({ lastMessage: "beta" }), "r2");
76
-
77
- const entry = history.first();
78
- expect(entry).toBeDefined();
79
- expect(entry!.request.lastMessage).toBe("alpha");
80
- expect(entry!.rule).toBe("r1");
81
- });
82
- });
83
-
84
- describe("last()", () => {
85
- it("returns undefined when history is empty", () => {
86
- expect(history.last()).toBeUndefined();
87
- });
88
-
89
- it("returns the most recent entry", () => {
90
- history.record(makeReq({ lastMessage: "alpha" }), "r1");
91
- history.record(makeReq({ lastMessage: "beta" }), "r2");
92
-
93
- const entry = history.last();
94
- expect(entry).toBeDefined();
95
- expect(entry!.request.lastMessage).toBe("beta");
96
- expect(entry!.rule).toBe("r2");
97
- });
98
-
99
- it("returns the same entry as first() when there is only one", () => {
100
- history.record(makeReq(), "only");
101
- expect(history.first()).toBe(history.last());
102
- });
103
- });
104
-
105
- describe("at()", () => {
106
- beforeEach(() => {
107
- history.record(makeReq({ lastMessage: "zero" }), "r0");
108
- history.record(makeReq({ lastMessage: "one" }), "r1");
109
- history.record(makeReq({ lastMessage: "two" }), "r2");
110
- });
111
-
112
- it("returns the entry at a positive index", () => {
113
- expect(history.at(0)?.request.lastMessage).toBe("zero");
114
- expect(history.at(1)?.request.lastMessage).toBe("one");
115
- expect(history.at(2)?.request.lastMessage).toBe("two");
116
- });
117
-
118
- it("returns the entry at a negative index", () => {
119
- expect(history.at(-1)?.request.lastMessage).toBe("two");
120
- expect(history.at(-2)?.request.lastMessage).toBe("one");
121
- expect(history.at(-3)?.request.lastMessage).toBe("zero");
122
- });
123
-
124
- it("returns undefined for out-of-bounds positive index", () => {
125
- expect(history.at(3)).toBeUndefined();
126
- expect(history.at(100)).toBeUndefined();
127
- });
128
-
129
- it("returns undefined for out-of-bounds negative index", () => {
130
- expect(history.at(-4)).toBeUndefined();
131
- expect(history.at(-100)).toBeUndefined();
132
- });
133
-
134
- it("returns undefined when history is empty", () => {
135
- const empty = new RequestHistory();
136
- expect(empty.at(0)).toBeUndefined();
137
- expect(empty.at(-1)).toBeUndefined();
138
- });
139
- });
140
-
141
- describe("where()", () => {
142
- beforeEach(() => {
143
- history.record(
144
- makeReq({ lastMessage: "hello", model: "gpt-5.4" }),
145
- "rule-a",
146
- );
147
- history.record(
148
- makeReq({ lastMessage: "world", model: "claude-4" }),
149
- undefined,
150
- );
151
- history.record(
152
- makeReq({ lastMessage: "hello again", model: "gpt-5.4" }),
153
- "rule-b",
154
- );
155
- });
156
-
157
- it("filters entries by predicate", () => {
158
- const matched = history.where((e) => e.rule !== undefined);
159
- expect(matched).toHaveLength(2);
160
- expect(matched[0].rule).toBe("rule-a");
161
- expect(matched[1].rule).toBe("rule-b");
162
- });
163
-
164
- it("filters by request properties", () => {
165
- const claudeRequests = history.where(
166
- (e) => e.request.model === "claude-4",
167
- );
168
- expect(claudeRequests).toHaveLength(1);
169
- expect(claudeRequests[0].request.lastMessage).toBe("world");
170
- });
171
-
172
- it("returns an empty array when nothing matches", () => {
173
- const none = history.where(
174
- (e) => e.request.lastMessage === "nonexistent",
175
- );
176
- expect(none).toEqual([]);
177
- });
178
-
179
- it("returns all entries when predicate always returns true", () => {
180
- const all = history.where(() => true);
181
- expect(all).toHaveLength(3);
182
- });
183
-
184
- it("returns an empty array on empty history", () => {
185
- const empty = new RequestHistory();
186
- expect(empty.where(() => true)).toEqual([]);
187
- });
188
- });
189
-
190
- describe("all getter", () => {
191
- it("returns an empty array when history is empty", () => {
192
- expect(history.all).toEqual([]);
193
- expect(history.all).toHaveLength(0);
194
- });
195
-
196
- it("returns all recorded entries in insertion order", () => {
197
- history.record(makeReq({ lastMessage: "a" }), "r1");
198
- history.record(makeReq({ lastMessage: "b" }), "r2");
199
-
200
- const entries = history.all;
201
- expect(entries).toHaveLength(2);
202
- expect(entries[0].request.lastMessage).toBe("a");
203
- expect(entries[1].request.lastMessage).toBe("b");
204
- });
205
-
206
- it("returns a readonly array (same reference as internal entries)", () => {
207
- history.record(makeReq(), "r");
208
- const a = history.all;
209
- const b = history.all;
210
- expect(a).toBe(b);
211
- });
212
-
213
- it("reflects mutations after further records", () => {
214
- history.record(makeReq({ lastMessage: "before" }), "r");
215
- const ref = history.all;
216
- expect(ref).toHaveLength(1);
217
-
218
- history.record(makeReq({ lastMessage: "after" }), "r2");
219
- // `all` exposes the internal array, so the earlier reference sees the new entry
220
- expect(ref).toHaveLength(2);
221
- });
222
- });
223
-
224
- describe("clear()", () => {
225
- it("empties the history", () => {
226
- history.record(makeReq(), "r1");
227
- history.record(makeReq(), "r2");
228
- expect(history.count()).toBe(2);
229
-
230
- history.clear();
231
- expect(history.count()).toBe(0);
232
- expect(history.first()).toBeUndefined();
233
- expect(history.last()).toBeUndefined();
234
- expect(history.all).toHaveLength(0);
235
- });
236
-
237
- it("is idempotent on empty history", () => {
238
- history.clear();
239
- expect(history.count()).toBe(0);
240
- history.clear();
241
- expect(history.count()).toBe(0);
242
- });
243
-
244
- it("allows recording again after clear", () => {
245
- history.record(makeReq({ lastMessage: "old" }), "r1");
246
- history.clear();
247
- history.record(makeReq({ lastMessage: "new" }), "r2");
248
-
249
- expect(history.count()).toBe(1);
250
- expect(history.first()?.request.lastMessage).toBe("new");
251
- });
252
- });
253
-
254
- describe("Iterator protocol (for...of)", () => {
255
- it("iterates over all entries in order", () => {
256
- history.record(makeReq({ lastMessage: "a" }), "r1");
257
- history.record(makeReq({ lastMessage: "b" }), "r2");
258
- history.record(makeReq({ lastMessage: "c" }), "r3");
259
-
260
- const messages: string[] = [];
261
- for (const entry of history) {
262
- messages.push(entry.request.lastMessage);
263
- }
264
-
265
- expect(messages).toEqual(["a", "b", "c"]);
266
- });
267
-
268
- it("yields nothing for empty history", () => {
269
- const messages: string[] = [];
270
- for (const entry of history) {
271
- messages.push(entry.request.lastMessage);
272
- }
273
- expect(messages).toEqual([]);
274
- });
275
-
276
- it("works with spread operator", () => {
277
- history.record(makeReq({ lastMessage: "x" }), "r1");
278
- history.record(makeReq({ lastMessage: "y" }), "r2");
279
-
280
- const entries: RecordedRequest[] = [...history];
281
- expect(entries).toHaveLength(2);
282
- expect(entries[0].request.lastMessage).toBe("x");
283
- expect(entries[1].request.lastMessage).toBe("y");
284
- });
285
-
286
- it("works with Array.from()", () => {
287
- history.record(makeReq(), "r1");
288
- history.record(makeReq(), "r2");
289
-
290
- const arr = Array.from(history);
291
- expect(arr).toHaveLength(2);
292
- });
293
-
294
- it("supports destructuring", () => {
295
- history.record(makeReq({ lastMessage: "first" }), "r1");
296
- history.record(makeReq({ lastMessage: "second" }), "r2");
297
- history.record(makeReq({ lastMessage: "third" }), "r3");
298
-
299
- const [first, second, third] = history;
300
- expect(first.request.lastMessage).toBe("first");
301
- expect(second.request.lastMessage).toBe("second");
302
- expect(third.request.lastMessage).toBe("third");
303
- });
304
- });
305
-
306
- describe("edge cases", () => {
307
- it("preserves the full MockRequest object", () => {
308
- const req = makeReq({
309
- format: "anthropic",
310
- model: "claude-4",
311
- streaming: false,
312
- lastMessage: "test message",
313
- systemMessage: "be helpful",
314
- toolNames: ["search", "calc"],
315
- lastToolCallId: "call_123",
316
- path: "/v1/messages",
317
- });
318
-
319
- history.record(req, "complex-rule");
320
- const recorded = history.first()!;
321
-
322
- expect(recorded.request.format).toBe("anthropic");
323
- expect(recorded.request.model).toBe("claude-4");
324
- expect(recorded.request.streaming).toBe(false);
325
- expect(recorded.request.lastMessage).toBe("test message");
326
- expect(recorded.request.systemMessage).toBe("be helpful");
327
- expect(recorded.request.toolNames).toEqual(["search", "calc"]);
328
- expect(recorded.request.lastToolCallId).toBe("call_123");
329
- expect(recorded.request.path).toBe("/v1/messages");
330
- });
331
-
332
- it("handles many entries without issue", () => {
333
- for (let i = 0; i < 1000; i++) {
334
- history.record(makeReq({ lastMessage: `msg-${i}` }), `rule-${i}`);
335
- }
336
-
337
- expect(history.count()).toBe(1000);
338
- expect(history.first()?.request.lastMessage).toBe("msg-0");
339
- expect(history.last()?.request.lastMessage).toBe("msg-999");
340
- expect(history.at(500)?.request.lastMessage).toBe("msg-500");
341
- });
342
-
343
- it("where() does not modify the original entries", () => {
344
- history.record(makeReq(), "r1");
345
- history.record(makeReq(), "r2");
346
-
347
- const filtered = history.where(() => false);
348
- expect(filtered).toHaveLength(0);
349
- expect(history.count()).toBe(2);
350
- });
351
-
352
- it("each entry gets its own timestamp", () => {
353
- history.record(makeReq(), "r1");
354
- history.record(makeReq(), "r2");
355
-
356
- const t1 = history.at(0)!.timestamp;
357
- const t2 = history.at(1)!.timestamp;
358
- expect(t2).toBeGreaterThanOrEqual(t1);
359
- });
360
- });
361
- });
@@ -1,333 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { writeFile, mkdir, rm } from "node:fs/promises";
3
- import { join } from "node:path";
4
- import { RuleEngine } from "#/rule-engine.js";
5
- import { loadRulesFromPath } from "#/loader.js";
6
- import type { MockRequest } from "#/types.js";
7
- import { makeReq } from "./helpers/make-req.js";
8
-
9
- const tmpDir = join(import.meta.dirname, ".tmp-loader-test");
10
-
11
- describe("Loader", () => {
12
- let engine: RuleEngine;
13
-
14
- beforeEach(async () => {
15
- engine = new RuleEngine();
16
- await mkdir(tmpDir, { recursive: true });
17
- });
18
-
19
- afterEach(async () => {
20
- await rm(tmpDir, { recursive: true, force: true });
21
- });
22
-
23
- describe("JSON5 files", () => {
24
- it("loads rules from a .json5 file", async () => {
25
- const rulesPath = join(tmpDir, "rules.json5");
26
- await writeFile(
27
- rulesPath,
28
- `[
29
- {
30
- when: "explain recursion",
31
- reply: "A function that calls itself.",
32
- },
33
- {
34
- when: { model: "gpt-5.4", message: "hello" },
35
- reply: "Hi from GPT-5.4!",
36
- },
37
- ]`,
38
- );
39
-
40
- await loadRulesFromPath(rulesPath, { engine });
41
- expect(engine.ruleCount).toBe(2);
42
-
43
- const match1 = engine.match(
44
- makeReq({ lastMessage: "Please explain recursion" }),
45
- );
46
- if (!match1) throw new Error("expected match for 'explain'");
47
- expect(match1.resolve).toBe("A function that calls itself.");
48
-
49
- const match2 = engine.match(
50
- makeReq({ model: "gpt-5.4", lastMessage: "hello" }),
51
- );
52
- expect(match2).toBeDefined();
53
- });
54
-
55
- it("loads regex patterns from JSON5", async () => {
56
- const rulesPath = join(tmpDir, "regex.json5");
57
- await writeFile(
58
- rulesPath,
59
- `[
60
- {
61
- when: "/explain (\\\\w+)/i",
62
- reply: "Here is an explanation.",
63
- },
64
- ]`,
65
- );
66
-
67
- await loadRulesFromPath(rulesPath, { engine });
68
- const match = engine.match(
69
- makeReq({ lastMessage: "explain polymorphism" }),
70
- );
71
- expect(match).toBeDefined();
72
- });
73
-
74
- it("loads regex patterns with modern flags (d, v)", async () => {
75
- const rulesPath = join(tmpDir, "flags.json5");
76
- await writeFile(
77
- rulesPath,
78
- `[{ when: "/hello/di", reply: "With d flag" }]`,
79
- );
80
-
81
- await loadRulesFromPath(rulesPath, { engine });
82
- expect(
83
- engine.match(makeReq({ lastMessage: "hello world" })),
84
- ).toBeDefined();
85
- });
86
-
87
- it("loads rules with times", async () => {
88
- const rulesPath = join(tmpDir, "times.json5");
89
- await writeFile(
90
- rulesPath,
91
- `[{ when: "once", reply: "One time!", times: 1 }]`,
92
- );
93
-
94
- await loadRulesFromPath(rulesPath, { engine });
95
- expect(engine.match(makeReq({ lastMessage: "once" }))).toBeDefined();
96
- expect(engine.match(makeReq({ lastMessage: "once" }))).toBeUndefined();
97
- });
98
-
99
- it("resolves $template references", async () => {
100
- const rulesPath = join(tmpDir, "templates.json5");
101
- await writeFile(
102
- rulesPath,
103
- `{
104
- templates: {
105
- greeting: "Hello from template!",
106
- toolReply: { tools: [{ name: "search", args: { q: "test" } }] },
107
- },
108
- rules: [
109
- { when: "hi", reply: "$greeting" },
110
- { when: "search", reply: "$toolReply" },
111
- ],
112
- }`,
113
- );
114
-
115
- await loadRulesFromPath(rulesPath, { engine });
116
- expect(engine.ruleCount).toBe(2);
117
-
118
- const match1 = engine.match(makeReq({ lastMessage: "hi" }));
119
- expect(match1?.resolve).toBe("Hello from template!");
120
-
121
- const match2 = engine.match(makeReq({ lastMessage: "search" }));
122
- expect(match2?.resolve).toMatchObject({ tools: [{ name: "search" }] });
123
- });
124
-
125
- it("throws on unknown template reference", async () => {
126
- const rulesPath = join(tmpDir, "bad-ref.json5");
127
- await writeFile(
128
- rulesPath,
129
- `{
130
- rules: [{ when: "hi", reply: "$nonexistent" }],
131
- }`,
132
- );
133
-
134
- await expect(loadRulesFromPath(rulesPath, { engine })).rejects.toThrow(
135
- "Unknown template",
136
- );
137
- });
138
-
139
- it("loads a replies sequence", async () => {
140
- const rulesPath = join(tmpDir, "seq.json5");
141
- await writeFile(
142
- rulesPath,
143
- `[{ when: "step", replies: ["First.", "Second."] }]`,
144
- );
145
-
146
- await loadRulesFromPath(rulesPath, { engine });
147
- expect(engine.ruleCount).toBe(1);
148
-
149
- const req = makeReq({ lastMessage: "step" });
150
- const match1 = engine.match(req);
151
- if (!match1) throw new Error("expected match1");
152
- expect((match1.resolve as () => string)()).toBe("First.");
153
-
154
- const match2 = engine.match(req);
155
- if (!match2) throw new Error("expected match2");
156
- expect((match2.resolve as () => string)()).toBe("Second.");
157
-
158
- expect(engine.match(req)).toBeUndefined();
159
- });
160
-
161
- it("loads fallback from JSON5 file", async () => {
162
- const rulesPath = join(tmpDir, "fb.json5");
163
- await writeFile(
164
- rulesPath,
165
- `{
166
- fallback: "Default reply.",
167
- rules: [{ when: "hi", reply: "Hello!" }],
168
- }`,
169
- );
170
-
171
- let capturedFallback: unknown;
172
- await loadRulesFromPath(rulesPath, {
173
- engine,
174
- setFallback: (reply) => {
175
- capturedFallback = reply;
176
- },
177
- });
178
-
179
- expect(capturedFallback).toBe("Default reply.");
180
- expect(engine.ruleCount).toBe(1);
181
- });
182
- });
183
-
184
- describe("handler files", () => {
185
- it("loads a single handler from a .ts file", async () => {
186
- const handlerPath = join(tmpDir, "single.ts");
187
- await writeFile(
188
- handlerPath,
189
- `export default {
190
- match: (req) => req.lastMessage.includes("summarize"),
191
- respond: (req) => "Here is a summary.",
192
- };`,
193
- );
194
-
195
- await loadRulesFromPath(handlerPath, { engine });
196
- expect(engine.ruleCount).toBe(1);
197
-
198
- const match = engine.match(
199
- makeReq({ lastMessage: "summarize this article" }),
200
- );
201
- if (!match) throw new Error("expected match for 'summarize'");
202
- expect(match.resolve).toBeTypeOf("function");
203
- });
204
-
205
- it("loads an array of handlers from a .ts file", async () => {
206
- const handlerPath = join(tmpDir, "multi.ts");
207
- await writeFile(
208
- handlerPath,
209
- `export default [
210
- {
211
- match: (req) => req.lastMessage.includes("hello"),
212
- respond: () => "Hi!",
213
- },
214
- {
215
- match: (req) => req.lastMessage.includes("bye"),
216
- respond: () => "Goodbye!",
217
- },
218
- ];`,
219
- );
220
-
221
- await loadRulesFromPath(handlerPath, { engine });
222
- expect(engine.ruleCount).toBe(2);
223
-
224
- expect(engine.match(makeReq({ lastMessage: "hello" }))).toBeDefined();
225
- expect(engine.match(makeReq({ lastMessage: "bye" }))).toBeDefined();
226
- expect(engine.match(makeReq({ lastMessage: "nothing" }))).toBeUndefined();
227
- });
228
-
229
- it("handler respond function receives the request", async () => {
230
- const handlerPath = join(tmpDir, "dynamic.ts");
231
- await writeFile(
232
- handlerPath,
233
- `export default {
234
- match: (req) => req.lastMessage.includes("echo"),
235
- respond: (req) => \`Echo: \${req.lastMessage}\`,
236
- };`,
237
- );
238
-
239
- await loadRulesFromPath(handlerPath, { engine });
240
- const rule = engine.match(makeReq({ lastMessage: "echo this" }));
241
- if (!rule) throw new Error("expected match for 'echo'");
242
-
243
- const resolver = rule.resolve as (req: MockRequest) => string;
244
- const result = resolver(makeReq({ lastMessage: "echo this" }));
245
- expect(result).toBe("Echo: echo this");
246
- });
247
-
248
- it("throws on invalid handler file (missing match/respond)", async () => {
249
- const handlerPath = join(tmpDir, "bad.ts");
250
- await writeFile(
251
- handlerPath,
252
- `export default { mach: () => true, respond: () => "hi" };`,
253
- );
254
-
255
- await expect(loadRulesFromPath(handlerPath, { engine })).rejects.toThrow(
256
- "Invalid handler file",
257
- );
258
- });
259
-
260
- it("loads fallback from handler file", async () => {
261
- const handlerPath = join(tmpDir, "with-fallback.ts");
262
- await writeFile(
263
- handlerPath,
264
- `export const fallback = "Default reply.";
265
- export default {
266
- match: (req) => req.lastMessage.includes("hello"),
267
- respond: () => "Hi!",
268
- };`,
269
- );
270
-
271
- let capturedFallback: unknown;
272
- await loadRulesFromPath(handlerPath, {
273
- engine,
274
- setFallback: (reply) => {
275
- capturedFallback = reply;
276
- },
277
- });
278
-
279
- expect(capturedFallback).toBe("Default reply.");
280
- expect(engine.ruleCount).toBe(1);
281
- });
282
- });
283
-
284
- describe("unsupported file extension", () => {
285
- it("throws when loading a file with unsupported extension", async () => {
286
- const yamlPath = join(tmpDir, "rules.yaml");
287
- await writeFile(yamlPath, "- when: hello\n reply: Hi!");
288
- await expect(loadRulesFromPath(yamlPath, { engine })).rejects.toThrow(
289
- 'Unsupported file extension ".yaml"',
290
- );
291
- });
292
-
293
- it("skips unsupported files when scanning a directory", async () => {
294
- await writeFile(
295
- join(tmpDir, "good.json5"),
296
- `[{ when: "hello", reply: "Hi!" }]`,
297
- );
298
- await writeFile(join(tmpDir, "notes.txt"), "not a rule file");
299
- await loadRulesFromPath(tmpDir, { engine });
300
- expect(engine.ruleCount).toBe(1);
301
- });
302
- });
303
-
304
- describe("directory loading", () => {
305
- it("loads all .json5 files from a directory", async () => {
306
- await writeFile(join(tmpDir, "a.json5"), `[{ when: "aaa", reply: "A" }]`);
307
- await writeFile(join(tmpDir, "b.json5"), `[{ when: "bbb", reply: "B" }]`);
308
-
309
- await loadRulesFromPath(tmpDir, { engine });
310
- expect(engine.ruleCount).toBe(2);
311
- });
312
-
313
- it("loads mixed .json5 and .ts files from a directory", async () => {
314
- await writeFile(
315
- join(tmpDir, "rules.json5"),
316
- `[{ when: "static", reply: "From JSON5" }]`,
317
- );
318
- await writeFile(
319
- join(tmpDir, "handler.ts"),
320
- `export default {
321
- match: (req) => req.lastMessage.includes("dynamic"),
322
- respond: () => "From handler",
323
- };`,
324
- );
325
-
326
- await loadRulesFromPath(tmpDir, { engine });
327
- expect(engine.ruleCount).toBe(2);
328
-
329
- expect(engine.match(makeReq({ lastMessage: "static" }))).toBeDefined();
330
- expect(engine.match(makeReq({ lastMessage: "dynamic" }))).toBeDefined();
331
- });
332
- });
333
- });