llm-mock-server 1.0.0 → 1.0.2

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 (179) 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/dist/cli-validators.d.ts +7 -0
  17. package/dist/cli-validators.d.ts.map +1 -0
  18. package/dist/cli-validators.js +51 -0
  19. package/dist/cli-validators.js.map +1 -0
  20. package/dist/cli.d.ts +3 -0
  21. package/dist/cli.d.ts.map +1 -0
  22. package/dist/cli.js +106 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/formats/anthropic/index.d.ts +3 -0
  25. package/dist/formats/anthropic/index.d.ts.map +1 -0
  26. package/dist/formats/anthropic/index.js +13 -0
  27. package/dist/formats/anthropic/index.js.map +1 -0
  28. package/dist/formats/anthropic/parse.d.ts +4 -0
  29. package/dist/formats/anthropic/parse.d.ts.map +1 -0
  30. package/dist/formats/anthropic/parse.js +47 -0
  31. package/dist/formats/anthropic/parse.js.map +1 -0
  32. package/dist/formats/anthropic/schema.d.ts +75 -0
  33. package/dist/formats/anthropic/schema.d.ts.map +1 -0
  34. package/dist/formats/anthropic/schema.js +50 -0
  35. package/dist/formats/anthropic/schema.js.map +1 -0
  36. package/dist/formats/anthropic/serialize.d.ts +10 -0
  37. package/dist/formats/anthropic/serialize.d.ts.map +1 -0
  38. package/dist/formats/anthropic/serialize.js +73 -0
  39. package/dist/formats/anthropic/serialize.js.map +1 -0
  40. package/dist/formats/openai/index.d.ts +3 -0
  41. package/dist/formats/openai/index.d.ts.map +1 -0
  42. package/dist/formats/openai/index.js +13 -0
  43. package/dist/formats/openai/index.js.map +1 -0
  44. package/dist/formats/openai/parse.d.ts +4 -0
  45. package/dist/formats/openai/parse.d.ts.map +1 -0
  46. package/dist/formats/openai/parse.js +33 -0
  47. package/dist/formats/openai/parse.js.map +1 -0
  48. package/dist/formats/openai/schema.d.ts +93 -0
  49. package/dist/formats/openai/schema.d.ts.map +1 -0
  50. package/dist/formats/openai/schema.js +68 -0
  51. package/dist/formats/openai/schema.js.map +1 -0
  52. package/dist/formats/openai/serialize.d.ts +10 -0
  53. package/dist/formats/openai/serialize.d.ts.map +1 -0
  54. package/dist/formats/openai/serialize.js +70 -0
  55. package/dist/formats/openai/serialize.js.map +1 -0
  56. package/dist/formats/parse-helpers.d.ts +24 -0
  57. package/dist/formats/parse-helpers.d.ts.map +1 -0
  58. package/dist/formats/parse-helpers.js +52 -0
  59. package/dist/formats/parse-helpers.js.map +1 -0
  60. package/dist/formats/request-helpers.d.ts +13 -0
  61. package/dist/formats/request-helpers.d.ts.map +1 -0
  62. package/dist/formats/request-helpers.js +28 -0
  63. package/dist/formats/request-helpers.js.map +1 -0
  64. package/dist/formats/responses/index.d.ts +3 -0
  65. package/dist/formats/responses/index.d.ts.map +1 -0
  66. package/dist/formats/responses/index.js +13 -0
  67. package/dist/formats/responses/index.js.map +1 -0
  68. package/dist/formats/responses/parse.d.ts +4 -0
  69. package/dist/formats/responses/parse.d.ts.map +1 -0
  70. package/dist/formats/responses/parse.js +51 -0
  71. package/dist/formats/responses/parse.js.map +1 -0
  72. package/dist/formats/responses/schema.d.ts +103 -0
  73. package/dist/formats/responses/schema.d.ts.map +1 -0
  74. package/dist/formats/responses/schema.js +61 -0
  75. package/dist/formats/responses/schema.js.map +1 -0
  76. package/dist/formats/responses/serialize.d.ts +10 -0
  77. package/dist/formats/responses/serialize.d.ts.map +1 -0
  78. package/dist/formats/responses/serialize.js +108 -0
  79. package/dist/formats/responses/serialize.js.map +1 -0
  80. package/dist/formats/serialize-helpers.d.ts +14 -0
  81. package/dist/formats/serialize-helpers.d.ts.map +1 -0
  82. package/dist/formats/serialize-helpers.js +25 -0
  83. package/dist/formats/serialize-helpers.js.map +1 -0
  84. package/dist/formats/types.d.ts +20 -0
  85. package/dist/formats/types.d.ts.map +1 -0
  86. package/dist/formats/types.js +2 -0
  87. package/dist/formats/types.js.map +1 -0
  88. package/dist/history.d.ts +38 -0
  89. package/dist/history.d.ts.map +1 -0
  90. package/dist/history.js +48 -0
  91. package/dist/history.js.map +1 -0
  92. package/dist/index.d.ts +21 -0
  93. package/dist/index.d.ts.map +1 -0
  94. package/dist/index.js +20 -0
  95. package/dist/index.js.map +1 -0
  96. package/dist/loader.d.ts +9 -0
  97. package/dist/loader.d.ts.map +1 -0
  98. package/dist/loader.js +169 -0
  99. package/dist/loader.js.map +1 -0
  100. package/dist/logger.d.ts +21 -0
  101. package/dist/logger.d.ts.map +1 -0
  102. package/dist/logger.js +42 -0
  103. package/dist/logger.js.map +1 -0
  104. package/dist/mock-server.d.ts +102 -0
  105. package/dist/mock-server.d.ts.map +1 -0
  106. package/dist/mock-server.js +195 -0
  107. package/dist/mock-server.js.map +1 -0
  108. package/dist/route-handler.d.ts +16 -0
  109. package/dist/route-handler.d.ts.map +1 -0
  110. package/dist/route-handler.js +75 -0
  111. package/dist/route-handler.js.map +1 -0
  112. package/dist/rule-engine.d.ts +24 -0
  113. package/dist/rule-engine.d.ts.map +1 -0
  114. package/dist/rule-engine.js +129 -0
  115. package/dist/rule-engine.js.map +1 -0
  116. package/dist/sse-writer.d.ts +5 -0
  117. package/dist/sse-writer.d.ts.map +1 -0
  118. package/dist/sse-writer.js +23 -0
  119. package/dist/sse-writer.js.map +1 -0
  120. package/{src/types/index.ts → dist/types/index.d.ts} +1 -0
  121. package/dist/types/index.d.ts.map +1 -0
  122. package/dist/types/index.js +2 -0
  123. package/dist/types/index.js.map +1 -0
  124. package/dist/types/reply.d.ts +45 -0
  125. package/dist/types/reply.d.ts.map +1 -0
  126. package/dist/types/reply.js +2 -0
  127. package/dist/types/reply.js.map +1 -0
  128. package/dist/types/request.d.ts +39 -0
  129. package/dist/types/request.d.ts.map +1 -0
  130. package/dist/types/request.js +2 -0
  131. package/dist/types/request.js.map +1 -0
  132. package/dist/types/rule.d.ts +57 -0
  133. package/dist/types/rule.d.ts.map +1 -0
  134. package/dist/types/rule.js +2 -0
  135. package/dist/types/rule.js.map +1 -0
  136. package/dist/types.d.ts +4 -0
  137. package/dist/types.d.ts.map +1 -0
  138. package/dist/types.js +2 -0
  139. package/dist/types.js.map +1 -0
  140. package/package.json +2 -1
  141. package/scorecard.png +0 -0
  142. package/src/cli-validators.ts +3 -0
  143. package/src/cli.ts +6 -2
  144. package/src/formats/anthropic/index.ts +1 -1
  145. package/src/formats/anthropic/parse.ts +4 -4
  146. package/src/formats/anthropic/schema.ts +1 -68
  147. package/src/formats/anthropic/serialize.ts +9 -5
  148. package/src/formats/openai/index.ts +1 -1
  149. package/src/formats/openai/parse.ts +1 -1
  150. package/src/formats/openai/schema.ts +1 -69
  151. package/src/formats/openai/serialize.ts +15 -17
  152. package/src/formats/{parse-helpers.ts → request-helpers.ts} +2 -31
  153. package/src/formats/responses/index.ts +1 -1
  154. package/src/formats/responses/parse.ts +1 -1
  155. package/src/formats/responses/schema.ts +1 -72
  156. package/src/formats/responses/serialize.ts +9 -5
  157. package/src/formats/serialize-helpers.ts +30 -0
  158. package/src/formats/types.ts +3 -3
  159. package/src/loader.ts +7 -11
  160. package/src/logger.ts +19 -25
  161. package/src/mock-server.ts +10 -14
  162. package/src/route-handler.ts +1 -1
  163. package/src/rule-engine.ts +23 -1
  164. package/src/types/reply.ts +6 -10
  165. package/src/types/request.ts +7 -11
  166. package/src/types/rule.ts +3 -10
  167. package/src/types.ts +3 -5
  168. package/test/formats/anthropic.test.ts +4 -4
  169. package/test/formats/parse-helpers.test.ts +275 -0
  170. package/test/formats/responses.test.ts +4 -4
  171. package/test/helpers/make-req.ts +18 -0
  172. package/test/history.test.ts +348 -0
  173. package/test/loader.test.ts +11 -27
  174. package/test/logger.test.ts +294 -0
  175. package/test/mock-server.test.ts +1 -1
  176. package/test/rule-engine.test.ts +8 -22
  177. package/test/formats/anthropic-schema.test.ts +0 -192
  178. package/test/formats/openai-schema.test.ts +0 -105
  179. package/test/formats/responses-schema.test.ts +0 -114
@@ -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
 
@@ -57,8 +41,8 @@ describe("Loader", () => {
57
41
  expect(engine.ruleCount).toBe(2);
58
42
 
59
43
  const match1 = engine.match(makeReq({ lastMessage: "Please explain recursion" }));
60
- expect(match1).toBeDefined();
61
- expect(match1!.resolve).toBe("A function that calls itself.");
44
+ if (!match1) throw new Error("expected match for 'explain'");
45
+ expect(match1.resolve).toBe("A function that calls itself.");
62
46
 
63
47
  const match2 = engine.match(makeReq({ model: "gpt-5.4", lastMessage: "hello" }));
64
48
  expect(match2).toBeDefined();
@@ -154,12 +138,12 @@ describe("Loader", () => {
154
138
 
155
139
  const req = makeReq({ lastMessage: "step" });
156
140
  const match1 = engine.match(req);
157
- expect(match1).toBeDefined();
158
- expect((match1!.resolve as () => string)()).toBe("First.");
141
+ if (!match1) throw new Error("expected match1");
142
+ expect((match1.resolve as () => string)()).toBe("First.");
159
143
 
160
144
  const match2 = engine.match(req);
161
- expect(match2).toBeDefined();
162
- expect((match2!.resolve as () => string)()).toBe("Second.");
145
+ if (!match2) throw new Error("expected match2");
146
+ expect((match2.resolve as () => string)()).toBe("Second.");
163
147
 
164
148
  expect(engine.match(req)).toBeUndefined();
165
149
  });
@@ -200,8 +184,8 @@ describe("Loader", () => {
200
184
  expect(engine.ruleCount).toBe(1);
201
185
 
202
186
  const match = engine.match(makeReq({ lastMessage: "summarize this article" }));
203
- expect(match).toBeDefined();
204
- expect(match!.resolve).toBeTypeOf("function");
187
+ if (!match) throw new Error("expected match for 'summarize'");
188
+ expect(match.resolve).toBeTypeOf("function");
205
189
  });
206
190
 
207
191
  it("loads an array of handlers from a .ts file", async () => {
@@ -240,9 +224,9 @@ describe("Loader", () => {
240
224
 
241
225
  await loadRulesFromPath(handlerPath, { engine });
242
226
  const rule = engine.match(makeReq({ lastMessage: "echo this" }));
243
- expect(rule).toBeDefined();
227
+ if (!rule) throw new Error("expected match for 'echo'");
244
228
 
245
- const resolver = rule!.resolve as (req: MockRequest) => string;
229
+ const resolver = rule.resolve as (req: MockRequest) => string;
246
230
  const result = resolver(makeReq({ lastMessage: "echo this" }));
247
231
  expect(result).toBe("Echo: echo this");
248
232
  });
@@ -0,0 +1,294 @@
1
+ import { describe, it, expect, vi, afterEach } from "vitest";
2
+ import { Logger, LEVEL_PRIORITY } from "../src/logger.js";
3
+ import type { LogLevel } from "../src/logger.js";
4
+
5
+ afterEach(() => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ describe("LEVEL_PRIORITY", () => {
10
+ it("has the expected keys and ascending values", () => {
11
+ expect(LEVEL_PRIORITY).toEqual({
12
+ none: 0,
13
+ error: 1,
14
+ warning: 2,
15
+ info: 3,
16
+ debug: 4,
17
+ all: 5,
18
+ });
19
+ });
20
+
21
+ it("is ordered so that each named level is strictly higher than the previous", () => {
22
+ expect(LEVEL_PRIORITY.none).toBeLessThan(LEVEL_PRIORITY.error);
23
+ expect(LEVEL_PRIORITY.error).toBeLessThan(LEVEL_PRIORITY.warning);
24
+ expect(LEVEL_PRIORITY.warning).toBeLessThan(LEVEL_PRIORITY.info);
25
+ expect(LEVEL_PRIORITY.info).toBeLessThan(LEVEL_PRIORITY.debug);
26
+ expect(LEVEL_PRIORITY.debug).toBeLessThan(LEVEL_PRIORITY.all);
27
+ });
28
+ });
29
+
30
+ describe("Logger", () => {
31
+ describe("constructor", () => {
32
+ it("defaults to 'info' level when no argument is provided", () => {
33
+ const logger = new Logger();
34
+ expect(logger.level).toBe("info");
35
+ });
36
+
37
+ it("accepts an explicit level", () => {
38
+ const logger = new Logger("debug");
39
+ expect(logger.level).toBe("debug");
40
+ });
41
+
42
+ it("level property is readonly and accessible", () => {
43
+ const logger = new Logger("warning");
44
+ expect(logger.level).toBe("warning");
45
+ });
46
+ });
47
+
48
+ describe("error()", () => {
49
+ it("logs to console.error when level is 'error'", () => {
50
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
51
+ const logger = new Logger("error");
52
+ logger.error("something broke");
53
+ expect(spy).toHaveBeenCalledOnce();
54
+ expect(spy.mock.calls[0]![0]).toContain("something broke");
55
+ });
56
+
57
+ it("logs to console.error when level is 'info' (threshold above error)", () => {
58
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
59
+ const logger = new Logger("info");
60
+ logger.error("boom");
61
+ expect(spy).toHaveBeenCalledOnce();
62
+ });
63
+
64
+ it("logs to console.error when level is 'all'", () => {
65
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
66
+ const logger = new Logger("all");
67
+ logger.error("critical");
68
+ expect(spy).toHaveBeenCalledOnce();
69
+ });
70
+
71
+ it("is silent when level is 'none'", () => {
72
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
73
+ const logger = new Logger("none");
74
+ logger.error("should not appear");
75
+ expect(spy).not.toHaveBeenCalled();
76
+ });
77
+
78
+ it("passes extra arguments through to console.error", () => {
79
+ const spy = vi.spyOn(console, "error").mockImplementation(() => {});
80
+ const logger = new Logger("error");
81
+ const extra = { code: 500 };
82
+ logger.error("fail", extra);
83
+ expect(spy).toHaveBeenCalledOnce();
84
+ expect(spy.mock.calls[0]).toContain(extra);
85
+ });
86
+ });
87
+
88
+ describe("warn()", () => {
89
+ it("logs to console.warn when level is 'warning'", () => {
90
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
91
+ const logger = new Logger("warning");
92
+ logger.warn("heads up");
93
+ expect(spy).toHaveBeenCalledOnce();
94
+ expect(spy.mock.calls[0]![0]).toContain("heads up");
95
+ });
96
+
97
+ it("logs to console.warn when level is 'info' (threshold above warning)", () => {
98
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
99
+ const logger = new Logger("info");
100
+ logger.warn("watch out");
101
+ expect(spy).toHaveBeenCalledOnce();
102
+ });
103
+
104
+ it("logs to console.warn when level is 'debug'", () => {
105
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
106
+ const logger = new Logger("debug");
107
+ logger.warn("careful");
108
+ expect(spy).toHaveBeenCalledOnce();
109
+ });
110
+
111
+ it("is silent when level is 'error' (threshold below warning)", () => {
112
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
113
+ const logger = new Logger("error");
114
+ logger.warn("should not appear");
115
+ expect(spy).not.toHaveBeenCalled();
116
+ });
117
+
118
+ it("is silent when level is 'none'", () => {
119
+ const spy = vi.spyOn(console, "warn").mockImplementation(() => {});
120
+ const logger = new Logger("none");
121
+ logger.warn("nope");
122
+ expect(spy).not.toHaveBeenCalled();
123
+ });
124
+ });
125
+
126
+ describe("info()", () => {
127
+ it("logs to console.log when level is 'info'", () => {
128
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
129
+ const logger = new Logger("info");
130
+ logger.info("status update");
131
+ expect(spy).toHaveBeenCalledOnce();
132
+ expect(spy.mock.calls[0]![0]).toContain("status update");
133
+ });
134
+
135
+ it("logs to console.log when level is 'debug' (threshold above info)", () => {
136
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
137
+ const logger = new Logger("debug");
138
+ logger.info("still visible");
139
+ expect(spy).toHaveBeenCalledOnce();
140
+ });
141
+
142
+ it("logs to console.log when level is 'all'", () => {
143
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
144
+ const logger = new Logger("all");
145
+ logger.info("everything mode");
146
+ expect(spy).toHaveBeenCalledOnce();
147
+ });
148
+
149
+ it("is silent when level is 'warning' (threshold below info)", () => {
150
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
151
+ const logger = new Logger("warning");
152
+ logger.info("should not appear");
153
+ expect(spy).not.toHaveBeenCalled();
154
+ });
155
+
156
+ it("is silent when level is 'error'", () => {
157
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
158
+ const logger = new Logger("error");
159
+ logger.info("nope");
160
+ expect(spy).not.toHaveBeenCalled();
161
+ });
162
+
163
+ it("is silent when level is 'none'", () => {
164
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
165
+ const logger = new Logger("none");
166
+ logger.info("nothing");
167
+ expect(spy).not.toHaveBeenCalled();
168
+ });
169
+ });
170
+
171
+ describe("debug()", () => {
172
+ it("logs to console.log when level is 'debug'", () => {
173
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
174
+ const logger = new Logger("debug");
175
+ logger.debug("trace data");
176
+ expect(spy).toHaveBeenCalledOnce();
177
+ expect(spy.mock.calls[0]![0]).toContain("trace data");
178
+ });
179
+
180
+ it("logs to console.log when level is 'all'", () => {
181
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
182
+ const logger = new Logger("all");
183
+ logger.debug("everything");
184
+ expect(spy).toHaveBeenCalledOnce();
185
+ });
186
+
187
+ it("is silent when level is 'info' (threshold below debug)", () => {
188
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
189
+ const logger = new Logger("info");
190
+ logger.debug("should not appear");
191
+ expect(spy).not.toHaveBeenCalled();
192
+ });
193
+
194
+ it("is silent when level is 'warning'", () => {
195
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
196
+ const logger = new Logger("warning");
197
+ logger.debug("nope");
198
+ expect(spy).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it("is silent when level is 'error'", () => {
202
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
203
+ const logger = new Logger("error");
204
+ logger.debug("nope");
205
+ expect(spy).not.toHaveBeenCalled();
206
+ });
207
+
208
+ it("is silent when level is 'none'", () => {
209
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
210
+ const logger = new Logger("none");
211
+ logger.debug("nothing");
212
+ expect(spy).not.toHaveBeenCalled();
213
+ });
214
+
215
+ it("passes extra arguments through to console.log", () => {
216
+ const spy = vi.spyOn(console, "log").mockImplementation(() => {});
217
+ const logger = new Logger("debug");
218
+ const obj = { detail: true };
219
+ logger.debug("check", obj);
220
+ expect(spy).toHaveBeenCalledOnce();
221
+ expect(spy.mock.calls[0]).toContain(obj);
222
+ });
223
+ });
224
+
225
+ describe("all methods silent at level 'none'", () => {
226
+ it("produces no output for any method", () => {
227
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
228
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
229
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
230
+
231
+ const logger = new Logger("none");
232
+ logger.error("e");
233
+ logger.warn("w");
234
+ logger.info("i");
235
+ logger.debug("d");
236
+
237
+ expect(errorSpy).not.toHaveBeenCalled();
238
+ expect(warnSpy).not.toHaveBeenCalled();
239
+ expect(logSpy).not.toHaveBeenCalled();
240
+ });
241
+ });
242
+
243
+ describe("all methods active at level 'all'", () => {
244
+ it("produces output for every method", () => {
245
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
246
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
247
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
248
+
249
+ const logger = new Logger("all");
250
+ logger.error("e");
251
+ logger.warn("w");
252
+ logger.info("i");
253
+ logger.debug("d");
254
+
255
+ expect(errorSpy).toHaveBeenCalledOnce();
256
+ expect(warnSpy).toHaveBeenCalledOnce();
257
+ // Info and debug both use console.log
258
+ expect(logSpy).toHaveBeenCalledTimes(2);
259
+ });
260
+ });
261
+
262
+ describe("each level as constructor argument", () => {
263
+ const cases: Array<{ level: LogLevel; expectError: boolean; expectWarn: boolean; expectInfo: boolean; expectDebug: boolean }> = [
264
+ { level: "none", expectError: false, expectWarn: false, expectInfo: false, expectDebug: false },
265
+ { level: "error", expectError: true, expectWarn: false, expectInfo: false, expectDebug: false },
266
+ { level: "warning", expectError: true, expectWarn: true, expectInfo: false, expectDebug: false },
267
+ { level: "info", expectError: true, expectWarn: true, expectInfo: true, expectDebug: false },
268
+ { level: "debug", expectError: true, expectWarn: true, expectInfo: true, expectDebug: true },
269
+ { level: "all", expectError: true, expectWarn: true, expectInfo: true, expectDebug: true },
270
+ ];
271
+
272
+ for (const { level, expectError, expectWarn, expectInfo, expectDebug } of cases) {
273
+ it(`level '${level}' enables the correct methods`, () => {
274
+ const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
275
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
276
+ const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
277
+
278
+ const logger = new Logger(level);
279
+ logger.error("e");
280
+ logger.warn("w");
281
+ logger.info("i");
282
+ logger.debug("d");
283
+
284
+ expect(errorSpy).toHaveBeenCalledTimes(expectError ? 1 : 0);
285
+ expect(warnSpy).toHaveBeenCalledTimes(expectWarn ? 1 : 0);
286
+
287
+ let expectedLogCalls = 0;
288
+ if (expectInfo) expectedLogCalls++;
289
+ if (expectDebug) expectedLogCalls++;
290
+ expect(logSpy).toHaveBeenCalledTimes(expectedLogCalls);
291
+ });
292
+ }
293
+ });
294
+ });
@@ -299,7 +299,7 @@ describe("MockServer (end-to-end)", () => {
299
299
 
300
300
  it("throws on empty sequence", () => {
301
301
  expect(() => server.when("step").replySequence([])).toThrow(
302
- "replySequence requires at least one entry",
302
+ "Sequence requires at least one entry",
303
303
  );
304
304
  });
305
305
  });
@@ -1,21 +1,6 @@
1
1
  import { describe, it, expect, beforeEach } from "vitest";
2
2
  import { RuleEngine } from "../src/rule-engine.js";
3
- import type { MockRequest } from "../src/types.js";
4
-
5
- function makeReq(overrides: Partial<MockRequest> = {}): MockRequest {
6
- return {
7
- format: "openai",
8
- model: "gpt-5.4",
9
- streaming: true,
10
- messages: [{ role: "user", content: "hello" }],
11
- lastMessage: "hello",
12
- systemMessage: "",
13
- toolNames: [],
14
- lastToolCallId: undefined,
15
- raw: {},
16
- ...overrides,
17
- };
18
- }
3
+ import { makeReq } from "./helpers/make-req.js";
19
4
 
20
5
  describe("RuleEngine", () => {
21
6
  let engine: RuleEngine;
@@ -27,8 +12,8 @@ describe("RuleEngine", () => {
27
12
  it("matches a string (substring, case-insensitive)", () => {
28
13
  engine.add("hello", "Hi!");
29
14
  const rule = engine.match(makeReq({ lastMessage: "say Hello world" }));
30
- expect(rule).toBeDefined();
31
- expect(rule!.description).toBe('"hello"');
15
+ if (!rule) throw new Error("expected match");
16
+ expect(rule.description).toBe('"hello"');
32
17
  });
33
18
 
34
19
  it("matches a regex", () => {
@@ -80,7 +65,8 @@ describe("RuleEngine", () => {
80
65
  engine.add("hello", "First");
81
66
  engine.add("hello", "Second");
82
67
  const rule = engine.match(makeReq());
83
- expect(rule!.resolve).toBe("First");
68
+ if (!rule) throw new Error("expected match");
69
+ expect(rule.resolve).toBe("First");
84
70
  });
85
71
 
86
72
  it("returns undefined when no rules match", () => {
@@ -155,7 +141,8 @@ describe("RuleEngine", () => {
155
141
  const rule = engine.add("hello", "Second");
156
142
  engine.moveToFront(rule);
157
143
  const matched = engine.match(makeReq());
158
- expect(matched!.resolve).toBe("Second");
144
+ if (!matched) throw new Error("expected match");
145
+ expect(matched.resolve).toBe("Second");
159
146
  });
160
147
  });
161
148
 
@@ -165,9 +152,8 @@ describe("RuleEngine", () => {
165
152
  { model: "gpt-5.4", predicate: (req) => req.messages.length > 2 },
166
153
  "Complex match",
167
154
  );
168
- // Model matches but predicate fails (only 1 message)
169
155
  expect(engine.match(makeReq({ model: "gpt-5.4" }))).toBeUndefined();
170
- // Both model and predicate pass
156
+
171
157
  expect(engine.match(makeReq({
172
158
  model: "gpt-5.4",
173
159
  messages: [
@@ -1,192 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { AnthropicRequestSchema } from "../../src/formats/anthropic/schema.js";
3
-
4
- describe("AnthropicRequestSchema", () => {
5
- const validRequest = {
6
- model: "claude-sonnet-4-6",
7
- max_tokens: 1024,
8
- messages: [{ role: "user", content: "Hello" }],
9
- };
10
-
11
- it("accepts a valid minimal request", () => {
12
- expect(AnthropicRequestSchema.safeParse(validRequest).success).toBe(true);
13
- });
14
-
15
- it("rejects missing model", () => {
16
- const { model: _model, ...rest } = validRequest;
17
- expect(AnthropicRequestSchema.safeParse(rest).success).toBe(false);
18
- });
19
-
20
- it("rejects empty model string", () => {
21
- expect(AnthropicRequestSchema.safeParse({
22
- ...validRequest, model: "",
23
- }).success).toBe(false);
24
- });
25
-
26
- it("rejects missing max_tokens", () => {
27
- const { max_tokens: _mt, ...rest } = validRequest;
28
- expect(AnthropicRequestSchema.safeParse(rest).success).toBe(false);
29
- });
30
-
31
- it("rejects non-positive max_tokens", () => {
32
- expect(AnthropicRequestSchema.safeParse({ ...validRequest, max_tokens: 0 }).success).toBe(false);
33
- expect(AnthropicRequestSchema.safeParse({ ...validRequest, max_tokens: -1 }).success).toBe(false);
34
- });
35
-
36
- it("rejects empty messages array", () => {
37
- expect(AnthropicRequestSchema.safeParse({ ...validRequest, messages: [] }).success).toBe(false);
38
- });
39
-
40
- it("rejects missing messages", () => {
41
- const { messages: _m, ...rest } = validRequest;
42
- expect(AnthropicRequestSchema.safeParse(rest).success).toBe(false);
43
- });
44
-
45
- it("accepts string content shorthand", () => {
46
- expect(AnthropicRequestSchema.safeParse(validRequest).success).toBe(true);
47
- });
48
-
49
- it("accepts array content with text blocks", () => {
50
- expect(AnthropicRequestSchema.safeParse({
51
- ...validRequest,
52
- messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
53
- }).success).toBe(true);
54
- });
55
-
56
- it("accepts array content with tool_use blocks", () => {
57
- const result = AnthropicRequestSchema.safeParse({
58
- ...validRequest,
59
- messages: [{
60
- role: "assistant",
61
- content: [{
62
- type: "tool_use", id: "toolu_01", name: "get_weather", input: { location: "SF" },
63
- }],
64
- }],
65
- });
66
- expect(result.success).toBe(true);
67
- if (result.success) {
68
- expect(result.data.messages[0]!.content).toEqual([
69
- { type: "tool_use", id: "toolu_01", name: "get_weather", input: { location: "SF" } },
70
- ]);
71
- }
72
- });
73
-
74
- it("accepts tool_result blocks with string content", () => {
75
- expect(AnthropicRequestSchema.safeParse({
76
- ...validRequest,
77
- messages: [{
78
- role: "user",
79
- content: [{ type: "tool_result", tool_use_id: "toolu_01", content: "Sunny, 72F" }],
80
- }],
81
- }).success).toBe(true);
82
- });
83
-
84
- it("accepts tool_result blocks with TextBlock[] content", () => {
85
- expect(AnthropicRequestSchema.safeParse({
86
- ...validRequest,
87
- messages: [{
88
- role: "user",
89
- content: [{ type: "tool_result", tool_use_id: "toolu_02", content: [{ type: "text", text: "Result" }] }],
90
- }],
91
- }).success).toBe(true);
92
- });
93
-
94
- it("accepts mixed content blocks in a single message", () => {
95
- const result = AnthropicRequestSchema.safeParse({
96
- ...validRequest,
97
- messages: [{
98
- role: "assistant",
99
- content: [
100
- { type: "text", text: "Let me check." },
101
- { type: "tool_use", id: "toolu_01", name: "get_weather", input: { location: "SF" } },
102
- ],
103
- }],
104
- });
105
- expect(result.success).toBe(true);
106
- if (result.success) {
107
- expect(result.data.messages[0]!.content).toHaveLength(2);
108
- }
109
- });
110
-
111
- it("filters out unknown content block types", () => {
112
- const result = AnthropicRequestSchema.safeParse({
113
- ...validRequest,
114
- messages: [{
115
- role: "assistant",
116
- content: [
117
- { type: "thinking", thinking: "Let me consider..." },
118
- { type: "text", text: "Here is my answer." },
119
- ],
120
- }],
121
- });
122
- expect(result.success).toBe(true);
123
- if (result.success) {
124
- const blocks = result.data.messages[0]!.content;
125
- expect(blocks).toHaveLength(1);
126
- expect(blocks).toEqual([{ type: "text", text: "Here is my answer." }]);
127
- }
128
- });
129
-
130
- it("accepts a message where all blocks are unknown", () => {
131
- const result = AnthropicRequestSchema.safeParse({
132
- ...validRequest,
133
- messages: [{
134
- role: "assistant",
135
- content: [
136
- { type: "thinking", thinking: "hmm" },
137
- { type: "server_tool_use", id: "st_01", name: "web_search" },
138
- ],
139
- }],
140
- });
141
- expect(result.success).toBe(true);
142
- if (result.success) {
143
- expect(result.data.messages[0]!.content).toHaveLength(0);
144
- }
145
- });
146
-
147
- it("accepts system as string", () => {
148
- expect(AnthropicRequestSchema.safeParse({
149
- ...validRequest, system: "You are a helpful assistant.",
150
- }).success).toBe(true);
151
- });
152
-
153
- it("accepts system as TextBlock array", () => {
154
- expect(AnthropicRequestSchema.safeParse({
155
- ...validRequest, system: [{ type: "text", text: "You are a helpful assistant." }],
156
- }).success).toBe(true);
157
- });
158
-
159
- it("accepts stream: true", () => {
160
- const result = AnthropicRequestSchema.safeParse({ ...validRequest, stream: true });
161
- expect(result.success).toBe(true);
162
- if (result.success) expect(result.data.stream).toBe(true);
163
- });
164
-
165
- it("accepts stream: false", () => {
166
- const result = AnthropicRequestSchema.safeParse({ ...validRequest, stream: false });
167
- expect(result.success).toBe(true);
168
- if (result.success) expect(result.data.stream).toBe(false);
169
- });
170
-
171
- it("accepts optional fields", () => {
172
- expect(AnthropicRequestSchema.safeParse({
173
- ...validRequest,
174
- temperature: 0.7,
175
- top_p: 0.9,
176
- top_k: 40,
177
- stop_sequences: ["Human:"],
178
- metadata: { user_id: "test" },
179
- }).success).toBe(true);
180
- });
181
-
182
- it("accepts tools array", () => {
183
- expect(AnthropicRequestSchema.safeParse({
184
- ...validRequest,
185
- tools: [{
186
- name: "get_weather",
187
- description: "Get the weather",
188
- input_schema: { type: "object", properties: { location: { type: "string" } } },
189
- }],
190
- }).success).toBe(true);
191
- });
192
- });