@copilotkit/aimock 1.14.0 → 1.14.2
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/README.md +6 -6
- package/dist/cli.cjs +24 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +24 -1
- package/dist/cli.js.map +1 -1
- package/dist/journal.cjs +30 -1
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +42 -1
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +42 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +30 -1
- package/dist/journal.js.map +1 -1
- package/dist/server.cjs +4 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -1
- package/dist/server.js.map +1 -1
- package/dist/types.d.cts +24 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ aimock mocks everything your AI app talks to:
|
|
|
36
36
|
| **VectorMock** | Pinecone, Qdrant, ChromaDB compatible endpoints | [Vector](https://aimock.copilotkit.dev/vector-mock) |
|
|
37
37
|
| **Services** | Tavily search, Cohere rerank, OpenAI moderation | [Services](https://aimock.copilotkit.dev/services) |
|
|
38
38
|
|
|
39
|
-
Run them all on one port with `npx aimock --config aimock.json`, or use the programmatic API to compose exactly what you need.
|
|
39
|
+
Run them all on one port with `npx @copilotkit/aimock --config aimock.json`, or use the programmatic API to compose exactly what you need.
|
|
40
40
|
|
|
41
41
|
## Features
|
|
42
42
|
|
|
@@ -72,17 +72,17 @@ See the [GitHub Action docs](https://aimock.copilotkit.dev/github-action) for al
|
|
|
72
72
|
|
|
73
73
|
```bash
|
|
74
74
|
# LLM mocking only
|
|
75
|
-
npx aimock -p 4010 -f ./fixtures
|
|
75
|
+
npx @copilotkit/aimock -p 4010 -f ./fixtures
|
|
76
76
|
|
|
77
77
|
# Full suite from config
|
|
78
|
-
npx aimock --config aimock.json
|
|
78
|
+
npx @copilotkit/aimock --config aimock.json
|
|
79
79
|
|
|
80
80
|
# Record mode: proxy to real APIs, save fixtures
|
|
81
|
-
npx aimock --record --provider-openai https://api.openai.com
|
|
81
|
+
npx @copilotkit/aimock --record --provider-openai https://api.openai.com
|
|
82
82
|
|
|
83
83
|
# Convert fixtures from other tools
|
|
84
|
-
npx aimock convert vidaimock ./templates/ ./fixtures/
|
|
85
|
-
npx aimock convert mockllm ./config.yaml ./fixtures/
|
|
84
|
+
npx @copilotkit/aimock convert vidaimock ./templates/ ./fixtures/
|
|
85
|
+
npx @copilotkit/aimock convert mockllm ./config.yaml ./fixtures/
|
|
86
86
|
|
|
87
87
|
# Docker
|
|
88
88
|
docker run -d -p 4010:4010 -v ./fixtures:/fixtures ghcr.io/copilotkit/aimock -f /fixtures
|
package/dist/cli.cjs
CHANGED
|
@@ -26,6 +26,8 @@ Options:
|
|
|
26
26
|
--record Record mode: proxy unmatched requests and save fixtures
|
|
27
27
|
--proxy-only Proxy mode: forward unmatched requests without saving
|
|
28
28
|
--strict Strict mode: fail on unmatched requests
|
|
29
|
+
--journal-max <n> Max request entries retained in memory (default: 1000, 0 = unbounded)
|
|
30
|
+
--fixture-counts-max <n> Max unique testIds retained in fixture match-count map (default: 500, 0 = unbounded)
|
|
29
31
|
--provider-openai <url> Upstream URL for OpenAI (used with --record)
|
|
30
32
|
--provider-anthropic <url> Upstream URL for Anthropic
|
|
31
33
|
--provider-gemini <url> Upstream URL for Gemini
|
|
@@ -118,6 +120,14 @@ const { values } = (0, node_util.parseArgs)({
|
|
|
118
120
|
"chaos-drop": { type: "string" },
|
|
119
121
|
"chaos-malformed": { type: "string" },
|
|
120
122
|
"chaos-disconnect": { type: "string" },
|
|
123
|
+
"journal-max": {
|
|
124
|
+
type: "string",
|
|
125
|
+
default: "1000"
|
|
126
|
+
},
|
|
127
|
+
"fixture-counts-max": {
|
|
128
|
+
type: "string",
|
|
129
|
+
default: "500"
|
|
130
|
+
},
|
|
121
131
|
help: {
|
|
122
132
|
type: "boolean",
|
|
123
133
|
default: false
|
|
@@ -158,6 +168,17 @@ if (Number.isNaN(chunkSize) || chunkSize < 1) {
|
|
|
158
168
|
console.error(`Invalid chunk-size: ${values["chunk-size"]}`);
|
|
159
169
|
process.exit(1);
|
|
160
170
|
}
|
|
171
|
+
const journalMax = Number(values["journal-max"]);
|
|
172
|
+
if (Number.isNaN(journalMax) || !Number.isInteger(journalMax) || journalMax < 0) {
|
|
173
|
+
console.error(`Invalid journal-max: ${values["journal-max"]} (must be a non-negative integer; 0 or omitted = unbounded)`);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
const fixtureCountsMaxStr = values["fixture-counts-max"];
|
|
177
|
+
const fixtureCountsMax = Number(fixtureCountsMaxStr);
|
|
178
|
+
if (Number.isNaN(fixtureCountsMax) || !Number.isInteger(fixtureCountsMax) || fixtureCountsMax < 0) {
|
|
179
|
+
console.error(`Invalid fixture-counts-max: ${fixtureCountsMaxStr} (must be a non-negative integer; 0 = unbounded)`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
161
182
|
const logger = new require_logger.Logger(logLevel);
|
|
162
183
|
let chaos;
|
|
163
184
|
{
|
|
@@ -274,7 +295,9 @@ async function main() {
|
|
|
274
295
|
chaos,
|
|
275
296
|
metrics: values.metrics,
|
|
276
297
|
record,
|
|
277
|
-
strict: values.strict
|
|
298
|
+
strict: values.strict,
|
|
299
|
+
journalMaxEntries: journalMax,
|
|
300
|
+
fixtureCountsMaxTestIds: fixtureCountsMax
|
|
278
301
|
}, mounts);
|
|
279
302
|
logger.info(`aimock server listening on ${instance.url}`);
|
|
280
303
|
let watcher = null;
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.cjs","names":["Logger","AGUIMock","loadFixturesFromDir","loadFixtureFile","validateFixtures","createServer","watchFixtures"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer } from \"./server.js\";\nimport { loadFixtureFile, loadFixturesFromDir, validateFixtures } from \"./fixture-loader.js\";\nimport { Logger, type LogLevel } from \"./logger.js\";\nimport { watchFixtures } from \"./watcher.js\";\nimport { AGUIMock } from \"./agui-mock.js\";\nimport type { ChaosConfig, RecordConfig } from \"./types.js\";\n\nconst HELP = `\nUsage: aimock [options]\n\nOptions:\n -p, --port <number> Port to listen on (default: 4010)\n -h, --host <string> Host to bind to (default: 127.0.0.1)\n -f, --fixtures <path> Path to fixtures directory or file (default: ./fixtures)\n -l, --latency <ms> Latency in ms between SSE chunks (default: 0)\n -c, --chunk-size <chars> Chunk size in characters (default: 20)\n -w, --watch Watch fixture path for changes and reload\n --log-level <level> Log verbosity: silent, info, debug (default: info)\n --validate-on-load Validate fixture schemas at startup\n --metrics Enable Prometheus metrics at GET /metrics\n --record Record mode: proxy unmatched requests and save fixtures\n --proxy-only Proxy mode: forward unmatched requests without saving\n --strict Strict mode: fail on unmatched requests\n --provider-openai <url> Upstream URL for OpenAI (used with --record)\n --provider-anthropic <url> Upstream URL for Anthropic\n --provider-gemini <url> Upstream URL for Gemini\n --provider-vertexai <url> Upstream URL for Vertex AI\n --provider-bedrock <url> Upstream URL for Bedrock\n --provider-azure <url> Upstream URL for Azure OpenAI\n --provider-ollama <url> Upstream URL for Ollama\n --provider-cohere <url> Upstream URL for Cohere\n --agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)\n --agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)\n --agui-proxy-only AG-UI proxy mode: forward without saving\n --chaos-drop <rate> Probability (0-1) of dropping requests with 500\n --chaos-malformed <rate> Probability (0-1) of returning malformed JSON\n --chaos-disconnect <rate> Probability (0-1) of destroying connection\n --help Show this help message\n`.trim();\n\nconst { values } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\", default: \"4010\" },\n host: { type: \"string\", short: \"h\", default: \"127.0.0.1\" },\n fixtures: { type: \"string\", short: \"f\", default: \"./fixtures\" },\n latency: { type: \"string\", short: \"l\", default: \"0\" },\n \"chunk-size\": { type: \"string\", short: \"c\", default: \"20\" },\n watch: { type: \"boolean\", short: \"w\", default: false },\n \"log-level\": { type: \"string\", default: \"info\" },\n \"validate-on-load\": { type: \"boolean\", default: false },\n metrics: { type: \"boolean\", default: false },\n record: { type: \"boolean\", default: false },\n \"proxy-only\": { type: \"boolean\", default: false },\n strict: { type: \"boolean\", default: false },\n \"provider-openai\": { type: \"string\" },\n \"provider-anthropic\": { type: \"string\" },\n \"provider-gemini\": { type: \"string\" },\n \"provider-vertexai\": { type: \"string\" },\n \"provider-bedrock\": { type: \"string\" },\n \"provider-azure\": { type: \"string\" },\n \"provider-ollama\": { type: \"string\" },\n \"provider-cohere\": { type: \"string\" },\n \"agui-record\": { type: \"boolean\", default: false },\n \"agui-upstream\": { type: \"string\" },\n \"agui-proxy-only\": { type: \"boolean\", default: false },\n \"chaos-drop\": { type: \"string\" },\n \"chaos-malformed\": { type: \"string\" },\n \"chaos-disconnect\": { type: \"string\" },\n help: { type: \"boolean\", default: false },\n },\n strict: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst port = Number(values.port);\nconst host = values.host!;\nconst latency = Number(values.latency);\nconst chunkSize = Number(values[\"chunk-size\"]);\nconst fixturePath = resolve(values.fixtures!);\nconst watchMode = values.watch!;\nconst validateOnLoad = values[\"validate-on-load\"]!;\nconst logLevelStr = values[\"log-level\"]!;\n\nif (![\"silent\", \"info\", \"debug\"].includes(logLevelStr)) {\n console.error(`Invalid log-level: ${logLevelStr} (must be silent, info, or debug)`);\n process.exit(1);\n}\nconst logLevel = logLevelStr as LogLevel;\n\nif (Number.isNaN(port) || port < 0 || port > 65535) {\n console.error(`Invalid port: ${values.port}`);\n process.exit(1);\n}\n\nif (Number.isNaN(latency) || latency < 0) {\n console.error(`Invalid latency: ${values.latency}`);\n process.exit(1);\n}\n\nif (Number.isNaN(chunkSize) || chunkSize < 1) {\n console.error(`Invalid chunk-size: ${values[\"chunk-size\"]}`);\n process.exit(1);\n}\n\nconst logger = new Logger(logLevel);\n\n// Parse chaos config from CLI flags\nlet chaos: ChaosConfig | undefined;\n{\n const dropStr = values[\"chaos-drop\"];\n const malformedStr = values[\"chaos-malformed\"];\n const disconnectStr = values[\"chaos-disconnect\"];\n\n if (dropStr !== undefined || malformedStr !== undefined || disconnectStr !== undefined) {\n chaos = {};\n if (dropStr !== undefined) {\n const val = parseFloat(dropStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-drop: ${dropStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.dropRate = val;\n }\n if (malformedStr !== undefined) {\n const val = parseFloat(malformedStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-malformed: ${malformedStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.malformedRate = val;\n }\n if (disconnectStr !== undefined) {\n const val = parseFloat(disconnectStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-disconnect: ${disconnectStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.disconnectRate = val;\n }\n }\n}\n\n// Parse record/proxy config from CLI flags\nlet record: RecordConfig | undefined;\nif (values.record || values[\"proxy-only\"]) {\n const providers: RecordConfig[\"providers\"] = {};\n if (values[\"provider-openai\"]) providers.openai = values[\"provider-openai\"];\n if (values[\"provider-anthropic\"]) providers.anthropic = values[\"provider-anthropic\"];\n if (values[\"provider-gemini\"]) providers.gemini = values[\"provider-gemini\"];\n if (values[\"provider-vertexai\"]) providers.vertexai = values[\"provider-vertexai\"];\n if (values[\"provider-bedrock\"]) providers.bedrock = values[\"provider-bedrock\"];\n if (values[\"provider-azure\"]) providers.azure = values[\"provider-azure\"];\n if (values[\"provider-ollama\"]) providers.ollama = values[\"provider-ollama\"];\n if (values[\"provider-cohere\"]) providers.cohere = values[\"provider-cohere\"];\n\n if (Object.keys(providers).length === 0) {\n console.error(\n `Error: --${values[\"proxy-only\"] ? \"proxy-only\" : \"record\"} requires at least one --provider-* flag`,\n );\n process.exit(1);\n }\n\n record = {\n providers,\n fixturePath: resolve(fixturePath, \"recorded\"),\n proxyOnly: values[\"proxy-only\"],\n };\n}\n\n// Parse AG-UI record/proxy config from CLI flags\nlet aguiMount: { path: string; handler: AGUIMock } | undefined;\nif (values[\"agui-record\"] || values[\"agui-proxy-only\"]) {\n if (!values[\"agui-upstream\"]) {\n console.error(\"Error: --agui-record/--agui-proxy-only requires --agui-upstream\");\n process.exit(1);\n }\n const agui = new AGUIMock();\n agui.enableRecording({\n upstream: values[\"agui-upstream\"],\n fixturePath: resolve(fixturePath, \"agui-recorded\"),\n proxyOnly: values[\"agui-proxy-only\"],\n });\n aguiMount = { path: \"/agui\", handler: agui };\n}\n\nasync function main() {\n // Load fixtures from path (detect file vs directory)\n let isDir: boolean;\n let fixtures;\n try {\n const stat = statSync(fixturePath);\n isDir = stat.isDirectory();\n if (isDir) {\n fixtures = loadFixturesFromDir(fixturePath, logger);\n } else {\n fixtures = loadFixtureFile(fixturePath, logger);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Fixtures path not found: ${fixturePath}`);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to load fixtures from ${fixturePath}: ${msg}`);\n }\n process.exit(1);\n }\n\n if (fixtures.length === 0) {\n if (validateOnLoad || values.strict) {\n console.error(\"Error: No fixtures loaded and validation/strict mode is enabled — aborting.\");\n process.exit(1);\n }\n console.warn(\"Warning: No fixtures loaded. The server will return 404 for all requests.\");\n }\n\n logger.info(`Loaded ${fixtures.length} fixture(s) from ${fixturePath}`);\n\n // Validate fixtures if requested\n if (validateOnLoad) {\n const results = validateFixtures(fixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n\n if (errors.length > 0) {\n console.error(`Validation failed: ${errors.length} error(s), ${warnings.length} warning(s)`);\n process.exit(1);\n }\n }\n\n const mounts = aguiMount ? [aguiMount] : undefined;\n\n const instance = await createServer(\n fixtures,\n {\n port,\n host,\n latency,\n chunkSize,\n logLevel,\n chaos,\n metrics: values.metrics,\n record,\n strict: values.strict,\n },\n mounts,\n );\n\n logger.info(`aimock server listening on ${instance.url}`);\n\n // Start file watcher if requested\n let watcher: { close: () => void } | null = null;\n if (watchMode) {\n const loadFn = isDir!\n ? () => loadFixturesFromDir(fixturePath, logger)\n : () => loadFixtureFile(fixturePath, logger);\n\n watcher = watchFixtures(fixturePath, fixtures, loadFn, {\n logger,\n validate: validateOnLoad,\n validateFn: validateFixtures,\n });\n logger.info(`Watching ${fixturePath} for changes`);\n }\n\n function shutdown() {\n logger.info(\"Shutting down...\");\n if (watcher) watcher.close();\n instance.server.close(() => {\n process.exit(0);\n });\n }\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.cjs","names":["Logger","AGUIMock","loadFixturesFromDir","loadFixtureFile","validateFixtures","createServer","watchFixtures"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer } from \"./server.js\";\nimport { loadFixtureFile, loadFixturesFromDir, validateFixtures } from \"./fixture-loader.js\";\nimport { Logger, type LogLevel } from \"./logger.js\";\nimport { watchFixtures } from \"./watcher.js\";\nimport { AGUIMock } from \"./agui-mock.js\";\nimport type { ChaosConfig, RecordConfig } from \"./types.js\";\n\nconst HELP = `\nUsage: aimock [options]\n\nOptions:\n -p, --port <number> Port to listen on (default: 4010)\n -h, --host <string> Host to bind to (default: 127.0.0.1)\n -f, --fixtures <path> Path to fixtures directory or file (default: ./fixtures)\n -l, --latency <ms> Latency in ms between SSE chunks (default: 0)\n -c, --chunk-size <chars> Chunk size in characters (default: 20)\n -w, --watch Watch fixture path for changes and reload\n --log-level <level> Log verbosity: silent, info, debug (default: info)\n --validate-on-load Validate fixture schemas at startup\n --metrics Enable Prometheus metrics at GET /metrics\n --record Record mode: proxy unmatched requests and save fixtures\n --proxy-only Proxy mode: forward unmatched requests without saving\n --strict Strict mode: fail on unmatched requests\n --journal-max <n> Max request entries retained in memory (default: 1000, 0 = unbounded)\n --fixture-counts-max <n> Max unique testIds retained in fixture match-count map (default: 500, 0 = unbounded)\n --provider-openai <url> Upstream URL for OpenAI (used with --record)\n --provider-anthropic <url> Upstream URL for Anthropic\n --provider-gemini <url> Upstream URL for Gemini\n --provider-vertexai <url> Upstream URL for Vertex AI\n --provider-bedrock <url> Upstream URL for Bedrock\n --provider-azure <url> Upstream URL for Azure OpenAI\n --provider-ollama <url> Upstream URL for Ollama\n --provider-cohere <url> Upstream URL for Cohere\n --agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)\n --agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)\n --agui-proxy-only AG-UI proxy mode: forward without saving\n --chaos-drop <rate> Probability (0-1) of dropping requests with 500\n --chaos-malformed <rate> Probability (0-1) of returning malformed JSON\n --chaos-disconnect <rate> Probability (0-1) of destroying connection\n --help Show this help message\n`.trim();\n\nconst { values } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\", default: \"4010\" },\n host: { type: \"string\", short: \"h\", default: \"127.0.0.1\" },\n fixtures: { type: \"string\", short: \"f\", default: \"./fixtures\" },\n latency: { type: \"string\", short: \"l\", default: \"0\" },\n \"chunk-size\": { type: \"string\", short: \"c\", default: \"20\" },\n watch: { type: \"boolean\", short: \"w\", default: false },\n \"log-level\": { type: \"string\", default: \"info\" },\n \"validate-on-load\": { type: \"boolean\", default: false },\n metrics: { type: \"boolean\", default: false },\n record: { type: \"boolean\", default: false },\n \"proxy-only\": { type: \"boolean\", default: false },\n strict: { type: \"boolean\", default: false },\n \"provider-openai\": { type: \"string\" },\n \"provider-anthropic\": { type: \"string\" },\n \"provider-gemini\": { type: \"string\" },\n \"provider-vertexai\": { type: \"string\" },\n \"provider-bedrock\": { type: \"string\" },\n \"provider-azure\": { type: \"string\" },\n \"provider-ollama\": { type: \"string\" },\n \"provider-cohere\": { type: \"string\" },\n \"agui-record\": { type: \"boolean\", default: false },\n \"agui-upstream\": { type: \"string\" },\n \"agui-proxy-only\": { type: \"boolean\", default: false },\n \"chaos-drop\": { type: \"string\" },\n \"chaos-malformed\": { type: \"string\" },\n \"chaos-disconnect\": { type: \"string\" },\n \"journal-max\": { type: \"string\", default: \"1000\" },\n \"fixture-counts-max\": { type: \"string\", default: \"500\" },\n help: { type: \"boolean\", default: false },\n },\n strict: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst port = Number(values.port);\nconst host = values.host!;\nconst latency = Number(values.latency);\nconst chunkSize = Number(values[\"chunk-size\"]);\nconst fixturePath = resolve(values.fixtures!);\nconst watchMode = values.watch!;\nconst validateOnLoad = values[\"validate-on-load\"]!;\nconst logLevelStr = values[\"log-level\"]!;\n\nif (![\"silent\", \"info\", \"debug\"].includes(logLevelStr)) {\n console.error(`Invalid log-level: ${logLevelStr} (must be silent, info, or debug)`);\n process.exit(1);\n}\nconst logLevel = logLevelStr as LogLevel;\n\nif (Number.isNaN(port) || port < 0 || port > 65535) {\n console.error(`Invalid port: ${values.port}`);\n process.exit(1);\n}\n\nif (Number.isNaN(latency) || latency < 0) {\n console.error(`Invalid latency: ${values.latency}`);\n process.exit(1);\n}\n\nif (Number.isNaN(chunkSize) || chunkSize < 1) {\n console.error(`Invalid chunk-size: ${values[\"chunk-size\"]}`);\n process.exit(1);\n}\n\nconst journalMax = Number(values[\"journal-max\"]);\nif (Number.isNaN(journalMax) || !Number.isInteger(journalMax) || journalMax < 0) {\n console.error(\n `Invalid journal-max: ${values[\"journal-max\"]} (must be a non-negative integer; 0 or omitted = unbounded)`,\n );\n process.exit(1);\n}\n\nconst fixtureCountsMaxStr = values[\"fixture-counts-max\"];\nconst fixtureCountsMax = Number(fixtureCountsMaxStr);\nif (Number.isNaN(fixtureCountsMax) || !Number.isInteger(fixtureCountsMax) || fixtureCountsMax < 0) {\n console.error(\n `Invalid fixture-counts-max: ${fixtureCountsMaxStr} (must be a non-negative integer; 0 = unbounded)`,\n );\n process.exit(1);\n}\n\nconst logger = new Logger(logLevel);\n\n// Parse chaos config from CLI flags\nlet chaos: ChaosConfig | undefined;\n{\n const dropStr = values[\"chaos-drop\"];\n const malformedStr = values[\"chaos-malformed\"];\n const disconnectStr = values[\"chaos-disconnect\"];\n\n if (dropStr !== undefined || malformedStr !== undefined || disconnectStr !== undefined) {\n chaos = {};\n if (dropStr !== undefined) {\n const val = parseFloat(dropStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-drop: ${dropStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.dropRate = val;\n }\n if (malformedStr !== undefined) {\n const val = parseFloat(malformedStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-malformed: ${malformedStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.malformedRate = val;\n }\n if (disconnectStr !== undefined) {\n const val = parseFloat(disconnectStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-disconnect: ${disconnectStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.disconnectRate = val;\n }\n }\n}\n\n// Parse record/proxy config from CLI flags\nlet record: RecordConfig | undefined;\nif (values.record || values[\"proxy-only\"]) {\n const providers: RecordConfig[\"providers\"] = {};\n if (values[\"provider-openai\"]) providers.openai = values[\"provider-openai\"];\n if (values[\"provider-anthropic\"]) providers.anthropic = values[\"provider-anthropic\"];\n if (values[\"provider-gemini\"]) providers.gemini = values[\"provider-gemini\"];\n if (values[\"provider-vertexai\"]) providers.vertexai = values[\"provider-vertexai\"];\n if (values[\"provider-bedrock\"]) providers.bedrock = values[\"provider-bedrock\"];\n if (values[\"provider-azure\"]) providers.azure = values[\"provider-azure\"];\n if (values[\"provider-ollama\"]) providers.ollama = values[\"provider-ollama\"];\n if (values[\"provider-cohere\"]) providers.cohere = values[\"provider-cohere\"];\n\n if (Object.keys(providers).length === 0) {\n console.error(\n `Error: --${values[\"proxy-only\"] ? \"proxy-only\" : \"record\"} requires at least one --provider-* flag`,\n );\n process.exit(1);\n }\n\n record = {\n providers,\n fixturePath: resolve(fixturePath, \"recorded\"),\n proxyOnly: values[\"proxy-only\"],\n };\n}\n\n// Parse AG-UI record/proxy config from CLI flags\nlet aguiMount: { path: string; handler: AGUIMock } | undefined;\nif (values[\"agui-record\"] || values[\"agui-proxy-only\"]) {\n if (!values[\"agui-upstream\"]) {\n console.error(\"Error: --agui-record/--agui-proxy-only requires --agui-upstream\");\n process.exit(1);\n }\n const agui = new AGUIMock();\n agui.enableRecording({\n upstream: values[\"agui-upstream\"],\n fixturePath: resolve(fixturePath, \"agui-recorded\"),\n proxyOnly: values[\"agui-proxy-only\"],\n });\n aguiMount = { path: \"/agui\", handler: agui };\n}\n\nasync function main() {\n // Load fixtures from path (detect file vs directory)\n let isDir: boolean;\n let fixtures;\n try {\n const stat = statSync(fixturePath);\n isDir = stat.isDirectory();\n if (isDir) {\n fixtures = loadFixturesFromDir(fixturePath, logger);\n } else {\n fixtures = loadFixtureFile(fixturePath, logger);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Fixtures path not found: ${fixturePath}`);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to load fixtures from ${fixturePath}: ${msg}`);\n }\n process.exit(1);\n }\n\n if (fixtures.length === 0) {\n if (validateOnLoad || values.strict) {\n console.error(\"Error: No fixtures loaded and validation/strict mode is enabled — aborting.\");\n process.exit(1);\n }\n console.warn(\"Warning: No fixtures loaded. The server will return 404 for all requests.\");\n }\n\n logger.info(`Loaded ${fixtures.length} fixture(s) from ${fixturePath}`);\n\n // Validate fixtures if requested\n if (validateOnLoad) {\n const results = validateFixtures(fixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n\n if (errors.length > 0) {\n console.error(`Validation failed: ${errors.length} error(s), ${warnings.length} warning(s)`);\n process.exit(1);\n }\n }\n\n const mounts = aguiMount ? [aguiMount] : undefined;\n\n const instance = await createServer(\n fixtures,\n {\n port,\n host,\n latency,\n chunkSize,\n logLevel,\n chaos,\n metrics: values.metrics,\n record,\n strict: values.strict,\n journalMaxEntries: journalMax,\n fixtureCountsMaxTestIds: fixtureCountsMax,\n },\n mounts,\n );\n\n logger.info(`aimock server listening on ${instance.url}`);\n\n // Start file watcher if requested\n let watcher: { close: () => void } | null = null;\n if (watchMode) {\n const loadFn = isDir!\n ? () => loadFixturesFromDir(fixturePath, logger)\n : () => loadFixtureFile(fixturePath, logger);\n\n watcher = watchFixtures(fixturePath, fixtures, loadFn, {\n logger,\n validate: validateOnLoad,\n validateFn: validateFixtures,\n });\n logger.info(`Watching ${fixturePath} for changes`);\n }\n\n function shutdown() {\n logger.info(\"Shutting down...\");\n if (watcher) watcher.close();\n instance.server.close(() => {\n process.exit(0);\n });\n }\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,8 @@ Options:
|
|
|
25
25
|
--record Record mode: proxy unmatched requests and save fixtures
|
|
26
26
|
--proxy-only Proxy mode: forward unmatched requests without saving
|
|
27
27
|
--strict Strict mode: fail on unmatched requests
|
|
28
|
+
--journal-max <n> Max request entries retained in memory (default: 1000, 0 = unbounded)
|
|
29
|
+
--fixture-counts-max <n> Max unique testIds retained in fixture match-count map (default: 500, 0 = unbounded)
|
|
28
30
|
--provider-openai <url> Upstream URL for OpenAI (used with --record)
|
|
29
31
|
--provider-anthropic <url> Upstream URL for Anthropic
|
|
30
32
|
--provider-gemini <url> Upstream URL for Gemini
|
|
@@ -117,6 +119,14 @@ const { values } = parseArgs({
|
|
|
117
119
|
"chaos-drop": { type: "string" },
|
|
118
120
|
"chaos-malformed": { type: "string" },
|
|
119
121
|
"chaos-disconnect": { type: "string" },
|
|
122
|
+
"journal-max": {
|
|
123
|
+
type: "string",
|
|
124
|
+
default: "1000"
|
|
125
|
+
},
|
|
126
|
+
"fixture-counts-max": {
|
|
127
|
+
type: "string",
|
|
128
|
+
default: "500"
|
|
129
|
+
},
|
|
120
130
|
help: {
|
|
121
131
|
type: "boolean",
|
|
122
132
|
default: false
|
|
@@ -157,6 +167,17 @@ if (Number.isNaN(chunkSize) || chunkSize < 1) {
|
|
|
157
167
|
console.error(`Invalid chunk-size: ${values["chunk-size"]}`);
|
|
158
168
|
process.exit(1);
|
|
159
169
|
}
|
|
170
|
+
const journalMax = Number(values["journal-max"]);
|
|
171
|
+
if (Number.isNaN(journalMax) || !Number.isInteger(journalMax) || journalMax < 0) {
|
|
172
|
+
console.error(`Invalid journal-max: ${values["journal-max"]} (must be a non-negative integer; 0 or omitted = unbounded)`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const fixtureCountsMaxStr = values["fixture-counts-max"];
|
|
176
|
+
const fixtureCountsMax = Number(fixtureCountsMaxStr);
|
|
177
|
+
if (Number.isNaN(fixtureCountsMax) || !Number.isInteger(fixtureCountsMax) || fixtureCountsMax < 0) {
|
|
178
|
+
console.error(`Invalid fixture-counts-max: ${fixtureCountsMaxStr} (must be a non-negative integer; 0 = unbounded)`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
160
181
|
const logger = new Logger(logLevel);
|
|
161
182
|
let chaos;
|
|
162
183
|
{
|
|
@@ -273,7 +294,9 @@ async function main() {
|
|
|
273
294
|
chaos,
|
|
274
295
|
metrics: values.metrics,
|
|
275
296
|
record,
|
|
276
|
-
strict: values.strict
|
|
297
|
+
strict: values.strict,
|
|
298
|
+
journalMaxEntries: journalMax,
|
|
299
|
+
fixtureCountsMaxTestIds: fixtureCountsMax
|
|
277
300
|
}, mounts);
|
|
278
301
|
logger.info(`aimock server listening on ${instance.url}`);
|
|
279
302
|
let watcher = null;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer } from \"./server.js\";\nimport { loadFixtureFile, loadFixturesFromDir, validateFixtures } from \"./fixture-loader.js\";\nimport { Logger, type LogLevel } from \"./logger.js\";\nimport { watchFixtures } from \"./watcher.js\";\nimport { AGUIMock } from \"./agui-mock.js\";\nimport type { ChaosConfig, RecordConfig } from \"./types.js\";\n\nconst HELP = `\nUsage: aimock [options]\n\nOptions:\n -p, --port <number> Port to listen on (default: 4010)\n -h, --host <string> Host to bind to (default: 127.0.0.1)\n -f, --fixtures <path> Path to fixtures directory or file (default: ./fixtures)\n -l, --latency <ms> Latency in ms between SSE chunks (default: 0)\n -c, --chunk-size <chars> Chunk size in characters (default: 20)\n -w, --watch Watch fixture path for changes and reload\n --log-level <level> Log verbosity: silent, info, debug (default: info)\n --validate-on-load Validate fixture schemas at startup\n --metrics Enable Prometheus metrics at GET /metrics\n --record Record mode: proxy unmatched requests and save fixtures\n --proxy-only Proxy mode: forward unmatched requests without saving\n --strict Strict mode: fail on unmatched requests\n --provider-openai <url> Upstream URL for OpenAI (used with --record)\n --provider-anthropic <url> Upstream URL for Anthropic\n --provider-gemini <url> Upstream URL for Gemini\n --provider-vertexai <url> Upstream URL for Vertex AI\n --provider-bedrock <url> Upstream URL for Bedrock\n --provider-azure <url> Upstream URL for Azure OpenAI\n --provider-ollama <url> Upstream URL for Ollama\n --provider-cohere <url> Upstream URL for Cohere\n --agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)\n --agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)\n --agui-proxy-only AG-UI proxy mode: forward without saving\n --chaos-drop <rate> Probability (0-1) of dropping requests with 500\n --chaos-malformed <rate> Probability (0-1) of returning malformed JSON\n --chaos-disconnect <rate> Probability (0-1) of destroying connection\n --help Show this help message\n`.trim();\n\nconst { values } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\", default: \"4010\" },\n host: { type: \"string\", short: \"h\", default: \"127.0.0.1\" },\n fixtures: { type: \"string\", short: \"f\", default: \"./fixtures\" },\n latency: { type: \"string\", short: \"l\", default: \"0\" },\n \"chunk-size\": { type: \"string\", short: \"c\", default: \"20\" },\n watch: { type: \"boolean\", short: \"w\", default: false },\n \"log-level\": { type: \"string\", default: \"info\" },\n \"validate-on-load\": { type: \"boolean\", default: false },\n metrics: { type: \"boolean\", default: false },\n record: { type: \"boolean\", default: false },\n \"proxy-only\": { type: \"boolean\", default: false },\n strict: { type: \"boolean\", default: false },\n \"provider-openai\": { type: \"string\" },\n \"provider-anthropic\": { type: \"string\" },\n \"provider-gemini\": { type: \"string\" },\n \"provider-vertexai\": { type: \"string\" },\n \"provider-bedrock\": { type: \"string\" },\n \"provider-azure\": { type: \"string\" },\n \"provider-ollama\": { type: \"string\" },\n \"provider-cohere\": { type: \"string\" },\n \"agui-record\": { type: \"boolean\", default: false },\n \"agui-upstream\": { type: \"string\" },\n \"agui-proxy-only\": { type: \"boolean\", default: false },\n \"chaos-drop\": { type: \"string\" },\n \"chaos-malformed\": { type: \"string\" },\n \"chaos-disconnect\": { type: \"string\" },\n help: { type: \"boolean\", default: false },\n },\n strict: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst port = Number(values.port);\nconst host = values.host!;\nconst latency = Number(values.latency);\nconst chunkSize = Number(values[\"chunk-size\"]);\nconst fixturePath = resolve(values.fixtures!);\nconst watchMode = values.watch!;\nconst validateOnLoad = values[\"validate-on-load\"]!;\nconst logLevelStr = values[\"log-level\"]!;\n\nif (![\"silent\", \"info\", \"debug\"].includes(logLevelStr)) {\n console.error(`Invalid log-level: ${logLevelStr} (must be silent, info, or debug)`);\n process.exit(1);\n}\nconst logLevel = logLevelStr as LogLevel;\n\nif (Number.isNaN(port) || port < 0 || port > 65535) {\n console.error(`Invalid port: ${values.port}`);\n process.exit(1);\n}\n\nif (Number.isNaN(latency) || latency < 0) {\n console.error(`Invalid latency: ${values.latency}`);\n process.exit(1);\n}\n\nif (Number.isNaN(chunkSize) || chunkSize < 1) {\n console.error(`Invalid chunk-size: ${values[\"chunk-size\"]}`);\n process.exit(1);\n}\n\nconst logger = new Logger(logLevel);\n\n// Parse chaos config from CLI flags\nlet chaos: ChaosConfig | undefined;\n{\n const dropStr = values[\"chaos-drop\"];\n const malformedStr = values[\"chaos-malformed\"];\n const disconnectStr = values[\"chaos-disconnect\"];\n\n if (dropStr !== undefined || malformedStr !== undefined || disconnectStr !== undefined) {\n chaos = {};\n if (dropStr !== undefined) {\n const val = parseFloat(dropStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-drop: ${dropStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.dropRate = val;\n }\n if (malformedStr !== undefined) {\n const val = parseFloat(malformedStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-malformed: ${malformedStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.malformedRate = val;\n }\n if (disconnectStr !== undefined) {\n const val = parseFloat(disconnectStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-disconnect: ${disconnectStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.disconnectRate = val;\n }\n }\n}\n\n// Parse record/proxy config from CLI flags\nlet record: RecordConfig | undefined;\nif (values.record || values[\"proxy-only\"]) {\n const providers: RecordConfig[\"providers\"] = {};\n if (values[\"provider-openai\"]) providers.openai = values[\"provider-openai\"];\n if (values[\"provider-anthropic\"]) providers.anthropic = values[\"provider-anthropic\"];\n if (values[\"provider-gemini\"]) providers.gemini = values[\"provider-gemini\"];\n if (values[\"provider-vertexai\"]) providers.vertexai = values[\"provider-vertexai\"];\n if (values[\"provider-bedrock\"]) providers.bedrock = values[\"provider-bedrock\"];\n if (values[\"provider-azure\"]) providers.azure = values[\"provider-azure\"];\n if (values[\"provider-ollama\"]) providers.ollama = values[\"provider-ollama\"];\n if (values[\"provider-cohere\"]) providers.cohere = values[\"provider-cohere\"];\n\n if (Object.keys(providers).length === 0) {\n console.error(\n `Error: --${values[\"proxy-only\"] ? \"proxy-only\" : \"record\"} requires at least one --provider-* flag`,\n );\n process.exit(1);\n }\n\n record = {\n providers,\n fixturePath: resolve(fixturePath, \"recorded\"),\n proxyOnly: values[\"proxy-only\"],\n };\n}\n\n// Parse AG-UI record/proxy config from CLI flags\nlet aguiMount: { path: string; handler: AGUIMock } | undefined;\nif (values[\"agui-record\"] || values[\"agui-proxy-only\"]) {\n if (!values[\"agui-upstream\"]) {\n console.error(\"Error: --agui-record/--agui-proxy-only requires --agui-upstream\");\n process.exit(1);\n }\n const agui = new AGUIMock();\n agui.enableRecording({\n upstream: values[\"agui-upstream\"],\n fixturePath: resolve(fixturePath, \"agui-recorded\"),\n proxyOnly: values[\"agui-proxy-only\"],\n });\n aguiMount = { path: \"/agui\", handler: agui };\n}\n\nasync function main() {\n // Load fixtures from path (detect file vs directory)\n let isDir: boolean;\n let fixtures;\n try {\n const stat = statSync(fixturePath);\n isDir = stat.isDirectory();\n if (isDir) {\n fixtures = loadFixturesFromDir(fixturePath, logger);\n } else {\n fixtures = loadFixtureFile(fixturePath, logger);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Fixtures path not found: ${fixturePath}`);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to load fixtures from ${fixturePath}: ${msg}`);\n }\n process.exit(1);\n }\n\n if (fixtures.length === 0) {\n if (validateOnLoad || values.strict) {\n console.error(\"Error: No fixtures loaded and validation/strict mode is enabled — aborting.\");\n process.exit(1);\n }\n console.warn(\"Warning: No fixtures loaded. The server will return 404 for all requests.\");\n }\n\n logger.info(`Loaded ${fixtures.length} fixture(s) from ${fixturePath}`);\n\n // Validate fixtures if requested\n if (validateOnLoad) {\n const results = validateFixtures(fixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n\n if (errors.length > 0) {\n console.error(`Validation failed: ${errors.length} error(s), ${warnings.length} warning(s)`);\n process.exit(1);\n }\n }\n\n const mounts = aguiMount ? [aguiMount] : undefined;\n\n const instance = await createServer(\n fixtures,\n {\n port,\n host,\n latency,\n chunkSize,\n logLevel,\n chaos,\n metrics: values.metrics,\n record,\n strict: values.strict,\n },\n mounts,\n );\n\n logger.info(`aimock server listening on ${instance.url}`);\n\n // Start file watcher if requested\n let watcher: { close: () => void } | null = null;\n if (watchMode) {\n const loadFn = isDir!\n ? () => loadFixturesFromDir(fixturePath, logger)\n : () => loadFixtureFile(fixturePath, logger);\n\n watcher = watchFixtures(fixturePath, fixtures, loadFn, {\n logger,\n validate: validateOnLoad,\n validateFn: validateFixtures,\n });\n logger.info(`Watching ${fixturePath} for changes`);\n }\n\n function shutdown() {\n logger.info(\"Shutting down...\");\n if (watcher) watcher.close();\n instance.server.close(() => {\n process.exit(0);\n });\n }\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { statSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport { createServer } from \"./server.js\";\nimport { loadFixtureFile, loadFixturesFromDir, validateFixtures } from \"./fixture-loader.js\";\nimport { Logger, type LogLevel } from \"./logger.js\";\nimport { watchFixtures } from \"./watcher.js\";\nimport { AGUIMock } from \"./agui-mock.js\";\nimport type { ChaosConfig, RecordConfig } from \"./types.js\";\n\nconst HELP = `\nUsage: aimock [options]\n\nOptions:\n -p, --port <number> Port to listen on (default: 4010)\n -h, --host <string> Host to bind to (default: 127.0.0.1)\n -f, --fixtures <path> Path to fixtures directory or file (default: ./fixtures)\n -l, --latency <ms> Latency in ms between SSE chunks (default: 0)\n -c, --chunk-size <chars> Chunk size in characters (default: 20)\n -w, --watch Watch fixture path for changes and reload\n --log-level <level> Log verbosity: silent, info, debug (default: info)\n --validate-on-load Validate fixture schemas at startup\n --metrics Enable Prometheus metrics at GET /metrics\n --record Record mode: proxy unmatched requests and save fixtures\n --proxy-only Proxy mode: forward unmatched requests without saving\n --strict Strict mode: fail on unmatched requests\n --journal-max <n> Max request entries retained in memory (default: 1000, 0 = unbounded)\n --fixture-counts-max <n> Max unique testIds retained in fixture match-count map (default: 500, 0 = unbounded)\n --provider-openai <url> Upstream URL for OpenAI (used with --record)\n --provider-anthropic <url> Upstream URL for Anthropic\n --provider-gemini <url> Upstream URL for Gemini\n --provider-vertexai <url> Upstream URL for Vertex AI\n --provider-bedrock <url> Upstream URL for Bedrock\n --provider-azure <url> Upstream URL for Azure OpenAI\n --provider-ollama <url> Upstream URL for Ollama\n --provider-cohere <url> Upstream URL for Cohere\n --agui-record Enable AG-UI recording (proxy unmatched AG-UI requests)\n --agui-upstream <url> Upstream AG-UI agent URL (used with --agui-record)\n --agui-proxy-only AG-UI proxy mode: forward without saving\n --chaos-drop <rate> Probability (0-1) of dropping requests with 500\n --chaos-malformed <rate> Probability (0-1) of returning malformed JSON\n --chaos-disconnect <rate> Probability (0-1) of destroying connection\n --help Show this help message\n`.trim();\n\nconst { values } = parseArgs({\n options: {\n port: { type: \"string\", short: \"p\", default: \"4010\" },\n host: { type: \"string\", short: \"h\", default: \"127.0.0.1\" },\n fixtures: { type: \"string\", short: \"f\", default: \"./fixtures\" },\n latency: { type: \"string\", short: \"l\", default: \"0\" },\n \"chunk-size\": { type: \"string\", short: \"c\", default: \"20\" },\n watch: { type: \"boolean\", short: \"w\", default: false },\n \"log-level\": { type: \"string\", default: \"info\" },\n \"validate-on-load\": { type: \"boolean\", default: false },\n metrics: { type: \"boolean\", default: false },\n record: { type: \"boolean\", default: false },\n \"proxy-only\": { type: \"boolean\", default: false },\n strict: { type: \"boolean\", default: false },\n \"provider-openai\": { type: \"string\" },\n \"provider-anthropic\": { type: \"string\" },\n \"provider-gemini\": { type: \"string\" },\n \"provider-vertexai\": { type: \"string\" },\n \"provider-bedrock\": { type: \"string\" },\n \"provider-azure\": { type: \"string\" },\n \"provider-ollama\": { type: \"string\" },\n \"provider-cohere\": { type: \"string\" },\n \"agui-record\": { type: \"boolean\", default: false },\n \"agui-upstream\": { type: \"string\" },\n \"agui-proxy-only\": { type: \"boolean\", default: false },\n \"chaos-drop\": { type: \"string\" },\n \"chaos-malformed\": { type: \"string\" },\n \"chaos-disconnect\": { type: \"string\" },\n \"journal-max\": { type: \"string\", default: \"1000\" },\n \"fixture-counts-max\": { type: \"string\", default: \"500\" },\n help: { type: \"boolean\", default: false },\n },\n strict: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst port = Number(values.port);\nconst host = values.host!;\nconst latency = Number(values.latency);\nconst chunkSize = Number(values[\"chunk-size\"]);\nconst fixturePath = resolve(values.fixtures!);\nconst watchMode = values.watch!;\nconst validateOnLoad = values[\"validate-on-load\"]!;\nconst logLevelStr = values[\"log-level\"]!;\n\nif (![\"silent\", \"info\", \"debug\"].includes(logLevelStr)) {\n console.error(`Invalid log-level: ${logLevelStr} (must be silent, info, or debug)`);\n process.exit(1);\n}\nconst logLevel = logLevelStr as LogLevel;\n\nif (Number.isNaN(port) || port < 0 || port > 65535) {\n console.error(`Invalid port: ${values.port}`);\n process.exit(1);\n}\n\nif (Number.isNaN(latency) || latency < 0) {\n console.error(`Invalid latency: ${values.latency}`);\n process.exit(1);\n}\n\nif (Number.isNaN(chunkSize) || chunkSize < 1) {\n console.error(`Invalid chunk-size: ${values[\"chunk-size\"]}`);\n process.exit(1);\n}\n\nconst journalMax = Number(values[\"journal-max\"]);\nif (Number.isNaN(journalMax) || !Number.isInteger(journalMax) || journalMax < 0) {\n console.error(\n `Invalid journal-max: ${values[\"journal-max\"]} (must be a non-negative integer; 0 or omitted = unbounded)`,\n );\n process.exit(1);\n}\n\nconst fixtureCountsMaxStr = values[\"fixture-counts-max\"];\nconst fixtureCountsMax = Number(fixtureCountsMaxStr);\nif (Number.isNaN(fixtureCountsMax) || !Number.isInteger(fixtureCountsMax) || fixtureCountsMax < 0) {\n console.error(\n `Invalid fixture-counts-max: ${fixtureCountsMaxStr} (must be a non-negative integer; 0 = unbounded)`,\n );\n process.exit(1);\n}\n\nconst logger = new Logger(logLevel);\n\n// Parse chaos config from CLI flags\nlet chaos: ChaosConfig | undefined;\n{\n const dropStr = values[\"chaos-drop\"];\n const malformedStr = values[\"chaos-malformed\"];\n const disconnectStr = values[\"chaos-disconnect\"];\n\n if (dropStr !== undefined || malformedStr !== undefined || disconnectStr !== undefined) {\n chaos = {};\n if (dropStr !== undefined) {\n const val = parseFloat(dropStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-drop: ${dropStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.dropRate = val;\n }\n if (malformedStr !== undefined) {\n const val = parseFloat(malformedStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-malformed: ${malformedStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.malformedRate = val;\n }\n if (disconnectStr !== undefined) {\n const val = parseFloat(disconnectStr);\n if (isNaN(val) || val < 0 || val > 1) {\n console.error(`Invalid chaos-disconnect: ${disconnectStr} (must be 0-1)`);\n process.exit(1);\n }\n chaos.disconnectRate = val;\n }\n }\n}\n\n// Parse record/proxy config from CLI flags\nlet record: RecordConfig | undefined;\nif (values.record || values[\"proxy-only\"]) {\n const providers: RecordConfig[\"providers\"] = {};\n if (values[\"provider-openai\"]) providers.openai = values[\"provider-openai\"];\n if (values[\"provider-anthropic\"]) providers.anthropic = values[\"provider-anthropic\"];\n if (values[\"provider-gemini\"]) providers.gemini = values[\"provider-gemini\"];\n if (values[\"provider-vertexai\"]) providers.vertexai = values[\"provider-vertexai\"];\n if (values[\"provider-bedrock\"]) providers.bedrock = values[\"provider-bedrock\"];\n if (values[\"provider-azure\"]) providers.azure = values[\"provider-azure\"];\n if (values[\"provider-ollama\"]) providers.ollama = values[\"provider-ollama\"];\n if (values[\"provider-cohere\"]) providers.cohere = values[\"provider-cohere\"];\n\n if (Object.keys(providers).length === 0) {\n console.error(\n `Error: --${values[\"proxy-only\"] ? \"proxy-only\" : \"record\"} requires at least one --provider-* flag`,\n );\n process.exit(1);\n }\n\n record = {\n providers,\n fixturePath: resolve(fixturePath, \"recorded\"),\n proxyOnly: values[\"proxy-only\"],\n };\n}\n\n// Parse AG-UI record/proxy config from CLI flags\nlet aguiMount: { path: string; handler: AGUIMock } | undefined;\nif (values[\"agui-record\"] || values[\"agui-proxy-only\"]) {\n if (!values[\"agui-upstream\"]) {\n console.error(\"Error: --agui-record/--agui-proxy-only requires --agui-upstream\");\n process.exit(1);\n }\n const agui = new AGUIMock();\n agui.enableRecording({\n upstream: values[\"agui-upstream\"],\n fixturePath: resolve(fixturePath, \"agui-recorded\"),\n proxyOnly: values[\"agui-proxy-only\"],\n });\n aguiMount = { path: \"/agui\", handler: agui };\n}\n\nasync function main() {\n // Load fixtures from path (detect file vs directory)\n let isDir: boolean;\n let fixtures;\n try {\n const stat = statSync(fixturePath);\n isDir = stat.isDirectory();\n if (isDir) {\n fixtures = loadFixturesFromDir(fixturePath, logger);\n } else {\n fixtures = loadFixtureFile(fixturePath, logger);\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Fixtures path not found: ${fixturePath}`);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.error(`Failed to load fixtures from ${fixturePath}: ${msg}`);\n }\n process.exit(1);\n }\n\n if (fixtures.length === 0) {\n if (validateOnLoad || values.strict) {\n console.error(\"Error: No fixtures loaded and validation/strict mode is enabled — aborting.\");\n process.exit(1);\n }\n console.warn(\"Warning: No fixtures loaded. The server will return 404 for all requests.\");\n }\n\n logger.info(`Loaded ${fixtures.length} fixture(s) from ${fixturePath}`);\n\n // Validate fixtures if requested\n if (validateOnLoad) {\n const results = validateFixtures(fixtures);\n const errors = results.filter((r) => r.severity === \"error\");\n const warnings = results.filter((r) => r.severity === \"warning\");\n\n for (const w of warnings) {\n logger.warn(`Fixture ${w.fixtureIndex}: ${w.message}`);\n }\n for (const e of errors) {\n logger.error(`Fixture ${e.fixtureIndex}: ${e.message}`);\n }\n\n if (errors.length > 0) {\n console.error(`Validation failed: ${errors.length} error(s), ${warnings.length} warning(s)`);\n process.exit(1);\n }\n }\n\n const mounts = aguiMount ? [aguiMount] : undefined;\n\n const instance = await createServer(\n fixtures,\n {\n port,\n host,\n latency,\n chunkSize,\n logLevel,\n chaos,\n metrics: values.metrics,\n record,\n strict: values.strict,\n journalMaxEntries: journalMax,\n fixtureCountsMaxTestIds: fixtureCountsMax,\n },\n mounts,\n );\n\n logger.info(`aimock server listening on ${instance.url}`);\n\n // Start file watcher if requested\n let watcher: { close: () => void } | null = null;\n if (watchMode) {\n const loadFn = isDir!\n ? () => loadFixturesFromDir(fixturePath, logger)\n : () => loadFixtureFile(fixturePath, logger);\n\n watcher = watchFixtures(fixturePath, fixtures, loadFn, {\n logger,\n validate: validateOnLoad,\n validateFn: validateFixtures,\n });\n logger.info(`Watching ${fixturePath} for changes`);\n }\n\n function shutdown() {\n logger.info(\"Shutting down...\");\n if (watcher) watcher.close();\n instance.server.close(() => {\n process.exit(0);\n });\n }\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n"],"mappings":"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"}
|
package/dist/journal.cjs
CHANGED
|
@@ -20,6 +20,14 @@ function matchCriteriaEqual(a, b) {
|
|
|
20
20
|
var Journal = class {
|
|
21
21
|
entries = [];
|
|
22
22
|
fixtureMatchCountsByTestId = /* @__PURE__ */ new Map();
|
|
23
|
+
maxEntries;
|
|
24
|
+
fixtureCountsMaxTestIds;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
const cap = options.maxEntries;
|
|
27
|
+
this.maxEntries = cap !== void 0 && cap > 0 ? cap : 0;
|
|
28
|
+
const testIdCap = options.fixtureCountsMaxTestIds;
|
|
29
|
+
this.fixtureCountsMaxTestIds = testIdCap !== void 0 && testIdCap > 0 ? testIdCap : 0;
|
|
30
|
+
}
|
|
23
31
|
/** Backwards-compatible accessor — returns the default (no testId) count map. */
|
|
24
32
|
get fixtureMatchCounts() {
|
|
25
33
|
return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);
|
|
@@ -31,6 +39,7 @@ var Journal = class {
|
|
|
31
39
|
...entry
|
|
32
40
|
};
|
|
33
41
|
this.entries.push(full);
|
|
42
|
+
if (this.maxEntries > 0 && this.entries.length > this.maxEntries) this.entries.shift();
|
|
34
43
|
return full;
|
|
35
44
|
}
|
|
36
45
|
getAll(opts) {
|
|
@@ -43,11 +52,31 @@ var Journal = class {
|
|
|
43
52
|
findByFixture(fixture) {
|
|
44
53
|
return this.entries.filter((e) => e.response.fixture === fixture);
|
|
45
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* READ-ONLY accessor. Returns the existing count map for `testId`, or an
|
|
57
|
+
* empty transient Map if none exists. Does NOT insert into the cache and
|
|
58
|
+
* does NOT trigger FIFO eviction — callers may read freely without
|
|
59
|
+
* perturbing cache state. For the write path, see
|
|
60
|
+
* `getOrCreateFixtureMatchCountsForTest`.
|
|
61
|
+
*/
|
|
46
62
|
getFixtureMatchCountsForTest(testId) {
|
|
63
|
+
return this.fixtureMatchCountsByTestId.get(testId) ?? /* @__PURE__ */ new Map();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* WRITE path: get the count map for `testId`, inserting a fresh empty Map
|
|
67
|
+
* if missing and running FIFO eviction when the testId cap is exceeded.
|
|
68
|
+
* Only callers that intend to mutate the map (e.g. incrementing a count)
|
|
69
|
+
* should use this.
|
|
70
|
+
*/
|
|
71
|
+
getOrCreateFixtureMatchCountsForTest(testId) {
|
|
47
72
|
let counts = this.fixtureMatchCountsByTestId.get(testId);
|
|
48
73
|
if (!counts) {
|
|
49
74
|
counts = /* @__PURE__ */ new Map();
|
|
50
75
|
this.fixtureMatchCountsByTestId.set(testId, counts);
|
|
76
|
+
if (this.fixtureCountsMaxTestIds > 0 && this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds) {
|
|
77
|
+
const oldest = this.fixtureMatchCountsByTestId.keys().next().value;
|
|
78
|
+
if (oldest !== void 0) this.fixtureMatchCountsByTestId.delete(oldest);
|
|
79
|
+
}
|
|
51
80
|
}
|
|
52
81
|
return counts;
|
|
53
82
|
}
|
|
@@ -55,7 +84,7 @@ var Journal = class {
|
|
|
55
84
|
return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;
|
|
56
85
|
}
|
|
57
86
|
incrementFixtureMatchCount(fixture, allFixtures, testId = DEFAULT_TEST_ID) {
|
|
58
|
-
const counts = this.
|
|
87
|
+
const counts = this.getOrCreateFixtureMatchCountsForTest(testId);
|
|
59
88
|
counts.set(fixture, (counts.get(fixture) ?? 0) + 1);
|
|
60
89
|
if (fixture.match.sequenceIndex !== void 0 && allFixtures) for (const sibling of allFixtures) {
|
|
61
90
|
if (sibling === fixture) continue;
|
package/dist/journal.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.cjs","names":["generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\n\n/** Sentinel testId used when no explicit test scope is provided. */\nexport const DEFAULT_TEST_ID = \"__default__\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate)\n );\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.
|
|
1
|
+
{"version":3,"file":"journal.cjs","names":["generateId"],"sources":["../src/journal.ts"],"sourcesContent":["import { generateId } from \"./helpers.js\";\nimport type { Fixture, FixtureMatch, JournalEntry } from \"./types.js\";\n\n/** Sentinel testId used when no explicit test scope is provided. */\nexport const DEFAULT_TEST_ID = \"__default__\";\n\n/**\n * Compare two field values, handling RegExp by source+flags rather than reference.\n */\nfunction fieldEqual(a: unknown, b: unknown): boolean {\n if (a instanceof RegExp && b instanceof RegExp)\n return a.source === b.source && a.flags === b.flags;\n return a === b;\n}\n\n/**\n * Check whether two fixture match objects have the same criteria\n * (ignoring sequenceIndex). Used to group sequenced fixtures.\n */\nfunction matchCriteriaEqual(a: FixtureMatch, b: FixtureMatch): boolean {\n return (\n fieldEqual(a.userMessage, b.userMessage) &&\n fieldEqual(a.inputText, b.inputText) &&\n fieldEqual(a.toolCallId, b.toolCallId) &&\n fieldEqual(a.toolName, b.toolName) &&\n fieldEqual(a.model, b.model) &&\n fieldEqual(a.responseFormat, b.responseFormat) &&\n fieldEqual(a.predicate, b.predicate)\n );\n}\n\nexport interface JournalOptions {\n /**\n * Maximum number of entries to retain. When exceeded, oldest entries are\n * dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical\n * default — suitable for short-lived test runs only). Negative values are\n * rejected at the CLI parse layer; programmatically they are treated as 0\n * (unbounded) for back-compat.\n *\n * Long-running servers (e.g. mock proxies in CI/demo environments) should\n * always set a finite cap: every request appends an entry holding the\n * request body + headers + fixture reference, and without a cap the\n * journal grows until the process OOMs.\n */\n maxEntries?: number;\n /**\n * Maximum number of unique testIds retained in the fixture match-count\n * map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId\n * (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for\n * unbounded retention. Negative values are rejected at the CLI parse\n * layer; programmatically they are treated as 0 (unbounded) for\n * back-compat. Without a cap this map can grow over time in long-running\n * servers that see many unique testIds.\n */\n fixtureCountsMaxTestIds?: number;\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly fixtureMatchCountsByTestId: Map<string, Map<Fixture, number>> = new Map();\n private readonly maxEntries: number;\n private readonly fixtureCountsMaxTestIds: number;\n\n constructor(options: JournalOptions = {}) {\n // Treat 0 or negative as \"unbounded\" to preserve prior behavior when\n // the option is omitted or explicitly disabled.\n const cap = options.maxEntries;\n this.maxEntries = cap !== undefined && cap > 0 ? cap : 0;\n const testIdCap = options.fixtureCountsMaxTestIds;\n this.fixtureCountsMaxTestIds = testIdCap !== undefined && testIdCap > 0 ? testIdCap : 0;\n }\n\n /** Backwards-compatible accessor — returns the default (no testId) count map. */\n get fixtureMatchCounts(): Map<Fixture, number> {\n return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);\n }\n\n add(entry: Omit<JournalEntry, \"id\" | \"timestamp\">): JournalEntry {\n const full: JournalEntry = {\n id: generateId(\"req\"),\n timestamp: Date.now(),\n ...entry,\n };\n this.entries.push(full);\n // FIFO eviction when over capacity. Array.prototype.shift() is O(n)\n // regardless of how many we drop per add; we accept it at small caps\n // (default 1000) because the constant factor is tiny and this runs once\n // per request. For much larger caps, switch to a ring buffer for true\n // O(1) eviction.\n if (this.maxEntries > 0 && this.entries.length > this.maxEntries) {\n this.entries.shift();\n }\n return full;\n }\n\n getAll(opts?: { limit?: number }): JournalEntry[] {\n if (opts?.limit !== undefined) {\n return this.entries.slice(-opts.limit);\n }\n return this.entries.slice();\n }\n\n getLast(): JournalEntry | null {\n return this.entries.length > 0 ? this.entries[this.entries.length - 1] : null;\n }\n\n findByFixture(fixture: Fixture): JournalEntry[] {\n return this.entries.filter((e) => e.response.fixture === fixture);\n }\n\n /**\n * READ-ONLY accessor. Returns the existing count map for `testId`, or an\n * empty transient Map if none exists. Does NOT insert into the cache and\n * does NOT trigger FIFO eviction — callers may read freely without\n * perturbing cache state. For the write path, see\n * `getOrCreateFixtureMatchCountsForTest`.\n */\n getFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n return this.fixtureMatchCountsByTestId.get(testId) ?? new Map();\n }\n\n /**\n * WRITE path: get the count map for `testId`, inserting a fresh empty Map\n * if missing and running FIFO eviction when the testId cap is exceeded.\n * Only callers that intend to mutate the map (e.g. incrementing a count)\n * should use this.\n */\n private getOrCreateFixtureMatchCountsForTest(testId: string): Map<Fixture, number> {\n let counts = this.fixtureMatchCountsByTestId.get(testId);\n if (!counts) {\n counts = new Map();\n this.fixtureMatchCountsByTestId.set(testId, counts);\n // FIFO eviction when over capacity. JS Map preserves insertion order,\n // so the first key returned by keys() is the oldest. Same O(n) shift\n // caveat as `entries`: acceptable at small caps (default 500).\n if (\n this.fixtureCountsMaxTestIds > 0 &&\n this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds\n ) {\n const oldest = this.fixtureMatchCountsByTestId.keys().next().value;\n if (oldest !== undefined) {\n this.fixtureMatchCountsByTestId.delete(oldest);\n }\n }\n }\n return counts;\n }\n\n getFixtureMatchCount(fixture: Fixture, testId = DEFAULT_TEST_ID): number {\n return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;\n }\n\n incrementFixtureMatchCount(\n fixture: Fixture,\n allFixtures?: readonly Fixture[],\n testId = DEFAULT_TEST_ID,\n ): void {\n const counts = this.getOrCreateFixtureMatchCountsForTest(testId);\n counts.set(fixture, (counts.get(fixture) ?? 0) + 1);\n // When a sequenced fixture matches, also increment all siblings with matching criteria\n if (fixture.match.sequenceIndex !== undefined && allFixtures) {\n for (const sibling of allFixtures) {\n if (sibling === fixture) continue;\n if (sibling.match.sequenceIndex === undefined) continue;\n if (matchCriteriaEqual(fixture.match, sibling.match)) {\n counts.set(sibling, (counts.get(sibling) ?? 0) + 1);\n }\n }\n }\n }\n\n clearMatchCounts(testId?: string): void {\n if (testId !== undefined) {\n this.fixtureMatchCountsByTestId.delete(testId);\n } else {\n this.fixtureMatchCountsByTestId.clear();\n }\n }\n\n clear(): void {\n this.entries = [];\n this.fixtureMatchCountsByTestId.clear();\n }\n\n get size(): number {\n return this.entries.length;\n }\n}\n"],"mappings":"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"}
|
package/dist/journal.d.cts
CHANGED
|
@@ -3,9 +3,37 @@ import { Fixture, JournalEntry } from "./types.cjs";
|
|
|
3
3
|
//#region src/journal.d.ts
|
|
4
4
|
/** Sentinel testId used when no explicit test scope is provided. */
|
|
5
5
|
declare const DEFAULT_TEST_ID = "__default__";
|
|
6
|
+
interface JournalOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Maximum number of entries to retain. When exceeded, oldest entries are
|
|
9
|
+
* dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical
|
|
10
|
+
* default — suitable for short-lived test runs only). Negative values are
|
|
11
|
+
* rejected at the CLI parse layer; programmatically they are treated as 0
|
|
12
|
+
* (unbounded) for back-compat.
|
|
13
|
+
*
|
|
14
|
+
* Long-running servers (e.g. mock proxies in CI/demo environments) should
|
|
15
|
+
* always set a finite cap: every request appends an entry holding the
|
|
16
|
+
* request body + headers + fixture reference, and without a cap the
|
|
17
|
+
* journal grows until the process OOMs.
|
|
18
|
+
*/
|
|
19
|
+
maxEntries?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum number of unique testIds retained in the fixture match-count
|
|
22
|
+
* map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId
|
|
23
|
+
* (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for
|
|
24
|
+
* unbounded retention. Negative values are rejected at the CLI parse
|
|
25
|
+
* layer; programmatically they are treated as 0 (unbounded) for
|
|
26
|
+
* back-compat. Without a cap this map can grow over time in long-running
|
|
27
|
+
* servers that see many unique testIds.
|
|
28
|
+
*/
|
|
29
|
+
fixtureCountsMaxTestIds?: number;
|
|
30
|
+
}
|
|
6
31
|
declare class Journal {
|
|
7
32
|
private entries;
|
|
8
33
|
private readonly fixtureMatchCountsByTestId;
|
|
34
|
+
private readonly maxEntries;
|
|
35
|
+
private readonly fixtureCountsMaxTestIds;
|
|
36
|
+
constructor(options?: JournalOptions);
|
|
9
37
|
/** Backwards-compatible accessor — returns the default (no testId) count map. */
|
|
10
38
|
get fixtureMatchCounts(): Map<Fixture, number>;
|
|
11
39
|
add(entry: Omit<JournalEntry, "id" | "timestamp">): JournalEntry;
|
|
@@ -14,7 +42,21 @@ declare class Journal {
|
|
|
14
42
|
}): JournalEntry[];
|
|
15
43
|
getLast(): JournalEntry | null;
|
|
16
44
|
findByFixture(fixture: Fixture): JournalEntry[];
|
|
45
|
+
/**
|
|
46
|
+
* READ-ONLY accessor. Returns the existing count map for `testId`, or an
|
|
47
|
+
* empty transient Map if none exists. Does NOT insert into the cache and
|
|
48
|
+
* does NOT trigger FIFO eviction — callers may read freely without
|
|
49
|
+
* perturbing cache state. For the write path, see
|
|
50
|
+
* `getOrCreateFixtureMatchCountsForTest`.
|
|
51
|
+
*/
|
|
17
52
|
getFixtureMatchCountsForTest(testId: string): Map<Fixture, number>;
|
|
53
|
+
/**
|
|
54
|
+
* WRITE path: get the count map for `testId`, inserting a fresh empty Map
|
|
55
|
+
* if missing and running FIFO eviction when the testId cap is exceeded.
|
|
56
|
+
* Only callers that intend to mutate the map (e.g. incrementing a count)
|
|
57
|
+
* should use this.
|
|
58
|
+
*/
|
|
59
|
+
private getOrCreateFixtureMatchCountsForTest;
|
|
18
60
|
getFixtureMatchCount(fixture: Fixture, testId?: string): number;
|
|
19
61
|
incrementFixtureMatchCount(fixture: Fixture, allFixtures?: readonly Fixture[], testId?: string): void;
|
|
20
62
|
clearMatchCounts(testId?: string): void;
|
|
@@ -22,7 +64,6 @@ declare class Journal {
|
|
|
22
64
|
get size(): number;
|
|
23
65
|
}
|
|
24
66
|
//# sourceMappingURL=journal.d.ts.map
|
|
25
|
-
|
|
26
67
|
//#endregion
|
|
27
68
|
export { DEFAULT_TEST_ID, Journal };
|
|
28
69
|
//# sourceMappingURL=journal.d.cts.map
|
package/dist/journal.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.d.cts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;cAIa,eAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"journal.d.cts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;cAIa,eAAA;AAAA,UA2BI,cAAA,CA3BW;EA2BX;AA0BjB;;;;;;;;;;;YAiDmC,CAAA,EAAA,MAAA;;;;;;;;;;;;cAjDtB,OAAA;;;;;wBAMU;;4BAUK,IAAI;aAInB,KAAK,oCAAoC;;;MAkBjB;aAOxB;yBAIY,UAAU;;;;;;;;gDAWa,IAAI;;;;;;;;gCA+BpB;sCAKnB,gCACc"}
|
package/dist/journal.d.ts
CHANGED
|
@@ -3,9 +3,37 @@ import { Fixture, JournalEntry } from "./types.js";
|
|
|
3
3
|
//#region src/journal.d.ts
|
|
4
4
|
/** Sentinel testId used when no explicit test scope is provided. */
|
|
5
5
|
declare const DEFAULT_TEST_ID = "__default__";
|
|
6
|
+
interface JournalOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Maximum number of entries to retain. When exceeded, oldest entries are
|
|
9
|
+
* dropped FIFO. Set to 0 (or omit) for unbounded retention (the historical
|
|
10
|
+
* default — suitable for short-lived test runs only). Negative values are
|
|
11
|
+
* rejected at the CLI parse layer; programmatically they are treated as 0
|
|
12
|
+
* (unbounded) for back-compat.
|
|
13
|
+
*
|
|
14
|
+
* Long-running servers (e.g. mock proxies in CI/demo environments) should
|
|
15
|
+
* always set a finite cap: every request appends an entry holding the
|
|
16
|
+
* request body + headers + fixture reference, and without a cap the
|
|
17
|
+
* journal grows until the process OOMs.
|
|
18
|
+
*/
|
|
19
|
+
maxEntries?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum number of unique testIds retained in the fixture match-count
|
|
22
|
+
* map (`fixtureMatchCountsByTestId`). When exceeded, the oldest testId
|
|
23
|
+
* (by first-insertion order) is evicted FIFO. Set to 0 (or omit) for
|
|
24
|
+
* unbounded retention. Negative values are rejected at the CLI parse
|
|
25
|
+
* layer; programmatically they are treated as 0 (unbounded) for
|
|
26
|
+
* back-compat. Without a cap this map can grow over time in long-running
|
|
27
|
+
* servers that see many unique testIds.
|
|
28
|
+
*/
|
|
29
|
+
fixtureCountsMaxTestIds?: number;
|
|
30
|
+
}
|
|
6
31
|
declare class Journal {
|
|
7
32
|
private entries;
|
|
8
33
|
private readonly fixtureMatchCountsByTestId;
|
|
34
|
+
private readonly maxEntries;
|
|
35
|
+
private readonly fixtureCountsMaxTestIds;
|
|
36
|
+
constructor(options?: JournalOptions);
|
|
9
37
|
/** Backwards-compatible accessor — returns the default (no testId) count map. */
|
|
10
38
|
get fixtureMatchCounts(): Map<Fixture, number>;
|
|
11
39
|
add(entry: Omit<JournalEntry, "id" | "timestamp">): JournalEntry;
|
|
@@ -14,7 +42,21 @@ declare class Journal {
|
|
|
14
42
|
}): JournalEntry[];
|
|
15
43
|
getLast(): JournalEntry | null;
|
|
16
44
|
findByFixture(fixture: Fixture): JournalEntry[];
|
|
45
|
+
/**
|
|
46
|
+
* READ-ONLY accessor. Returns the existing count map for `testId`, or an
|
|
47
|
+
* empty transient Map if none exists. Does NOT insert into the cache and
|
|
48
|
+
* does NOT trigger FIFO eviction — callers may read freely without
|
|
49
|
+
* perturbing cache state. For the write path, see
|
|
50
|
+
* `getOrCreateFixtureMatchCountsForTest`.
|
|
51
|
+
*/
|
|
17
52
|
getFixtureMatchCountsForTest(testId: string): Map<Fixture, number>;
|
|
53
|
+
/**
|
|
54
|
+
* WRITE path: get the count map for `testId`, inserting a fresh empty Map
|
|
55
|
+
* if missing and running FIFO eviction when the testId cap is exceeded.
|
|
56
|
+
* Only callers that intend to mutate the map (e.g. incrementing a count)
|
|
57
|
+
* should use this.
|
|
58
|
+
*/
|
|
59
|
+
private getOrCreateFixtureMatchCountsForTest;
|
|
18
60
|
getFixtureMatchCount(fixture: Fixture, testId?: string): number;
|
|
19
61
|
incrementFixtureMatchCount(fixture: Fixture, allFixtures?: readonly Fixture[], testId?: string): void;
|
|
20
62
|
clearMatchCounts(testId?: string): void;
|
|
@@ -22,7 +64,6 @@ declare class Journal {
|
|
|
22
64
|
get size(): number;
|
|
23
65
|
}
|
|
24
66
|
//# sourceMappingURL=journal.d.ts.map
|
|
25
|
-
|
|
26
67
|
//#endregion
|
|
27
68
|
export { DEFAULT_TEST_ID, Journal };
|
|
28
69
|
//# sourceMappingURL=journal.d.ts.map
|
package/dist/journal.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.d.ts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;cAIa,eAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"journal.d.ts","names":[],"sources":["../src/journal.ts"],"sourcesContent":[],"mappings":";;;;cAIa,eAAA;AAAA,UA2BI,cAAA,CA3BW;EA2BX;AA0BjB;;;;;;;;;;;YAiDmC,CAAA,EAAA,MAAA;;;;;;;;;;;;cAjDtB,OAAA;;;;;wBAMU;;4BAUK,IAAI;aAInB,KAAK,oCAAoC;;;MAkBjB;aAOxB;yBAIY,UAAU;;;;;;;;gDAWa,IAAI;;;;;;;;gCA+BpB;sCAKnB,gCACc"}
|
package/dist/journal.js
CHANGED
|
@@ -20,6 +20,14 @@ function matchCriteriaEqual(a, b) {
|
|
|
20
20
|
var Journal = class {
|
|
21
21
|
entries = [];
|
|
22
22
|
fixtureMatchCountsByTestId = /* @__PURE__ */ new Map();
|
|
23
|
+
maxEntries;
|
|
24
|
+
fixtureCountsMaxTestIds;
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
const cap = options.maxEntries;
|
|
27
|
+
this.maxEntries = cap !== void 0 && cap > 0 ? cap : 0;
|
|
28
|
+
const testIdCap = options.fixtureCountsMaxTestIds;
|
|
29
|
+
this.fixtureCountsMaxTestIds = testIdCap !== void 0 && testIdCap > 0 ? testIdCap : 0;
|
|
30
|
+
}
|
|
23
31
|
/** Backwards-compatible accessor — returns the default (no testId) count map. */
|
|
24
32
|
get fixtureMatchCounts() {
|
|
25
33
|
return this.getFixtureMatchCountsForTest(DEFAULT_TEST_ID);
|
|
@@ -31,6 +39,7 @@ var Journal = class {
|
|
|
31
39
|
...entry
|
|
32
40
|
};
|
|
33
41
|
this.entries.push(full);
|
|
42
|
+
if (this.maxEntries > 0 && this.entries.length > this.maxEntries) this.entries.shift();
|
|
34
43
|
return full;
|
|
35
44
|
}
|
|
36
45
|
getAll(opts) {
|
|
@@ -43,11 +52,31 @@ var Journal = class {
|
|
|
43
52
|
findByFixture(fixture) {
|
|
44
53
|
return this.entries.filter((e) => e.response.fixture === fixture);
|
|
45
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* READ-ONLY accessor. Returns the existing count map for `testId`, or an
|
|
57
|
+
* empty transient Map if none exists. Does NOT insert into the cache and
|
|
58
|
+
* does NOT trigger FIFO eviction — callers may read freely without
|
|
59
|
+
* perturbing cache state. For the write path, see
|
|
60
|
+
* `getOrCreateFixtureMatchCountsForTest`.
|
|
61
|
+
*/
|
|
46
62
|
getFixtureMatchCountsForTest(testId) {
|
|
63
|
+
return this.fixtureMatchCountsByTestId.get(testId) ?? /* @__PURE__ */ new Map();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* WRITE path: get the count map for `testId`, inserting a fresh empty Map
|
|
67
|
+
* if missing and running FIFO eviction when the testId cap is exceeded.
|
|
68
|
+
* Only callers that intend to mutate the map (e.g. incrementing a count)
|
|
69
|
+
* should use this.
|
|
70
|
+
*/
|
|
71
|
+
getOrCreateFixtureMatchCountsForTest(testId) {
|
|
47
72
|
let counts = this.fixtureMatchCountsByTestId.get(testId);
|
|
48
73
|
if (!counts) {
|
|
49
74
|
counts = /* @__PURE__ */ new Map();
|
|
50
75
|
this.fixtureMatchCountsByTestId.set(testId, counts);
|
|
76
|
+
if (this.fixtureCountsMaxTestIds > 0 && this.fixtureMatchCountsByTestId.size > this.fixtureCountsMaxTestIds) {
|
|
77
|
+
const oldest = this.fixtureMatchCountsByTestId.keys().next().value;
|
|
78
|
+
if (oldest !== void 0) this.fixtureMatchCountsByTestId.delete(oldest);
|
|
79
|
+
}
|
|
51
80
|
}
|
|
52
81
|
return counts;
|
|
53
82
|
}
|
|
@@ -55,7 +84,7 @@ var Journal = class {
|
|
|
55
84
|
return this.getFixtureMatchCountsForTest(testId).get(fixture) ?? 0;
|
|
56
85
|
}
|
|
57
86
|
incrementFixtureMatchCount(fixture, allFixtures, testId = DEFAULT_TEST_ID) {
|
|
58
|
-
const counts = this.
|
|
87
|
+
const counts = this.getOrCreateFixtureMatchCountsForTest(testId);
|
|
59
88
|
counts.set(fixture, (counts.get(fixture) ?? 0) + 1);
|
|
60
89
|
if (fixture.match.sequenceIndex !== void 0 && allFixtures) for (const sibling of allFixtures) {
|
|
61
90
|
if (sibling === fixture) continue;
|