llm-mock-server 1.0.1 → 1.0.3

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 (129) hide show
  1. package/.claude/skills/desloppify/SKILL.md +308 -0
  2. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +242 -0
  3. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +248 -0
  4. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +248 -0
  5. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +311 -0
  6. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +17 -0
  7. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +255 -0
  8. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +22 -0
  9. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +20 -0
  10. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +20 -0
  11. package/.desloppify/query.json +284 -0
  12. package/.desloppify/review_packet_blind.json +1303 -0
  13. package/.desloppify/review_packets/holistic_packet_20260315_000339.json +1471 -0
  14. package/.desloppify/state-typescript.json +5114 -0
  15. package/.desloppify/state-typescript.json.bak +5108 -0
  16. package/.editorconfig +12 -0
  17. package/.github/workflows/test.yml +3 -0
  18. package/.oxfmtrc.json +9 -0
  19. package/dist/cli.js +5 -2
  20. package/dist/cli.js.map +1 -1
  21. package/dist/formats/anthropic/index.js +1 -1
  22. package/dist/formats/anthropic/index.js.map +1 -1
  23. package/dist/formats/anthropic/parse.d.ts +1 -1
  24. package/dist/formats/anthropic/parse.d.ts.map +1 -1
  25. package/dist/formats/anthropic/parse.js +1 -1
  26. package/dist/formats/anthropic/parse.js.map +1 -1
  27. package/dist/formats/anthropic/serialize.d.ts +2 -2
  28. package/dist/formats/anthropic/serialize.d.ts.map +1 -1
  29. package/dist/formats/anthropic/serialize.js +6 -3
  30. package/dist/formats/anthropic/serialize.js.map +1 -1
  31. package/dist/formats/openai/index.js +1 -1
  32. package/dist/formats/openai/index.js.map +1 -1
  33. package/dist/formats/openai/parse.d.ts +1 -1
  34. package/dist/formats/openai/parse.d.ts.map +1 -1
  35. package/dist/formats/openai/parse.js +1 -1
  36. package/dist/formats/openai/parse.js.map +1 -1
  37. package/dist/formats/openai/serialize.d.ts +2 -2
  38. package/dist/formats/openai/serialize.d.ts.map +1 -1
  39. package/dist/formats/openai/serialize.js +12 -15
  40. package/dist/formats/openai/serialize.js.map +1 -1
  41. package/dist/formats/request-helpers.d.ts +13 -0
  42. package/dist/formats/request-helpers.d.ts.map +1 -0
  43. package/dist/formats/request-helpers.js +28 -0
  44. package/dist/formats/request-helpers.js.map +1 -0
  45. package/dist/formats/responses/index.js +1 -1
  46. package/dist/formats/responses/index.js.map +1 -1
  47. package/dist/formats/responses/parse.d.ts +1 -1
  48. package/dist/formats/responses/parse.d.ts.map +1 -1
  49. package/dist/formats/responses/parse.js +1 -1
  50. package/dist/formats/responses/parse.js.map +1 -1
  51. package/dist/formats/responses/schema.d.ts +1 -20
  52. package/dist/formats/responses/schema.d.ts.map +1 -1
  53. package/dist/formats/responses/schema.js.map +1 -1
  54. package/dist/formats/responses/serialize.d.ts +2 -2
  55. package/dist/formats/responses/serialize.d.ts.map +1 -1
  56. package/dist/formats/responses/serialize.js +6 -3
  57. package/dist/formats/responses/serialize.js.map +1 -1
  58. package/dist/formats/serialize-helpers.d.ts +14 -0
  59. package/dist/formats/serialize-helpers.d.ts.map +1 -0
  60. package/dist/formats/serialize-helpers.js +25 -0
  61. package/dist/formats/serialize-helpers.js.map +1 -0
  62. package/dist/formats/types.d.ts +3 -3
  63. package/dist/formats/types.d.ts.map +1 -1
  64. package/dist/loader.d.ts +3 -2
  65. package/dist/loader.d.ts.map +1 -1
  66. package/dist/loader.js +6 -9
  67. package/dist/loader.js.map +1 -1
  68. package/dist/logger.d.ts +1 -0
  69. package/dist/logger.d.ts.map +1 -1
  70. package/dist/logger.js +17 -23
  71. package/dist/logger.js.map +1 -1
  72. package/dist/mock-server.d.ts.map +1 -1
  73. package/dist/mock-server.js +8 -15
  74. package/dist/mock-server.js.map +1 -1
  75. package/dist/route-handler.d.ts +2 -1
  76. package/dist/route-handler.d.ts.map +1 -1
  77. package/dist/rule-engine.d.ts +12 -1
  78. package/dist/rule-engine.d.ts.map +1 -1
  79. package/dist/rule-engine.js +14 -0
  80. package/dist/rule-engine.js.map +1 -1
  81. package/dist/types/reply.d.ts +6 -10
  82. package/dist/types/reply.d.ts.map +1 -1
  83. package/dist/types/request.d.ts +7 -11
  84. package/dist/types/request.d.ts.map +1 -1
  85. package/dist/types/rule.d.ts +3 -10
  86. package/dist/types/rule.d.ts.map +1 -1
  87. package/dist/types.d.ts +3 -1
  88. package/dist/types.d.ts.map +1 -1
  89. package/package.json +5 -2
  90. package/scorecard.png +0 -0
  91. package/src/cli-validators.ts +12 -4
  92. package/src/cli.ts +27 -7
  93. package/src/formats/anthropic/index.ts +1 -1
  94. package/src/formats/anthropic/parse.ts +25 -6
  95. package/src/formats/anthropic/schema.ts +16 -8
  96. package/src/formats/anthropic/serialize.ts +116 -28
  97. package/src/formats/openai/index.ts +1 -1
  98. package/src/formats/openai/parse.ts +13 -3
  99. package/src/formats/openai/schema.ts +43 -30
  100. package/src/formats/openai/serialize.ts +84 -30
  101. package/src/formats/{parse-helpers.ts → request-helpers.ts} +4 -32
  102. package/src/formats/responses/index.ts +1 -1
  103. package/src/formats/responses/parse.ts +18 -4
  104. package/src/formats/responses/schema.ts +34 -22
  105. package/src/formats/responses/serialize.ts +237 -38
  106. package/src/formats/serialize-helpers.ts +38 -0
  107. package/src/formats/types.ts +18 -5
  108. package/src/index.ts +3 -1
  109. package/src/loader.ts +43 -20
  110. package/src/logger.ts +31 -19
  111. package/src/mock-server.ts +38 -21
  112. package/src/route-handler.ts +50 -15
  113. package/src/rule-engine.ts +64 -11
  114. package/src/types/reply.ts +12 -12
  115. package/src/types/request.ts +7 -11
  116. package/src/types/rule.ts +3 -10
  117. package/src/types.ts +23 -4
  118. package/test/cli-validators.test.ts +16 -4
  119. package/test/formats/anthropic.test.ts +84 -23
  120. package/test/formats/openai.test.ts +85 -24
  121. package/test/formats/parse-helpers.test.ts +315 -0
  122. package/test/formats/responses.test.ts +99 -34
  123. package/test/helpers/make-req.ts +18 -0
  124. package/test/history.test.ts +361 -0
  125. package/test/loader.test.ts +44 -45
  126. package/test/logger.test.ts +344 -0
  127. package/test/mock-server.test.ts +77 -23
  128. package/test/rule-engine.test.ts +57 -41
  129. package/src/types/index.ts +0 -4
@@ -0,0 +1,361 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { RequestHistory, type RecordedRequest } from "../src/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
+ });
@@ -4,26 +4,10 @@ import { join } from "node:path";
4
4
  import { RuleEngine } from "../src/rule-engine.js";
5
5
  import { loadRulesFromPath } from "../src/loader.js";
6
6
  import type { MockRequest } from "../src/types.js";
7
+ import { makeReq } from "./helpers/make-req.js";
7
8
 
8
9
  const tmpDir = join(import.meta.dirname, ".tmp-loader-test");
9
10
 
10
- function makeReq(overrides: Partial<MockRequest> = {}): MockRequest {
11
- return {
12
- format: "openai",
13
- model: "gpt-5.4",
14
- streaming: true,
15
- messages: [{ role: "user", content: "hello" }],
16
- lastMessage: "hello",
17
- systemMessage: "",
18
- toolNames: [],
19
- lastToolCallId: undefined,
20
- raw: {},
21
- headers: {},
22
- path: "/v1/chat/completions",
23
- ...overrides,
24
- };
25
- }
26
-
27
11
  describe("Loader", () => {
28
12
  let engine: RuleEngine;
29
13
 
@@ -56,11 +40,15 @@ describe("Loader", () => {
56
40
  await loadRulesFromPath(rulesPath, { engine });
57
41
  expect(engine.ruleCount).toBe(2);
58
42
 
59
- const match1 = engine.match(makeReq({ lastMessage: "Please explain recursion" }));
60
- expect(match1).toBeDefined();
61
- expect(match1!.resolve).toBe("A function that calls itself.");
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.");
62
48
 
63
- const match2 = engine.match(makeReq({ model: "gpt-5.4", lastMessage: "hello" }));
49
+ const match2 = engine.match(
50
+ makeReq({ model: "gpt-5.4", lastMessage: "hello" }),
51
+ );
64
52
  expect(match2).toBeDefined();
65
53
  });
66
54
 
@@ -77,7 +65,9 @@ describe("Loader", () => {
77
65
  );
78
66
 
79
67
  await loadRulesFromPath(rulesPath, { engine });
80
- const match = engine.match(makeReq({ lastMessage: "explain polymorphism" }));
68
+ const match = engine.match(
69
+ makeReq({ lastMessage: "explain polymorphism" }),
70
+ );
81
71
  expect(match).toBeDefined();
82
72
  });
83
73
 
@@ -89,7 +79,9 @@ describe("Loader", () => {
89
79
  );
90
80
 
91
81
  await loadRulesFromPath(rulesPath, { engine });
92
- expect(engine.match(makeReq({ lastMessage: "hello world" }))).toBeDefined();
82
+ expect(
83
+ engine.match(makeReq({ lastMessage: "hello world" })),
84
+ ).toBeDefined();
93
85
  });
94
86
 
95
87
  it("loads rules with times", async () => {
@@ -139,7 +131,9 @@ describe("Loader", () => {
139
131
  }`,
140
132
  );
141
133
 
142
- await expect(loadRulesFromPath(rulesPath, { engine })).rejects.toThrow("Unknown template");
134
+ await expect(loadRulesFromPath(rulesPath, { engine })).rejects.toThrow(
135
+ "Unknown template",
136
+ );
143
137
  });
144
138
 
145
139
  it("loads a replies sequence", async () => {
@@ -154,12 +148,12 @@ describe("Loader", () => {
154
148
 
155
149
  const req = makeReq({ lastMessage: "step" });
156
150
  const match1 = engine.match(req);
157
- expect(match1).toBeDefined();
158
- expect((match1!.resolve as () => string)()).toBe("First.");
151
+ if (!match1) throw new Error("expected match1");
152
+ expect((match1.resolve as () => string)()).toBe("First.");
159
153
 
160
154
  const match2 = engine.match(req);
161
- expect(match2).toBeDefined();
162
- expect((match2!.resolve as () => string)()).toBe("Second.");
155
+ if (!match2) throw new Error("expected match2");
156
+ expect((match2.resolve as () => string)()).toBe("Second.");
163
157
 
164
158
  expect(engine.match(req)).toBeUndefined();
165
159
  });
@@ -177,7 +171,9 @@ describe("Loader", () => {
177
171
  let capturedFallback: unknown;
178
172
  await loadRulesFromPath(rulesPath, {
179
173
  engine,
180
- setFallback: (reply) => { capturedFallback = reply; },
174
+ setFallback: (reply) => {
175
+ capturedFallback = reply;
176
+ },
181
177
  });
182
178
 
183
179
  expect(capturedFallback).toBe("Default reply.");
@@ -199,9 +195,11 @@ describe("Loader", () => {
199
195
  await loadRulesFromPath(handlerPath, { engine });
200
196
  expect(engine.ruleCount).toBe(1);
201
197
 
202
- const match = engine.match(makeReq({ lastMessage: "summarize this article" }));
203
- expect(match).toBeDefined();
204
- expect(match!.resolve).toBeTypeOf("function");
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");
205
203
  });
206
204
 
207
205
  it("loads an array of handlers from a .ts file", async () => {
@@ -240,18 +238,23 @@ describe("Loader", () => {
240
238
 
241
239
  await loadRulesFromPath(handlerPath, { engine });
242
240
  const rule = engine.match(makeReq({ lastMessage: "echo this" }));
243
- expect(rule).toBeDefined();
241
+ if (!rule) throw new Error("expected match for 'echo'");
244
242
 
245
- const resolver = rule!.resolve as (req: MockRequest) => string;
243
+ const resolver = rule.resolve as (req: MockRequest) => string;
246
244
  const result = resolver(makeReq({ lastMessage: "echo this" }));
247
245
  expect(result).toBe("Echo: echo this");
248
246
  });
249
247
 
250
248
  it("throws on invalid handler file (missing match/respond)", async () => {
251
249
  const handlerPath = join(tmpDir, "bad.ts");
252
- await writeFile(handlerPath, `export default { mach: () => true, respond: () => "hi" };`);
250
+ await writeFile(
251
+ handlerPath,
252
+ `export default { mach: () => true, respond: () => "hi" };`,
253
+ );
253
254
 
254
- await expect(loadRulesFromPath(handlerPath, { engine })).rejects.toThrow("Invalid handler file");
255
+ await expect(loadRulesFromPath(handlerPath, { engine })).rejects.toThrow(
256
+ "Invalid handler file",
257
+ );
255
258
  });
256
259
 
257
260
  it("loads fallback from handler file", async () => {
@@ -268,7 +271,9 @@ describe("Loader", () => {
268
271
  let capturedFallback: unknown;
269
272
  await loadRulesFromPath(handlerPath, {
270
273
  engine,
271
- setFallback: (reply) => { capturedFallback = reply; },
274
+ setFallback: (reply) => {
275
+ capturedFallback = reply;
276
+ },
272
277
  });
273
278
 
274
279
  expect(capturedFallback).toBe("Default reply.");
@@ -278,14 +283,8 @@ describe("Loader", () => {
278
283
 
279
284
  describe("directory loading", () => {
280
285
  it("loads all .json5 files from a directory", async () => {
281
- await writeFile(
282
- join(tmpDir, "a.json5"),
283
- `[{ when: "aaa", reply: "A" }]`,
284
- );
285
- await writeFile(
286
- join(tmpDir, "b.json5"),
287
- `[{ when: "bbb", reply: "B" }]`,
288
- );
286
+ await writeFile(join(tmpDir, "a.json5"), `[{ when: "aaa", reply: "A" }]`);
287
+ await writeFile(join(tmpDir, "b.json5"), `[{ when: "bbb", reply: "B" }]`);
289
288
 
290
289
  await loadRulesFromPath(tmpDir, { engine });
291
290
  expect(engine.ruleCount).toBe(2);