llm-mock-server 1.0.6 → 1.0.7
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/dist/cli/cli.d.ts +3 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +103 -0
- package/dist/cli/cli.js.map +1 -0
- package/dist/cli/validators.d.ts +7 -0
- package/dist/cli/validators.d.ts.map +1 -0
- package/dist/cli/validators.js +53 -0
- package/dist/cli/validators.js.map +1 -0
- package/dist/formats/anthropic/index.d.ts +1 -1
- package/dist/formats/anthropic/index.d.ts.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 +2 -2
- package/dist/formats/anthropic/parse.d.ts.map +1 -1
- package/dist/formats/anthropic/parse.js +4 -2
- package/dist/formats/anthropic/parse.js.map +1 -1
- package/dist/formats/anthropic/schema.d.ts +1 -1
- package/dist/formats/anthropic/schema.d.ts.map +1 -1
- package/dist/formats/anthropic/schema.js +9 -4
- package/dist/formats/anthropic/schema.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 +76 -19
- package/dist/formats/anthropic/serialize.js.map +1 -1
- package/dist/formats/openai/chat-completions/index.d.ts +3 -0
- package/dist/formats/openai/chat-completions/index.d.ts.map +1 -0
- package/dist/formats/openai/chat-completions/index.js +13 -0
- package/dist/formats/openai/chat-completions/index.js.map +1 -0
- package/dist/formats/openai/chat-completions/parse.d.ts +4 -0
- package/dist/formats/openai/chat-completions/parse.d.ts.map +1 -0
- package/dist/formats/openai/chat-completions/parse.js +33 -0
- package/dist/formats/openai/chat-completions/parse.js.map +1 -0
- package/dist/formats/openai/chat-completions/schema.d.ts +93 -0
- package/dist/formats/openai/chat-completions/schema.d.ts.map +1 -0
- package/dist/formats/openai/chat-completions/schema.js +74 -0
- package/dist/formats/openai/chat-completions/schema.js.map +1 -0
- package/dist/formats/openai/chat-completions/serialize.d.ts +10 -0
- package/dist/formats/openai/chat-completions/serialize.d.ts.map +1 -0
- package/dist/formats/openai/chat-completions/serialize.js +99 -0
- package/dist/formats/openai/chat-completions/serialize.js.map +1 -0
- package/dist/formats/openai/responses/index.d.ts +3 -0
- package/dist/formats/openai/responses/index.d.ts.map +1 -0
- package/dist/formats/openai/responses/index.js +13 -0
- package/dist/formats/openai/responses/index.js.map +1 -0
- package/dist/formats/openai/responses/parse.d.ts +4 -0
- package/dist/formats/openai/responses/parse.d.ts.map +1 -0
- package/dist/formats/openai/responses/parse.js +51 -0
- package/dist/formats/openai/responses/parse.js.map +1 -0
- package/dist/formats/openai/responses/schema.d.ts +103 -0
- package/dist/formats/openai/responses/schema.d.ts.map +1 -0
- package/dist/formats/openai/responses/schema.js +71 -0
- package/dist/formats/openai/responses/schema.js.map +1 -0
- package/dist/formats/openai/responses/serialize.d.ts +10 -0
- package/dist/formats/openai/responses/serialize.d.ts.map +1 -0
- package/dist/formats/openai/responses/serialize.js +273 -0
- package/dist/formats/openai/responses/serialize.js.map +1 -0
- package/dist/formats/request-helpers.d.ts +1 -1
- package/dist/formats/request-helpers.d.ts.map +1 -1
- package/dist/formats/request-helpers.js.map +1 -1
- package/dist/formats/serialize-helpers.d.ts +1 -1
- package/dist/formats/serialize-helpers.d.ts.map +1 -1
- package/dist/formats/serialize-helpers.js +6 -3
- package/dist/formats/serialize-helpers.js.map +1 -1
- package/dist/formats/types.d.ts +2 -1
- package/dist/formats/types.d.ts.map +1 -1
- package/dist/history.d.ts +6 -2
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +2 -0
- package/dist/history.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +26 -9
- package/dist/loader.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +12 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.d.ts +44 -48
- package/dist/mock-server.d.ts.map +1 -1
- package/dist/mock-server.js +37 -85
- package/dist/mock-server.js.map +1 -1
- package/dist/route-handler.d.ts +1 -1
- package/dist/route-handler.d.ts.map +1 -1
- package/dist/route-handler.js +19 -7
- package/dist/route-handler.js.map +1 -1
- package/dist/rule-builder.d.ts +21 -0
- package/dist/rule-builder.d.ts.map +1 -0
- package/dist/rule-builder.js +58 -0
- package/dist/rule-builder.js.map +1 -0
- package/dist/rule-engine.d.ts +3 -1
- package/dist/rule-engine.d.ts.map +1 -1
- package/dist/rule-engine.js +7 -2
- package/dist/rule-engine.js.map +1 -1
- package/dist/sse-writer.d.ts +1 -1
- package/dist/sse-writer.d.ts.map +1 -1
- package/dist/types/reply.d.ts +51 -8
- package/dist/types/reply.d.ts.map +1 -1
- package/dist/types/request.d.ts +21 -6
- package/dist/types/request.d.ts.map +1 -1
- package/dist/types/rule.d.ts +65 -7
- package/dist/types/rule.d.ts.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -9
- package/.claude/skills/desloppify/SKILL.md +0 -308
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +0 -242
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +0 -248
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +0 -248
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +0 -311
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +0 -17
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +0 -255
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +0 -22
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +0 -20
- package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +0 -20
- package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050000.json +0 -286
- package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050028.json +0 -303
- package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/claude_launch_prompt.md +0 -17
- package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json +0 -297
- package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json +0 -22
- package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/reviewer_instructions.md +0 -20
- package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/session.json +0 -20
- package/.desloppify/query.json +0 -1312
- package/.desloppify/review_packet_blind.json +0 -1249
- package/.desloppify/review_packets/holistic_packet_20260315_000339.json +0 -1471
- package/.desloppify/review_packets/holistic_packet_20260315_045546.json +0 -1480
- package/.desloppify/review_packets/holistic_packet_20260315_185401.json +0 -1407
- package/.desloppify/review_packets/holistic_packet_20260315_185613.json +0 -1407
- package/.desloppify/state-typescript.json +0 -8438
- package/.desloppify/state-typescript.json.bak +0 -8432
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-1.log +0 -384
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-10.log +0 -484
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-2.log +0 -408
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-3.log +0 -416
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-4.log +0 -360
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-5.log +0 -360
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-6.log +0 -364
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-7.log +0 -428
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-8.log +0 -388
- package/.desloppify/subagents/runs/20260315_185401/logs/batch-9.log +0 -500
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-1.md +0 -83
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-10.md +0 -108
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-2.md +0 -89
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-3.md +0 -91
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-4.md +0 -77
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-5.md +0 -77
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-6.md +0 -78
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-7.md +0 -94
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-8.md +0 -84
- package/.desloppify/subagents/runs/20260315_185401/prompts/batch-9.md +0 -112
- package/.desloppify/subagents/runs/20260315_185401/results/batch-1.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-10.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-2.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-3.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-4.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-5.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-6.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-7.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-8.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/results/batch-9.raw.txt +0 -0
- package/.desloppify/subagents/runs/20260315_185401/run.log +0 -36
- package/.desloppify/subagents/runs/20260315_185401/run_summary.json +0 -156
- package/.desloppify/subagents/runs/20260315_185613/holistic_findings_merged.json +0 -741
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-1.log +0 -579
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-10.log +0 -1537
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-2.log +0 -829
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-3.log +0 -927
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-4.log +0 -429
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-5.log +0 -276
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-6.log +0 -450
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-7.log +0 -730
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-8.log +0 -698
- package/.desloppify/subagents/runs/20260315_185613/logs/batch-9.log +0 -938
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-1.md +0 -83
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-10.md +0 -108
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-2.md +0 -89
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-3.md +0 -91
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-4.md +0 -77
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-5.md +0 -77
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-6.md +0 -78
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-7.md +0 -94
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-8.md +0 -84
- package/.desloppify/subagents/runs/20260315_185613/prompts/batch-9.md +0 -112
- package/.desloppify/subagents/runs/20260315_185613/results/batch-1.raw.txt +0 -78
- package/.desloppify/subagents/runs/20260315_185613/results/batch-10.raw.txt +0 -242
- package/.desloppify/subagents/runs/20260315_185613/results/batch-2.raw.txt +0 -102
- package/.desloppify/subagents/runs/20260315_185613/results/batch-3.raw.txt +0 -94
- package/.desloppify/subagents/runs/20260315_185613/results/batch-4.raw.txt +0 -86
- package/.desloppify/subagents/runs/20260315_185613/results/batch-5.raw.txt +0 -1
- package/.desloppify/subagents/runs/20260315_185613/results/batch-6.raw.txt +0 -87
- package/.desloppify/subagents/runs/20260315_185613/results/batch-7.raw.txt +0 -1
- package/.desloppify/subagents/runs/20260315_185613/results/batch-8.raw.txt +0 -107
- package/.desloppify/subagents/runs/20260315_185613/results/batch-9.raw.txt +0 -67
- package/.desloppify/subagents/runs/20260315_185613/run.log +0 -96
- package/.desloppify/subagents/runs/20260315_185613/run_summary.json +0 -156
- package/.editorconfig +0 -12
- package/.github/dependabot.yml +0 -11
- package/.github/workflows/docs.yml +0 -46
- package/.github/workflows/test.yml +0 -40
- package/.markdownlint.jsonc +0 -11
- package/.node-version +0 -1
- package/.oxfmtrc.json +0 -9
- package/.oxlintrc.json +0 -35
- package/docs/ARCHITECTURE.md +0 -125
- package/scorecard.png +0 -0
- package/src/cli/cli.ts +0 -141
- package/src/cli/validators.ts +0 -68
- package/src/formats/anthropic/index.ts +0 -14
- package/src/formats/anthropic/parse.ts +0 -70
- package/src/formats/anthropic/schema.ts +0 -74
- package/src/formats/anthropic/serialize.ts +0 -179
- package/src/formats/openai/chat-completions/index.ts +0 -14
- package/src/formats/openai/chat-completions/parse.ts +0 -47
- package/src/formats/openai/chat-completions/schema.ts +0 -92
- package/src/formats/openai/chat-completions/serialize.ts +0 -146
- package/src/formats/openai/responses/index.ts +0 -14
- package/src/formats/openai/responses/parse.ts +0 -73
- package/src/formats/openai/responses/schema.ts +0 -86
- package/src/formats/openai/responses/serialize.ts +0 -328
- package/src/formats/request-helpers.ts +0 -56
- package/src/formats/serialize-helpers.ts +0 -43
- package/src/formats/types.ts +0 -26
- package/src/history.ts +0 -70
- package/src/index.ts +0 -46
- package/src/loader.ts +0 -246
- package/src/logger.ts +0 -70
- package/src/mock-server.ts +0 -203
- package/src/route-handler.ts +0 -144
- package/src/rule-builder.ts +0 -73
- package/src/rule-engine.ts +0 -165
- package/src/sse-writer.ts +0 -35
- package/src/types/reply.ts +0 -92
- package/src/types/request.ts +0 -56
- package/src/types/rule.ts +0 -125
- package/src/types.ts +0 -24
- package/test/cli-validators.test.ts +0 -151
- package/test/formats/anthropic.test.ts +0 -336
- package/test/formats/openai.test.ts +0 -316
- package/test/formats/parse-helpers.test.ts +0 -315
- package/test/formats/responses.test.ts +0 -380
- package/test/helpers/make-req.ts +0 -18
- package/test/history.test.ts +0 -361
- package/test/loader.test.ts +0 -333
- package/test/logger.test.ts +0 -344
- package/test/mock-server.test.ts +0 -619
- package/test/rule-engine.test.ts +0 -229
- package/tsconfig.json +0 -24
- package/tsconfig.test.json +0 -11
- package/typedoc.json +0 -9
- package/vitest.config.ts +0 -18
package/test/history.test.ts
DELETED
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
import { RequestHistory, type RecordedRequest } from "#/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
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { writeFile, mkdir, rm } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { RuleEngine } from "#/rule-engine.js";
|
|
5
|
-
import { loadRulesFromPath } from "#/loader.js";
|
|
6
|
-
import type { MockRequest } from "#/types.js";
|
|
7
|
-
import { makeReq } from "./helpers/make-req.js";
|
|
8
|
-
|
|
9
|
-
const tmpDir = join(import.meta.dirname, ".tmp-loader-test");
|
|
10
|
-
|
|
11
|
-
describe("Loader", () => {
|
|
12
|
-
let engine: RuleEngine;
|
|
13
|
-
|
|
14
|
-
beforeEach(async () => {
|
|
15
|
-
engine = new RuleEngine();
|
|
16
|
-
await mkdir(tmpDir, { recursive: true });
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterEach(async () => {
|
|
20
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("JSON5 files", () => {
|
|
24
|
-
it("loads rules from a .json5 file", async () => {
|
|
25
|
-
const rulesPath = join(tmpDir, "rules.json5");
|
|
26
|
-
await writeFile(
|
|
27
|
-
rulesPath,
|
|
28
|
-
`[
|
|
29
|
-
{
|
|
30
|
-
when: "explain recursion",
|
|
31
|
-
reply: "A function that calls itself.",
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
when: { model: "gpt-5.4", message: "hello" },
|
|
35
|
-
reply: "Hi from GPT-5.4!",
|
|
36
|
-
},
|
|
37
|
-
]`,
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
await loadRulesFromPath(rulesPath, { engine });
|
|
41
|
-
expect(engine.ruleCount).toBe(2);
|
|
42
|
-
|
|
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.");
|
|
48
|
-
|
|
49
|
-
const match2 = engine.match(
|
|
50
|
-
makeReq({ model: "gpt-5.4", lastMessage: "hello" }),
|
|
51
|
-
);
|
|
52
|
-
expect(match2).toBeDefined();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("loads regex patterns from JSON5", async () => {
|
|
56
|
-
const rulesPath = join(tmpDir, "regex.json5");
|
|
57
|
-
await writeFile(
|
|
58
|
-
rulesPath,
|
|
59
|
-
`[
|
|
60
|
-
{
|
|
61
|
-
when: "/explain (\\\\w+)/i",
|
|
62
|
-
reply: "Here is an explanation.",
|
|
63
|
-
},
|
|
64
|
-
]`,
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
await loadRulesFromPath(rulesPath, { engine });
|
|
68
|
-
const match = engine.match(
|
|
69
|
-
makeReq({ lastMessage: "explain polymorphism" }),
|
|
70
|
-
);
|
|
71
|
-
expect(match).toBeDefined();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("loads regex patterns with modern flags (d, v)", async () => {
|
|
75
|
-
const rulesPath = join(tmpDir, "flags.json5");
|
|
76
|
-
await writeFile(
|
|
77
|
-
rulesPath,
|
|
78
|
-
`[{ when: "/hello/di", reply: "With d flag" }]`,
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
await loadRulesFromPath(rulesPath, { engine });
|
|
82
|
-
expect(
|
|
83
|
-
engine.match(makeReq({ lastMessage: "hello world" })),
|
|
84
|
-
).toBeDefined();
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("loads rules with times", async () => {
|
|
88
|
-
const rulesPath = join(tmpDir, "times.json5");
|
|
89
|
-
await writeFile(
|
|
90
|
-
rulesPath,
|
|
91
|
-
`[{ when: "once", reply: "One time!", times: 1 }]`,
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
await loadRulesFromPath(rulesPath, { engine });
|
|
95
|
-
expect(engine.match(makeReq({ lastMessage: "once" }))).toBeDefined();
|
|
96
|
-
expect(engine.match(makeReq({ lastMessage: "once" }))).toBeUndefined();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("resolves $template references", async () => {
|
|
100
|
-
const rulesPath = join(tmpDir, "templates.json5");
|
|
101
|
-
await writeFile(
|
|
102
|
-
rulesPath,
|
|
103
|
-
`{
|
|
104
|
-
templates: {
|
|
105
|
-
greeting: "Hello from template!",
|
|
106
|
-
toolReply: { tools: [{ name: "search", args: { q: "test" } }] },
|
|
107
|
-
},
|
|
108
|
-
rules: [
|
|
109
|
-
{ when: "hi", reply: "$greeting" },
|
|
110
|
-
{ when: "search", reply: "$toolReply" },
|
|
111
|
-
],
|
|
112
|
-
}`,
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
await loadRulesFromPath(rulesPath, { engine });
|
|
116
|
-
expect(engine.ruleCount).toBe(2);
|
|
117
|
-
|
|
118
|
-
const match1 = engine.match(makeReq({ lastMessage: "hi" }));
|
|
119
|
-
expect(match1?.resolve).toBe("Hello from template!");
|
|
120
|
-
|
|
121
|
-
const match2 = engine.match(makeReq({ lastMessage: "search" }));
|
|
122
|
-
expect(match2?.resolve).toMatchObject({ tools: [{ name: "search" }] });
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("throws on unknown template reference", async () => {
|
|
126
|
-
const rulesPath = join(tmpDir, "bad-ref.json5");
|
|
127
|
-
await writeFile(
|
|
128
|
-
rulesPath,
|
|
129
|
-
`{
|
|
130
|
-
rules: [{ when: "hi", reply: "$nonexistent" }],
|
|
131
|
-
}`,
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
await expect(loadRulesFromPath(rulesPath, { engine })).rejects.toThrow(
|
|
135
|
-
"Unknown template",
|
|
136
|
-
);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("loads a replies sequence", async () => {
|
|
140
|
-
const rulesPath = join(tmpDir, "seq.json5");
|
|
141
|
-
await writeFile(
|
|
142
|
-
rulesPath,
|
|
143
|
-
`[{ when: "step", replies: ["First.", "Second."] }]`,
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
await loadRulesFromPath(rulesPath, { engine });
|
|
147
|
-
expect(engine.ruleCount).toBe(1);
|
|
148
|
-
|
|
149
|
-
const req = makeReq({ lastMessage: "step" });
|
|
150
|
-
const match1 = engine.match(req);
|
|
151
|
-
if (!match1) throw new Error("expected match1");
|
|
152
|
-
expect((match1.resolve as () => string)()).toBe("First.");
|
|
153
|
-
|
|
154
|
-
const match2 = engine.match(req);
|
|
155
|
-
if (!match2) throw new Error("expected match2");
|
|
156
|
-
expect((match2.resolve as () => string)()).toBe("Second.");
|
|
157
|
-
|
|
158
|
-
expect(engine.match(req)).toBeUndefined();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("loads fallback from JSON5 file", async () => {
|
|
162
|
-
const rulesPath = join(tmpDir, "fb.json5");
|
|
163
|
-
await writeFile(
|
|
164
|
-
rulesPath,
|
|
165
|
-
`{
|
|
166
|
-
fallback: "Default reply.",
|
|
167
|
-
rules: [{ when: "hi", reply: "Hello!" }],
|
|
168
|
-
}`,
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
let capturedFallback: unknown;
|
|
172
|
-
await loadRulesFromPath(rulesPath, {
|
|
173
|
-
engine,
|
|
174
|
-
setFallback: (reply) => {
|
|
175
|
-
capturedFallback = reply;
|
|
176
|
-
},
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
expect(capturedFallback).toBe("Default reply.");
|
|
180
|
-
expect(engine.ruleCount).toBe(1);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe("handler files", () => {
|
|
185
|
-
it("loads a single handler from a .ts file", async () => {
|
|
186
|
-
const handlerPath = join(tmpDir, "single.ts");
|
|
187
|
-
await writeFile(
|
|
188
|
-
handlerPath,
|
|
189
|
-
`export default {
|
|
190
|
-
match: (req) => req.lastMessage.includes("summarize"),
|
|
191
|
-
respond: (req) => "Here is a summary.",
|
|
192
|
-
};`,
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
await loadRulesFromPath(handlerPath, { engine });
|
|
196
|
-
expect(engine.ruleCount).toBe(1);
|
|
197
|
-
|
|
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");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("loads an array of handlers from a .ts file", async () => {
|
|
206
|
-
const handlerPath = join(tmpDir, "multi.ts");
|
|
207
|
-
await writeFile(
|
|
208
|
-
handlerPath,
|
|
209
|
-
`export default [
|
|
210
|
-
{
|
|
211
|
-
match: (req) => req.lastMessage.includes("hello"),
|
|
212
|
-
respond: () => "Hi!",
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
match: (req) => req.lastMessage.includes("bye"),
|
|
216
|
-
respond: () => "Goodbye!",
|
|
217
|
-
},
|
|
218
|
-
];`,
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
await loadRulesFromPath(handlerPath, { engine });
|
|
222
|
-
expect(engine.ruleCount).toBe(2);
|
|
223
|
-
|
|
224
|
-
expect(engine.match(makeReq({ lastMessage: "hello" }))).toBeDefined();
|
|
225
|
-
expect(engine.match(makeReq({ lastMessage: "bye" }))).toBeDefined();
|
|
226
|
-
expect(engine.match(makeReq({ lastMessage: "nothing" }))).toBeUndefined();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("handler respond function receives the request", async () => {
|
|
230
|
-
const handlerPath = join(tmpDir, "dynamic.ts");
|
|
231
|
-
await writeFile(
|
|
232
|
-
handlerPath,
|
|
233
|
-
`export default {
|
|
234
|
-
match: (req) => req.lastMessage.includes("echo"),
|
|
235
|
-
respond: (req) => \`Echo: \${req.lastMessage}\`,
|
|
236
|
-
};`,
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
await loadRulesFromPath(handlerPath, { engine });
|
|
240
|
-
const rule = engine.match(makeReq({ lastMessage: "echo this" }));
|
|
241
|
-
if (!rule) throw new Error("expected match for 'echo'");
|
|
242
|
-
|
|
243
|
-
const resolver = rule.resolve as (req: MockRequest) => string;
|
|
244
|
-
const result = resolver(makeReq({ lastMessage: "echo this" }));
|
|
245
|
-
expect(result).toBe("Echo: echo this");
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("throws on invalid handler file (missing match/respond)", async () => {
|
|
249
|
-
const handlerPath = join(tmpDir, "bad.ts");
|
|
250
|
-
await writeFile(
|
|
251
|
-
handlerPath,
|
|
252
|
-
`export default { mach: () => true, respond: () => "hi" };`,
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
await expect(loadRulesFromPath(handlerPath, { engine })).rejects.toThrow(
|
|
256
|
-
"Invalid handler file",
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("loads fallback from handler file", async () => {
|
|
261
|
-
const handlerPath = join(tmpDir, "with-fallback.ts");
|
|
262
|
-
await writeFile(
|
|
263
|
-
handlerPath,
|
|
264
|
-
`export const fallback = "Default reply.";
|
|
265
|
-
export default {
|
|
266
|
-
match: (req) => req.lastMessage.includes("hello"),
|
|
267
|
-
respond: () => "Hi!",
|
|
268
|
-
};`,
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
let capturedFallback: unknown;
|
|
272
|
-
await loadRulesFromPath(handlerPath, {
|
|
273
|
-
engine,
|
|
274
|
-
setFallback: (reply) => {
|
|
275
|
-
capturedFallback = reply;
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
expect(capturedFallback).toBe("Default reply.");
|
|
280
|
-
expect(engine.ruleCount).toBe(1);
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
describe("unsupported file extension", () => {
|
|
285
|
-
it("throws when loading a file with unsupported extension", async () => {
|
|
286
|
-
const yamlPath = join(tmpDir, "rules.yaml");
|
|
287
|
-
await writeFile(yamlPath, "- when: hello\n reply: Hi!");
|
|
288
|
-
await expect(loadRulesFromPath(yamlPath, { engine })).rejects.toThrow(
|
|
289
|
-
'Unsupported file extension ".yaml"',
|
|
290
|
-
);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it("skips unsupported files when scanning a directory", async () => {
|
|
294
|
-
await writeFile(
|
|
295
|
-
join(tmpDir, "good.json5"),
|
|
296
|
-
`[{ when: "hello", reply: "Hi!" }]`,
|
|
297
|
-
);
|
|
298
|
-
await writeFile(join(tmpDir, "notes.txt"), "not a rule file");
|
|
299
|
-
await loadRulesFromPath(tmpDir, { engine });
|
|
300
|
-
expect(engine.ruleCount).toBe(1);
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
describe("directory loading", () => {
|
|
305
|
-
it("loads all .json5 files from a directory", async () => {
|
|
306
|
-
await writeFile(join(tmpDir, "a.json5"), `[{ when: "aaa", reply: "A" }]`);
|
|
307
|
-
await writeFile(join(tmpDir, "b.json5"), `[{ when: "bbb", reply: "B" }]`);
|
|
308
|
-
|
|
309
|
-
await loadRulesFromPath(tmpDir, { engine });
|
|
310
|
-
expect(engine.ruleCount).toBe(2);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("loads mixed .json5 and .ts files from a directory", async () => {
|
|
314
|
-
await writeFile(
|
|
315
|
-
join(tmpDir, "rules.json5"),
|
|
316
|
-
`[{ when: "static", reply: "From JSON5" }]`,
|
|
317
|
-
);
|
|
318
|
-
await writeFile(
|
|
319
|
-
join(tmpDir, "handler.ts"),
|
|
320
|
-
`export default {
|
|
321
|
-
match: (req) => req.lastMessage.includes("dynamic"),
|
|
322
|
-
respond: () => "From handler",
|
|
323
|
-
};`,
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
await loadRulesFromPath(tmpDir, { engine });
|
|
327
|
-
expect(engine.ruleCount).toBe(2);
|
|
328
|
-
|
|
329
|
-
expect(engine.match(makeReq({ lastMessage: "static" }))).toBeDefined();
|
|
330
|
-
expect(engine.match(makeReq({ lastMessage: "dynamic" }))).toBeDefined();
|
|
331
|
-
});
|
|
332
|
-
});
|
|
333
|
-
});
|