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.
- package/.editorconfig +12 -0
- package/.github/workflows/test.yml +3 -0
- package/.oxfmtrc.json +9 -0
- package/package.json +5 -2
- package/src/cli-validators.ts +12 -4
- package/src/cli.ts +22 -6
- package/src/formats/anthropic/parse.ts +24 -5
- package/src/formats/anthropic/schema.ts +16 -8
- package/src/formats/anthropic/serialize.ts +111 -27
- package/src/formats/openai/parse.ts +12 -2
- package/src/formats/openai/schema.ts +43 -30
- package/src/formats/openai/serialize.ts +73 -17
- package/src/formats/request-helpers.ts +2 -1
- package/src/formats/responses/parse.ts +17 -3
- package/src/formats/responses/schema.ts +34 -20
- package/src/formats/responses/serialize.ts +233 -38
- package/src/formats/serialize-helpers.ts +10 -2
- package/src/formats/types.ts +16 -3
- package/src/index.ts +3 -1
- package/src/loader.ts +36 -9
- package/src/logger.ts +25 -7
- package/src/mock-server.ts +28 -7
- package/src/route-handler.ts +49 -14
- package/src/rule-engine.ts +43 -12
- package/src/types/reply.ts +6 -2
- package/src/types.ts +24 -3
- package/test/cli-validators.test.ts +16 -4
- package/test/formats/anthropic.test.ts +80 -19
- package/test/formats/openai.test.ts +85 -24
- package/test/formats/parse-helpers.test.ts +47 -7
- package/test/formats/responses.test.ts +95 -30
- package/test/history.test.ts +18 -5
- package/test/loader.test.ts +33 -18
- package/test/logger.test.ts +59 -9
- package/test/mock-server.test.ts +76 -22
- package/test/rule-engine.test.ts +49 -19
package/test/mock-server.test.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import { createMock, MockServer } from "../src/index.js";
|
|
3
3
|
|
|
4
4
|
interface OpenAIResponse {
|
|
5
|
-
choices: {
|
|
5
|
+
choices: {
|
|
6
|
+
message: { role: string; content: string };
|
|
7
|
+
finish_reason: string;
|
|
8
|
+
}[];
|
|
6
9
|
error?: { type: string; message: string };
|
|
7
10
|
}
|
|
8
11
|
|
|
@@ -34,7 +37,10 @@ describe("MockServer (end-to-end)", () => {
|
|
|
34
37
|
});
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
async function postOpenAI(
|
|
40
|
+
async function postOpenAI(
|
|
41
|
+
content: string,
|
|
42
|
+
opts: Record<string, unknown> = {},
|
|
43
|
+
): Promise<OpenAIResponse> {
|
|
38
44
|
const res = await post("/v1/chat/completions", {
|
|
39
45
|
model: "gpt-5.4",
|
|
40
46
|
messages: [{ role: "user", content }],
|
|
@@ -44,7 +50,10 @@ describe("MockServer (end-to-end)", () => {
|
|
|
44
50
|
return res.json() as Promise<OpenAIResponse>;
|
|
45
51
|
}
|
|
46
52
|
|
|
47
|
-
async function postAnthropic(
|
|
53
|
+
async function postAnthropic(
|
|
54
|
+
content: string,
|
|
55
|
+
opts: Record<string, unknown> = {},
|
|
56
|
+
): Promise<AnthropicResponse> {
|
|
48
57
|
const res = await post("/v1/messages", {
|
|
49
58
|
model: "claude-sonnet-4-6",
|
|
50
59
|
messages: [{ role: "user", content }],
|
|
@@ -55,7 +64,10 @@ describe("MockServer (end-to-end)", () => {
|
|
|
55
64
|
return res.json() as Promise<AnthropicResponse>;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
async function postResponses(
|
|
67
|
+
async function postResponses(
|
|
68
|
+
input: string,
|
|
69
|
+
opts: Record<string, unknown> = {},
|
|
70
|
+
): Promise<ResponsesAPIResponse> {
|
|
59
71
|
const res = await post("/v1/responses", {
|
|
60
72
|
model: "codex-mini",
|
|
61
73
|
input,
|
|
@@ -216,7 +228,10 @@ describe("MockServer (end-to-end)", () => {
|
|
|
216
228
|
server.when("hello").reply("Hi!");
|
|
217
229
|
await fetch(`${server.url}/v1/chat/completions`, {
|
|
218
230
|
method: "POST",
|
|
219
|
-
headers: {
|
|
231
|
+
headers: {
|
|
232
|
+
"Content-Type": "application/json",
|
|
233
|
+
"X-Custom": "test-value",
|
|
234
|
+
},
|
|
220
235
|
body: JSON.stringify({
|
|
221
236
|
model: "gpt-5.4",
|
|
222
237
|
messages: [{ role: "user", content: "hello" }],
|
|
@@ -232,7 +247,9 @@ describe("MockServer (end-to-end)", () => {
|
|
|
232
247
|
|
|
233
248
|
describe("request metadata in predicates", () => {
|
|
234
249
|
it("matches on headers", async () => {
|
|
235
|
-
server
|
|
250
|
+
server
|
|
251
|
+
.when({ predicate: (req) => req.headers["x-team"] === "alpha" })
|
|
252
|
+
.reply("Alpha team!");
|
|
236
253
|
server.when("hello").reply("Default");
|
|
237
254
|
|
|
238
255
|
const res = await fetch(`${server.url}/v1/chat/completions`, {
|
|
@@ -288,10 +305,12 @@ describe("MockServer (end-to-end)", () => {
|
|
|
288
305
|
});
|
|
289
306
|
|
|
290
307
|
it("supports per-step options", async () => {
|
|
291
|
-
server
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
308
|
+
server
|
|
309
|
+
.when("step")
|
|
310
|
+
.replySequence([
|
|
311
|
+
"Plain.",
|
|
312
|
+
{ reply: { text: "With options." }, options: { chunkSize: 5 } },
|
|
313
|
+
]);
|
|
295
314
|
|
|
296
315
|
const json = await postOpenAI("step");
|
|
297
316
|
expect(json.choices[0]!.message.content).toBe("Plain.");
|
|
@@ -407,7 +426,9 @@ describe("MockServer (end-to-end)", () => {
|
|
|
407
426
|
});
|
|
408
427
|
|
|
409
428
|
it("error reply works as a normal rule", async () => {
|
|
410
|
-
server
|
|
429
|
+
server
|
|
430
|
+
.when("fail")
|
|
431
|
+
.reply({ error: { status: 500, message: "Internal error" } });
|
|
411
432
|
server.when("hello").reply("Hi!");
|
|
412
433
|
|
|
413
434
|
const r1 = await post("/v1/chat/completions", {
|
|
@@ -437,11 +458,13 @@ describe("MockServer (end-to-end)", () => {
|
|
|
437
458
|
const contentDeltas = data
|
|
438
459
|
.filter((d) => d !== "[DONE]")
|
|
439
460
|
.map((d) => JSON.parse(d))
|
|
440
|
-
.filter(
|
|
441
|
-
d
|
|
461
|
+
.filter(
|
|
462
|
+
(d: { choices?: { delta?: { content?: string } }[] }) =>
|
|
463
|
+
d.choices?.[0]?.delta?.content !== undefined,
|
|
442
464
|
)
|
|
443
|
-
.map(
|
|
444
|
-
d
|
|
465
|
+
.map(
|
|
466
|
+
(d: { choices: { delta: { content: string } }[] }) =>
|
|
467
|
+
d.choices[0]!.delta.content,
|
|
445
468
|
);
|
|
446
469
|
expect(contentDeltas.length).toBe(3);
|
|
447
470
|
expect(contentDeltas.join("")).toBe("Hello, world!");
|
|
@@ -454,7 +477,12 @@ describe("MockServer (end-to-end)", () => {
|
|
|
454
477
|
server.fallback("No match.");
|
|
455
478
|
|
|
456
479
|
const j1 = await postOpenAI("what's the weather?", {
|
|
457
|
-
tools: [
|
|
480
|
+
tools: [
|
|
481
|
+
{
|
|
482
|
+
type: "function",
|
|
483
|
+
function: { name: "get_weather", parameters: {} },
|
|
484
|
+
},
|
|
485
|
+
],
|
|
458
486
|
});
|
|
459
487
|
expect(j1.choices[0]!.message.content).toBe("Weather tool detected!");
|
|
460
488
|
|
|
@@ -471,7 +499,17 @@ describe("MockServer (end-to-end)", () => {
|
|
|
471
499
|
const json = await postOpenAI("use the tool", {
|
|
472
500
|
messages: [
|
|
473
501
|
{ role: "user", content: "use the tool" },
|
|
474
|
-
{
|
|
502
|
+
{
|
|
503
|
+
role: "assistant",
|
|
504
|
+
content: null,
|
|
505
|
+
tool_calls: [
|
|
506
|
+
{
|
|
507
|
+
id: "call_abc",
|
|
508
|
+
type: "function",
|
|
509
|
+
function: { name: "test", arguments: "{}" },
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
},
|
|
475
513
|
{ role: "tool", tool_call_id: "call_abc", content: "result data" },
|
|
476
514
|
],
|
|
477
515
|
});
|
|
@@ -505,7 +543,9 @@ describe("MockServer (end-to-end)", () => {
|
|
|
505
543
|
|
|
506
544
|
describe("resolver error handling", () => {
|
|
507
545
|
it("falls back when resolver throws", async () => {
|
|
508
|
-
server.when("boom").reply(() => {
|
|
546
|
+
server.when("boom").reply(() => {
|
|
547
|
+
throw new Error("resolver failed");
|
|
548
|
+
});
|
|
509
549
|
server.fallback("Safe fallback.");
|
|
510
550
|
|
|
511
551
|
const json = await postOpenAI("boom");
|
|
@@ -530,20 +570,34 @@ describe("MockServer (end-to-end)", () => {
|
|
|
530
570
|
await fetch(`${s.url}/v1/chat/completions`, {
|
|
531
571
|
method: "POST",
|
|
532
572
|
headers: { "Content-Type": "application/json" },
|
|
533
|
-
body: JSON.stringify({
|
|
573
|
+
body: JSON.stringify({
|
|
574
|
+
model: "gpt-5.4",
|
|
575
|
+
messages: [{ role: "user", content: "test" }],
|
|
576
|
+
stream: false,
|
|
577
|
+
}),
|
|
534
578
|
});
|
|
535
579
|
|
|
536
580
|
await fetch(`${s.url}/v1/chat/completions`, {
|
|
537
581
|
method: "POST",
|
|
538
582
|
headers: { "Content-Type": "application/json" },
|
|
539
|
-
body: JSON.stringify({
|
|
583
|
+
body: JSON.stringify({
|
|
584
|
+
model: "gpt-5.4",
|
|
585
|
+
messages: [{ role: "user", content: "unmatched" }],
|
|
586
|
+
stream: false,
|
|
587
|
+
}),
|
|
540
588
|
});
|
|
541
589
|
|
|
542
|
-
s.when("throw").reply(() => {
|
|
590
|
+
s.when("throw").reply(() => {
|
|
591
|
+
throw new Error("boom");
|
|
592
|
+
});
|
|
543
593
|
await fetch(`${s.url}/v1/chat/completions`, {
|
|
544
594
|
method: "POST",
|
|
545
595
|
headers: { "Content-Type": "application/json" },
|
|
546
|
-
body: JSON.stringify({
|
|
596
|
+
body: JSON.stringify({
|
|
597
|
+
model: "gpt-5.4",
|
|
598
|
+
messages: [{ role: "user", content: "throw" }],
|
|
599
|
+
stream: false,
|
|
600
|
+
}),
|
|
547
601
|
});
|
|
548
602
|
|
|
549
603
|
await s.stop();
|
package/test/rule-engine.test.ts
CHANGED
|
@@ -18,7 +18,9 @@ describe("RuleEngine", () => {
|
|
|
18
18
|
|
|
19
19
|
it("matches a regex", () => {
|
|
20
20
|
engine.add(/explain (\w+)/i, "Here is an explanation.");
|
|
21
|
-
const rule = engine.match(
|
|
21
|
+
const rule = engine.match(
|
|
22
|
+
makeReq({ lastMessage: "Can you explain recursion?" }),
|
|
23
|
+
);
|
|
22
24
|
expect(rule).toBeDefined();
|
|
23
25
|
});
|
|
24
26
|
|
|
@@ -44,15 +46,25 @@ describe("RuleEngine", () => {
|
|
|
44
46
|
|
|
45
47
|
it("matches a MatchObject with message + model", () => {
|
|
46
48
|
engine.add({ model: "gpt-5.4", message: "hello" }, "Hi from GPT-5.4");
|
|
47
|
-
expect(
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
expect(
|
|
50
|
+
engine.match(makeReq({ model: "gpt-5.4", lastMessage: "hello" })),
|
|
51
|
+
).toBeDefined();
|
|
52
|
+
expect(
|
|
53
|
+
engine.match(makeReq({ model: "gpt-5.4", lastMessage: "bye" })),
|
|
54
|
+
).toBeUndefined();
|
|
55
|
+
expect(
|
|
56
|
+
engine.match(makeReq({ model: "claude", lastMessage: "hello" })),
|
|
57
|
+
).toBeUndefined();
|
|
50
58
|
});
|
|
51
59
|
|
|
52
60
|
it("matches a MatchObject with system", () => {
|
|
53
61
|
engine.add({ system: /pirate/i }, "Arrr!");
|
|
54
|
-
expect(
|
|
55
|
-
|
|
62
|
+
expect(
|
|
63
|
+
engine.match(makeReq({ systemMessage: "You are a pirate" })),
|
|
64
|
+
).toBeDefined();
|
|
65
|
+
expect(
|
|
66
|
+
engine.match(makeReq({ systemMessage: "You are helpful" })),
|
|
67
|
+
).toBeUndefined();
|
|
56
68
|
});
|
|
57
69
|
|
|
58
70
|
it("matches a MatchObject with format", () => {
|
|
@@ -115,13 +127,17 @@ describe("RuleEngine", () => {
|
|
|
115
127
|
(req) => req.lastMessage.includes("test"),
|
|
116
128
|
"Handler reply",
|
|
117
129
|
);
|
|
118
|
-
expect(
|
|
130
|
+
expect(
|
|
131
|
+
engine.match(makeReq({ lastMessage: "this is a test" })),
|
|
132
|
+
).toBeDefined();
|
|
119
133
|
});
|
|
120
134
|
|
|
121
135
|
describe("toolName matching", () => {
|
|
122
136
|
it("matches when toolNames includes the specified tool", () => {
|
|
123
137
|
engine.add({ toolName: "get_weather" }, "Weather tool present");
|
|
124
|
-
expect(
|
|
138
|
+
expect(
|
|
139
|
+
engine.match(makeReq({ toolNames: ["get_weather", "search"] })),
|
|
140
|
+
).toBeDefined();
|
|
125
141
|
expect(engine.match(makeReq({ toolNames: ["search"] }))).toBeUndefined();
|
|
126
142
|
});
|
|
127
143
|
});
|
|
@@ -129,8 +145,12 @@ describe("RuleEngine", () => {
|
|
|
129
145
|
describe("toolCallId matching", () => {
|
|
130
146
|
it("matches when lastToolCallId equals the specified id", () => {
|
|
131
147
|
engine.add({ toolCallId: "call_abc" }, "Tool result");
|
|
132
|
-
expect(
|
|
133
|
-
|
|
148
|
+
expect(
|
|
149
|
+
engine.match(makeReq({ lastToolCallId: "call_abc" })),
|
|
150
|
+
).toBeDefined();
|
|
151
|
+
expect(
|
|
152
|
+
engine.match(makeReq({ lastToolCallId: "call_xyz" })),
|
|
153
|
+
).toBeUndefined();
|
|
134
154
|
expect(engine.match(makeReq())).toBeUndefined();
|
|
135
155
|
});
|
|
136
156
|
});
|
|
@@ -154,20 +174,30 @@ describe("RuleEngine", () => {
|
|
|
154
174
|
);
|
|
155
175
|
expect(engine.match(makeReq({ model: "gpt-5.4" }))).toBeUndefined();
|
|
156
176
|
|
|
157
|
-
expect(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
177
|
+
expect(
|
|
178
|
+
engine.match(
|
|
179
|
+
makeReq({
|
|
180
|
+
model: "gpt-5.4",
|
|
181
|
+
messages: [
|
|
182
|
+
{ role: "system", content: "sys" },
|
|
183
|
+
{ role: "user", content: "a" },
|
|
184
|
+
{ role: "assistant", content: "b" },
|
|
185
|
+
],
|
|
186
|
+
}),
|
|
187
|
+
),
|
|
188
|
+
).toBeDefined();
|
|
165
189
|
});
|
|
166
190
|
|
|
167
191
|
it("predicate runs after other fields (short-circuits)", () => {
|
|
168
192
|
let called = false;
|
|
169
193
|
engine.add(
|
|
170
|
-
{
|
|
194
|
+
{
|
|
195
|
+
model: "claude",
|
|
196
|
+
predicate: () => {
|
|
197
|
+
called = true;
|
|
198
|
+
return true;
|
|
199
|
+
},
|
|
200
|
+
},
|
|
171
201
|
"Never reached",
|
|
172
202
|
);
|
|
173
203
|
engine.match(makeReq({ model: "gpt-5.4" }));
|