llm-mock-server 1.0.6 → 1.0.7

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 (250) hide show
  1. package/dist/cli/cli.d.ts +3 -0
  2. package/dist/cli/cli.d.ts.map +1 -0
  3. package/dist/cli/cli.js +103 -0
  4. package/dist/cli/cli.js.map +1 -0
  5. package/dist/cli/validators.d.ts +7 -0
  6. package/dist/cli/validators.d.ts.map +1 -0
  7. package/dist/cli/validators.js +53 -0
  8. package/dist/cli/validators.js.map +1 -0
  9. package/dist/formats/anthropic/index.d.ts +1 -1
  10. package/dist/formats/anthropic/index.d.ts.map +1 -1
  11. package/dist/formats/anthropic/index.js +1 -1
  12. package/dist/formats/anthropic/index.js.map +1 -1
  13. package/dist/formats/anthropic/parse.d.ts +2 -2
  14. package/dist/formats/anthropic/parse.d.ts.map +1 -1
  15. package/dist/formats/anthropic/parse.js +4 -2
  16. package/dist/formats/anthropic/parse.js.map +1 -1
  17. package/dist/formats/anthropic/schema.d.ts +1 -1
  18. package/dist/formats/anthropic/schema.d.ts.map +1 -1
  19. package/dist/formats/anthropic/schema.js +9 -4
  20. package/dist/formats/anthropic/schema.js.map +1 -1
  21. package/dist/formats/anthropic/serialize.d.ts +2 -2
  22. package/dist/formats/anthropic/serialize.d.ts.map +1 -1
  23. package/dist/formats/anthropic/serialize.js +76 -19
  24. package/dist/formats/anthropic/serialize.js.map +1 -1
  25. package/dist/formats/openai/chat-completions/index.d.ts +3 -0
  26. package/dist/formats/openai/chat-completions/index.d.ts.map +1 -0
  27. package/dist/formats/openai/chat-completions/index.js +13 -0
  28. package/dist/formats/openai/chat-completions/index.js.map +1 -0
  29. package/dist/formats/openai/chat-completions/parse.d.ts +4 -0
  30. package/dist/formats/openai/chat-completions/parse.d.ts.map +1 -0
  31. package/dist/formats/openai/chat-completions/parse.js +33 -0
  32. package/dist/formats/openai/chat-completions/parse.js.map +1 -0
  33. package/dist/formats/openai/chat-completions/schema.d.ts +93 -0
  34. package/dist/formats/openai/chat-completions/schema.d.ts.map +1 -0
  35. package/dist/formats/openai/chat-completions/schema.js +74 -0
  36. package/dist/formats/openai/chat-completions/schema.js.map +1 -0
  37. package/dist/formats/openai/chat-completions/serialize.d.ts +10 -0
  38. package/dist/formats/openai/chat-completions/serialize.d.ts.map +1 -0
  39. package/dist/formats/openai/chat-completions/serialize.js +99 -0
  40. package/dist/formats/openai/chat-completions/serialize.js.map +1 -0
  41. package/dist/formats/openai/responses/index.d.ts +3 -0
  42. package/dist/formats/openai/responses/index.d.ts.map +1 -0
  43. package/dist/formats/openai/responses/index.js +13 -0
  44. package/dist/formats/openai/responses/index.js.map +1 -0
  45. package/dist/formats/openai/responses/parse.d.ts +4 -0
  46. package/dist/formats/openai/responses/parse.d.ts.map +1 -0
  47. package/dist/formats/openai/responses/parse.js +51 -0
  48. package/dist/formats/openai/responses/parse.js.map +1 -0
  49. package/dist/formats/openai/responses/schema.d.ts +103 -0
  50. package/dist/formats/openai/responses/schema.d.ts.map +1 -0
  51. package/dist/formats/openai/responses/schema.js +71 -0
  52. package/dist/formats/openai/responses/schema.js.map +1 -0
  53. package/dist/formats/openai/responses/serialize.d.ts +10 -0
  54. package/dist/formats/openai/responses/serialize.d.ts.map +1 -0
  55. package/dist/formats/openai/responses/serialize.js +273 -0
  56. package/dist/formats/openai/responses/serialize.js.map +1 -0
  57. package/dist/formats/request-helpers.d.ts +1 -1
  58. package/dist/formats/request-helpers.d.ts.map +1 -1
  59. package/dist/formats/request-helpers.js.map +1 -1
  60. package/dist/formats/serialize-helpers.d.ts +1 -1
  61. package/dist/formats/serialize-helpers.d.ts.map +1 -1
  62. package/dist/formats/serialize-helpers.js +6 -3
  63. package/dist/formats/serialize-helpers.js.map +1 -1
  64. package/dist/formats/types.d.ts +2 -1
  65. package/dist/formats/types.d.ts.map +1 -1
  66. package/dist/history.d.ts +6 -2
  67. package/dist/history.d.ts.map +1 -1
  68. package/dist/history.js +2 -0
  69. package/dist/history.js.map +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/loader.d.ts +1 -1
  73. package/dist/loader.d.ts.map +1 -1
  74. package/dist/loader.js +26 -9
  75. package/dist/loader.js.map +1 -1
  76. package/dist/logger.d.ts.map +1 -1
  77. package/dist/logger.js +12 -4
  78. package/dist/logger.js.map +1 -1
  79. package/dist/mock-server.d.ts +44 -48
  80. package/dist/mock-server.d.ts.map +1 -1
  81. package/dist/mock-server.js +37 -85
  82. package/dist/mock-server.js.map +1 -1
  83. package/dist/route-handler.d.ts +1 -1
  84. package/dist/route-handler.d.ts.map +1 -1
  85. package/dist/route-handler.js +19 -7
  86. package/dist/route-handler.js.map +1 -1
  87. package/dist/rule-builder.d.ts +21 -0
  88. package/dist/rule-builder.d.ts.map +1 -0
  89. package/dist/rule-builder.js +58 -0
  90. package/dist/rule-builder.js.map +1 -0
  91. package/dist/rule-engine.d.ts +3 -1
  92. package/dist/rule-engine.d.ts.map +1 -1
  93. package/dist/rule-engine.js +7 -2
  94. package/dist/rule-engine.js.map +1 -1
  95. package/dist/sse-writer.d.ts +1 -1
  96. package/dist/sse-writer.d.ts.map +1 -1
  97. package/dist/types/reply.d.ts +51 -8
  98. package/dist/types/reply.d.ts.map +1 -1
  99. package/dist/types/request.d.ts +21 -6
  100. package/dist/types/request.d.ts.map +1 -1
  101. package/dist/types/rule.d.ts +65 -7
  102. package/dist/types/rule.d.ts.map +1 -1
  103. package/dist/types.d.ts +3 -3
  104. package/dist/types.d.ts.map +1 -1
  105. package/package.json +15 -9
  106. package/.claude/skills/desloppify/SKILL.md +0 -308
  107. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +0 -242
  108. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +0 -248
  109. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +0 -248
  110. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +0 -311
  111. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +0 -17
  112. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +0 -255
  113. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +0 -22
  114. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +0 -20
  115. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +0 -20
  116. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050000.json +0 -286
  117. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050028.json +0 -303
  118. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/claude_launch_prompt.md +0 -17
  119. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json +0 -297
  120. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json +0 -22
  121. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/reviewer_instructions.md +0 -20
  122. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/session.json +0 -20
  123. package/.desloppify/query.json +0 -1312
  124. package/.desloppify/review_packet_blind.json +0 -1249
  125. package/.desloppify/review_packets/holistic_packet_20260315_000339.json +0 -1471
  126. package/.desloppify/review_packets/holistic_packet_20260315_045546.json +0 -1480
  127. package/.desloppify/review_packets/holistic_packet_20260315_185401.json +0 -1407
  128. package/.desloppify/review_packets/holistic_packet_20260315_185613.json +0 -1407
  129. package/.desloppify/state-typescript.json +0 -8438
  130. package/.desloppify/state-typescript.json.bak +0 -8432
  131. package/.desloppify/subagents/runs/20260315_185401/logs/batch-1.log +0 -384
  132. package/.desloppify/subagents/runs/20260315_185401/logs/batch-10.log +0 -484
  133. package/.desloppify/subagents/runs/20260315_185401/logs/batch-2.log +0 -408
  134. package/.desloppify/subagents/runs/20260315_185401/logs/batch-3.log +0 -416
  135. package/.desloppify/subagents/runs/20260315_185401/logs/batch-4.log +0 -360
  136. package/.desloppify/subagents/runs/20260315_185401/logs/batch-5.log +0 -360
  137. package/.desloppify/subagents/runs/20260315_185401/logs/batch-6.log +0 -364
  138. package/.desloppify/subagents/runs/20260315_185401/logs/batch-7.log +0 -428
  139. package/.desloppify/subagents/runs/20260315_185401/logs/batch-8.log +0 -388
  140. package/.desloppify/subagents/runs/20260315_185401/logs/batch-9.log +0 -500
  141. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-1.md +0 -83
  142. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-10.md +0 -108
  143. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-2.md +0 -89
  144. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-3.md +0 -91
  145. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-4.md +0 -77
  146. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-5.md +0 -77
  147. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-6.md +0 -78
  148. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-7.md +0 -94
  149. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-8.md +0 -84
  150. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-9.md +0 -112
  151. package/.desloppify/subagents/runs/20260315_185401/results/batch-1.raw.txt +0 -0
  152. package/.desloppify/subagents/runs/20260315_185401/results/batch-10.raw.txt +0 -0
  153. package/.desloppify/subagents/runs/20260315_185401/results/batch-2.raw.txt +0 -0
  154. package/.desloppify/subagents/runs/20260315_185401/results/batch-3.raw.txt +0 -0
  155. package/.desloppify/subagents/runs/20260315_185401/results/batch-4.raw.txt +0 -0
  156. package/.desloppify/subagents/runs/20260315_185401/results/batch-5.raw.txt +0 -0
  157. package/.desloppify/subagents/runs/20260315_185401/results/batch-6.raw.txt +0 -0
  158. package/.desloppify/subagents/runs/20260315_185401/results/batch-7.raw.txt +0 -0
  159. package/.desloppify/subagents/runs/20260315_185401/results/batch-8.raw.txt +0 -0
  160. package/.desloppify/subagents/runs/20260315_185401/results/batch-9.raw.txt +0 -0
  161. package/.desloppify/subagents/runs/20260315_185401/run.log +0 -36
  162. package/.desloppify/subagents/runs/20260315_185401/run_summary.json +0 -156
  163. package/.desloppify/subagents/runs/20260315_185613/holistic_findings_merged.json +0 -741
  164. package/.desloppify/subagents/runs/20260315_185613/logs/batch-1.log +0 -579
  165. package/.desloppify/subagents/runs/20260315_185613/logs/batch-10.log +0 -1537
  166. package/.desloppify/subagents/runs/20260315_185613/logs/batch-2.log +0 -829
  167. package/.desloppify/subagents/runs/20260315_185613/logs/batch-3.log +0 -927
  168. package/.desloppify/subagents/runs/20260315_185613/logs/batch-4.log +0 -429
  169. package/.desloppify/subagents/runs/20260315_185613/logs/batch-5.log +0 -276
  170. package/.desloppify/subagents/runs/20260315_185613/logs/batch-6.log +0 -450
  171. package/.desloppify/subagents/runs/20260315_185613/logs/batch-7.log +0 -730
  172. package/.desloppify/subagents/runs/20260315_185613/logs/batch-8.log +0 -698
  173. package/.desloppify/subagents/runs/20260315_185613/logs/batch-9.log +0 -938
  174. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-1.md +0 -83
  175. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-10.md +0 -108
  176. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-2.md +0 -89
  177. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-3.md +0 -91
  178. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-4.md +0 -77
  179. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-5.md +0 -77
  180. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-6.md +0 -78
  181. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-7.md +0 -94
  182. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-8.md +0 -84
  183. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-9.md +0 -112
  184. package/.desloppify/subagents/runs/20260315_185613/results/batch-1.raw.txt +0 -78
  185. package/.desloppify/subagents/runs/20260315_185613/results/batch-10.raw.txt +0 -242
  186. package/.desloppify/subagents/runs/20260315_185613/results/batch-2.raw.txt +0 -102
  187. package/.desloppify/subagents/runs/20260315_185613/results/batch-3.raw.txt +0 -94
  188. package/.desloppify/subagents/runs/20260315_185613/results/batch-4.raw.txt +0 -86
  189. package/.desloppify/subagents/runs/20260315_185613/results/batch-5.raw.txt +0 -1
  190. package/.desloppify/subagents/runs/20260315_185613/results/batch-6.raw.txt +0 -87
  191. package/.desloppify/subagents/runs/20260315_185613/results/batch-7.raw.txt +0 -1
  192. package/.desloppify/subagents/runs/20260315_185613/results/batch-8.raw.txt +0 -107
  193. package/.desloppify/subagents/runs/20260315_185613/results/batch-9.raw.txt +0 -67
  194. package/.desloppify/subagents/runs/20260315_185613/run.log +0 -96
  195. package/.desloppify/subagents/runs/20260315_185613/run_summary.json +0 -156
  196. package/.editorconfig +0 -12
  197. package/.github/dependabot.yml +0 -11
  198. package/.github/workflows/docs.yml +0 -46
  199. package/.github/workflows/test.yml +0 -40
  200. package/.markdownlint.jsonc +0 -11
  201. package/.node-version +0 -1
  202. package/.oxfmtrc.json +0 -9
  203. package/.oxlintrc.json +0 -35
  204. package/docs/ARCHITECTURE.md +0 -125
  205. package/scorecard.png +0 -0
  206. package/src/cli/cli.ts +0 -141
  207. package/src/cli/validators.ts +0 -68
  208. package/src/formats/anthropic/index.ts +0 -14
  209. package/src/formats/anthropic/parse.ts +0 -70
  210. package/src/formats/anthropic/schema.ts +0 -74
  211. package/src/formats/anthropic/serialize.ts +0 -179
  212. package/src/formats/openai/chat-completions/index.ts +0 -14
  213. package/src/formats/openai/chat-completions/parse.ts +0 -47
  214. package/src/formats/openai/chat-completions/schema.ts +0 -92
  215. package/src/formats/openai/chat-completions/serialize.ts +0 -146
  216. package/src/formats/openai/responses/index.ts +0 -14
  217. package/src/formats/openai/responses/parse.ts +0 -73
  218. package/src/formats/openai/responses/schema.ts +0 -86
  219. package/src/formats/openai/responses/serialize.ts +0 -328
  220. package/src/formats/request-helpers.ts +0 -56
  221. package/src/formats/serialize-helpers.ts +0 -43
  222. package/src/formats/types.ts +0 -26
  223. package/src/history.ts +0 -70
  224. package/src/index.ts +0 -46
  225. package/src/loader.ts +0 -246
  226. package/src/logger.ts +0 -70
  227. package/src/mock-server.ts +0 -203
  228. package/src/route-handler.ts +0 -144
  229. package/src/rule-builder.ts +0 -73
  230. package/src/rule-engine.ts +0 -165
  231. package/src/sse-writer.ts +0 -35
  232. package/src/types/reply.ts +0 -92
  233. package/src/types/request.ts +0 -56
  234. package/src/types/rule.ts +0 -125
  235. package/src/types.ts +0 -24
  236. package/test/cli-validators.test.ts +0 -151
  237. package/test/formats/anthropic.test.ts +0 -336
  238. package/test/formats/openai.test.ts +0 -316
  239. package/test/formats/parse-helpers.test.ts +0 -315
  240. package/test/formats/responses.test.ts +0 -380
  241. package/test/helpers/make-req.ts +0 -18
  242. package/test/history.test.ts +0 -361
  243. package/test/loader.test.ts +0 -333
  244. package/test/logger.test.ts +0 -344
  245. package/test/mock-server.test.ts +0 -619
  246. package/test/rule-engine.test.ts +0 -229
  247. package/tsconfig.json +0 -24
  248. package/tsconfig.test.json +0 -11
  249. package/typedoc.json +0 -9
  250. 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
- });