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.
- package/.claude/skills/desloppify/SKILL.md +308 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +242 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +248 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +248 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +311 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +17 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +255 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +22 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +20 -0
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +20 -0
- package/.desloppify/query.json +284 -0
- package/.desloppify/review_packet_blind.json +1303 -0
- package/.desloppify/review_packets/holistic_packet_20260315_000339.json +1471 -0
- package/.desloppify/state-typescript.json +5114 -0
- package/.desloppify/state-typescript.json.bak +5108 -0
- package/.editorconfig +12 -0
- package/.github/workflows/test.yml +3 -0
- package/.oxfmtrc.json +9 -0
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/formats/anthropic/index.js +1 -1
- package/dist/formats/anthropic/index.js.map +1 -1
- package/dist/formats/anthropic/parse.d.ts +1 -1
- package/dist/formats/anthropic/parse.d.ts.map +1 -1
- package/dist/formats/anthropic/parse.js +1 -1
- package/dist/formats/anthropic/parse.js.map +1 -1
- package/dist/formats/anthropic/serialize.d.ts +2 -2
- package/dist/formats/anthropic/serialize.d.ts.map +1 -1
- package/dist/formats/anthropic/serialize.js +6 -3
- package/dist/formats/anthropic/serialize.js.map +1 -1
- package/dist/formats/openai/index.js +1 -1
- package/dist/formats/openai/index.js.map +1 -1
- package/dist/formats/openai/parse.d.ts +1 -1
- package/dist/formats/openai/parse.d.ts.map +1 -1
- package/dist/formats/openai/parse.js +1 -1
- package/dist/formats/openai/parse.js.map +1 -1
- package/dist/formats/openai/serialize.d.ts +2 -2
- package/dist/formats/openai/serialize.d.ts.map +1 -1
- package/dist/formats/openai/serialize.js +12 -15
- package/dist/formats/openai/serialize.js.map +1 -1
- package/dist/formats/request-helpers.d.ts +13 -0
- package/dist/formats/request-helpers.d.ts.map +1 -0
- package/dist/formats/request-helpers.js +28 -0
- package/dist/formats/request-helpers.js.map +1 -0
- package/dist/formats/responses/index.js +1 -1
- package/dist/formats/responses/index.js.map +1 -1
- package/dist/formats/responses/parse.d.ts +1 -1
- package/dist/formats/responses/parse.d.ts.map +1 -1
- package/dist/formats/responses/parse.js +1 -1
- package/dist/formats/responses/parse.js.map +1 -1
- package/dist/formats/responses/schema.d.ts +1 -20
- package/dist/formats/responses/schema.d.ts.map +1 -1
- package/dist/formats/responses/schema.js.map +1 -1
- package/dist/formats/responses/serialize.d.ts +2 -2
- package/dist/formats/responses/serialize.d.ts.map +1 -1
- package/dist/formats/responses/serialize.js +6 -3
- package/dist/formats/responses/serialize.js.map +1 -1
- package/dist/formats/serialize-helpers.d.ts +14 -0
- package/dist/formats/serialize-helpers.d.ts.map +1 -0
- package/dist/formats/serialize-helpers.js +25 -0
- package/dist/formats/serialize-helpers.js.map +1 -0
- package/dist/formats/types.d.ts +3 -3
- package/dist/formats/types.d.ts.map +1 -1
- package/dist/loader.d.ts +3 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +6 -9
- package/dist/loader.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +17 -23
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.d.ts.map +1 -1
- package/dist/mock-server.js +8 -15
- package/dist/mock-server.js.map +1 -1
- package/dist/route-handler.d.ts +2 -1
- package/dist/route-handler.d.ts.map +1 -1
- package/dist/rule-engine.d.ts +12 -1
- package/dist/rule-engine.d.ts.map +1 -1
- package/dist/rule-engine.js +14 -0
- package/dist/rule-engine.js.map +1 -1
- package/dist/types/reply.d.ts +6 -10
- package/dist/types/reply.d.ts.map +1 -1
- package/dist/types/request.d.ts +7 -11
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/rule.d.ts +3 -10
- package/dist/types/rule.d.ts.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/scorecard.png +0 -0
- package/src/cli-validators.ts +12 -4
- package/src/cli.ts +27 -7
- package/src/formats/anthropic/index.ts +1 -1
- package/src/formats/anthropic/parse.ts +25 -6
- package/src/formats/anthropic/schema.ts +16 -8
- package/src/formats/anthropic/serialize.ts +116 -28
- package/src/formats/openai/index.ts +1 -1
- package/src/formats/openai/parse.ts +13 -3
- package/src/formats/openai/schema.ts +43 -30
- package/src/formats/openai/serialize.ts +84 -30
- package/src/formats/{parse-helpers.ts → request-helpers.ts} +4 -32
- package/src/formats/responses/index.ts +1 -1
- package/src/formats/responses/parse.ts +18 -4
- package/src/formats/responses/schema.ts +34 -22
- package/src/formats/responses/serialize.ts +237 -38
- package/src/formats/serialize-helpers.ts +38 -0
- package/src/formats/types.ts +18 -5
- package/src/index.ts +3 -1
- package/src/loader.ts +43 -20
- package/src/logger.ts +31 -19
- package/src/mock-server.ts +38 -21
- package/src/route-handler.ts +50 -15
- package/src/rule-engine.ts +64 -11
- package/src/types/reply.ts +12 -12
- package/src/types/request.ts +7 -11
- package/src/types/rule.ts +3 -10
- package/src/types.ts +23 -4
- package/test/cli-validators.test.ts +16 -4
- package/test/formats/anthropic.test.ts +84 -23
- package/test/formats/openai.test.ts +85 -24
- package/test/formats/parse-helpers.test.ts +315 -0
- package/test/formats/responses.test.ts +99 -34
- package/test/helpers/make-req.ts +18 -0
- package/test/history.test.ts +361 -0
- package/test/loader.test.ts +44 -45
- package/test/logger.test.ts +344 -0
- package/test/mock-server.test.ts +77 -23
- package/test/rule-engine.test.ts +57 -41
- package/src/types/index.ts +0 -4
|
@@ -0,0 +1,344 @@
|
|
|
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<{
|
|
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
|
+
},
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
for (const {
|
|
315
|
+
level,
|
|
316
|
+
expectError,
|
|
317
|
+
expectWarn,
|
|
318
|
+
expectInfo,
|
|
319
|
+
expectDebug,
|
|
320
|
+
} of cases) {
|
|
321
|
+
it(`level '${level}' enables the correct methods`, () => {
|
|
322
|
+
const errorSpy = vi
|
|
323
|
+
.spyOn(console, "error")
|
|
324
|
+
.mockImplementation(() => {});
|
|
325
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
326
|
+
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
327
|
+
|
|
328
|
+
const logger = new Logger(level);
|
|
329
|
+
logger.error("e");
|
|
330
|
+
logger.warn("w");
|
|
331
|
+
logger.info("i");
|
|
332
|
+
logger.debug("d");
|
|
333
|
+
|
|
334
|
+
expect(errorSpy).toHaveBeenCalledTimes(expectError ? 1 : 0);
|
|
335
|
+
expect(warnSpy).toHaveBeenCalledTimes(expectWarn ? 1 : 0);
|
|
336
|
+
|
|
337
|
+
let expectedLogCalls = 0;
|
|
338
|
+
if (expectInfo) expectedLogCalls++;
|
|
339
|
+
if (expectDebug) expectedLogCalls++;
|
|
340
|
+
expect(logSpy).toHaveBeenCalledTimes(expectedLogCalls);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
});
|
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.");
|
|
@@ -299,7 +318,7 @@ describe("MockServer (end-to-end)", () => {
|
|
|
299
318
|
|
|
300
319
|
it("throws on empty sequence", () => {
|
|
301
320
|
expect(() => server.when("step").replySequence([])).toThrow(
|
|
302
|
-
"
|
|
321
|
+
"Sequence requires at least one entry",
|
|
303
322
|
);
|
|
304
323
|
});
|
|
305
324
|
});
|
|
@@ -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();
|