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.
- 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/dist/cli-validators.d.ts +7 -0
- package/dist/cli-validators.d.ts.map +1 -0
- package/dist/cli-validators.js +51 -0
- package/dist/cli-validators.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +106 -0
- package/dist/cli.js.map +1 -0
- package/dist/formats/anthropic/index.d.ts +3 -0
- package/dist/formats/anthropic/index.d.ts.map +1 -0
- package/dist/formats/anthropic/index.js +13 -0
- package/dist/formats/anthropic/index.js.map +1 -0
- package/dist/formats/anthropic/parse.d.ts +4 -0
- package/dist/formats/anthropic/parse.d.ts.map +1 -0
- package/dist/formats/anthropic/parse.js +47 -0
- package/dist/formats/anthropic/parse.js.map +1 -0
- package/dist/formats/anthropic/schema.d.ts +75 -0
- package/dist/formats/anthropic/schema.d.ts.map +1 -0
- package/dist/formats/anthropic/schema.js +50 -0
- package/dist/formats/anthropic/schema.js.map +1 -0
- package/dist/formats/anthropic/serialize.d.ts +10 -0
- package/dist/formats/anthropic/serialize.d.ts.map +1 -0
- package/dist/formats/anthropic/serialize.js +73 -0
- package/dist/formats/anthropic/serialize.js.map +1 -0
- package/dist/formats/openai/index.d.ts +3 -0
- package/dist/formats/openai/index.d.ts.map +1 -0
- package/dist/formats/openai/index.js +13 -0
- package/dist/formats/openai/index.js.map +1 -0
- package/dist/formats/openai/parse.d.ts +4 -0
- package/dist/formats/openai/parse.d.ts.map +1 -0
- package/dist/formats/openai/parse.js +33 -0
- package/dist/formats/openai/parse.js.map +1 -0
- package/dist/formats/openai/schema.d.ts +93 -0
- package/dist/formats/openai/schema.d.ts.map +1 -0
- package/dist/formats/openai/schema.js +68 -0
- package/dist/formats/openai/schema.js.map +1 -0
- package/dist/formats/openai/serialize.d.ts +10 -0
- package/dist/formats/openai/serialize.d.ts.map +1 -0
- package/dist/formats/openai/serialize.js +70 -0
- package/dist/formats/openai/serialize.js.map +1 -0
- package/dist/formats/parse-helpers.d.ts +24 -0
- package/dist/formats/parse-helpers.d.ts.map +1 -0
- package/dist/formats/parse-helpers.js +52 -0
- package/dist/formats/parse-helpers.js.map +1 -0
- 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.d.ts +3 -0
- package/dist/formats/responses/index.d.ts.map +1 -0
- package/dist/formats/responses/index.js +13 -0
- package/dist/formats/responses/index.js.map +1 -0
- package/dist/formats/responses/parse.d.ts +4 -0
- package/dist/formats/responses/parse.d.ts.map +1 -0
- package/dist/formats/responses/parse.js +51 -0
- package/dist/formats/responses/parse.js.map +1 -0
- package/dist/formats/responses/schema.d.ts +103 -0
- package/dist/formats/responses/schema.d.ts.map +1 -0
- package/dist/formats/responses/schema.js +61 -0
- package/dist/formats/responses/schema.js.map +1 -0
- package/dist/formats/responses/serialize.d.ts +10 -0
- package/dist/formats/responses/serialize.d.ts.map +1 -0
- package/dist/formats/responses/serialize.js +108 -0
- package/dist/formats/responses/serialize.js.map +1 -0
- 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 +20 -0
- package/dist/formats/types.d.ts.map +1 -0
- package/dist/formats/types.js +2 -0
- package/dist/formats/types.js.map +1 -0
- package/dist/history.d.ts +38 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +48 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +9 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +169 -0
- package/dist/loader.js.map +1 -0
- package/dist/logger.d.ts +21 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +42 -0
- package/dist/logger.js.map +1 -0
- package/dist/mock-server.d.ts +102 -0
- package/dist/mock-server.d.ts.map +1 -0
- package/dist/mock-server.js +195 -0
- package/dist/mock-server.js.map +1 -0
- package/dist/route-handler.d.ts +16 -0
- package/dist/route-handler.d.ts.map +1 -0
- package/dist/route-handler.js +75 -0
- package/dist/route-handler.js.map +1 -0
- package/dist/rule-engine.d.ts +24 -0
- package/dist/rule-engine.d.ts.map +1 -0
- package/dist/rule-engine.js +129 -0
- package/dist/rule-engine.js.map +1 -0
- package/dist/sse-writer.d.ts +5 -0
- package/dist/sse-writer.d.ts.map +1 -0
- package/dist/sse-writer.js +23 -0
- package/dist/sse-writer.js.map +1 -0
- package/{src/types/index.ts → dist/types/index.d.ts} +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/reply.d.ts +45 -0
- package/dist/types/reply.d.ts.map +1 -0
- package/dist/types/reply.js +2 -0
- package/dist/types/reply.js.map +1 -0
- package/dist/types/request.d.ts +39 -0
- package/dist/types/request.d.ts.map +1 -0
- package/dist/types/request.js +2 -0
- package/dist/types/request.js.map +1 -0
- package/dist/types/rule.d.ts +57 -0
- package/dist/types/rule.d.ts.map +1 -0
- package/dist/types/rule.js +2 -0
- package/dist/types/rule.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +2 -1
- package/scorecard.png +0 -0
- package/src/cli-validators.ts +3 -0
- package/src/cli.ts +6 -2
- package/src/formats/anthropic/index.ts +1 -1
- package/src/formats/anthropic/parse.ts +4 -4
- package/src/formats/anthropic/schema.ts +1 -68
- package/src/formats/anthropic/serialize.ts +9 -5
- package/src/formats/openai/index.ts +1 -1
- package/src/formats/openai/parse.ts +1 -1
- package/src/formats/openai/schema.ts +1 -69
- package/src/formats/openai/serialize.ts +15 -17
- package/src/formats/{parse-helpers.ts → request-helpers.ts} +2 -31
- package/src/formats/responses/index.ts +1 -1
- package/src/formats/responses/parse.ts +1 -1
- package/src/formats/responses/schema.ts +1 -72
- package/src/formats/responses/serialize.ts +9 -5
- package/src/formats/serialize-helpers.ts +30 -0
- package/src/formats/types.ts +3 -3
- package/src/loader.ts +7 -11
- package/src/logger.ts +19 -25
- package/src/mock-server.ts +10 -14
- package/src/route-handler.ts +1 -1
- package/src/rule-engine.ts +23 -1
- package/src/types/reply.ts +6 -10
- package/src/types/request.ts +7 -11
- package/src/types/rule.ts +3 -10
- package/src/types.ts +3 -5
- package/test/formats/anthropic.test.ts +4 -4
- package/test/formats/parse-helpers.test.ts +275 -0
- package/test/formats/responses.test.ts +4 -4
- package/test/helpers/make-req.ts +18 -0
- package/test/history.test.ts +348 -0
- package/test/loader.test.ts +11 -27
- package/test/logger.test.ts +294 -0
- package/test/mock-server.test.ts +1 -1
- package/test/rule-engine.test.ts +8 -22
- package/test/formats/anthropic-schema.test.ts +0 -192
- package/test/formats/openai-schema.test.ts +0 -105
- package/test/formats/responses-schema.test.ts +0 -114
package/test/loader.test.ts
CHANGED
|
@@ -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
|
-
|
|
61
|
-
expect(match1
|
|
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
|
-
|
|
158
|
-
expect((match1
|
|
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
|
-
|
|
162
|
-
expect((match2
|
|
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
|
-
|
|
204
|
-
expect(match
|
|
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
|
-
|
|
227
|
+
if (!rule) throw new Error("expected match for 'echo'");
|
|
244
228
|
|
|
245
|
-
const resolver = rule
|
|
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
|
+
});
|
package/test/mock-server.test.ts
CHANGED
|
@@ -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
|
-
"
|
|
302
|
+
"Sequence requires at least one entry",
|
|
303
303
|
);
|
|
304
304
|
});
|
|
305
305
|
});
|
package/test/rule-engine.test.ts
CHANGED
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { RuleEngine } from "../src/rule-engine.js";
|
|
3
|
-
import
|
|
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
|
-
|
|
31
|
-
expect(rule
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
});
|