llm-mock-server 1.0.2 → 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.
@@ -1,6 +1,10 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { responsesFormat } from "../../src/formats/responses/index.js";
3
- import type { ResponsesEvent, ResponsesComplete, ResponsesError } from "../../src/formats/responses/schema.js";
3
+ import type {
4
+ ResponsesEvent,
5
+ ResponsesComplete,
6
+ ResponsesError,
7
+ } from "../../src/formats/responses/schema.js";
4
8
 
5
9
  function parse<T>(chunk: { data: string }): T {
6
10
  return JSON.parse(chunk.data) as T;
@@ -9,7 +13,10 @@ function parse<T>(chunk: { data: string }): T {
9
13
  describe("Responses Format", () => {
10
14
  describe("parseRequest", () => {
11
15
  it("parses string input", () => {
12
- const req = responsesFormat.parseRequest({ model: "codex-mini", input: "Hello world" });
16
+ const req = responsesFormat.parseRequest({
17
+ model: "codex-mini",
18
+ input: "Hello world",
19
+ });
13
20
  expect(req.format).toBe("responses");
14
21
  expect(req.model).toBe("codex-mini");
15
22
  expect(req.lastMessage).toBe("Hello world");
@@ -40,7 +47,12 @@ describe("Responses Format", () => {
40
47
  it("parses content block arrays", () => {
41
48
  const req = responsesFormat.parseRequest({
42
49
  model: "codex-mini",
43
- input: [{ role: "user", content: [{ type: "input_text", text: "Hello there" }] }],
50
+ input: [
51
+ {
52
+ role: "user",
53
+ content: [{ type: "input_text", text: "Hello there" }],
54
+ },
55
+ ],
44
56
  });
45
57
  expect(req.lastMessage).toBe("Hello there");
46
58
  });
@@ -49,7 +61,14 @@ describe("Responses Format", () => {
49
61
  const req = responsesFormat.parseRequest({
50
62
  model: "codex-mini",
51
63
  input: "read file",
52
- tools: [{ type: "function", name: "read_file", description: "Read", parameters: {} }],
64
+ tools: [
65
+ {
66
+ type: "function",
67
+ name: "read_file",
68
+ description: "Read",
69
+ parameters: {},
70
+ },
71
+ ],
53
72
  });
54
73
  expect(req.tools).toHaveLength(1);
55
74
  expect(req.toolNames).toEqual(["read_file"]);
@@ -60,7 +79,11 @@ describe("Responses Format", () => {
60
79
  model: "codex-mini",
61
80
  input: [
62
81
  { role: "user", content: "hi" },
63
- { type: "function_call_output", call_id: "call_abc", output: "result" },
82
+ {
83
+ type: "function_call_output",
84
+ call_id: "call_abc",
85
+ output: "result",
86
+ },
64
87
  ],
65
88
  });
66
89
  expect(req.lastToolCallId).toBe("call_abc");
@@ -69,10 +92,15 @@ describe("Responses Format", () => {
69
92
  it("handles content blocks with non-text types (image, etc.)", () => {
70
93
  const req = responsesFormat.parseRequest({
71
94
  model: "codex-mini",
72
- input: [{ role: "user", content: [
73
- { type: "image_url", url: "https://example.com/img.png" },
74
- { type: "input_text", text: "describe this" },
75
- ]}],
95
+ input: [
96
+ {
97
+ role: "user",
98
+ content: [
99
+ { type: "image_url", url: "https://example.com/img.png" },
100
+ { type: "input_text", text: "describe this" },
101
+ ],
102
+ },
103
+ ],
76
104
  });
77
105
  expect(req.lastMessage).toBe("describe this");
78
106
  });
@@ -100,10 +128,7 @@ describe("Responses Format", () => {
100
128
  const req = responsesFormat.parseRequest({
101
129
  model: "codex-mini",
102
130
  input: "hi",
103
- tools: [
104
- { type: "function", name: "run_code" },
105
- { type: "web_search" },
106
- ],
131
+ tools: [{ type: "function", name: "run_code" }, { type: "web_search" }],
107
132
  });
108
133
  expect(req.tools).toHaveLength(1);
109
134
  expect(req.tools![0]!.name).toBe("run_code");
@@ -114,17 +139,23 @@ describe("Responses Format", () => {
114
139
  it("starts with response.created and response.in_progress", () => {
115
140
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
116
141
  expect(parse<ResponsesEvent>(chunks[0]!).type).toBe("response.created");
117
- expect(parse<ResponsesEvent>(chunks[1]!).type).toBe("response.in_progress");
142
+ expect(parse<ResponsesEvent>(chunks[1]!).type).toBe(
143
+ "response.in_progress",
144
+ );
118
145
  });
119
146
 
120
147
  it("ends with response.completed", () => {
121
148
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
122
- expect(parse<ResponsesEvent>(chunks.at(-1)!).type).toBe("response.completed");
149
+ expect(parse<ResponsesEvent>(chunks.at(-1)!).type).toBe(
150
+ "response.completed",
151
+ );
123
152
  });
124
153
 
125
154
  it("assigns incrementing sequence_number to every event", () => {
126
155
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
127
- const seqNumbers = chunks.map((c) => parse<ResponsesEvent>(c).sequence_number!);
156
+ const seqNumbers = chunks.map(
157
+ (c) => parse<ResponsesEvent>(c).sequence_number!,
158
+ );
128
159
  for (let i = 1; i < seqNumbers.length; i++) {
129
160
  expect(seqNumbers[i]).toBe(seqNumbers[i - 1]! + 1);
130
161
  }
@@ -135,14 +166,17 @@ describe("Responses Format", () => {
135
166
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
136
167
  const created = parse<ResponsesEvent>(chunks[0]!).response?.created_at;
137
168
  const inProgress = parse<ResponsesEvent>(chunks[1]!).response?.created_at;
138
- const completed = parse<ResponsesEvent>(chunks.at(-1)!).response?.created_at;
169
+ const completed = parse<ResponsesEvent>(chunks.at(-1)!).response
170
+ ?.created_at;
139
171
  expect(created).toBe(inProgress);
140
172
  expect(created).toBe(completed);
141
173
  });
142
174
 
143
175
  it("produces text delta events with item_id", () => {
144
176
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
145
- const delta = chunks.find((c) => parse<ResponsesEvent>(c).type === "response.output_text.delta");
177
+ const delta = chunks.find(
178
+ (c) => parse<ResponsesEvent>(c).type === "response.output_text.delta",
179
+ );
146
180
  expect(delta).toBeDefined();
147
181
  const data = parse<ResponsesEvent>(delta!);
148
182
  expect(data.delta).toBe("Hello");
@@ -153,26 +187,34 @@ describe("Responses Format", () => {
153
187
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
154
188
  const added = chunks.find((c) => {
155
189
  const d = parse<ResponsesEvent>(c);
156
- return d.type === "response.output_item.added" && d.item?.type === "message";
190
+ return (
191
+ d.type === "response.output_item.added" && d.item?.type === "message"
192
+ );
157
193
  });
158
194
  expect(parse<ResponsesEvent>(added!).item?.status).toBe("in_progress");
159
195
 
160
196
  const done = chunks.find((c) => {
161
197
  const d = parse<ResponsesEvent>(c);
162
- return d.type === "response.output_item.done" && d.item?.type === "message";
198
+ return (
199
+ d.type === "response.output_item.done" && d.item?.type === "message"
200
+ );
163
201
  });
164
202
  expect(parse<ResponsesEvent>(done!).item?.status).toBe("completed");
165
203
  });
166
204
 
167
205
  it("includes annotations on output_text parts", () => {
168
206
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
169
- const partAdded = chunks.find((c) => parse<ResponsesEvent>(c).type === "response.content_part.added");
207
+ const partAdded = chunks.find(
208
+ (c) => parse<ResponsesEvent>(c).type === "response.content_part.added",
209
+ );
170
210
  expect(parse<ResponsesEvent>(partAdded!).part?.annotations).toEqual([]);
171
211
  });
172
212
 
173
213
  it("includes content_part.done event with full text", () => {
174
214
  const chunks = responsesFormat.serialize({ text: "Hello" }, "codex-mini");
175
- const partDone = chunks.find((c) => parse<ResponsesEvent>(c).type === "response.content_part.done");
215
+ const partDone = chunks.find(
216
+ (c) => parse<ResponsesEvent>(c).type === "response.content_part.done",
217
+ );
176
218
  expect(partDone).toBeDefined();
177
219
  expect(parse<ResponsesEvent>(partDone!).part?.text).toBe("Hello");
178
220
  });
@@ -189,7 +231,9 @@ describe("Responses Format", () => {
189
231
  expect(types).toContain("response.reasoning_summary_text.done");
190
232
  expect(types).toContain("response.reasoning_summary_part.done");
191
233
 
192
- const reasoningDone = types.indexOf("response.reasoning_summary_text.done");
234
+ const reasoningDone = types.indexOf(
235
+ "response.reasoning_summary_text.done",
236
+ );
193
237
  const textDelta = types.indexOf("response.output_text.delta");
194
238
  expect(reasoningDone).toBeLessThan(textDelta);
195
239
  });
@@ -205,9 +249,14 @@ describe("Responses Format", () => {
205
249
  });
206
250
 
207
251
  it("accumulates text in completed output", () => {
208
- const chunks = responsesFormat.serialize({ text: "hello world" }, "codex-mini");
252
+ const chunks = responsesFormat.serialize(
253
+ { text: "hello world" },
254
+ "codex-mini",
255
+ );
209
256
  const completed = parse<ResponsesEvent>(chunks.at(-1)!);
210
- expect(completed.response?.output[0]!.content?.[0]?.text).toBe("hello world");
257
+ expect(completed.response?.output[0]!.content?.[0]?.text).toBe(
258
+ "hello world",
259
+ );
211
260
  });
212
261
 
213
262
  it("produces function_call events for tool calls", () => {
@@ -217,7 +266,10 @@ describe("Responses Format", () => {
217
266
  );
218
267
  const fnAdded = chunks.find((c) => {
219
268
  const d = parse<ResponsesEvent>(c);
220
- return d.type === "response.output_item.added" && d.item?.type === "function_call";
269
+ return (
270
+ d.type === "response.output_item.added" &&
271
+ d.item?.type === "function_call"
272
+ );
221
273
  });
222
274
  expect(fnAdded).toBeDefined();
223
275
  const item = parse<ResponsesEvent>(fnAdded!).item!;
@@ -242,7 +294,10 @@ describe("Responses Format", () => {
242
294
 
243
295
  describe("serializeComplete (non-streaming)", () => {
244
296
  it("produces correct top-level structure", () => {
245
- const result = responsesFormat.serializeComplete({ text: "Hello" }, "codex-mini") as ResponsesComplete;
297
+ const result = responsesFormat.serializeComplete(
298
+ { text: "Hello" },
299
+ "codex-mini",
300
+ ) as ResponsesComplete;
246
301
  expect(result.object).toBe("response");
247
302
  expect(result.status).toBe("completed");
248
303
  expect(result.model).toBe("codex-mini");
@@ -250,7 +305,10 @@ describe("Responses Format", () => {
250
305
  });
251
306
 
252
307
  it("includes message output item with status and annotations", () => {
253
- const result = responsesFormat.serializeComplete({ text: "Hello, world!" }, "codex-mini") as ResponsesComplete;
308
+ const result = responsesFormat.serializeComplete(
309
+ { text: "Hello, world!" },
310
+ "codex-mini",
311
+ ) as ResponsesComplete;
254
312
  const msg = result.output[0]!;
255
313
  expect(msg.type).toBe("message");
256
314
  expect(msg.status).toBe("completed");
@@ -286,13 +344,20 @@ describe("Responses Format", () => {
286
344
  { text: "hi", usage: { input: 20, output: 15 } },
287
345
  "codex-mini",
288
346
  ) as ResponsesComplete;
289
- expect(result.usage).toEqual({ input_tokens: 20, output_tokens: 15, total_tokens: 35 });
347
+ expect(result.usage).toEqual({
348
+ input_tokens: 20,
349
+ output_tokens: 15,
350
+ total_tokens: 35,
351
+ });
290
352
  });
291
353
  });
292
354
 
293
355
  describe("serializeError", () => {
294
356
  it("produces Responses error format", () => {
295
- const result = responsesFormat.serializeError({ status: 500, message: "Internal error" }) as ResponsesError;
357
+ const result = responsesFormat.serializeError({
358
+ status: 500,
359
+ message: "Internal error",
360
+ }) as ResponsesError;
296
361
  expect(result.error.message).toBe("Internal error");
297
362
  });
298
363
  });
@@ -140,9 +140,18 @@ describe("RequestHistory", () => {
140
140
 
141
141
  describe("where()", () => {
142
142
  beforeEach(() => {
143
- history.record(makeReq({ lastMessage: "hello", model: "gpt-5.4" }), "rule-a");
144
- history.record(makeReq({ lastMessage: "world", model: "claude-4" }), undefined);
145
- history.record(makeReq({ lastMessage: "hello again", model: "gpt-5.4" }), "rule-b");
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
+ );
146
155
  });
147
156
 
148
157
  it("filters entries by predicate", () => {
@@ -153,13 +162,17 @@ describe("RequestHistory", () => {
153
162
  });
154
163
 
155
164
  it("filters by request properties", () => {
156
- const claudeRequests = history.where((e) => e.request.model === "claude-4");
165
+ const claudeRequests = history.where(
166
+ (e) => e.request.model === "claude-4",
167
+ );
157
168
  expect(claudeRequests).toHaveLength(1);
158
169
  expect(claudeRequests[0].request.lastMessage).toBe("world");
159
170
  });
160
171
 
161
172
  it("returns an empty array when nothing matches", () => {
162
- const none = history.where((e) => e.request.lastMessage === "nonexistent");
173
+ const none = history.where(
174
+ (e) => e.request.lastMessage === "nonexistent",
175
+ );
163
176
  expect(none).toEqual([]);
164
177
  });
165
178
 
@@ -40,11 +40,15 @@ describe("Loader", () => {
40
40
  await loadRulesFromPath(rulesPath, { engine });
41
41
  expect(engine.ruleCount).toBe(2);
42
42
 
43
- const match1 = engine.match(makeReq({ lastMessage: "Please explain recursion" }));
43
+ const match1 = engine.match(
44
+ makeReq({ lastMessage: "Please explain recursion" }),
45
+ );
44
46
  if (!match1) throw new Error("expected match for 'explain'");
45
47
  expect(match1.resolve).toBe("A function that calls itself.");
46
48
 
47
- 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
+ );
48
52
  expect(match2).toBeDefined();
49
53
  });
50
54
 
@@ -61,7 +65,9 @@ describe("Loader", () => {
61
65
  );
62
66
 
63
67
  await loadRulesFromPath(rulesPath, { engine });
64
- const match = engine.match(makeReq({ lastMessage: "explain polymorphism" }));
68
+ const match = engine.match(
69
+ makeReq({ lastMessage: "explain polymorphism" }),
70
+ );
65
71
  expect(match).toBeDefined();
66
72
  });
67
73
 
@@ -73,7 +79,9 @@ describe("Loader", () => {
73
79
  );
74
80
 
75
81
  await loadRulesFromPath(rulesPath, { engine });
76
- expect(engine.match(makeReq({ lastMessage: "hello world" }))).toBeDefined();
82
+ expect(
83
+ engine.match(makeReq({ lastMessage: "hello world" })),
84
+ ).toBeDefined();
77
85
  });
78
86
 
79
87
  it("loads rules with times", async () => {
@@ -123,7 +131,9 @@ describe("Loader", () => {
123
131
  }`,
124
132
  );
125
133
 
126
- await expect(loadRulesFromPath(rulesPath, { engine })).rejects.toThrow("Unknown template");
134
+ await expect(loadRulesFromPath(rulesPath, { engine })).rejects.toThrow(
135
+ "Unknown template",
136
+ );
127
137
  });
128
138
 
129
139
  it("loads a replies sequence", async () => {
@@ -161,7 +171,9 @@ describe("Loader", () => {
161
171
  let capturedFallback: unknown;
162
172
  await loadRulesFromPath(rulesPath, {
163
173
  engine,
164
- setFallback: (reply) => { capturedFallback = reply; },
174
+ setFallback: (reply) => {
175
+ capturedFallback = reply;
176
+ },
165
177
  });
166
178
 
167
179
  expect(capturedFallback).toBe("Default reply.");
@@ -183,7 +195,9 @@ describe("Loader", () => {
183
195
  await loadRulesFromPath(handlerPath, { engine });
184
196
  expect(engine.ruleCount).toBe(1);
185
197
 
186
- const match = engine.match(makeReq({ lastMessage: "summarize this article" }));
198
+ const match = engine.match(
199
+ makeReq({ lastMessage: "summarize this article" }),
200
+ );
187
201
  if (!match) throw new Error("expected match for 'summarize'");
188
202
  expect(match.resolve).toBeTypeOf("function");
189
203
  });
@@ -233,9 +247,14 @@ describe("Loader", () => {
233
247
 
234
248
  it("throws on invalid handler file (missing match/respond)", async () => {
235
249
  const handlerPath = join(tmpDir, "bad.ts");
236
- await writeFile(handlerPath, `export default { mach: () => true, respond: () => "hi" };`);
250
+ await writeFile(
251
+ handlerPath,
252
+ `export default { mach: () => true, respond: () => "hi" };`,
253
+ );
237
254
 
238
- await expect(loadRulesFromPath(handlerPath, { engine })).rejects.toThrow("Invalid handler file");
255
+ await expect(loadRulesFromPath(handlerPath, { engine })).rejects.toThrow(
256
+ "Invalid handler file",
257
+ );
239
258
  });
240
259
 
241
260
  it("loads fallback from handler file", async () => {
@@ -252,7 +271,9 @@ describe("Loader", () => {
252
271
  let capturedFallback: unknown;
253
272
  await loadRulesFromPath(handlerPath, {
254
273
  engine,
255
- setFallback: (reply) => { capturedFallback = reply; },
274
+ setFallback: (reply) => {
275
+ capturedFallback = reply;
276
+ },
256
277
  });
257
278
 
258
279
  expect(capturedFallback).toBe("Default reply.");
@@ -262,14 +283,8 @@ describe("Loader", () => {
262
283
 
263
284
  describe("directory loading", () => {
264
285
  it("loads all .json5 files from a directory", async () => {
265
- await writeFile(
266
- join(tmpDir, "a.json5"),
267
- `[{ when: "aaa", reply: "A" }]`,
268
- );
269
- await writeFile(
270
- join(tmpDir, "b.json5"),
271
- `[{ when: "bbb", reply: "B" }]`,
272
- );
286
+ await writeFile(join(tmpDir, "a.json5"), `[{ when: "aaa", reply: "A" }]`);
287
+ await writeFile(join(tmpDir, "b.json5"), `[{ when: "bbb", reply: "B" }]`);
273
288
 
274
289
  await loadRulesFromPath(tmpDir, { engine });
275
290
  expect(engine.ruleCount).toBe(2);
@@ -260,18 +260,68 @@ describe("Logger", () => {
260
260
  });
261
261
 
262
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 },
263
+ const cases: Array<{
264
+ level: LogLevel;
265
+ expectError: boolean;
266
+ expectWarn: boolean;
267
+ expectInfo: boolean;
268
+ expectDebug: boolean;
269
+ }> = [
270
+ {
271
+ level: "none",
272
+ expectError: false,
273
+ expectWarn: false,
274
+ expectInfo: false,
275
+ expectDebug: false,
276
+ },
277
+ {
278
+ level: "error",
279
+ expectError: true,
280
+ expectWarn: false,
281
+ expectInfo: false,
282
+ expectDebug: false,
283
+ },
284
+ {
285
+ level: "warning",
286
+ expectError: true,
287
+ expectWarn: true,
288
+ expectInfo: false,
289
+ expectDebug: false,
290
+ },
291
+ {
292
+ level: "info",
293
+ expectError: true,
294
+ expectWarn: true,
295
+ expectInfo: true,
296
+ expectDebug: false,
297
+ },
298
+ {
299
+ level: "debug",
300
+ expectError: true,
301
+ expectWarn: true,
302
+ expectInfo: true,
303
+ expectDebug: true,
304
+ },
305
+ {
306
+ level: "all",
307
+ expectError: true,
308
+ expectWarn: true,
309
+ expectInfo: true,
310
+ expectDebug: true,
311
+ },
270
312
  ];
271
313
 
272
- for (const { level, expectError, expectWarn, expectInfo, expectDebug } of cases) {
314
+ for (const {
315
+ level,
316
+ expectError,
317
+ expectWarn,
318
+ expectInfo,
319
+ expectDebug,
320
+ } of cases) {
273
321
  it(`level '${level}' enables the correct methods`, () => {
274
- const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
322
+ const errorSpy = vi
323
+ .spyOn(console, "error")
324
+ .mockImplementation(() => {});
275
325
  const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
276
326
  const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
277
327