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.
Files changed (250) hide show
  1. package/dist/cli/cli.d.ts +3 -0
  2. package/dist/cli/cli.d.ts.map +1 -0
  3. package/dist/cli/cli.js +103 -0
  4. package/dist/cli/cli.js.map +1 -0
  5. package/dist/cli/validators.d.ts +7 -0
  6. package/dist/cli/validators.d.ts.map +1 -0
  7. package/dist/cli/validators.js +53 -0
  8. package/dist/cli/validators.js.map +1 -0
  9. package/dist/formats/anthropic/index.d.ts +1 -1
  10. package/dist/formats/anthropic/index.d.ts.map +1 -1
  11. package/dist/formats/anthropic/index.js +1 -1
  12. package/dist/formats/anthropic/index.js.map +1 -1
  13. package/dist/formats/anthropic/parse.d.ts +2 -2
  14. package/dist/formats/anthropic/parse.d.ts.map +1 -1
  15. package/dist/formats/anthropic/parse.js +4 -2
  16. package/dist/formats/anthropic/parse.js.map +1 -1
  17. package/dist/formats/anthropic/schema.d.ts +1 -1
  18. package/dist/formats/anthropic/schema.d.ts.map +1 -1
  19. package/dist/formats/anthropic/schema.js +9 -4
  20. package/dist/formats/anthropic/schema.js.map +1 -1
  21. package/dist/formats/anthropic/serialize.d.ts +2 -2
  22. package/dist/formats/anthropic/serialize.d.ts.map +1 -1
  23. package/dist/formats/anthropic/serialize.js +76 -19
  24. package/dist/formats/anthropic/serialize.js.map +1 -1
  25. package/dist/formats/openai/chat-completions/index.d.ts +3 -0
  26. package/dist/formats/openai/chat-completions/index.d.ts.map +1 -0
  27. package/dist/formats/openai/chat-completions/index.js +13 -0
  28. package/dist/formats/openai/chat-completions/index.js.map +1 -0
  29. package/dist/formats/openai/chat-completions/parse.d.ts +4 -0
  30. package/dist/formats/openai/chat-completions/parse.d.ts.map +1 -0
  31. package/dist/formats/openai/chat-completions/parse.js +33 -0
  32. package/dist/formats/openai/chat-completions/parse.js.map +1 -0
  33. package/dist/formats/openai/chat-completions/schema.d.ts +93 -0
  34. package/dist/formats/openai/chat-completions/schema.d.ts.map +1 -0
  35. package/dist/formats/openai/chat-completions/schema.js +74 -0
  36. package/dist/formats/openai/chat-completions/schema.js.map +1 -0
  37. package/dist/formats/openai/chat-completions/serialize.d.ts +10 -0
  38. package/dist/formats/openai/chat-completions/serialize.d.ts.map +1 -0
  39. package/dist/formats/openai/chat-completions/serialize.js +99 -0
  40. package/dist/formats/openai/chat-completions/serialize.js.map +1 -0
  41. package/dist/formats/openai/responses/index.d.ts +3 -0
  42. package/dist/formats/openai/responses/index.d.ts.map +1 -0
  43. package/dist/formats/openai/responses/index.js +13 -0
  44. package/dist/formats/openai/responses/index.js.map +1 -0
  45. package/dist/formats/openai/responses/parse.d.ts +4 -0
  46. package/dist/formats/openai/responses/parse.d.ts.map +1 -0
  47. package/dist/formats/openai/responses/parse.js +51 -0
  48. package/dist/formats/openai/responses/parse.js.map +1 -0
  49. package/dist/formats/openai/responses/schema.d.ts +103 -0
  50. package/dist/formats/openai/responses/schema.d.ts.map +1 -0
  51. package/dist/formats/openai/responses/schema.js +71 -0
  52. package/dist/formats/openai/responses/schema.js.map +1 -0
  53. package/dist/formats/openai/responses/serialize.d.ts +10 -0
  54. package/dist/formats/openai/responses/serialize.d.ts.map +1 -0
  55. package/dist/formats/openai/responses/serialize.js +273 -0
  56. package/dist/formats/openai/responses/serialize.js.map +1 -0
  57. package/dist/formats/request-helpers.d.ts +1 -1
  58. package/dist/formats/request-helpers.d.ts.map +1 -1
  59. package/dist/formats/request-helpers.js.map +1 -1
  60. package/dist/formats/serialize-helpers.d.ts +1 -1
  61. package/dist/formats/serialize-helpers.d.ts.map +1 -1
  62. package/dist/formats/serialize-helpers.js +6 -3
  63. package/dist/formats/serialize-helpers.js.map +1 -1
  64. package/dist/formats/types.d.ts +2 -1
  65. package/dist/formats/types.d.ts.map +1 -1
  66. package/dist/history.d.ts +6 -2
  67. package/dist/history.d.ts.map +1 -1
  68. package/dist/history.js +2 -0
  69. package/dist/history.js.map +1 -1
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/loader.d.ts +1 -1
  73. package/dist/loader.d.ts.map +1 -1
  74. package/dist/loader.js +26 -9
  75. package/dist/loader.js.map +1 -1
  76. package/dist/logger.d.ts.map +1 -1
  77. package/dist/logger.js +12 -4
  78. package/dist/logger.js.map +1 -1
  79. package/dist/mock-server.d.ts +44 -48
  80. package/dist/mock-server.d.ts.map +1 -1
  81. package/dist/mock-server.js +37 -85
  82. package/dist/mock-server.js.map +1 -1
  83. package/dist/route-handler.d.ts +1 -1
  84. package/dist/route-handler.d.ts.map +1 -1
  85. package/dist/route-handler.js +19 -7
  86. package/dist/route-handler.js.map +1 -1
  87. package/dist/rule-builder.d.ts +21 -0
  88. package/dist/rule-builder.d.ts.map +1 -0
  89. package/dist/rule-builder.js +58 -0
  90. package/dist/rule-builder.js.map +1 -0
  91. package/dist/rule-engine.d.ts +3 -1
  92. package/dist/rule-engine.d.ts.map +1 -1
  93. package/dist/rule-engine.js +7 -2
  94. package/dist/rule-engine.js.map +1 -1
  95. package/dist/sse-writer.d.ts +1 -1
  96. package/dist/sse-writer.d.ts.map +1 -1
  97. package/dist/types/reply.d.ts +51 -8
  98. package/dist/types/reply.d.ts.map +1 -1
  99. package/dist/types/request.d.ts +21 -6
  100. package/dist/types/request.d.ts.map +1 -1
  101. package/dist/types/rule.d.ts +65 -7
  102. package/dist/types/rule.d.ts.map +1 -1
  103. package/dist/types.d.ts +3 -3
  104. package/dist/types.d.ts.map +1 -1
  105. package/package.json +15 -9
  106. package/.claude/skills/desloppify/SKILL.md +0 -308
  107. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +0 -242
  108. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +0 -248
  109. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +0 -248
  110. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +0 -311
  111. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +0 -17
  112. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +0 -255
  113. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +0 -22
  114. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +0 -20
  115. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +0 -20
  116. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050000.json +0 -286
  117. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050028.json +0 -303
  118. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/claude_launch_prompt.md +0 -17
  119. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json +0 -297
  120. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json +0 -22
  121. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/reviewer_instructions.md +0 -20
  122. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/session.json +0 -20
  123. package/.desloppify/query.json +0 -1312
  124. package/.desloppify/review_packet_blind.json +0 -1249
  125. package/.desloppify/review_packets/holistic_packet_20260315_000339.json +0 -1471
  126. package/.desloppify/review_packets/holistic_packet_20260315_045546.json +0 -1480
  127. package/.desloppify/review_packets/holistic_packet_20260315_185401.json +0 -1407
  128. package/.desloppify/review_packets/holistic_packet_20260315_185613.json +0 -1407
  129. package/.desloppify/state-typescript.json +0 -8438
  130. package/.desloppify/state-typescript.json.bak +0 -8432
  131. package/.desloppify/subagents/runs/20260315_185401/logs/batch-1.log +0 -384
  132. package/.desloppify/subagents/runs/20260315_185401/logs/batch-10.log +0 -484
  133. package/.desloppify/subagents/runs/20260315_185401/logs/batch-2.log +0 -408
  134. package/.desloppify/subagents/runs/20260315_185401/logs/batch-3.log +0 -416
  135. package/.desloppify/subagents/runs/20260315_185401/logs/batch-4.log +0 -360
  136. package/.desloppify/subagents/runs/20260315_185401/logs/batch-5.log +0 -360
  137. package/.desloppify/subagents/runs/20260315_185401/logs/batch-6.log +0 -364
  138. package/.desloppify/subagents/runs/20260315_185401/logs/batch-7.log +0 -428
  139. package/.desloppify/subagents/runs/20260315_185401/logs/batch-8.log +0 -388
  140. package/.desloppify/subagents/runs/20260315_185401/logs/batch-9.log +0 -500
  141. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-1.md +0 -83
  142. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-10.md +0 -108
  143. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-2.md +0 -89
  144. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-3.md +0 -91
  145. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-4.md +0 -77
  146. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-5.md +0 -77
  147. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-6.md +0 -78
  148. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-7.md +0 -94
  149. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-8.md +0 -84
  150. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-9.md +0 -112
  151. package/.desloppify/subagents/runs/20260315_185401/results/batch-1.raw.txt +0 -0
  152. package/.desloppify/subagents/runs/20260315_185401/results/batch-10.raw.txt +0 -0
  153. package/.desloppify/subagents/runs/20260315_185401/results/batch-2.raw.txt +0 -0
  154. package/.desloppify/subagents/runs/20260315_185401/results/batch-3.raw.txt +0 -0
  155. package/.desloppify/subagents/runs/20260315_185401/results/batch-4.raw.txt +0 -0
  156. package/.desloppify/subagents/runs/20260315_185401/results/batch-5.raw.txt +0 -0
  157. package/.desloppify/subagents/runs/20260315_185401/results/batch-6.raw.txt +0 -0
  158. package/.desloppify/subagents/runs/20260315_185401/results/batch-7.raw.txt +0 -0
  159. package/.desloppify/subagents/runs/20260315_185401/results/batch-8.raw.txt +0 -0
  160. package/.desloppify/subagents/runs/20260315_185401/results/batch-9.raw.txt +0 -0
  161. package/.desloppify/subagents/runs/20260315_185401/run.log +0 -36
  162. package/.desloppify/subagents/runs/20260315_185401/run_summary.json +0 -156
  163. package/.desloppify/subagents/runs/20260315_185613/holistic_findings_merged.json +0 -741
  164. package/.desloppify/subagents/runs/20260315_185613/logs/batch-1.log +0 -579
  165. package/.desloppify/subagents/runs/20260315_185613/logs/batch-10.log +0 -1537
  166. package/.desloppify/subagents/runs/20260315_185613/logs/batch-2.log +0 -829
  167. package/.desloppify/subagents/runs/20260315_185613/logs/batch-3.log +0 -927
  168. package/.desloppify/subagents/runs/20260315_185613/logs/batch-4.log +0 -429
  169. package/.desloppify/subagents/runs/20260315_185613/logs/batch-5.log +0 -276
  170. package/.desloppify/subagents/runs/20260315_185613/logs/batch-6.log +0 -450
  171. package/.desloppify/subagents/runs/20260315_185613/logs/batch-7.log +0 -730
  172. package/.desloppify/subagents/runs/20260315_185613/logs/batch-8.log +0 -698
  173. package/.desloppify/subagents/runs/20260315_185613/logs/batch-9.log +0 -938
  174. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-1.md +0 -83
  175. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-10.md +0 -108
  176. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-2.md +0 -89
  177. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-3.md +0 -91
  178. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-4.md +0 -77
  179. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-5.md +0 -77
  180. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-6.md +0 -78
  181. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-7.md +0 -94
  182. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-8.md +0 -84
  183. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-9.md +0 -112
  184. package/.desloppify/subagents/runs/20260315_185613/results/batch-1.raw.txt +0 -78
  185. package/.desloppify/subagents/runs/20260315_185613/results/batch-10.raw.txt +0 -242
  186. package/.desloppify/subagents/runs/20260315_185613/results/batch-2.raw.txt +0 -102
  187. package/.desloppify/subagents/runs/20260315_185613/results/batch-3.raw.txt +0 -94
  188. package/.desloppify/subagents/runs/20260315_185613/results/batch-4.raw.txt +0 -86
  189. package/.desloppify/subagents/runs/20260315_185613/results/batch-5.raw.txt +0 -1
  190. package/.desloppify/subagents/runs/20260315_185613/results/batch-6.raw.txt +0 -87
  191. package/.desloppify/subagents/runs/20260315_185613/results/batch-7.raw.txt +0 -1
  192. package/.desloppify/subagents/runs/20260315_185613/results/batch-8.raw.txt +0 -107
  193. package/.desloppify/subagents/runs/20260315_185613/results/batch-9.raw.txt +0 -67
  194. package/.desloppify/subagents/runs/20260315_185613/run.log +0 -96
  195. package/.desloppify/subagents/runs/20260315_185613/run_summary.json +0 -156
  196. package/.editorconfig +0 -12
  197. package/.github/dependabot.yml +0 -11
  198. package/.github/workflows/docs.yml +0 -46
  199. package/.github/workflows/test.yml +0 -40
  200. package/.markdownlint.jsonc +0 -11
  201. package/.node-version +0 -1
  202. package/.oxfmtrc.json +0 -9
  203. package/.oxlintrc.json +0 -35
  204. package/docs/ARCHITECTURE.md +0 -125
  205. package/scorecard.png +0 -0
  206. package/src/cli/cli.ts +0 -141
  207. package/src/cli/validators.ts +0 -68
  208. package/src/formats/anthropic/index.ts +0 -14
  209. package/src/formats/anthropic/parse.ts +0 -70
  210. package/src/formats/anthropic/schema.ts +0 -74
  211. package/src/formats/anthropic/serialize.ts +0 -179
  212. package/src/formats/openai/chat-completions/index.ts +0 -14
  213. package/src/formats/openai/chat-completions/parse.ts +0 -47
  214. package/src/formats/openai/chat-completions/schema.ts +0 -92
  215. package/src/formats/openai/chat-completions/serialize.ts +0 -146
  216. package/src/formats/openai/responses/index.ts +0 -14
  217. package/src/formats/openai/responses/parse.ts +0 -73
  218. package/src/formats/openai/responses/schema.ts +0 -86
  219. package/src/formats/openai/responses/serialize.ts +0 -328
  220. package/src/formats/request-helpers.ts +0 -56
  221. package/src/formats/serialize-helpers.ts +0 -43
  222. package/src/formats/types.ts +0 -26
  223. package/src/history.ts +0 -70
  224. package/src/index.ts +0 -46
  225. package/src/loader.ts +0 -246
  226. package/src/logger.ts +0 -70
  227. package/src/mock-server.ts +0 -203
  228. package/src/route-handler.ts +0 -144
  229. package/src/rule-builder.ts +0 -73
  230. package/src/rule-engine.ts +0 -165
  231. package/src/sse-writer.ts +0 -35
  232. package/src/types/reply.ts +0 -92
  233. package/src/types/request.ts +0 -56
  234. package/src/types/rule.ts +0 -125
  235. package/src/types.ts +0 -24
  236. package/test/cli-validators.test.ts +0 -151
  237. package/test/formats/anthropic.test.ts +0 -336
  238. package/test/formats/openai.test.ts +0 -316
  239. package/test/formats/parse-helpers.test.ts +0 -315
  240. package/test/formats/responses.test.ts +0 -380
  241. package/test/helpers/make-req.ts +0 -18
  242. package/test/history.test.ts +0 -361
  243. package/test/loader.test.ts +0 -333
  244. package/test/logger.test.ts +0 -344
  245. package/test/mock-server.test.ts +0 -619
  246. package/test/rule-engine.test.ts +0 -229
  247. package/tsconfig.json +0 -24
  248. package/tsconfig.test.json +0 -11
  249. package/typedoc.json +0 -9
  250. package/vitest.config.ts +0 -18
@@ -1,619 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
- import { createMock, MockServer } from "#/index.js";
3
-
4
- interface OpenAIResponse {
5
- choices: {
6
- message: { role: string; content: string };
7
- finish_reason: string;
8
- }[];
9
- error?: { type: string; message: string };
10
- }
11
-
12
- interface AnthropicResponse {
13
- content: { type: string; text?: string; thinking?: string }[];
14
- error?: { type: string; message: string };
15
- }
16
-
17
- interface ResponsesAPIResponse {
18
- output: { type: string; content: { type: string; text: string }[] }[];
19
- }
20
-
21
- describe("MockServer (end-to-end)", () => {
22
- let server: MockServer;
23
-
24
- beforeEach(async () => {
25
- server = await createMock({ port: 0 });
26
- });
27
-
28
- afterEach(async () => {
29
- await server.stop();
30
- });
31
-
32
- async function post(path: string, body: unknown): Promise<Response> {
33
- return fetch(`${server.url}${path}`, {
34
- method: "POST",
35
- headers: { "Content-Type": "application/json" },
36
- body: JSON.stringify(body),
37
- });
38
- }
39
-
40
- async function postOpenAI(
41
- content: string,
42
- opts: Record<string, unknown> = {},
43
- ): Promise<OpenAIResponse> {
44
- const res = await post("/v1/chat/completions", {
45
- model: "gpt-5.4",
46
- messages: [{ role: "user", content }],
47
- stream: false,
48
- ...opts,
49
- });
50
- return res.json() as Promise<OpenAIResponse>;
51
- }
52
-
53
- async function postAnthropic(
54
- content: string,
55
- opts: Record<string, unknown> = {},
56
- ): Promise<AnthropicResponse> {
57
- const res = await post("/v1/messages", {
58
- model: "claude-sonnet-4-6",
59
- messages: [{ role: "user", content }],
60
- max_tokens: 100,
61
- stream: false,
62
- ...opts,
63
- });
64
- return res.json() as Promise<AnthropicResponse>;
65
- }
66
-
67
- async function postResponses(
68
- input: string,
69
- opts: Record<string, unknown> = {},
70
- ): Promise<ResponsesAPIResponse> {
71
- const res = await post("/v1/responses", {
72
- model: "codex-mini",
73
- input,
74
- stream: false,
75
- ...opts,
76
- });
77
- return res.json() as Promise<ResponsesAPIResponse>;
78
- }
79
-
80
- async function readSSE(res: Response): Promise<string[]> {
81
- const text = await res.text();
82
- return text
83
- .split("\n")
84
- .filter((line) => line.startsWith("data: "))
85
- .map((line) => line.slice(6));
86
- }
87
-
88
- describe("shared rules across endpoints", () => {
89
- it("same rule matches on all three endpoints", async () => {
90
- server.when("hello").reply("Hi there!");
91
-
92
- const openai = await postOpenAI("hello");
93
- expect(openai.choices[0]!.message.content).toBe("Hi there!");
94
-
95
- const anthropic = await postAnthropic("hello");
96
- expect(anthropic.content[0]!.text).toBe("Hi there!");
97
-
98
- const responses = await postResponses("hello");
99
- expect(responses.output[0]!.content[0]!.text).toBe("Hi there!");
100
- });
101
- });
102
-
103
- describe("OpenAI streaming", () => {
104
- it("streams SSE chunks ending with [DONE]", async () => {
105
- server.when("hello").reply("Hi!");
106
- const res = await post("/v1/chat/completions", {
107
- model: "gpt-5.4",
108
- messages: [{ role: "user", content: "hello" }],
109
- });
110
- expect(res.headers.get("content-type")).toBe("text/event-stream");
111
- const data = await readSSE(res);
112
- expect(data.at(-1)).toBe("[DONE]");
113
-
114
- const contentChunk = JSON.parse(data[1]!);
115
- expect(contentChunk.choices[0].delta.content).toBe("Hi!");
116
- });
117
- });
118
-
119
- describe("Anthropic streaming", () => {
120
- it("streams named SSE events", async () => {
121
- server.when("hello").reply("Hi!");
122
- const res = await post("/v1/messages", {
123
- model: "claude-sonnet-4-6",
124
- messages: [{ role: "user", content: "hello" }],
125
- max_tokens: 100,
126
- });
127
- const text = await res.text();
128
- expect(text).toContain("event: message_start");
129
- expect(text).toContain("event: content_block_delta");
130
- expect(text).toContain("event: message_stop");
131
- });
132
- });
133
-
134
- describe("Responses API streaming", () => {
135
- it("streams response events", async () => {
136
- server.when("hello").reply("Hi!");
137
- const res = await post("/v1/responses", {
138
- model: "codex-mini",
139
- input: "hello",
140
- });
141
- const data = await readSSE(res);
142
- const types = data.map((d) => JSON.parse(d).type);
143
- expect(types).toContain("response.created");
144
- expect(types).toContain("response.output_text.delta");
145
- expect(types).toContain("response.completed");
146
- });
147
- });
148
-
149
- describe("regex match", () => {
150
- it("matches regex against last user message", async () => {
151
- server.when(/explain (\w+)/i).reply("Here is an explanation.");
152
- const json = await postOpenAI("Can you explain recursion?");
153
- expect(json.choices[0]!.message.content).toBe("Here is an explanation.");
154
- });
155
- });
156
-
157
- describe("dynamic resolver", () => {
158
- it("calls resolver function with MockRequest", async () => {
159
- server.when("hello").reply((req) => `You said: ${req.lastMessage}`);
160
- const json = await postOpenAI("hello");
161
- expect(json.choices[0]!.message.content).toBe("You said: hello");
162
- });
163
- });
164
-
165
- describe("async resolver", () => {
166
- it("supports async resolver functions", async () => {
167
- server.when("async").reply(async () => {
168
- return { text: "async result" };
169
- });
170
- const json = await postOpenAI("async");
171
- expect(json.choices[0]!.message.content).toBe("async result");
172
- });
173
- });
174
-
175
- describe("structured reply (text + reasoning)", () => {
176
- it("sends text and reasoning in Anthropic format", async () => {
177
- server.when("think").reply({ text: "42", reasoning: "Deep thought..." });
178
- const json = await postAnthropic("think");
179
- expect(json.content[0]!.type).toBe("thinking");
180
- expect(json.content[0]!.thinking).toBe("Deep thought...");
181
- expect(json.content[1]!.type).toBe("text");
182
- expect(json.content[1]!.text).toBe("42");
183
- });
184
- });
185
-
186
- describe("tool call reply", () => {
187
- it("returns tool calls in OpenAI format", async () => {
188
- server.when("read").reply({
189
- tools: [{ name: "read_file", args: { path: "/tmp/foo" } }],
190
- });
191
- const json = await postOpenAI("read the file");
192
- expect(json.choices[0]!.finish_reason).toBe("tool_calls");
193
- });
194
- });
195
-
196
- describe("times()", () => {
197
- it("rule is consumed after N matches", async () => {
198
- server.when("once").reply("First time!").times(1);
199
- server.fallback("Fallback.");
200
-
201
- const j1 = await postOpenAI("once");
202
- expect(j1.choices[0]!.message.content).toBe("First time!");
203
-
204
- const j2 = await postOpenAI("once");
205
- expect(j2.choices[0]!.message.content).toBe("Fallback.");
206
- });
207
- });
208
-
209
- describe("fallback", () => {
210
- it("uses fallback when no rule matches", async () => {
211
- server.fallback("I don't understand.");
212
- const json = await postOpenAI("something random");
213
- expect(json.choices[0]!.message.content).toBe("I don't understand.");
214
- });
215
- });
216
-
217
- describe("history", () => {
218
- it("records requests with matched rule info", async () => {
219
- server.when("hello").reply("Hi!");
220
- await postOpenAI("hello");
221
-
222
- expect(server.history.count()).toBe(1);
223
- expect(server.history.last()?.request.lastMessage).toBe("hello");
224
- expect(server.history.first()?.rule).toBe('"hello"');
225
- });
226
-
227
- it("captures request headers and path", async () => {
228
- server.when("hello").reply("Hi!");
229
- await fetch(`${server.url}/v1/chat/completions`, {
230
- method: "POST",
231
- headers: {
232
- "Content-Type": "application/json",
233
- "X-Custom": "test-value",
234
- },
235
- body: JSON.stringify({
236
- model: "gpt-5.4",
237
- messages: [{ role: "user", content: "hello" }],
238
- stream: false,
239
- }),
240
- });
241
-
242
- const entry = server.history.last()!;
243
- expect(entry.request.path).toBe("/v1/chat/completions");
244
- expect(entry.request.headers["x-custom"]).toBe("test-value");
245
- });
246
- });
247
-
248
- describe("request metadata in predicates", () => {
249
- it("matches on headers", async () => {
250
- server
251
- .when({ predicate: (req) => req.headers["x-team"] === "alpha" })
252
- .reply("Alpha team!");
253
- server.when("hello").reply("Default");
254
-
255
- const res = await fetch(`${server.url}/v1/chat/completions`, {
256
- method: "POST",
257
- headers: { "Content-Type": "application/json", "X-Team": "alpha" },
258
- body: JSON.stringify({
259
- model: "gpt-5.4",
260
- messages: [{ role: "user", content: "hello" }],
261
- stream: false,
262
- }),
263
- });
264
-
265
- expect(await res.json()).toMatchObject({
266
- choices: [{ message: { content: "Alpha team!" } }],
267
- });
268
- });
269
- });
270
-
271
- describe("rules", () => {
272
- it("returns summaries of registered rules", () => {
273
- server.when("hello").reply("Hi!");
274
- server.when(/bye/i).reply("Goodbye!").times(3);
275
-
276
- expect(server.rules).toEqual([
277
- { description: '"hello"', remaining: Infinity },
278
- { description: "/bye/i", remaining: 3 },
279
- ]);
280
- });
281
- });
282
-
283
- describe("isDone()", () => {
284
- it("returns true when all limited rules consumed", async () => {
285
- server.when("hello").reply("Hi!").times(1);
286
- expect(server.isDone()).toBe(false);
287
-
288
- await postOpenAI("hello");
289
- expect(server.isDone()).toBe(true);
290
- });
291
- });
292
-
293
- describe("replySequence()", () => {
294
- it("advances through the sequence and then stops matching", async () => {
295
- server.when("step").replySequence(["First.", "Second.", "Third."]);
296
- server.fallback("Done.");
297
-
298
- const results: string[] = [];
299
- for (let i = 0; i < 4; i++) {
300
- const json = await postOpenAI("step");
301
- results.push(json.choices[0]!.message.content);
302
- }
303
-
304
- expect(results).toEqual(["First.", "Second.", "Third.", "Done."]);
305
- });
306
-
307
- it("supports per-step options", async () => {
308
- server
309
- .when("step")
310
- .replySequence([
311
- "Plain.",
312
- { reply: { text: "With options." }, options: { chunkSize: 5 } },
313
- ]);
314
-
315
- const json = await postOpenAI("step");
316
- expect(json.choices[0]!.message.content).toBe("Plain.");
317
- });
318
-
319
- it("throws on empty sequence", () => {
320
- expect(() => server.when("step").replySequence([])).toThrow(
321
- "Sequence requires at least one entry",
322
- );
323
- });
324
- });
325
-
326
- describe("request validation", () => {
327
- it("returns 400 for invalid request body", async () => {
328
- const res = await post("/v1/chat/completions", { invalid: true });
329
- expect(res.status).toBe(400);
330
- const json = (await res.json()) as OpenAIResponse;
331
- expect(json.error?.type).toBe("invalid_request_error");
332
- });
333
-
334
- it("returns 400 for Anthropic request missing max_tokens", async () => {
335
- const res = await post("/v1/messages", {
336
- model: "claude-sonnet-4-6",
337
- messages: [{ role: "user", content: "hi" }],
338
- });
339
- expect(res.status).toBe(400);
340
- });
341
- });
342
-
343
- describe("history fluent API", () => {
344
- it("at() returns entry by index", async () => {
345
- server.when("a").reply("A");
346
- server.when("b").reply("B");
347
- await postOpenAI("a");
348
- await postOpenAI("b");
349
-
350
- expect(server.history.at(0)?.request.lastMessage).toBe("a");
351
- expect(server.history.at(1)?.request.lastMessage).toBe("b");
352
- expect(server.history.at(99)).toBeUndefined();
353
- });
354
-
355
- it("all returns readonly array of entries", async () => {
356
- server.when("hello").reply("Hi!");
357
- await postOpenAI("hello");
358
-
359
- expect(server.history.all).toHaveLength(1);
360
- expect(server.history.all[0]?.request.lastMessage).toBe("hello");
361
- });
362
-
363
- it("is iterable with for...of", async () => {
364
- server.when("hello").reply("Hi!");
365
- await postOpenAI("hello");
366
-
367
- const messages: string[] = [];
368
- for (const entry of server.history) {
369
- messages.push(entry.request.lastMessage);
370
- }
371
- expect(messages).toEqual(["hello"]);
372
- });
373
- });
374
-
375
- describe("reset()", () => {
376
- it("clears rules and history", async () => {
377
- server.when("hello").reply("Hi!");
378
- await postOpenAI("hello");
379
- expect(server.history.count()).toBe(1);
380
-
381
- server.reset();
382
- expect(server.history.count()).toBe(0);
383
- expect(server.ruleCount).toBe(0);
384
- });
385
- });
386
-
387
- describe("model match", () => {
388
- it("matches on model name", async () => {
389
- server.when({ model: "gpt-5.4" }).reply("I'm GPT-5.4.");
390
- server.when({ model: /claude/ }).reply("I'm Claude.");
391
-
392
- const openai = await postOpenAI("who are you");
393
- expect(openai.choices[0]!.message.content).toBe("I'm GPT-5.4.");
394
-
395
- const anthropic = await postAnthropic("who are you");
396
- expect(anthropic.content[0]!.text).toBe("I'm Claude.");
397
- });
398
- });
399
-
400
- describe("url property", () => {
401
- it("returns the base URL", () => {
402
- expect(server.url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
403
- });
404
- });
405
-
406
- describe("error injection", () => {
407
- it("nextError returns a one-shot error response", async () => {
408
- server.nextError(429, "Rate limited", "rate_limit_error");
409
- server.when("hello").reply("Hi!");
410
-
411
- const r1 = await post("/v1/chat/completions", {
412
- model: "gpt-5.4",
413
- messages: [{ role: "user", content: "hello" }],
414
- stream: false,
415
- });
416
- expect(r1.status).toBe(429);
417
- const err = (await r1.json()) as OpenAIResponse;
418
- expect(err.error?.message).toBe("Rate limited");
419
-
420
- const r2 = await post("/v1/chat/completions", {
421
- model: "gpt-5.4",
422
- messages: [{ role: "user", content: "hello" }],
423
- stream: false,
424
- });
425
- expect(r2.status).toBe(200);
426
- });
427
-
428
- it("error reply works as a normal rule", async () => {
429
- server
430
- .when("fail")
431
- .reply({ error: { status: 500, message: "Internal error" } });
432
- server.when("hello").reply("Hi!");
433
-
434
- const r1 = await post("/v1/chat/completions", {
435
- model: "gpt-5.4",
436
- messages: [{ role: "user", content: "fail please" }],
437
- stream: false,
438
- });
439
- expect(r1.status).toBe(500);
440
-
441
- const r2 = await post("/v1/chat/completions", {
442
- model: "gpt-5.4",
443
- messages: [{ role: "user", content: "hello" }],
444
- stream: false,
445
- });
446
- expect(r2.status).toBe(200);
447
- });
448
- });
449
-
450
- describe("chunkSize", () => {
451
- it("splits text into multiple SSE delta chunks", async () => {
452
- server.when("hello").reply("Hello, world!", { chunkSize: 5 });
453
- const res = await post("/v1/chat/completions", {
454
- model: "gpt-5.4",
455
- messages: [{ role: "user", content: "hello" }],
456
- });
457
- const data = await readSSE(res);
458
- const contentDeltas = data
459
- .filter((d) => d !== "[DONE]")
460
- .map((d) => JSON.parse(d))
461
- .filter(
462
- (d: { choices?: { delta?: { content?: string } }[] }) =>
463
- d.choices?.[0]?.delta?.content !== undefined,
464
- )
465
- .map(
466
- (d: { choices: { delta: { content: string } }[] }) =>
467
- d.choices[0]!.delta.content,
468
- );
469
- expect(contentDeltas.length).toBe(3);
470
- expect(contentDeltas.join("")).toBe("Hello, world!");
471
- });
472
- });
473
-
474
- describe("whenTool()", () => {
475
- it("matches when request has the specified tool", async () => {
476
- server.whenTool("get_weather").reply("Weather tool detected!");
477
- server.fallback("No match.");
478
-
479
- const j1 = await postOpenAI("what's the weather?", {
480
- tools: [
481
- {
482
- type: "function",
483
- function: { name: "get_weather", parameters: {} },
484
- },
485
- ],
486
- });
487
- expect(j1.choices[0]!.message.content).toBe("Weather tool detected!");
488
-
489
- const j2 = await postOpenAI("what's the weather?");
490
- expect(j2.choices[0]!.message.content).toBe("No match.");
491
- });
492
- });
493
-
494
- describe("whenToolResult()", () => {
495
- it("matches when request has a tool result with the specified id", async () => {
496
- server.whenToolResult("call_abc").reply("Got the tool result!");
497
- server.fallback("No match.");
498
-
499
- const json = await postOpenAI("use the tool", {
500
- messages: [
501
- { role: "user", content: "use the tool" },
502
- {
503
- role: "assistant",
504
- content: null,
505
- tool_calls: [
506
- {
507
- id: "call_abc",
508
- type: "function",
509
- function: { name: "test", arguments: "{}" },
510
- },
511
- ],
512
- },
513
- { role: "tool", tool_call_id: "call_abc", content: "result data" },
514
- ],
515
- });
516
- expect(json.choices[0]!.message.content).toBe("Got the tool result!");
517
- });
518
- });
519
-
520
- describe(".first()", () => {
521
- it("moves a rule to the front of the match list", async () => {
522
- server.when("hello").reply("First rule");
523
- server.when("hello").reply("Second rule").first();
524
-
525
- const json = await postOpenAI("hello");
526
- expect(json.choices[0]!.message.content).toBe("Second rule");
527
- });
528
- });
529
-
530
- describe(".times() chaining", () => {
531
- it("returns RuleHandle for chaining with .first()", async () => {
532
- server.when("hello").reply("Normal");
533
- server.when("hello").reply("Priority one-shot").times(1).first();
534
- server.fallback("Fallback.");
535
-
536
- const j1 = await postOpenAI("hello");
537
- expect(j1.choices[0]!.message.content).toBe("Priority one-shot");
538
-
539
- const j2 = await postOpenAI("hello");
540
- expect(j2.choices[0]!.message.content).toBe("Normal");
541
- });
542
- });
543
-
544
- describe("resolver error handling", () => {
545
- it("falls back when resolver throws", async () => {
546
- server.when("boom").reply(() => {
547
- throw new Error("resolver failed");
548
- });
549
- server.fallback("Safe fallback.");
550
-
551
- const json = await postOpenAI("boom");
552
- expect(json.choices[0]!.message.content).toBe("Safe fallback.");
553
- });
554
- });
555
-
556
- describe("async dispose", () => {
557
- it("stops the server via Symbol.asyncDispose", async () => {
558
- const s = await createMock({ port: 0 });
559
- const url = s.url;
560
- await s[Symbol.asyncDispose]();
561
- await expect(fetch(`${url}/v1/chat/completions`)).rejects.toThrow();
562
- });
563
- });
564
-
565
- describe("logging", () => {
566
- it("exercises all log levels including warn and error paths", async () => {
567
- const s = await createMock({ port: 0, logLevel: "debug" });
568
-
569
- s.when("test").reply("ok");
570
- await fetch(`${s.url}/v1/chat/completions`, {
571
- method: "POST",
572
- headers: { "Content-Type": "application/json" },
573
- body: JSON.stringify({
574
- model: "gpt-5.4",
575
- messages: [{ role: "user", content: "test" }],
576
- stream: false,
577
- }),
578
- });
579
-
580
- await fetch(`${s.url}/v1/chat/completions`, {
581
- method: "POST",
582
- headers: { "Content-Type": "application/json" },
583
- body: JSON.stringify({
584
- model: "gpt-5.4",
585
- messages: [{ role: "user", content: "unmatched" }],
586
- stream: false,
587
- }),
588
- });
589
-
590
- s.when("throw").reply(() => {
591
- throw new Error("boom");
592
- });
593
- await fetch(`${s.url}/v1/chat/completions`, {
594
- method: "POST",
595
- headers: { "Content-Type": "application/json" },
596
- body: JSON.stringify({
597
- model: "gpt-5.4",
598
- messages: [{ role: "user", content: "throw" }],
599
- stream: false,
600
- }),
601
- });
602
-
603
- await s.stop();
604
- });
605
- });
606
-
607
- describe("streaming with latency", () => {
608
- it("streams with latency between chunks", async () => {
609
- server.when("hello").reply("Hi!", { latency: 5 });
610
- const res = await post("/v1/chat/completions", {
611
- model: "gpt-5.4",
612
- messages: [{ role: "user", content: "hello" }],
613
- });
614
- expect(res.headers.get("content-type")).toBe("text/event-stream");
615
- const data = await readSSE(res);
616
- expect(data.at(-1)).toBe("[DONE]");
617
- });
618
- });
619
- });