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,361 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { RequestHistory, type RecordedRequest } from "../src/history.js";
|
|
3
|
+
import { makeReq } from "./helpers/make-req.js";
|
|
4
|
+
|
|
5
|
+
describe("RequestHistory", () => {
|
|
6
|
+
let history: RequestHistory;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
history = new RequestHistory();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("record()", () => {
|
|
13
|
+
it("adds an entry", () => {
|
|
14
|
+
history.record(makeReq(), "rule-1");
|
|
15
|
+
expect(history.count()).toBe(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("adds multiple entries in order", () => {
|
|
19
|
+
history.record(makeReq({ lastMessage: "first" }), "r1");
|
|
20
|
+
history.record(makeReq({ lastMessage: "second" }), "r2");
|
|
21
|
+
history.record(makeReq({ lastMessage: "third" }), undefined);
|
|
22
|
+
|
|
23
|
+
expect(history.count()).toBe(3);
|
|
24
|
+
expect(history.first()?.request.lastMessage).toBe("first");
|
|
25
|
+
expect(history.last()?.request.lastMessage).toBe("third");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("stores the matched rule name", () => {
|
|
29
|
+
history.record(makeReq(), "my-rule");
|
|
30
|
+
expect(history.first()?.rule).toBe("my-rule");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("stores undefined rule when fallback was used", () => {
|
|
34
|
+
history.record(makeReq(), undefined);
|
|
35
|
+
expect(history.first()?.rule).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("sets a numeric timestamp", () => {
|
|
39
|
+
const before = Date.now();
|
|
40
|
+
history.record(makeReq(), "r");
|
|
41
|
+
const after = Date.now();
|
|
42
|
+
|
|
43
|
+
const ts = history.first()!.timestamp;
|
|
44
|
+
expect(ts).toBeGreaterThanOrEqual(before);
|
|
45
|
+
expect(ts).toBeLessThanOrEqual(after);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("count()", () => {
|
|
50
|
+
it("returns 0 for empty history", () => {
|
|
51
|
+
expect(history.count()).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns the correct count after multiple records", () => {
|
|
55
|
+
history.record(makeReq(), "a");
|
|
56
|
+
history.record(makeReq(), "b");
|
|
57
|
+
history.record(makeReq(), "c");
|
|
58
|
+
expect(history.count()).toBe(3);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns 0 after clear", () => {
|
|
62
|
+
history.record(makeReq(), "a");
|
|
63
|
+
history.clear();
|
|
64
|
+
expect(history.count()).toBe(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("first()", () => {
|
|
69
|
+
it("returns undefined when history is empty", () => {
|
|
70
|
+
expect(history.first()).toBeUndefined();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("returns the first recorded entry", () => {
|
|
74
|
+
history.record(makeReq({ lastMessage: "alpha" }), "r1");
|
|
75
|
+
history.record(makeReq({ lastMessage: "beta" }), "r2");
|
|
76
|
+
|
|
77
|
+
const entry = history.first();
|
|
78
|
+
expect(entry).toBeDefined();
|
|
79
|
+
expect(entry!.request.lastMessage).toBe("alpha");
|
|
80
|
+
expect(entry!.rule).toBe("r1");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("last()", () => {
|
|
85
|
+
it("returns undefined when history is empty", () => {
|
|
86
|
+
expect(history.last()).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("returns the most recent entry", () => {
|
|
90
|
+
history.record(makeReq({ lastMessage: "alpha" }), "r1");
|
|
91
|
+
history.record(makeReq({ lastMessage: "beta" }), "r2");
|
|
92
|
+
|
|
93
|
+
const entry = history.last();
|
|
94
|
+
expect(entry).toBeDefined();
|
|
95
|
+
expect(entry!.request.lastMessage).toBe("beta");
|
|
96
|
+
expect(entry!.rule).toBe("r2");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("returns the same entry as first() when there is only one", () => {
|
|
100
|
+
history.record(makeReq(), "only");
|
|
101
|
+
expect(history.first()).toBe(history.last());
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("at()", () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
history.record(makeReq({ lastMessage: "zero" }), "r0");
|
|
108
|
+
history.record(makeReq({ lastMessage: "one" }), "r1");
|
|
109
|
+
history.record(makeReq({ lastMessage: "two" }), "r2");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("returns the entry at a positive index", () => {
|
|
113
|
+
expect(history.at(0)?.request.lastMessage).toBe("zero");
|
|
114
|
+
expect(history.at(1)?.request.lastMessage).toBe("one");
|
|
115
|
+
expect(history.at(2)?.request.lastMessage).toBe("two");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns the entry at a negative index", () => {
|
|
119
|
+
expect(history.at(-1)?.request.lastMessage).toBe("two");
|
|
120
|
+
expect(history.at(-2)?.request.lastMessage).toBe("one");
|
|
121
|
+
expect(history.at(-3)?.request.lastMessage).toBe("zero");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("returns undefined for out-of-bounds positive index", () => {
|
|
125
|
+
expect(history.at(3)).toBeUndefined();
|
|
126
|
+
expect(history.at(100)).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("returns undefined for out-of-bounds negative index", () => {
|
|
130
|
+
expect(history.at(-4)).toBeUndefined();
|
|
131
|
+
expect(history.at(-100)).toBeUndefined();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("returns undefined when history is empty", () => {
|
|
135
|
+
const empty = new RequestHistory();
|
|
136
|
+
expect(empty.at(0)).toBeUndefined();
|
|
137
|
+
expect(empty.at(-1)).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("where()", () => {
|
|
142
|
+
beforeEach(() => {
|
|
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
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("filters entries by predicate", () => {
|
|
158
|
+
const matched = history.where((e) => e.rule !== undefined);
|
|
159
|
+
expect(matched).toHaveLength(2);
|
|
160
|
+
expect(matched[0].rule).toBe("rule-a");
|
|
161
|
+
expect(matched[1].rule).toBe("rule-b");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("filters by request properties", () => {
|
|
165
|
+
const claudeRequests = history.where(
|
|
166
|
+
(e) => e.request.model === "claude-4",
|
|
167
|
+
);
|
|
168
|
+
expect(claudeRequests).toHaveLength(1);
|
|
169
|
+
expect(claudeRequests[0].request.lastMessage).toBe("world");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns an empty array when nothing matches", () => {
|
|
173
|
+
const none = history.where(
|
|
174
|
+
(e) => e.request.lastMessage === "nonexistent",
|
|
175
|
+
);
|
|
176
|
+
expect(none).toEqual([]);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("returns all entries when predicate always returns true", () => {
|
|
180
|
+
const all = history.where(() => true);
|
|
181
|
+
expect(all).toHaveLength(3);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("returns an empty array on empty history", () => {
|
|
185
|
+
const empty = new RequestHistory();
|
|
186
|
+
expect(empty.where(() => true)).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("all getter", () => {
|
|
191
|
+
it("returns an empty array when history is empty", () => {
|
|
192
|
+
expect(history.all).toEqual([]);
|
|
193
|
+
expect(history.all).toHaveLength(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("returns all recorded entries in insertion order", () => {
|
|
197
|
+
history.record(makeReq({ lastMessage: "a" }), "r1");
|
|
198
|
+
history.record(makeReq({ lastMessage: "b" }), "r2");
|
|
199
|
+
|
|
200
|
+
const entries = history.all;
|
|
201
|
+
expect(entries).toHaveLength(2);
|
|
202
|
+
expect(entries[0].request.lastMessage).toBe("a");
|
|
203
|
+
expect(entries[1].request.lastMessage).toBe("b");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("returns a readonly array (same reference as internal entries)", () => {
|
|
207
|
+
history.record(makeReq(), "r");
|
|
208
|
+
const a = history.all;
|
|
209
|
+
const b = history.all;
|
|
210
|
+
expect(a).toBe(b);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("reflects mutations after further records", () => {
|
|
214
|
+
history.record(makeReq({ lastMessage: "before" }), "r");
|
|
215
|
+
const ref = history.all;
|
|
216
|
+
expect(ref).toHaveLength(1);
|
|
217
|
+
|
|
218
|
+
history.record(makeReq({ lastMessage: "after" }), "r2");
|
|
219
|
+
// `all` exposes the internal array, so the earlier reference sees the new entry
|
|
220
|
+
expect(ref).toHaveLength(2);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("clear()", () => {
|
|
225
|
+
it("empties the history", () => {
|
|
226
|
+
history.record(makeReq(), "r1");
|
|
227
|
+
history.record(makeReq(), "r2");
|
|
228
|
+
expect(history.count()).toBe(2);
|
|
229
|
+
|
|
230
|
+
history.clear();
|
|
231
|
+
expect(history.count()).toBe(0);
|
|
232
|
+
expect(history.first()).toBeUndefined();
|
|
233
|
+
expect(history.last()).toBeUndefined();
|
|
234
|
+
expect(history.all).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("is idempotent on empty history", () => {
|
|
238
|
+
history.clear();
|
|
239
|
+
expect(history.count()).toBe(0);
|
|
240
|
+
history.clear();
|
|
241
|
+
expect(history.count()).toBe(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("allows recording again after clear", () => {
|
|
245
|
+
history.record(makeReq({ lastMessage: "old" }), "r1");
|
|
246
|
+
history.clear();
|
|
247
|
+
history.record(makeReq({ lastMessage: "new" }), "r2");
|
|
248
|
+
|
|
249
|
+
expect(history.count()).toBe(1);
|
|
250
|
+
expect(history.first()?.request.lastMessage).toBe("new");
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("Iterator protocol (for...of)", () => {
|
|
255
|
+
it("iterates over all entries in order", () => {
|
|
256
|
+
history.record(makeReq({ lastMessage: "a" }), "r1");
|
|
257
|
+
history.record(makeReq({ lastMessage: "b" }), "r2");
|
|
258
|
+
history.record(makeReq({ lastMessage: "c" }), "r3");
|
|
259
|
+
|
|
260
|
+
const messages: string[] = [];
|
|
261
|
+
for (const entry of history) {
|
|
262
|
+
messages.push(entry.request.lastMessage);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
expect(messages).toEqual(["a", "b", "c"]);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("yields nothing for empty history", () => {
|
|
269
|
+
const messages: string[] = [];
|
|
270
|
+
for (const entry of history) {
|
|
271
|
+
messages.push(entry.request.lastMessage);
|
|
272
|
+
}
|
|
273
|
+
expect(messages).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("works with spread operator", () => {
|
|
277
|
+
history.record(makeReq({ lastMessage: "x" }), "r1");
|
|
278
|
+
history.record(makeReq({ lastMessage: "y" }), "r2");
|
|
279
|
+
|
|
280
|
+
const entries: RecordedRequest[] = [...history];
|
|
281
|
+
expect(entries).toHaveLength(2);
|
|
282
|
+
expect(entries[0].request.lastMessage).toBe("x");
|
|
283
|
+
expect(entries[1].request.lastMessage).toBe("y");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("works with Array.from()", () => {
|
|
287
|
+
history.record(makeReq(), "r1");
|
|
288
|
+
history.record(makeReq(), "r2");
|
|
289
|
+
|
|
290
|
+
const arr = Array.from(history);
|
|
291
|
+
expect(arr).toHaveLength(2);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("supports destructuring", () => {
|
|
295
|
+
history.record(makeReq({ lastMessage: "first" }), "r1");
|
|
296
|
+
history.record(makeReq({ lastMessage: "second" }), "r2");
|
|
297
|
+
history.record(makeReq({ lastMessage: "third" }), "r3");
|
|
298
|
+
|
|
299
|
+
const [first, second, third] = history;
|
|
300
|
+
expect(first.request.lastMessage).toBe("first");
|
|
301
|
+
expect(second.request.lastMessage).toBe("second");
|
|
302
|
+
expect(third.request.lastMessage).toBe("third");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("edge cases", () => {
|
|
307
|
+
it("preserves the full MockRequest object", () => {
|
|
308
|
+
const req = makeReq({
|
|
309
|
+
format: "anthropic",
|
|
310
|
+
model: "claude-4",
|
|
311
|
+
streaming: false,
|
|
312
|
+
lastMessage: "test message",
|
|
313
|
+
systemMessage: "be helpful",
|
|
314
|
+
toolNames: ["search", "calc"],
|
|
315
|
+
lastToolCallId: "call_123",
|
|
316
|
+
path: "/v1/messages",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
history.record(req, "complex-rule");
|
|
320
|
+
const recorded = history.first()!;
|
|
321
|
+
|
|
322
|
+
expect(recorded.request.format).toBe("anthropic");
|
|
323
|
+
expect(recorded.request.model).toBe("claude-4");
|
|
324
|
+
expect(recorded.request.streaming).toBe(false);
|
|
325
|
+
expect(recorded.request.lastMessage).toBe("test message");
|
|
326
|
+
expect(recorded.request.systemMessage).toBe("be helpful");
|
|
327
|
+
expect(recorded.request.toolNames).toEqual(["search", "calc"]);
|
|
328
|
+
expect(recorded.request.lastToolCallId).toBe("call_123");
|
|
329
|
+
expect(recorded.request.path).toBe("/v1/messages");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("handles many entries without issue", () => {
|
|
333
|
+
for (let i = 0; i < 1000; i++) {
|
|
334
|
+
history.record(makeReq({ lastMessage: `msg-${i}` }), `rule-${i}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
expect(history.count()).toBe(1000);
|
|
338
|
+
expect(history.first()?.request.lastMessage).toBe("msg-0");
|
|
339
|
+
expect(history.last()?.request.lastMessage).toBe("msg-999");
|
|
340
|
+
expect(history.at(500)?.request.lastMessage).toBe("msg-500");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("where() does not modify the original entries", () => {
|
|
344
|
+
history.record(makeReq(), "r1");
|
|
345
|
+
history.record(makeReq(), "r2");
|
|
346
|
+
|
|
347
|
+
const filtered = history.where(() => false);
|
|
348
|
+
expect(filtered).toHaveLength(0);
|
|
349
|
+
expect(history.count()).toBe(2);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("each entry gets its own timestamp", () => {
|
|
353
|
+
history.record(makeReq(), "r1");
|
|
354
|
+
history.record(makeReq(), "r2");
|
|
355
|
+
|
|
356
|
+
const t1 = history.at(0)!.timestamp;
|
|
357
|
+
const t2 = history.at(1)!.timestamp;
|
|
358
|
+
expect(t2).toBeGreaterThanOrEqual(t1);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
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
|
|
|
@@ -56,11 +40,15 @@ describe("Loader", () => {
|
|
|
56
40
|
await loadRulesFromPath(rulesPath, { engine });
|
|
57
41
|
expect(engine.ruleCount).toBe(2);
|
|
58
42
|
|
|
59
|
-
const match1 = engine.match(
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
const match1 = engine.match(
|
|
44
|
+
makeReq({ lastMessage: "Please explain recursion" }),
|
|
45
|
+
);
|
|
46
|
+
if (!match1) throw new Error("expected match for 'explain'");
|
|
47
|
+
expect(match1.resolve).toBe("A function that calls itself.");
|
|
62
48
|
|
|
63
|
-
const match2 = engine.match(
|
|
49
|
+
const match2 = engine.match(
|
|
50
|
+
makeReq({ model: "gpt-5.4", lastMessage: "hello" }),
|
|
51
|
+
);
|
|
64
52
|
expect(match2).toBeDefined();
|
|
65
53
|
});
|
|
66
54
|
|
|
@@ -77,7 +65,9 @@ describe("Loader", () => {
|
|
|
77
65
|
);
|
|
78
66
|
|
|
79
67
|
await loadRulesFromPath(rulesPath, { engine });
|
|
80
|
-
const match = engine.match(
|
|
68
|
+
const match = engine.match(
|
|
69
|
+
makeReq({ lastMessage: "explain polymorphism" }),
|
|
70
|
+
);
|
|
81
71
|
expect(match).toBeDefined();
|
|
82
72
|
});
|
|
83
73
|
|
|
@@ -89,7 +79,9 @@ describe("Loader", () => {
|
|
|
89
79
|
);
|
|
90
80
|
|
|
91
81
|
await loadRulesFromPath(rulesPath, { engine });
|
|
92
|
-
expect(
|
|
82
|
+
expect(
|
|
83
|
+
engine.match(makeReq({ lastMessage: "hello world" })),
|
|
84
|
+
).toBeDefined();
|
|
93
85
|
});
|
|
94
86
|
|
|
95
87
|
it("loads rules with times", async () => {
|
|
@@ -139,7 +131,9 @@ describe("Loader", () => {
|
|
|
139
131
|
}`,
|
|
140
132
|
);
|
|
141
133
|
|
|
142
|
-
await expect(loadRulesFromPath(rulesPath, { engine })).rejects.toThrow(
|
|
134
|
+
await expect(loadRulesFromPath(rulesPath, { engine })).rejects.toThrow(
|
|
135
|
+
"Unknown template",
|
|
136
|
+
);
|
|
143
137
|
});
|
|
144
138
|
|
|
145
139
|
it("loads a replies sequence", async () => {
|
|
@@ -154,12 +148,12 @@ describe("Loader", () => {
|
|
|
154
148
|
|
|
155
149
|
const req = makeReq({ lastMessage: "step" });
|
|
156
150
|
const match1 = engine.match(req);
|
|
157
|
-
|
|
158
|
-
expect((match1
|
|
151
|
+
if (!match1) throw new Error("expected match1");
|
|
152
|
+
expect((match1.resolve as () => string)()).toBe("First.");
|
|
159
153
|
|
|
160
154
|
const match2 = engine.match(req);
|
|
161
|
-
|
|
162
|
-
expect((match2
|
|
155
|
+
if (!match2) throw new Error("expected match2");
|
|
156
|
+
expect((match2.resolve as () => string)()).toBe("Second.");
|
|
163
157
|
|
|
164
158
|
expect(engine.match(req)).toBeUndefined();
|
|
165
159
|
});
|
|
@@ -177,7 +171,9 @@ describe("Loader", () => {
|
|
|
177
171
|
let capturedFallback: unknown;
|
|
178
172
|
await loadRulesFromPath(rulesPath, {
|
|
179
173
|
engine,
|
|
180
|
-
setFallback: (reply) => {
|
|
174
|
+
setFallback: (reply) => {
|
|
175
|
+
capturedFallback = reply;
|
|
176
|
+
},
|
|
181
177
|
});
|
|
182
178
|
|
|
183
179
|
expect(capturedFallback).toBe("Default reply.");
|
|
@@ -199,9 +195,11 @@ describe("Loader", () => {
|
|
|
199
195
|
await loadRulesFromPath(handlerPath, { engine });
|
|
200
196
|
expect(engine.ruleCount).toBe(1);
|
|
201
197
|
|
|
202
|
-
const match = engine.match(
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
const match = engine.match(
|
|
199
|
+
makeReq({ lastMessage: "summarize this article" }),
|
|
200
|
+
);
|
|
201
|
+
if (!match) throw new Error("expected match for 'summarize'");
|
|
202
|
+
expect(match.resolve).toBeTypeOf("function");
|
|
205
203
|
});
|
|
206
204
|
|
|
207
205
|
it("loads an array of handlers from a .ts file", async () => {
|
|
@@ -240,18 +238,23 @@ describe("Loader", () => {
|
|
|
240
238
|
|
|
241
239
|
await loadRulesFromPath(handlerPath, { engine });
|
|
242
240
|
const rule = engine.match(makeReq({ lastMessage: "echo this" }));
|
|
243
|
-
|
|
241
|
+
if (!rule) throw new Error("expected match for 'echo'");
|
|
244
242
|
|
|
245
|
-
const resolver = rule
|
|
243
|
+
const resolver = rule.resolve as (req: MockRequest) => string;
|
|
246
244
|
const result = resolver(makeReq({ lastMessage: "echo this" }));
|
|
247
245
|
expect(result).toBe("Echo: echo this");
|
|
248
246
|
});
|
|
249
247
|
|
|
250
248
|
it("throws on invalid handler file (missing match/respond)", async () => {
|
|
251
249
|
const handlerPath = join(tmpDir, "bad.ts");
|
|
252
|
-
await writeFile(
|
|
250
|
+
await writeFile(
|
|
251
|
+
handlerPath,
|
|
252
|
+
`export default { mach: () => true, respond: () => "hi" };`,
|
|
253
|
+
);
|
|
253
254
|
|
|
254
|
-
await expect(loadRulesFromPath(handlerPath, { engine })).rejects.toThrow(
|
|
255
|
+
await expect(loadRulesFromPath(handlerPath, { engine })).rejects.toThrow(
|
|
256
|
+
"Invalid handler file",
|
|
257
|
+
);
|
|
255
258
|
});
|
|
256
259
|
|
|
257
260
|
it("loads fallback from handler file", async () => {
|
|
@@ -268,7 +271,9 @@ describe("Loader", () => {
|
|
|
268
271
|
let capturedFallback: unknown;
|
|
269
272
|
await loadRulesFromPath(handlerPath, {
|
|
270
273
|
engine,
|
|
271
|
-
setFallback: (reply) => {
|
|
274
|
+
setFallback: (reply) => {
|
|
275
|
+
capturedFallback = reply;
|
|
276
|
+
},
|
|
272
277
|
});
|
|
273
278
|
|
|
274
279
|
expect(capturedFallback).toBe("Default reply.");
|
|
@@ -278,14 +283,8 @@ describe("Loader", () => {
|
|
|
278
283
|
|
|
279
284
|
describe("directory loading", () => {
|
|
280
285
|
it("loads all .json5 files from a directory", async () => {
|
|
281
|
-
await writeFile(
|
|
282
|
-
|
|
283
|
-
`[{ when: "aaa", reply: "A" }]`,
|
|
284
|
-
);
|
|
285
|
-
await writeFile(
|
|
286
|
-
join(tmpDir, "b.json5"),
|
|
287
|
-
`[{ when: "bbb", reply: "B" }]`,
|
|
288
|
-
);
|
|
286
|
+
await writeFile(join(tmpDir, "a.json5"), `[{ when: "aaa", reply: "A" }]`);
|
|
287
|
+
await writeFile(join(tmpDir, "b.json5"), `[{ when: "bbb", reply: "B" }]`);
|
|
289
288
|
|
|
290
289
|
await loadRulesFromPath(tmpDir, { engine });
|
|
291
290
|
expect(engine.ruleCount).toBe(2);
|