llm-mock-server 1.0.6 → 1.0.8

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 (251) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/cli.d.ts +3 -0
  3. package/dist/cli/cli.d.ts.map +1 -0
  4. package/dist/cli/cli.js +103 -0
  5. package/dist/cli/cli.js.map +1 -0
  6. package/dist/cli/validators.d.ts +7 -0
  7. package/dist/cli/validators.d.ts.map +1 -0
  8. package/dist/cli/validators.js +53 -0
  9. package/dist/cli/validators.js.map +1 -0
  10. package/dist/formats/anthropic/index.d.ts +1 -1
  11. package/dist/formats/anthropic/index.d.ts.map +1 -1
  12. package/dist/formats/anthropic/index.js +1 -1
  13. package/dist/formats/anthropic/index.js.map +1 -1
  14. package/dist/formats/anthropic/parse.d.ts +2 -2
  15. package/dist/formats/anthropic/parse.d.ts.map +1 -1
  16. package/dist/formats/anthropic/parse.js +4 -2
  17. package/dist/formats/anthropic/parse.js.map +1 -1
  18. package/dist/formats/anthropic/schema.d.ts +1 -1
  19. package/dist/formats/anthropic/schema.d.ts.map +1 -1
  20. package/dist/formats/anthropic/schema.js +9 -4
  21. package/dist/formats/anthropic/schema.js.map +1 -1
  22. package/dist/formats/anthropic/serialize.d.ts +2 -2
  23. package/dist/formats/anthropic/serialize.d.ts.map +1 -1
  24. package/dist/formats/anthropic/serialize.js +76 -19
  25. package/dist/formats/anthropic/serialize.js.map +1 -1
  26. package/dist/formats/openai/chat-completions/index.d.ts +3 -0
  27. package/dist/formats/openai/chat-completions/index.d.ts.map +1 -0
  28. package/dist/formats/openai/chat-completions/index.js +13 -0
  29. package/dist/formats/openai/chat-completions/index.js.map +1 -0
  30. package/dist/formats/openai/chat-completions/parse.d.ts +4 -0
  31. package/dist/formats/openai/chat-completions/parse.d.ts.map +1 -0
  32. package/dist/formats/openai/chat-completions/parse.js +33 -0
  33. package/dist/formats/openai/chat-completions/parse.js.map +1 -0
  34. package/dist/formats/openai/chat-completions/schema.d.ts +93 -0
  35. package/dist/formats/openai/chat-completions/schema.d.ts.map +1 -0
  36. package/dist/formats/openai/chat-completions/schema.js +74 -0
  37. package/dist/formats/openai/chat-completions/schema.js.map +1 -0
  38. package/dist/formats/openai/chat-completions/serialize.d.ts +10 -0
  39. package/dist/formats/openai/chat-completions/serialize.d.ts.map +1 -0
  40. package/dist/formats/openai/chat-completions/serialize.js +99 -0
  41. package/dist/formats/openai/chat-completions/serialize.js.map +1 -0
  42. package/dist/formats/openai/responses/index.d.ts +3 -0
  43. package/dist/formats/openai/responses/index.d.ts.map +1 -0
  44. package/dist/formats/openai/responses/index.js +13 -0
  45. package/dist/formats/openai/responses/index.js.map +1 -0
  46. package/dist/formats/openai/responses/parse.d.ts +4 -0
  47. package/dist/formats/openai/responses/parse.d.ts.map +1 -0
  48. package/dist/formats/openai/responses/parse.js +51 -0
  49. package/dist/formats/openai/responses/parse.js.map +1 -0
  50. package/dist/formats/openai/responses/schema.d.ts +103 -0
  51. package/dist/formats/openai/responses/schema.d.ts.map +1 -0
  52. package/dist/formats/openai/responses/schema.js +71 -0
  53. package/dist/formats/openai/responses/schema.js.map +1 -0
  54. package/dist/formats/openai/responses/serialize.d.ts +10 -0
  55. package/dist/formats/openai/responses/serialize.d.ts.map +1 -0
  56. package/dist/formats/openai/responses/serialize.js +273 -0
  57. package/dist/formats/openai/responses/serialize.js.map +1 -0
  58. package/dist/formats/request-helpers.d.ts +1 -1
  59. package/dist/formats/request-helpers.d.ts.map +1 -1
  60. package/dist/formats/request-helpers.js.map +1 -1
  61. package/dist/formats/serialize-helpers.d.ts +1 -1
  62. package/dist/formats/serialize-helpers.d.ts.map +1 -1
  63. package/dist/formats/serialize-helpers.js +6 -3
  64. package/dist/formats/serialize-helpers.js.map +1 -1
  65. package/dist/formats/types.d.ts +2 -1
  66. package/dist/formats/types.d.ts.map +1 -1
  67. package/dist/history.d.ts +6 -2
  68. package/dist/history.d.ts.map +1 -1
  69. package/dist/history.js +2 -0
  70. package/dist/history.js.map +1 -1
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/loader.d.ts +1 -1
  74. package/dist/loader.d.ts.map +1 -1
  75. package/dist/loader.js +26 -9
  76. package/dist/loader.js.map +1 -1
  77. package/dist/logger.d.ts.map +1 -1
  78. package/dist/logger.js +12 -4
  79. package/dist/logger.js.map +1 -1
  80. package/dist/mock-server.d.ts +44 -48
  81. package/dist/mock-server.d.ts.map +1 -1
  82. package/dist/mock-server.js +37 -85
  83. package/dist/mock-server.js.map +1 -1
  84. package/dist/route-handler.d.ts +1 -1
  85. package/dist/route-handler.d.ts.map +1 -1
  86. package/dist/route-handler.js +19 -7
  87. package/dist/route-handler.js.map +1 -1
  88. package/dist/rule-builder.d.ts +21 -0
  89. package/dist/rule-builder.d.ts.map +1 -0
  90. package/dist/rule-builder.js +58 -0
  91. package/dist/rule-builder.js.map +1 -0
  92. package/dist/rule-engine.d.ts +3 -1
  93. package/dist/rule-engine.d.ts.map +1 -1
  94. package/dist/rule-engine.js +7 -2
  95. package/dist/rule-engine.js.map +1 -1
  96. package/dist/sse-writer.d.ts +1 -1
  97. package/dist/sse-writer.d.ts.map +1 -1
  98. package/dist/types/reply.d.ts +51 -8
  99. package/dist/types/reply.d.ts.map +1 -1
  100. package/dist/types/request.d.ts +21 -6
  101. package/dist/types/request.d.ts.map +1 -1
  102. package/dist/types/rule.d.ts +65 -7
  103. package/dist/types/rule.d.ts.map +1 -1
  104. package/dist/types.d.ts +3 -3
  105. package/dist/types.d.ts.map +1 -1
  106. package/package.json +15 -9
  107. package/.claude/skills/desloppify/SKILL.md +0 -308
  108. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000801.json +0 -242
  109. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000905.json +0 -248
  110. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000917.json +0 -248
  111. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/canonical_import_20260315_000950.json +0 -311
  112. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/claude_launch_prompt.md +0 -17
  113. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.json +0 -255
  114. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/review_result.template.json +0 -22
  115. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/reviewer_instructions.md +0 -20
  116. package/.desloppify/external_review_sessions/ext_20260315_000339_a6cdc3e6/session.json +0 -20
  117. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050000.json +0 -286
  118. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/canonical_import_20260315_050028.json +0 -303
  119. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/claude_launch_prompt.md +0 -17
  120. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.json +0 -297
  121. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/review_result.template.json +0 -22
  122. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/reviewer_instructions.md +0 -20
  123. package/.desloppify/external_review_sessions/ext_20260315_045546_0587ea3b/session.json +0 -20
  124. package/.desloppify/query.json +0 -1312
  125. package/.desloppify/review_packet_blind.json +0 -1249
  126. package/.desloppify/review_packets/holistic_packet_20260315_000339.json +0 -1471
  127. package/.desloppify/review_packets/holistic_packet_20260315_045546.json +0 -1480
  128. package/.desloppify/review_packets/holistic_packet_20260315_185401.json +0 -1407
  129. package/.desloppify/review_packets/holistic_packet_20260315_185613.json +0 -1407
  130. package/.desloppify/state-typescript.json +0 -8438
  131. package/.desloppify/state-typescript.json.bak +0 -8432
  132. package/.desloppify/subagents/runs/20260315_185401/logs/batch-1.log +0 -384
  133. package/.desloppify/subagents/runs/20260315_185401/logs/batch-10.log +0 -484
  134. package/.desloppify/subagents/runs/20260315_185401/logs/batch-2.log +0 -408
  135. package/.desloppify/subagents/runs/20260315_185401/logs/batch-3.log +0 -416
  136. package/.desloppify/subagents/runs/20260315_185401/logs/batch-4.log +0 -360
  137. package/.desloppify/subagents/runs/20260315_185401/logs/batch-5.log +0 -360
  138. package/.desloppify/subagents/runs/20260315_185401/logs/batch-6.log +0 -364
  139. package/.desloppify/subagents/runs/20260315_185401/logs/batch-7.log +0 -428
  140. package/.desloppify/subagents/runs/20260315_185401/logs/batch-8.log +0 -388
  141. package/.desloppify/subagents/runs/20260315_185401/logs/batch-9.log +0 -500
  142. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-1.md +0 -83
  143. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-10.md +0 -108
  144. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-2.md +0 -89
  145. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-3.md +0 -91
  146. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-4.md +0 -77
  147. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-5.md +0 -77
  148. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-6.md +0 -78
  149. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-7.md +0 -94
  150. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-8.md +0 -84
  151. package/.desloppify/subagents/runs/20260315_185401/prompts/batch-9.md +0 -112
  152. package/.desloppify/subagents/runs/20260315_185401/results/batch-1.raw.txt +0 -0
  153. package/.desloppify/subagents/runs/20260315_185401/results/batch-10.raw.txt +0 -0
  154. package/.desloppify/subagents/runs/20260315_185401/results/batch-2.raw.txt +0 -0
  155. package/.desloppify/subagents/runs/20260315_185401/results/batch-3.raw.txt +0 -0
  156. package/.desloppify/subagents/runs/20260315_185401/results/batch-4.raw.txt +0 -0
  157. package/.desloppify/subagents/runs/20260315_185401/results/batch-5.raw.txt +0 -0
  158. package/.desloppify/subagents/runs/20260315_185401/results/batch-6.raw.txt +0 -0
  159. package/.desloppify/subagents/runs/20260315_185401/results/batch-7.raw.txt +0 -0
  160. package/.desloppify/subagents/runs/20260315_185401/results/batch-8.raw.txt +0 -0
  161. package/.desloppify/subagents/runs/20260315_185401/results/batch-9.raw.txt +0 -0
  162. package/.desloppify/subagents/runs/20260315_185401/run.log +0 -36
  163. package/.desloppify/subagents/runs/20260315_185401/run_summary.json +0 -156
  164. package/.desloppify/subagents/runs/20260315_185613/holistic_findings_merged.json +0 -741
  165. package/.desloppify/subagents/runs/20260315_185613/logs/batch-1.log +0 -579
  166. package/.desloppify/subagents/runs/20260315_185613/logs/batch-10.log +0 -1537
  167. package/.desloppify/subagents/runs/20260315_185613/logs/batch-2.log +0 -829
  168. package/.desloppify/subagents/runs/20260315_185613/logs/batch-3.log +0 -927
  169. package/.desloppify/subagents/runs/20260315_185613/logs/batch-4.log +0 -429
  170. package/.desloppify/subagents/runs/20260315_185613/logs/batch-5.log +0 -276
  171. package/.desloppify/subagents/runs/20260315_185613/logs/batch-6.log +0 -450
  172. package/.desloppify/subagents/runs/20260315_185613/logs/batch-7.log +0 -730
  173. package/.desloppify/subagents/runs/20260315_185613/logs/batch-8.log +0 -698
  174. package/.desloppify/subagents/runs/20260315_185613/logs/batch-9.log +0 -938
  175. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-1.md +0 -83
  176. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-10.md +0 -108
  177. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-2.md +0 -89
  178. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-3.md +0 -91
  179. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-4.md +0 -77
  180. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-5.md +0 -77
  181. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-6.md +0 -78
  182. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-7.md +0 -94
  183. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-8.md +0 -84
  184. package/.desloppify/subagents/runs/20260315_185613/prompts/batch-9.md +0 -112
  185. package/.desloppify/subagents/runs/20260315_185613/results/batch-1.raw.txt +0 -78
  186. package/.desloppify/subagents/runs/20260315_185613/results/batch-10.raw.txt +0 -242
  187. package/.desloppify/subagents/runs/20260315_185613/results/batch-2.raw.txt +0 -102
  188. package/.desloppify/subagents/runs/20260315_185613/results/batch-3.raw.txt +0 -94
  189. package/.desloppify/subagents/runs/20260315_185613/results/batch-4.raw.txt +0 -86
  190. package/.desloppify/subagents/runs/20260315_185613/results/batch-5.raw.txt +0 -1
  191. package/.desloppify/subagents/runs/20260315_185613/results/batch-6.raw.txt +0 -87
  192. package/.desloppify/subagents/runs/20260315_185613/results/batch-7.raw.txt +0 -1
  193. package/.desloppify/subagents/runs/20260315_185613/results/batch-8.raw.txt +0 -107
  194. package/.desloppify/subagents/runs/20260315_185613/results/batch-9.raw.txt +0 -67
  195. package/.desloppify/subagents/runs/20260315_185613/run.log +0 -96
  196. package/.desloppify/subagents/runs/20260315_185613/run_summary.json +0 -156
  197. package/.editorconfig +0 -12
  198. package/.github/dependabot.yml +0 -11
  199. package/.github/workflows/docs.yml +0 -46
  200. package/.github/workflows/test.yml +0 -40
  201. package/.markdownlint.jsonc +0 -11
  202. package/.node-version +0 -1
  203. package/.oxfmtrc.json +0 -9
  204. package/.oxlintrc.json +0 -35
  205. package/docs/ARCHITECTURE.md +0 -125
  206. package/scorecard.png +0 -0
  207. package/src/cli/cli.ts +0 -141
  208. package/src/cli/validators.ts +0 -68
  209. package/src/formats/anthropic/index.ts +0 -14
  210. package/src/formats/anthropic/parse.ts +0 -70
  211. package/src/formats/anthropic/schema.ts +0 -74
  212. package/src/formats/anthropic/serialize.ts +0 -179
  213. package/src/formats/openai/chat-completions/index.ts +0 -14
  214. package/src/formats/openai/chat-completions/parse.ts +0 -47
  215. package/src/formats/openai/chat-completions/schema.ts +0 -92
  216. package/src/formats/openai/chat-completions/serialize.ts +0 -146
  217. package/src/formats/openai/responses/index.ts +0 -14
  218. package/src/formats/openai/responses/parse.ts +0 -73
  219. package/src/formats/openai/responses/schema.ts +0 -86
  220. package/src/formats/openai/responses/serialize.ts +0 -328
  221. package/src/formats/request-helpers.ts +0 -56
  222. package/src/formats/serialize-helpers.ts +0 -43
  223. package/src/formats/types.ts +0 -26
  224. package/src/history.ts +0 -70
  225. package/src/index.ts +0 -46
  226. package/src/loader.ts +0 -246
  227. package/src/logger.ts +0 -70
  228. package/src/mock-server.ts +0 -203
  229. package/src/route-handler.ts +0 -144
  230. package/src/rule-builder.ts +0 -73
  231. package/src/rule-engine.ts +0 -165
  232. package/src/sse-writer.ts +0 -35
  233. package/src/types/reply.ts +0 -92
  234. package/src/types/request.ts +0 -56
  235. package/src/types/rule.ts +0 -125
  236. package/src/types.ts +0 -24
  237. package/test/cli-validators.test.ts +0 -151
  238. package/test/formats/anthropic.test.ts +0 -336
  239. package/test/formats/openai.test.ts +0 -316
  240. package/test/formats/parse-helpers.test.ts +0 -315
  241. package/test/formats/responses.test.ts +0 -380
  242. package/test/helpers/make-req.ts +0 -18
  243. package/test/history.test.ts +0 -361
  244. package/test/loader.test.ts +0 -333
  245. package/test/logger.test.ts +0 -344
  246. package/test/mock-server.test.ts +0 -619
  247. package/test/rule-engine.test.ts +0 -229
  248. package/tsconfig.json +0 -24
  249. package/tsconfig.test.json +0 -11
  250. package/typedoc.json +0 -9
  251. 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
- });