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
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { ReplyObject } from "#/types/reply.js";
|
|
2
|
-
|
|
3
|
-
export const MS_PER_SECOND = 1000;
|
|
4
|
-
export const DEFAULT_USAGE = { input: 10, output: 5 } as const;
|
|
5
|
-
|
|
6
|
-
export function splitText(text: string, chunkSize: number): string[] {
|
|
7
|
-
if (chunkSize <= 0 || text.length <= chunkSize) return [text];
|
|
8
|
-
const chunks: string[] = [];
|
|
9
|
-
for (let i = 0; i < text.length; i += chunkSize) {
|
|
10
|
-
chunks.push(text.slice(i, i + chunkSize));
|
|
11
|
-
}
|
|
12
|
-
return chunks;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const ID_SUFFIX_LENGTH = 12;
|
|
16
|
-
|
|
17
|
-
function randomSuffix(): string {
|
|
18
|
-
return crypto.randomUUID().replaceAll("-", "").slice(0, ID_SUFFIX_LENGTH);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function genId(prefix: string): string {
|
|
22
|
-
return `${prefix}_${randomSuffix()}`;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function toolId(
|
|
26
|
-
tool: { id?: string | undefined },
|
|
27
|
-
prefix: string,
|
|
28
|
-
index: number,
|
|
29
|
-
): string {
|
|
30
|
-
return tool.id ?? `${prefix}_${randomSuffix()}_${index}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function shouldEmitText(reply: ReplyObject): boolean {
|
|
34
|
-
return Boolean(reply.text) || (!reply.tools?.length && !reply.reasoning);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function finishReason(
|
|
38
|
-
reply: ReplyObject,
|
|
39
|
-
onTools: string,
|
|
40
|
-
onStop: string,
|
|
41
|
-
): string {
|
|
42
|
-
return reply.tools?.length ? onTools : onStop;
|
|
43
|
-
}
|
package/src/formats/types.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { FormatName, MockRequest } from "#/types/request.js";
|
|
2
|
-
import type { ReplyObject, ReplyOptions } from "#/types/reply.js";
|
|
3
|
-
import type { RequestMeta } from "./request-helpers.js";
|
|
4
|
-
|
|
5
|
-
export interface SSEChunk {
|
|
6
|
-
readonly event?: string | undefined;
|
|
7
|
-
readonly data: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface Format {
|
|
11
|
-
readonly name: FormatName;
|
|
12
|
-
readonly route: string;
|
|
13
|
-
parseRequest(body: unknown, meta?: RequestMeta): MockRequest;
|
|
14
|
-
isStreaming(body: unknown): boolean;
|
|
15
|
-
serialize(
|
|
16
|
-
reply: ReplyObject,
|
|
17
|
-
model: string,
|
|
18
|
-
options?: ReplyOptions,
|
|
19
|
-
): readonly SSEChunk[];
|
|
20
|
-
serializeComplete(reply: ReplyObject, model: string): Record<string, unknown>;
|
|
21
|
-
serializeError(error: {
|
|
22
|
-
status: number;
|
|
23
|
-
message: string;
|
|
24
|
-
type?: string | undefined;
|
|
25
|
-
}): Record<string, unknown>;
|
|
26
|
-
}
|
package/src/history.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { MockRequest } from "./types/request.js";
|
|
2
|
-
|
|
3
|
-
/** A recorded request with the rule that matched and when it happened. */
|
|
4
|
-
export interface RecordedRequest {
|
|
5
|
-
/** The normalised request that was received. */
|
|
6
|
-
readonly request: MockRequest;
|
|
7
|
-
/** Description of the rule that matched, or `undefined` if the fallback was used. */
|
|
8
|
-
readonly rule: string | undefined;
|
|
9
|
-
/** When the request was recorded (`Date.now()` value). */
|
|
10
|
-
readonly timestamp: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Records every request the server handles.
|
|
15
|
-
* Iterable and has fluent query methods for test assertions.
|
|
16
|
-
*
|
|
17
|
-
* @example
|
|
18
|
-
* ```ts
|
|
19
|
-
* expect(server.history.count()).toBe(3);
|
|
20
|
-
* expect(server.history.last()?.request.lastMessage).toBe("hello");
|
|
21
|
-
* const matched = server.history.where(r => r.rule !== undefined);
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
export class RequestHistory {
|
|
25
|
-
private readonly entries: RecordedRequest[] = [];
|
|
26
|
-
|
|
27
|
-
record(request: MockRequest, rule: string | undefined): void {
|
|
28
|
-
this.entries.push({ request, rule, timestamp: Date.now() });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** Number of recorded requests. */
|
|
32
|
-
count(): number {
|
|
33
|
-
return this.entries.length;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** First recorded request, or `undefined` if empty. */
|
|
37
|
-
first(): RecordedRequest | undefined {
|
|
38
|
-
return this.entries[0];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Most recent recorded request, or `undefined` if empty. */
|
|
42
|
-
last(): RecordedRequest | undefined {
|
|
43
|
-
return this.entries.at(-1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Get the entry at a specific index. Supports negative indices. */
|
|
47
|
-
at(index: number): RecordedRequest | undefined {
|
|
48
|
-
return this.entries.at(index);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Filter entries by a predicate. */
|
|
52
|
-
where(predicate: (entry: RecordedRequest) => boolean): RecordedRequest[] {
|
|
53
|
-
return this.entries.filter(predicate);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** All entries as a readonly array. */
|
|
57
|
-
get all(): readonly RecordedRequest[] {
|
|
58
|
-
return this.entries;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Remove all recorded entries. */
|
|
62
|
-
clear(): void {
|
|
63
|
-
this.entries.length = 0;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Enables `for...of` iteration over recorded entries. */
|
|
67
|
-
[Symbol.iterator](): Iterator<RecordedRequest> {
|
|
68
|
-
return this.entries[Symbol.iterator]();
|
|
69
|
-
}
|
|
70
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
export { MockServer } from "./mock-server.js";
|
|
2
|
-
export type { MockServerOptions } from "./mock-server.js";
|
|
3
|
-
export type { LogLevel } from "./logger.js";
|
|
4
|
-
export { RequestHistory } from "./history.js";
|
|
5
|
-
export type { RecordedRequest } from "./history.js";
|
|
6
|
-
export type {
|
|
7
|
-
FormatName,
|
|
8
|
-
MockRequest,
|
|
9
|
-
Message,
|
|
10
|
-
ToolDef,
|
|
11
|
-
Reply,
|
|
12
|
-
ReplyObject,
|
|
13
|
-
ToolCall,
|
|
14
|
-
Resolver,
|
|
15
|
-
Match,
|
|
16
|
-
MatchObject,
|
|
17
|
-
ReplyOptions,
|
|
18
|
-
ErrorReply,
|
|
19
|
-
PendingRule,
|
|
20
|
-
RuleHandle,
|
|
21
|
-
RuleSummary,
|
|
22
|
-
SequenceEntry,
|
|
23
|
-
Handler,
|
|
24
|
-
} from "./types.js";
|
|
25
|
-
|
|
26
|
-
import { MockServer } from "./mock-server.js";
|
|
27
|
-
import type { MockServerOptions } from "./mock-server.js";
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Create a server and start it in one go.
|
|
31
|
-
*
|
|
32
|
-
* @example
|
|
33
|
-
* ```ts
|
|
34
|
-
* const server = await createMock({ port: 0, logLevel: "info" });
|
|
35
|
-
* server.when("hello").reply("Hi!");
|
|
36
|
-
* // run your tests
|
|
37
|
-
* await server.stop();
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
40
|
-
export async function createMock(
|
|
41
|
-
options: MockServerOptions = {},
|
|
42
|
-
): Promise<MockServer> {
|
|
43
|
-
const server = new MockServer(options);
|
|
44
|
-
await server.start(options.port ?? 0);
|
|
45
|
-
return server;
|
|
46
|
-
}
|
package/src/loader.ts
DELETED
|
@@ -1,246 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
-
import { join, extname } from "node:path";
|
|
3
|
-
import JSON5 from "json5";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import type { Reply } from "./types/reply.js";
|
|
6
|
-
import type { Handler, Match, MatchObject } from "./types/rule.js";
|
|
7
|
-
import { type RuleEngine, createSequenceResolver } from "./rule-engine.js";
|
|
8
|
-
|
|
9
|
-
interface LoadContext {
|
|
10
|
-
engine: RuleEngine;
|
|
11
|
-
setFallback?: (reply: Reply) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const json5MatchSchema = z.union([
|
|
15
|
-
z.string(),
|
|
16
|
-
z.object({
|
|
17
|
-
message: z.string().optional(),
|
|
18
|
-
model: z.string().optional(),
|
|
19
|
-
system: z.string().optional(),
|
|
20
|
-
format: z.enum(["openai", "anthropic", "responses"]).optional(),
|
|
21
|
-
}),
|
|
22
|
-
]);
|
|
23
|
-
|
|
24
|
-
const json5ReplySchema = z.union([
|
|
25
|
-
z.string(),
|
|
26
|
-
z.object({
|
|
27
|
-
text: z.string().optional(),
|
|
28
|
-
reasoning: z.string().optional(),
|
|
29
|
-
tools: z
|
|
30
|
-
.array(
|
|
31
|
-
z.object({ name: z.string(), args: z.record(z.string(), z.unknown()) }),
|
|
32
|
-
)
|
|
33
|
-
.optional(),
|
|
34
|
-
}),
|
|
35
|
-
]);
|
|
36
|
-
|
|
37
|
-
const json5ReplyRef = z.union([json5ReplySchema, z.string().startsWith("$")]);
|
|
38
|
-
|
|
39
|
-
const json5SequenceEntrySchema = z.union([
|
|
40
|
-
json5ReplyRef,
|
|
41
|
-
z.object({
|
|
42
|
-
reply: json5ReplyRef,
|
|
43
|
-
latency: z.int().nonnegative().optional(),
|
|
44
|
-
chunkSize: z.int().nonnegative().optional(),
|
|
45
|
-
}),
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
const json5RuleSchema = z.union([
|
|
49
|
-
z.object({
|
|
50
|
-
when: json5MatchSchema,
|
|
51
|
-
reply: json5ReplyRef,
|
|
52
|
-
times: z.int().positive().optional(),
|
|
53
|
-
}),
|
|
54
|
-
z.object({
|
|
55
|
-
when: json5MatchSchema,
|
|
56
|
-
replies: z.array(json5SequenceEntrySchema).min(1),
|
|
57
|
-
}),
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
const json5FileSchema = z.union([
|
|
61
|
-
z.array(json5RuleSchema),
|
|
62
|
-
z.object({
|
|
63
|
-
templates: z.record(z.string(), json5ReplySchema).optional(),
|
|
64
|
-
fallback: json5ReplySchema.optional(),
|
|
65
|
-
rules: z.array(json5RuleSchema),
|
|
66
|
-
}),
|
|
67
|
-
]);
|
|
68
|
-
|
|
69
|
-
function parseRegexString(s: string): RegExp | string {
|
|
70
|
-
const match = /^\/(.+)\/([dgimsuyv]*)$/.exec(s);
|
|
71
|
-
if (match) {
|
|
72
|
-
return new RegExp(match[1]!, match[2]);
|
|
73
|
-
}
|
|
74
|
-
return s;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
type Json5ReplyRef = z.infer<typeof json5ReplyRef>;
|
|
78
|
-
type Templates = Record<string, z.infer<typeof json5ReplySchema>> | undefined;
|
|
79
|
-
|
|
80
|
-
function compileMatch(when: z.infer<typeof json5MatchSchema>): Match {
|
|
81
|
-
if (typeof when === "string") {
|
|
82
|
-
return parseRegexString(when);
|
|
83
|
-
}
|
|
84
|
-
const obj: MatchObject = {
|
|
85
|
-
...(when.message !== undefined && {
|
|
86
|
-
message: parseRegexString(when.message),
|
|
87
|
-
}),
|
|
88
|
-
...(when.model !== undefined && { model: parseRegexString(when.model) }),
|
|
89
|
-
...(when.system !== undefined && { system: parseRegexString(when.system) }),
|
|
90
|
-
...(when.format !== undefined && { format: when.format }),
|
|
91
|
-
};
|
|
92
|
-
return obj;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function resolveReplyRef(
|
|
96
|
-
ref: Json5ReplyRef,
|
|
97
|
-
templates: Templates,
|
|
98
|
-
filePath: string,
|
|
99
|
-
): z.infer<typeof json5ReplySchema> {
|
|
100
|
-
if (typeof ref === "string" && ref.startsWith("$")) {
|
|
101
|
-
const name = ref.slice(1);
|
|
102
|
-
const resolved = templates?.[name];
|
|
103
|
-
if (!resolved) throw new Error(`Unknown template "${name}" in ${filePath}`);
|
|
104
|
-
return resolved;
|
|
105
|
-
}
|
|
106
|
-
return ref;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function addSequenceRule(
|
|
110
|
-
engine: RuleEngine,
|
|
111
|
-
match: Match,
|
|
112
|
-
entries: z.infer<typeof json5SequenceEntrySchema>[],
|
|
113
|
-
templates: Templates,
|
|
114
|
-
filePath: string,
|
|
115
|
-
): void {
|
|
116
|
-
const steps = entries.map((entry) => {
|
|
117
|
-
if (typeof entry === "string" || !("reply" in entry)) {
|
|
118
|
-
return { reply: resolveReplyRef(entry, templates, filePath) };
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
reply: resolveReplyRef(entry.reply, templates, filePath),
|
|
122
|
-
options: {
|
|
123
|
-
...(entry.latency !== undefined && { latency: entry.latency }),
|
|
124
|
-
...(entry.chunkSize !== undefined && { chunkSize: entry.chunkSize }),
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
});
|
|
128
|
-
const rule = engine.add(match, "");
|
|
129
|
-
const { resolver, entryCount } = createSequenceResolver(steps, rule);
|
|
130
|
-
rule.resolve = resolver;
|
|
131
|
-
rule.remaining = entryCount;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async function loadJson5File(
|
|
135
|
-
filePath: string,
|
|
136
|
-
ctx: LoadContext,
|
|
137
|
-
): Promise<void> {
|
|
138
|
-
const content = await readFile(filePath, "utf-8");
|
|
139
|
-
const parsed = json5FileSchema.parse(JSON5.parse(content));
|
|
140
|
-
|
|
141
|
-
const rules = Array.isArray(parsed) ? parsed : parsed.rules;
|
|
142
|
-
const templates = Array.isArray(parsed) ? undefined : parsed.templates;
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
!Array.isArray(parsed) &&
|
|
146
|
-
parsed.fallback !== undefined &&
|
|
147
|
-
ctx.setFallback
|
|
148
|
-
) {
|
|
149
|
-
ctx.setFallback(parsed.fallback);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
for (const r of rules) {
|
|
153
|
-
const match = compileMatch(r.when);
|
|
154
|
-
if ("replies" in r) {
|
|
155
|
-
addSequenceRule(ctx.engine, match, r.replies, templates, filePath);
|
|
156
|
-
} else {
|
|
157
|
-
const reply = resolveReplyRef(r.reply, templates, filePath);
|
|
158
|
-
const rule = ctx.engine.add(match, reply);
|
|
159
|
-
if (r.times !== undefined) {
|
|
160
|
-
rule.remaining = r.times;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const handlerSchema = z.custom<Handler>((val): val is Handler => {
|
|
167
|
-
if (typeof val !== "object" || val === null) return false;
|
|
168
|
-
const obj = val as Record<string, unknown>;
|
|
169
|
-
return (
|
|
170
|
-
typeof obj["match"] === "function" && typeof obj["respond"] === "function"
|
|
171
|
-
);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const handlerExportSchema = z.object({
|
|
175
|
-
default: z.union([handlerSchema, z.array(handlerSchema)]),
|
|
176
|
-
fallback: json5ReplySchema.optional(),
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
async function loadHandlerFile(
|
|
180
|
-
filePath: string,
|
|
181
|
-
ctx: LoadContext,
|
|
182
|
-
): Promise<void> {
|
|
183
|
-
const mod = await import(filePath);
|
|
184
|
-
const parsed = handlerExportSchema.safeParse(mod);
|
|
185
|
-
if (!parsed.success) {
|
|
186
|
-
throw new Error(
|
|
187
|
-
`Invalid handler file ${filePath}. Expected default export with { match: Function, respond: Function }.`,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
const handlers = Array.isArray(parsed.data.default)
|
|
191
|
-
? parsed.data.default
|
|
192
|
-
: [parsed.data.default];
|
|
193
|
-
|
|
194
|
-
if (parsed.data.fallback !== undefined && ctx.setFallback) {
|
|
195
|
-
ctx.setFallback(parsed.data.fallback);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
for (const handler of handlers) {
|
|
199
|
-
ctx.engine.addHandler(
|
|
200
|
-
handler.match,
|
|
201
|
-
handler.respond,
|
|
202
|
-
`(handler: ${filePath})`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
type FileLoader = (filePath: string, ctx: LoadContext) => Promise<void>;
|
|
208
|
-
|
|
209
|
-
const loaderByExtension: ReadonlyMap<string, FileLoader> = new Map([
|
|
210
|
-
[".json5", loadJson5File],
|
|
211
|
-
[".json", loadJson5File],
|
|
212
|
-
[".ts", loadHandlerFile],
|
|
213
|
-
[".js", loadHandlerFile],
|
|
214
|
-
[".mjs", loadHandlerFile],
|
|
215
|
-
]);
|
|
216
|
-
|
|
217
|
-
export async function loadRulesFromPath(
|
|
218
|
-
pathOrDir: string,
|
|
219
|
-
ctx: LoadContext,
|
|
220
|
-
): Promise<void> {
|
|
221
|
-
const info = await stat(pathOrDir);
|
|
222
|
-
|
|
223
|
-
if (info.isFile()) {
|
|
224
|
-
const ext = extname(pathOrDir);
|
|
225
|
-
const loader = loaderByExtension.get(ext);
|
|
226
|
-
if (!loader) {
|
|
227
|
-
throw new Error(`Unsupported file extension "${ext}" for ${pathOrDir}`);
|
|
228
|
-
}
|
|
229
|
-
await loader(pathOrDir, ctx);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (!info.isDirectory()) return;
|
|
234
|
-
|
|
235
|
-
const entries = (await readdir(pathOrDir)).toSorted();
|
|
236
|
-
for (const entry of entries) {
|
|
237
|
-
const fullPath = join(pathOrDir, entry);
|
|
238
|
-
const entryStat = await stat(fullPath);
|
|
239
|
-
if (entryStat.isDirectory()) {
|
|
240
|
-
await loadRulesFromPath(fullPath, ctx);
|
|
241
|
-
} else if (entryStat.isFile()) {
|
|
242
|
-
const loader = loaderByExtension.get(extname(fullPath));
|
|
243
|
-
if (loader) await loader(fullPath, ctx);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
package/src/logger.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import pc from "picocolors";
|
|
2
|
-
|
|
3
|
-
export const LEVEL_PRIORITY = {
|
|
4
|
-
none: 0,
|
|
5
|
-
error: 1,
|
|
6
|
-
warning: 2,
|
|
7
|
-
info: 3,
|
|
8
|
-
debug: 4,
|
|
9
|
-
all: 5,
|
|
10
|
-
} as const satisfies Record<string, number>;
|
|
11
|
-
|
|
12
|
-
/** Log verbosity, from `"none"` (silent) through to `"all"` (everything). */
|
|
13
|
-
export type LogLevel = keyof typeof LEVEL_PRIORITY;
|
|
14
|
-
|
|
15
|
-
const LEVEL_STYLE = {
|
|
16
|
-
error: { label: pc.red(pc.bold("ERROR")), symbol: pc.red("✗") },
|
|
17
|
-
warn: { label: pc.yellow(pc.bold("WARN")), symbol: pc.yellow("!") },
|
|
18
|
-
info: { label: pc.cyan("INFO"), symbol: pc.cyan("●") },
|
|
19
|
-
debug: { label: pc.dim("DEBUG"), symbol: pc.dim("·") },
|
|
20
|
-
} as const;
|
|
21
|
-
|
|
22
|
-
type ConsoleMethod = "error" | "warn" | "log";
|
|
23
|
-
|
|
24
|
-
const LEVEL_CONFIG: Record<
|
|
25
|
-
keyof typeof LEVEL_STYLE,
|
|
26
|
-
{ priority: number; method: ConsoleMethod; dim?: boolean }
|
|
27
|
-
> = {
|
|
28
|
-
error: { priority: LEVEL_PRIORITY.error, method: "error" },
|
|
29
|
-
warn: { priority: LEVEL_PRIORITY.warning, method: "warn" },
|
|
30
|
-
info: { priority: LEVEL_PRIORITY.info, method: "log" },
|
|
31
|
-
debug: { priority: LEVEL_PRIORITY.debug, method: "log", dim: true },
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export class Logger {
|
|
35
|
-
readonly level: LogLevel;
|
|
36
|
-
private threshold: number;
|
|
37
|
-
|
|
38
|
-
constructor(level: LogLevel = "info") {
|
|
39
|
-
this.level = level;
|
|
40
|
-
this.threshold = LEVEL_PRIORITY[level];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private log(
|
|
44
|
-
key: keyof typeof LEVEL_STYLE,
|
|
45
|
-
msg: string,
|
|
46
|
-
args: unknown[],
|
|
47
|
-
): void {
|
|
48
|
-
const config = LEVEL_CONFIG[key];
|
|
49
|
-
if (this.threshold < config.priority) return;
|
|
50
|
-
const { label, symbol } = LEVEL_STYLE[key];
|
|
51
|
-
const text = config.dim ? pc.dim(msg) : msg;
|
|
52
|
-
console[config.method](
|
|
53
|
-
`${pc.dim(new Date().toISOString())} ${symbol} ${label} ${text}`,
|
|
54
|
-
...args,
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
error(msg: string, ...args: unknown[]): void {
|
|
59
|
-
this.log("error", msg, args);
|
|
60
|
-
}
|
|
61
|
-
warn(msg: string, ...args: unknown[]): void {
|
|
62
|
-
this.log("warn", msg, args);
|
|
63
|
-
}
|
|
64
|
-
info(msg: string, ...args: unknown[]): void {
|
|
65
|
-
this.log("info", msg, args);
|
|
66
|
-
}
|
|
67
|
-
debug(msg: string, ...args: unknown[]): void {
|
|
68
|
-
this.log("debug", msg, args);
|
|
69
|
-
}
|
|
70
|
-
}
|
package/src/mock-server.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import Fastify from "fastify";
|
|
2
|
-
import type { FastifyInstance } from "fastify";
|
|
3
|
-
import type { Reply, ReplyOptions } from "./types/reply.js";
|
|
4
|
-
import type { RuleSummary } from "./types/rule.js";
|
|
5
|
-
import { RuleEngine } from "./rule-engine.js";
|
|
6
|
-
import { RuleBuilder } from "./rule-builder.js";
|
|
7
|
-
import { RequestHistory } from "./history.js";
|
|
8
|
-
import { chatCompletionsFormat } from "./formats/openai/chat-completions/index.js";
|
|
9
|
-
import { anthropicFormat } from "./formats/anthropic/index.js";
|
|
10
|
-
import { responsesFormat } from "./formats/openai/responses/index.js";
|
|
11
|
-
import type { Format } from "./formats/types.js";
|
|
12
|
-
import { Logger } from "./logger.js";
|
|
13
|
-
import type { LogLevel } from "./logger.js";
|
|
14
|
-
import { createRouteHandler } from "./route-handler.js";
|
|
15
|
-
|
|
16
|
-
const formats: readonly Format[] = [
|
|
17
|
-
chatCompletionsFormat,
|
|
18
|
-
anthropicFormat,
|
|
19
|
-
responsesFormat,
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
/** Options for constructing a `MockServer` or calling `createMock()`. */
|
|
23
|
-
export interface MockServerOptions {
|
|
24
|
-
/**
|
|
25
|
-
* Port to listen on. Pass `0` for a random port (useful in tests).
|
|
26
|
-
* @defaultValue `0`
|
|
27
|
-
*/
|
|
28
|
-
readonly port?: number;
|
|
29
|
-
/**
|
|
30
|
-
* Host to bind to. Set to `"0.0.0.0"` to listen on all interfaces.
|
|
31
|
-
* @defaultValue `"127.0.0.1"`
|
|
32
|
-
*/
|
|
33
|
-
readonly host?: string;
|
|
34
|
-
/**
|
|
35
|
-
* Log verbosity.
|
|
36
|
-
* @defaultValue `"none"`
|
|
37
|
-
*/
|
|
38
|
-
readonly logLevel?: LogLevel;
|
|
39
|
-
/**
|
|
40
|
-
* Default ms delay between SSE chunks. Individual rules can override this.
|
|
41
|
-
* @defaultValue `0`
|
|
42
|
-
*/
|
|
43
|
-
readonly defaultLatency?: number;
|
|
44
|
-
/**
|
|
45
|
-
* Default characters per SSE text chunk. Individual rules can override this.
|
|
46
|
-
* @defaultValue `0`
|
|
47
|
-
*/
|
|
48
|
-
readonly defaultChunkSize?: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
type RuleAPI = Pick<
|
|
52
|
-
RuleBuilder,
|
|
53
|
-
"when" | "whenTool" | "whenToolResult" | "nextError"
|
|
54
|
-
>;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Mock LLM server that handles OpenAI Chat Completions, Anthropic Messages, and OpenAI Responses API formats.
|
|
58
|
-
* Register rules with `when()`, point your SDK at `url`, and go.
|
|
59
|
-
*
|
|
60
|
-
* Supports `await using` for automatic cleanup.
|
|
61
|
-
*
|
|
62
|
-
* @example
|
|
63
|
-
* ```ts
|
|
64
|
-
* const server = new MockServer({ logLevel: "info" });
|
|
65
|
-
* server.when("hello").reply("Hi there!");
|
|
66
|
-
* await server.start();
|
|
67
|
-
* // Point your client at server.url
|
|
68
|
-
* await server.stop();
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
export class MockServer implements RuleAPI {
|
|
72
|
-
private readonly app: FastifyInstance;
|
|
73
|
-
private readonly engine = new RuleEngine();
|
|
74
|
-
private readonly rules_ = new RuleBuilder(this.engine);
|
|
75
|
-
private readonly history_ = new RequestHistory();
|
|
76
|
-
private readonly logger: Logger;
|
|
77
|
-
private readonly host: string;
|
|
78
|
-
private readonly defaultOptions: ReplyOptions;
|
|
79
|
-
private fallbackReply: Reply = "Mock server: no matching rule.";
|
|
80
|
-
private listening = false;
|
|
81
|
-
|
|
82
|
-
/** Register a matching rule. Call `.reply()` on the result to set the response. */
|
|
83
|
-
when = this.rules_.when.bind(this.rules_);
|
|
84
|
-
/** Shorthand for `when({ toolName })`. */
|
|
85
|
-
whenTool = this.rules_.whenTool.bind(this.rules_);
|
|
86
|
-
/** Shorthand for `when({ toolCallId })`. */
|
|
87
|
-
whenToolResult = this.rules_.whenToolResult.bind(this.rules_);
|
|
88
|
-
/** Queue a one-shot error for the very next request. Fires once then removes itself. */
|
|
89
|
-
nextError = this.rules_.nextError.bind(this.rules_);
|
|
90
|
-
|
|
91
|
-
constructor(options: MockServerOptions = {}) {
|
|
92
|
-
this.host = options.host ?? "127.0.0.1";
|
|
93
|
-
this.logger = new Logger(options.logLevel ?? "none");
|
|
94
|
-
this.defaultOptions = {
|
|
95
|
-
...(options.defaultLatency !== undefined && {
|
|
96
|
-
latency: options.defaultLatency,
|
|
97
|
-
}),
|
|
98
|
-
...(options.defaultChunkSize !== undefined && {
|
|
99
|
-
chunkSize: options.defaultChunkSize,
|
|
100
|
-
}),
|
|
101
|
-
};
|
|
102
|
-
this.app = Fastify({ logger: false });
|
|
103
|
-
|
|
104
|
-
const deps = {
|
|
105
|
-
engine: this.engine,
|
|
106
|
-
history: this.history_,
|
|
107
|
-
logger: this.logger,
|
|
108
|
-
defaultOptions: this.defaultOptions,
|
|
109
|
-
getFallback: () => this.fallbackReply,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
for (const format of formats) {
|
|
113
|
-
this.app.post(format.route, createRouteHandler(format, deps));
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Set the reply used when no rule matches.
|
|
119
|
-
* @defaultValue `"Mock server: no matching rule."`
|
|
120
|
-
*/
|
|
121
|
-
fallback(reply: Reply): void {
|
|
122
|
-
this.fallbackReply = reply;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/** Load rules from a `.json5` file, a `.ts`/`.js` handler file, or a directory containing them. */
|
|
126
|
-
async load(pathOrDir: string): Promise<void> {
|
|
127
|
-
const before = this.engine.ruleCount;
|
|
128
|
-
const { loadRulesFromPath } = await import("./loader.js");
|
|
129
|
-
await loadRulesFromPath(pathOrDir, {
|
|
130
|
-
engine: this.engine,
|
|
131
|
-
setFallback: (reply) => {
|
|
132
|
-
this.fallbackReply = reply;
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
const loaded = this.engine.ruleCount - before;
|
|
136
|
-
this.logger.info(
|
|
137
|
-
`Loaded ${loaded} rule${loaded !== 1 ? "s" : ""} from ${pathOrDir}`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** Every request the server has handled. */
|
|
142
|
-
get history(): RequestHistory {
|
|
143
|
-
return this.history_;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** Returns `true` when all rules with a `.times()` limit have been consumed. */
|
|
147
|
-
isDone(): boolean {
|
|
148
|
-
return this.engine.isDone();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/** Clear all rules, request history, and reset the fallback to its default. */
|
|
152
|
-
reset(): void {
|
|
153
|
-
this.engine.clear();
|
|
154
|
-
this.history_.clear();
|
|
155
|
-
this.fallbackReply = "Mock server: no matching rule.";
|
|
156
|
-
this.logger.info("Server reset: rules and history cleared");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** The base URL the server is listening on, e.g. `http://127.0.0.1:12345`. Throws if the server hasn't started. */
|
|
160
|
-
get url(): string {
|
|
161
|
-
if (!this.listening)
|
|
162
|
-
throw new Error("Server is not running. Call start() first.");
|
|
163
|
-
const addr = this.app.server.address();
|
|
164
|
-
const port = addr !== null && typeof addr === "object" ? addr.port : 0;
|
|
165
|
-
return `http://${this.host}:${port}`;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/** The API routes registered on this server, e.g. `["/v1/chat/completions", ...]`. */
|
|
169
|
-
get routes(): readonly string[] {
|
|
170
|
-
return formats.map((f) => f.route);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** Number of currently registered rules. */
|
|
174
|
-
get ruleCount(): number {
|
|
175
|
-
return this.engine.ruleCount;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** A snapshot of all registered rules with their descriptions and remaining match counts. */
|
|
179
|
-
get rules(): readonly RuleSummary[] {
|
|
180
|
-
return this.engine.describe();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Start listening. Pass `0` (the default) for a random port. */
|
|
184
|
-
async start(port = 0): Promise<void> {
|
|
185
|
-
if (this.listening) throw new Error("Server is already running.");
|
|
186
|
-
await this.app.listen({ port, host: this.host });
|
|
187
|
-
this.listening = true;
|
|
188
|
-
this.logger.info(`Listening on ${this.url}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/** Stop the server. Safe to call multiple times. */
|
|
192
|
-
async stop(): Promise<void> {
|
|
193
|
-
if (!this.listening) return;
|
|
194
|
-
await this.app.close();
|
|
195
|
-
this.listening = false;
|
|
196
|
-
this.logger.info("Server stopped");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** Calls `stop()`. Enables `await using server = ...` for automatic cleanup. */
|
|
200
|
-
async [Symbol.asyncDispose](): Promise<void> {
|
|
201
|
-
await this.stop();
|
|
202
|
-
}
|
|
203
|
-
}
|