@copilotkit/aimock 1.44.0 → 1.45.0
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +31 -0
- package/README.md +6 -0
- package/dist/agui-types.d.ts.map +1 -1
- package/dist/aimock-cli.cjs +31 -5
- package/dist/aimock-cli.cjs.map +1 -1
- package/dist/aimock-cli.d.cts.map +1 -1
- package/dist/aimock-cli.d.ts.map +1 -1
- package/dist/aimock-cli.js +31 -5
- package/dist/aimock-cli.js.map +1 -1
- package/dist/bedrock-converse.cjs +312 -39
- package/dist/bedrock-converse.cjs.map +1 -1
- package/dist/bedrock-converse.d.cts.map +1 -1
- package/dist/bedrock-converse.d.ts.map +1 -1
- package/dist/bedrock-converse.js +313 -40
- package/dist/bedrock-converse.js.map +1 -1
- package/dist/bedrock.cjs +195 -42
- package/dist/bedrock.cjs.map +1 -1
- package/dist/bedrock.d.cts.map +1 -1
- package/dist/bedrock.d.ts.map +1 -1
- package/dist/bedrock.js +196 -43
- package/dist/bedrock.js.map +1 -1
- package/dist/chaos.cjs +1 -0
- package/dist/chaos.js +1 -1
- package/dist/cli.cjs +152 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +155 -8
- package/dist/cli.js.map +1 -1
- package/dist/cohere.cjs +132 -71
- package/dist/cohere.cjs.map +1 -1
- package/dist/cohere.d.cts.map +1 -1
- package/dist/cohere.d.ts.map +1 -1
- package/dist/cohere.js +133 -72
- package/dist/cohere.js.map +1 -1
- package/dist/config-loader.cjs +62 -4
- package/dist/config-loader.cjs.map +1 -1
- package/dist/config-loader.d.cts +37 -4
- package/dist/config-loader.d.cts.map +1 -1
- package/dist/config-loader.d.ts +37 -4
- package/dist/config-loader.d.ts.map +1 -1
- package/dist/config-loader.js +62 -4
- package/dist/config-loader.js.map +1 -1
- package/dist/fakes-plugin.cjs +131 -0
- package/dist/fakes-plugin.cjs.map +1 -0
- package/dist/fakes-plugin.d.cts +23 -0
- package/dist/fakes-plugin.d.cts.map +1 -0
- package/dist/fakes-plugin.d.ts +23 -0
- package/dist/fakes-plugin.d.ts.map +1 -0
- package/dist/fakes-plugin.js +127 -0
- package/dist/fakes-plugin.js.map +1 -0
- package/dist/fixture-loader-services.cjs +1 -1
- package/dist/fixture-loader-services.cjs.map +1 -1
- package/dist/fixture-loader-services.js +1 -1
- package/dist/fixture-loader-services.js.map +1 -1
- package/dist/fixture-loader.cjs +389 -27
- package/dist/fixture-loader.cjs.map +1 -1
- package/dist/fixture-loader.d.cts +47 -3
- package/dist/fixture-loader.d.cts.map +1 -1
- package/dist/fixture-loader.d.ts +47 -3
- package/dist/fixture-loader.d.ts.map +1 -1
- package/dist/fixture-loader.js +384 -29
- package/dist/fixture-loader.js.map +1 -1
- package/dist/gemini-interactions.cjs +250 -52
- package/dist/gemini-interactions.cjs.map +1 -1
- package/dist/gemini-interactions.d.cts.map +1 -1
- package/dist/gemini-interactions.d.ts.map +1 -1
- package/dist/gemini-interactions.js +251 -53
- package/dist/gemini-interactions.js.map +1 -1
- package/dist/gemini.cjs +226 -37
- package/dist/gemini.cjs.map +1 -1
- package/dist/gemini.d.cts.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +227 -38
- package/dist/gemini.js.map +1 -1
- package/dist/helpers.cjs +881 -7
- package/dist/helpers.cjs.map +1 -1
- package/dist/helpers.d.cts +85 -4
- package/dist/helpers.d.cts.map +1 -1
- package/dist/helpers.d.ts +85 -4
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +839 -8
- package/dist/helpers.js.map +1 -1
- package/dist/index.cjs +9 -0
- package/dist/index.d.cts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/jest.cjs +31 -7
- package/dist/jest.cjs.map +1 -1
- package/dist/jest.d.cts +43 -3
- package/dist/jest.d.cts.map +1 -1
- package/dist/jest.d.ts +43 -3
- package/dist/jest.d.ts.map +1 -1
- package/dist/jest.js +33 -9
- package/dist/jest.js.map +1 -1
- package/dist/journal.cjs +135 -13
- package/dist/journal.cjs.map +1 -1
- package/dist/journal.d.cts +38 -4
- package/dist/journal.d.cts.map +1 -1
- package/dist/journal.d.ts +38 -4
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +135 -13
- package/dist/journal.js.map +1 -1
- package/dist/live-sanitize.cjs +4 -32
- package/dist/live-sanitize.cjs.map +1 -1
- package/dist/live-sanitize.js +4 -32
- package/dist/live-sanitize.js.map +1 -1
- package/dist/llmock.cjs +123 -30
- package/dist/llmock.cjs.map +1 -1
- package/dist/llmock.d.cts +16 -1
- package/dist/llmock.d.cts.map +1 -1
- package/dist/llmock.d.ts +16 -1
- package/dist/llmock.d.ts.map +1 -1
- package/dist/llmock.js +125 -32
- package/dist/llmock.js.map +1 -1
- package/dist/mcp-fakes-mount.cjs +1 -0
- package/dist/mcp-fakes-mount.cjs.map +1 -1
- package/dist/mcp-fakes-mount.js +1 -0
- package/dist/mcp-fakes-mount.js.map +1 -1
- package/dist/mcp-fakes-report.cjs +111 -0
- package/dist/mcp-fakes-report.cjs.map +1 -0
- package/dist/mcp-fakes-report.d.cts +40 -0
- package/dist/mcp-fakes-report.d.cts.map +1 -0
- package/dist/mcp-fakes-report.d.ts +40 -0
- package/dist/mcp-fakes-report.d.ts.map +1 -0
- package/dist/mcp-fakes-report.js +107 -0
- package/dist/mcp-fakes-report.js.map +1 -0
- package/dist/mcp-fakes.cjs +331 -8
- package/dist/mcp-fakes.cjs.map +1 -1
- package/dist/mcp-fakes.d.cts +406 -3
- package/dist/mcp-fakes.d.cts.map +1 -1
- package/dist/mcp-fakes.d.ts +406 -3
- package/dist/mcp-fakes.d.ts.map +1 -1
- package/dist/mcp-fakes.js +331 -8
- package/dist/mcp-fakes.js.map +1 -1
- package/dist/mcp-handler.cjs +209 -16
- package/dist/mcp-handler.cjs.map +1 -1
- package/dist/mcp-handler.d.cts +49 -0
- package/dist/mcp-handler.d.cts.map +1 -0
- package/dist/mcp-handler.d.ts +49 -0
- package/dist/mcp-handler.d.ts.map +1 -0
- package/dist/mcp-handler.js +207 -17
- package/dist/mcp-handler.js.map +1 -1
- package/dist/mcp-mock.cjs +112 -1
- package/dist/mcp-mock.cjs.map +1 -1
- package/dist/mcp-mock.d.cts +33 -2
- package/dist/mcp-mock.d.cts.map +1 -1
- package/dist/mcp-mock.d.ts +33 -2
- package/dist/mcp-mock.d.ts.map +1 -1
- package/dist/mcp-mock.js +112 -1
- package/dist/mcp-mock.js.map +1 -1
- package/dist/mcp-recorder.cjs +1224 -0
- package/dist/mcp-recorder.cjs.map +1 -0
- package/dist/mcp-recorder.d.cts +108 -0
- package/dist/mcp-recorder.d.cts.map +1 -0
- package/dist/mcp-recorder.d.ts +108 -0
- package/dist/mcp-recorder.d.ts.map +1 -0
- package/dist/mcp-recorder.js +1215 -0
- package/dist/mcp-recorder.js.map +1 -0
- package/dist/messages.cjs +362 -56
- package/dist/messages.cjs.map +1 -1
- package/dist/messages.d.cts.map +1 -1
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +363 -58
- package/dist/messages.js.map +1 -1
- package/dist/misbehavior.cjs +1523 -0
- package/dist/misbehavior.cjs.map +1 -0
- package/dist/misbehavior.d.cts +43 -0
- package/dist/misbehavior.d.cts.map +1 -0
- package/dist/misbehavior.d.ts +43 -0
- package/dist/misbehavior.d.ts.map +1 -0
- package/dist/misbehavior.js +1511 -0
- package/dist/misbehavior.js.map +1 -0
- package/dist/ollama.cjs +310 -112
- package/dist/ollama.cjs.map +1 -1
- package/dist/ollama.d.cts.map +1 -1
- package/dist/ollama.d.ts.map +1 -1
- package/dist/ollama.js +311 -113
- package/dist/ollama.js.map +1 -1
- package/dist/record-sanitize.cjs +231 -0
- package/dist/record-sanitize.cjs.map +1 -0
- package/dist/record-sanitize.js +224 -0
- package/dist/record-sanitize.js.map +1 -0
- package/dist/recorder.cjs +106 -5
- package/dist/recorder.cjs.map +1 -1
- package/dist/recorder.d.cts +2 -1
- package/dist/recorder.d.cts.map +1 -1
- package/dist/recorder.d.ts +2 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +107 -7
- package/dist/recorder.js.map +1 -1
- package/dist/responses.cjs +667 -171
- package/dist/responses.cjs.map +1 -1
- package/dist/responses.d.cts +5 -13
- package/dist/responses.d.cts.map +1 -1
- package/dist/responses.d.ts +5 -13
- package/dist/responses.d.ts.map +1 -1
- package/dist/responses.js +665 -174
- package/dist/responses.js.map +1 -1
- package/dist/router.cjs +24 -2
- package/dist/router.cjs.map +1 -1
- package/dist/router.d.cts.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +25 -4
- package/dist/router.js.map +1 -1
- package/dist/server.cjs +572 -76
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +574 -78
- package/dist/server.js.map +1 -1
- package/dist/stream-collapse.cjs +236 -34
- package/dist/stream-collapse.cjs.map +1 -1
- package/dist/stream-collapse.d.cts +39 -4
- package/dist/stream-collapse.d.cts.map +1 -1
- package/dist/stream-collapse.d.ts +39 -4
- package/dist/stream-collapse.d.ts.map +1 -1
- package/dist/stream-collapse.js +235 -35
- package/dist/stream-collapse.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +315 -4
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +315 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate-cli.cjs +36 -4
- package/dist/validate-cli.cjs.map +1 -1
- package/dist/validate-cli.js +37 -5
- package/dist/validate-cli.js.map +1 -1
- package/dist/vector-types.d.ts.map +1 -1
- package/dist/version.cjs +45 -0
- package/dist/version.cjs.map +1 -0
- package/dist/version.js +44 -0
- package/dist/version.js.map +1 -0
- package/dist/vitest.cjs +66 -10
- package/dist/vitest.cjs.map +1 -1
- package/dist/vitest.d.cts +35 -4
- package/dist/vitest.d.cts.map +1 -1
- package/dist/vitest.d.ts +35 -4
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +67 -11
- package/dist/vitest.js.map +1 -1
- package/dist/watcher.cjs +6 -2
- package/dist/watcher.cjs.map +1 -1
- package/dist/watcher.js +6 -2
- package/dist/watcher.js.map +1 -1
- package/dist/ws-gemini-live.cjs +210 -101
- package/dist/ws-gemini-live.cjs.map +1 -1
- package/dist/ws-gemini-live.d.cts +9 -3
- package/dist/ws-gemini-live.d.cts.map +1 -1
- package/dist/ws-gemini-live.d.ts +9 -3
- package/dist/ws-gemini-live.d.ts.map +1 -1
- package/dist/ws-gemini-live.js +211 -102
- package/dist/ws-gemini-live.js.map +1 -1
- package/dist/ws-realtime.cjs +175 -62
- package/dist/ws-realtime.cjs.map +1 -1
- package/dist/ws-realtime.d.cts +8 -3
- package/dist/ws-realtime.d.cts.map +1 -1
- package/dist/ws-realtime.d.ts +8 -3
- package/dist/ws-realtime.d.ts.map +1 -1
- package/dist/ws-realtime.js +176 -63
- package/dist/ws-realtime.js.map +1 -1
- package/dist/ws-responses.cjs +109 -16
- package/dist/ws-responses.cjs.map +1 -1
- package/dist/ws-responses.d.cts +8 -12
- package/dist/ws-responses.d.cts.map +1 -1
- package/dist/ws-responses.d.ts +8 -12
- package/dist/ws-responses.d.ts.map +1 -1
- package/dist/ws-responses.js +111 -18
- package/dist/ws-responses.js.map +1 -1
- package/package.json +10 -2
- package/skills/write-fixtures/SKILL.md +179 -21
package/dist/mcp-fakes.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_TEST_ID, MCP_FAKES_ECHO_LIMIT } from "./constants.js";
|
|
2
|
+
import { cutText } from "./echo-text.js";
|
|
2
3
|
import { build, fixed, fullText, jsonText, msg, quote, quoteJson } from "./message-text.js";
|
|
3
4
|
import { FixtureLoadError } from "./fixture-loader.js";
|
|
4
5
|
|
|
@@ -46,7 +47,10 @@ const BLOCK_KEYS = new Set([
|
|
|
46
47
|
"scope",
|
|
47
48
|
"mount",
|
|
48
49
|
"undeclaredTools",
|
|
49
|
-
"tools"
|
|
50
|
+
"tools",
|
|
51
|
+
"list",
|
|
52
|
+
"recorded",
|
|
53
|
+
"timing"
|
|
50
54
|
]);
|
|
51
55
|
const TOOL_KEYS = new Set([
|
|
52
56
|
"name",
|
|
@@ -59,8 +63,21 @@ const CALL_KEYS = new Set([
|
|
|
59
63
|
"anyArgs",
|
|
60
64
|
"result",
|
|
61
65
|
"error",
|
|
62
|
-
"id"
|
|
66
|
+
"id",
|
|
67
|
+
"notifications",
|
|
68
|
+
"durationMs"
|
|
63
69
|
]);
|
|
70
|
+
/**
|
|
71
|
+
* C9: most events one fake event log keeps (per test id, and the untagged
|
|
72
|
+
* log). Past it the oldest event is dropped and the log's report says
|
|
73
|
+
* `evicted: true`. Report-only: claims and snapshots never read it.
|
|
74
|
+
*/
|
|
75
|
+
const MCP_FAKES_MAX_EVENTS_PER_LOG = 1e3;
|
|
76
|
+
/**
|
|
77
|
+
* Largest `args` (JSON, UTF-8 bytes) an event keeps. A larger one is stored as
|
|
78
|
+
* `{ __aimock_truncated: true, originalByteSize }`, the journal `capBody` shape.
|
|
79
|
+
*/
|
|
80
|
+
const MCP_FAKES_MAX_EVENT_ARGS_BYTES = 4096;
|
|
64
81
|
const SCOPE_KEYS = new Set(["testId", "context"]);
|
|
65
82
|
const TIER_ORDER = [
|
|
66
83
|
"testId+context",
|
|
@@ -68,6 +85,29 @@ const TIER_ORDER = [
|
|
|
68
85
|
"context",
|
|
69
86
|
"shared"
|
|
70
87
|
];
|
|
88
|
+
/** The last `seq` given to an event, shared by every store in the process. */
|
|
89
|
+
let lastEventSeq = 0;
|
|
90
|
+
/**
|
|
91
|
+
* An event's `args` as the log keeps it: a frozen copy when its JSON is at
|
|
92
|
+
* most `MCP_FAKES_MAX_EVENT_ARGS_BYTES` UTF-8 bytes, else the journal's
|
|
93
|
+
* truncation marker. A value with no JSON text (`undefined`, or one whose
|
|
94
|
+
* `JSON.stringify` throws) is kept as its rendered text.
|
|
95
|
+
*/
|
|
96
|
+
function boundedArgs(args) {
|
|
97
|
+
let text;
|
|
98
|
+
try {
|
|
99
|
+
text = JSON.stringify(args);
|
|
100
|
+
} catch {
|
|
101
|
+
text = void 0;
|
|
102
|
+
}
|
|
103
|
+
if (text === void 0) return args === void 0 ? void 0 : echo(args);
|
|
104
|
+
const size = Buffer.byteLength(text, "utf8");
|
|
105
|
+
if (size > MCP_FAKES_MAX_EVENT_ARGS_BYTES) return Object.freeze({
|
|
106
|
+
__aimock_truncated: true,
|
|
107
|
+
originalByteSize: size
|
|
108
|
+
});
|
|
109
|
+
return deepFreeze(JSON.parse(text));
|
|
110
|
+
}
|
|
71
111
|
/**
|
|
72
112
|
* The error a failed add throws. It is a `FixtureLoadError` whose `rule`,
|
|
73
113
|
* `file`, `blockId` and `entryId` are those of the first error, and whose
|
|
@@ -150,6 +190,17 @@ function hasOwnKey(obj, key) {
|
|
|
150
190
|
function shown(value, max = MCP_FAKES_ECHO_LIMIT) {
|
|
151
191
|
return jsonText(render(value), max);
|
|
152
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* One user value as message text: `shown`, built alone, so cut to `limit`
|
|
195
|
+
* chars as the builder cuts it. A `limit` below 1 keeps nothing (`""`), as
|
|
196
|
+
* `cutText` does; a `NaN` limit is a `RangeError`.
|
|
197
|
+
*
|
|
198
|
+
* @internal
|
|
199
|
+
*/
|
|
200
|
+
function echo(value, limit = MCP_FAKES_ECHO_LIMIT) {
|
|
201
|
+
if (!(limit >= 1)) return cutText(render(value), limit, "json");
|
|
202
|
+
return build(msg`${shown(value, limit)}`);
|
|
203
|
+
}
|
|
153
204
|
function render(value) {
|
|
154
205
|
if (value === void 0) return "undefined";
|
|
155
206
|
if (typeof value === "number" && !Number.isFinite(value)) return String(value);
|
|
@@ -651,10 +702,11 @@ function shape(required, optional = {}) {
|
|
|
651
702
|
* `Annotations.lastModified`.
|
|
652
703
|
*/
|
|
653
704
|
const ISO_DATETIME = /^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
|
|
705
|
+
const mustBeIsoDateTime = (v, p) => typeof v === "string" && ISO_DATETIME.test(v) ? null : msg`${pathPart(p)} must be an ISO 8601 date-time with seconds and an offset, got ${shown(v)}`;
|
|
654
706
|
const ANNOTATIONS = shape({}, {
|
|
655
707
|
audience: arrayOf(oneOf(["user", "assistant"])),
|
|
656
708
|
priority: (v, p) => typeof v === "number" && v >= 0 && v <= 1 ? null : msg`${pathPart(p)} must be a number from 0 to 1, got ${shown(v)}`,
|
|
657
|
-
lastModified:
|
|
709
|
+
lastModified: mustBeIsoDateTime
|
|
658
710
|
});
|
|
659
711
|
const ICON = shape({ src: mustBeString }, {
|
|
660
712
|
mimeType: mustBeString,
|
|
@@ -772,6 +824,59 @@ function inputSchemaProblem(schema) {
|
|
|
772
824
|
if (!isPlainObject(schema)) return mustBeObject(schema, "inputSchema");
|
|
773
825
|
return jsonProblem(schema, "inputSchema") ?? INPUT_SCHEMA(schema, "inputSchema");
|
|
774
826
|
}
|
|
827
|
+
/** A finite number >= 0 (FA3 `atMs`, FA4 `durationMs`). */
|
|
828
|
+
const mustBeNonNegative = (v, p) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? null : msg`${pathPart(p)} must be a number >= 0, got ${shown(v)}`;
|
|
829
|
+
/** FA2 `recorded`. Other keys are not checked. */
|
|
830
|
+
const RECORDED = shape({
|
|
831
|
+
upstream: mustBeString,
|
|
832
|
+
protocolVersion: mustBeString,
|
|
833
|
+
aimockVersion: mustBeString,
|
|
834
|
+
at: mustBeIsoDateTime
|
|
835
|
+
}, { serverInfo: mustBeObject });
|
|
836
|
+
/** One FA3 notification. Other keys are not checked. */
|
|
837
|
+
const NOTIFICATION = shape({
|
|
838
|
+
atMs: mustBeNonNegative,
|
|
839
|
+
method: mustBeString,
|
|
840
|
+
params: mustBeObject
|
|
841
|
+
});
|
|
842
|
+
/** FA1: an array of JSON objects, each with a string `name`, no name twice. */
|
|
843
|
+
function listProblem(v) {
|
|
844
|
+
if (!Array.isArray(v)) return msg`list must be an array of tool objects, got ${shown(v)}`;
|
|
845
|
+
const json = jsonProblem(v, "list");
|
|
846
|
+
if (json) return json;
|
|
847
|
+
const names = /* @__PURE__ */ new Set();
|
|
848
|
+
for (let i = 0; i < v.length; i++) {
|
|
849
|
+
const tool = v[i];
|
|
850
|
+
if (!isPlainObject(tool) || typeof tool.name !== "string") return msg`list[${i}] must be a tool object with a string name, got ${shown(tool)}`;
|
|
851
|
+
if (names.has(tool.name)) return msg`list[${i}] repeats the tool name ${quote(tool.name)}`;
|
|
852
|
+
names.add(tool.name);
|
|
853
|
+
}
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
/** FA2. */
|
|
857
|
+
function recordedProblem(v) {
|
|
858
|
+
if (!isPlainObject(v)) return msg`recorded must be a JSON object, got ${shown(v)}`;
|
|
859
|
+
return jsonProblem(v, "recorded") ?? RECORDED(v, "recorded");
|
|
860
|
+
}
|
|
861
|
+
/** FA3. */
|
|
862
|
+
function notificationsProblem(v) {
|
|
863
|
+
if (!Array.isArray(v)) return msg`notifications must be an array, got ${shown(v)}`;
|
|
864
|
+
return jsonProblem(v, "notifications") ?? arrayOf(NOTIFICATION, "an array of notifications")(v, "notifications");
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* A malformed FA key of one block (and, for FA3/FA4, one entry): the 1.44.0
|
|
868
|
+
* rule `mcp-fakes/bad-block:h`, with the `record-replay/fa-*` value first in
|
|
869
|
+
* the message detail (`[mcp-fakes/bad-block:h] [record-replay/fa-list] ...`).
|
|
870
|
+
*/
|
|
871
|
+
function faError(ctx, faRule, detail, entryId = null) {
|
|
872
|
+
return new FixtureLoadError({
|
|
873
|
+
rule: "mcp-fakes/bad-block:h",
|
|
874
|
+
file: ctx.source,
|
|
875
|
+
blockId: ctx.blockId,
|
|
876
|
+
entryId,
|
|
877
|
+
detail: msg`[${fixed(faRule)}] ${detail}`
|
|
878
|
+
});
|
|
879
|
+
}
|
|
775
880
|
/**
|
|
776
881
|
* Validate one call entry of the input snapshot. Every error goes to
|
|
777
882
|
* `errors`; returns its entry id whether or not it is clean, so that the
|
|
@@ -795,6 +900,14 @@ function validateCall(raw, toolName, label, index, ctx, errors) {
|
|
|
795
900
|
}
|
|
796
901
|
const before = errors.length;
|
|
797
902
|
for (const key of presentKeys(raw)) if (!CALL_KEYS.has(key)) bad("h", msg`has unknown key ${shown(key)}`);
|
|
903
|
+
if (has(raw, "notifications")) {
|
|
904
|
+
const problem = notificationsProblem(raw.notifications);
|
|
905
|
+
if (problem) errors.push(faError(ctx, "record-replay/fa-notifications", msg`${where} ${problem}`, entryId));
|
|
906
|
+
}
|
|
907
|
+
if (has(raw, "durationMs")) {
|
|
908
|
+
const problem = mustBeNonNegative(raw.durationMs, "durationMs");
|
|
909
|
+
if (problem) errors.push(faError(ctx, "record-replay/fa-duration", msg`${where} ${problem}`, entryId));
|
|
910
|
+
}
|
|
798
911
|
if (has(raw, "id") && !isNonEmptyString(raw.id)) bad("d", msg`id must be a non-empty string, got ${shown(raw.id)}`);
|
|
799
912
|
if (has(raw, "anyArgs") && raw.anyArgs !== true) bad("j", msg`anyArgs must be true, got ${shown(raw.anyArgs)}`);
|
|
800
913
|
const hasArgs = has(raw, "args");
|
|
@@ -817,13 +930,17 @@ function validateCall(raw, toolName, label, index, ctx, errors) {
|
|
|
817
930
|
};
|
|
818
931
|
const match = hasArgs ? { args: copyJson(raw.args, true) } : { anyArgs: true };
|
|
819
932
|
const answer = hasResult ? { result: copyJson(raw.result, true) } : { error: raw.error };
|
|
933
|
+
const recorded = {};
|
|
934
|
+
if (has(raw, "notifications")) recorded.notifications = copyJson(raw.notifications, true);
|
|
935
|
+
if (has(raw, "durationMs")) recorded.durationMs = raw.durationMs;
|
|
820
936
|
return {
|
|
821
937
|
entryId,
|
|
822
938
|
call: Object.freeze({
|
|
823
939
|
id: entryId,
|
|
824
940
|
index,
|
|
825
941
|
...match,
|
|
826
|
-
...answer
|
|
942
|
+
...answer,
|
|
943
|
+
...recorded
|
|
827
944
|
})
|
|
828
945
|
};
|
|
829
946
|
}
|
|
@@ -891,6 +1008,17 @@ function validateBlock(raw, source, blockIndex, load, errors) {
|
|
|
891
1008
|
}
|
|
892
1009
|
const before = errors.length;
|
|
893
1010
|
for (const key of presentKeys(raw)) if (!BLOCK_KEYS.has(key)) errors.push(badBlock(ctx, "h", msg`block has unknown key ${shown(key)}`));
|
|
1011
|
+
if (has(raw, "list")) {
|
|
1012
|
+
const problem = listProblem(raw.list);
|
|
1013
|
+
if (problem) errors.push(faError(ctx, "record-replay/fa-list", problem));
|
|
1014
|
+
}
|
|
1015
|
+
if (has(raw, "recorded")) {
|
|
1016
|
+
const problem = recordedProblem(raw.recorded);
|
|
1017
|
+
if (problem) errors.push(faError(ctx, "record-replay/fa-recorded", problem));
|
|
1018
|
+
}
|
|
1019
|
+
let timing = "recorded";
|
|
1020
|
+
if (has(raw, "timing")) if (raw.timing === "recorded" || raw.timing === "immediate") timing = raw.timing;
|
|
1021
|
+
else errors.push(faError(ctx, "record-replay/fa-timing", msg`timing must be "recorded" or "immediate", got ${shown(raw.timing)}`));
|
|
894
1022
|
const scope = validateScope(raw.scope, ctx, errors);
|
|
895
1023
|
let undeclared;
|
|
896
1024
|
if (has(raw, "undeclaredTools")) if (raw.undeclaredTools === "allow" || raw.undeclaredTools === "deny") undeclared = raw.undeclaredTools;
|
|
@@ -947,7 +1075,10 @@ function validateBlock(raw, source, blockIndex, load, errors) {
|
|
|
947
1075
|
tier: tierOf(scope),
|
|
948
1076
|
mount,
|
|
949
1077
|
...undeclared !== void 0 ? { undeclaredTools: undeclared } : {},
|
|
950
|
-
tools: Object.freeze(tools)
|
|
1078
|
+
tools: Object.freeze(tools),
|
|
1079
|
+
...has(raw, "list") ? { list: copyJson(raw.list, true) } : {},
|
|
1080
|
+
...has(raw, "recorded") ? { recorded: copyJson(raw.recorded, true) } : {},
|
|
1081
|
+
timing
|
|
951
1082
|
}),
|
|
952
1083
|
entryIds,
|
|
953
1084
|
warnings
|
|
@@ -1194,6 +1325,21 @@ var McpFakeStore = class {
|
|
|
1194
1325
|
*/
|
|
1195
1326
|
loadCounts = /* @__PURE__ */ new Map();
|
|
1196
1327
|
maxTestIds;
|
|
1328
|
+
/**
|
|
1329
|
+
* RP2 fake event logs: supplied test id → its events, oldest first. Map
|
|
1330
|
+
* insertion order is the FIFO order of the test-id cap.
|
|
1331
|
+
*/
|
|
1332
|
+
events = /* @__PURE__ */ new Map();
|
|
1333
|
+
/** The event log of requests with no test id: a ring of the newest events. */
|
|
1334
|
+
untaggedEvents = [];
|
|
1335
|
+
/** The untagged event log dropped an event (C9). */
|
|
1336
|
+
untaggedOverflow = false;
|
|
1337
|
+
/**
|
|
1338
|
+
* C9: test ids whose event log overflowed or was evicted, so their report
|
|
1339
|
+
* is incomplete (`evicted: true`). Report-only: `claim`, `snapshot` and
|
|
1340
|
+
* `policy` never read it, so tool calls are unchanged.
|
|
1341
|
+
*/
|
|
1342
|
+
reportOverflow = /* @__PURE__ */ new Set();
|
|
1197
1343
|
constructor(options = {}) {
|
|
1198
1344
|
this.maxTestIds = checkCap(options.maxTestIds ?? MCP_FAKES_DEFAULT_MAX_TEST_IDS);
|
|
1199
1345
|
}
|
|
@@ -1238,10 +1384,10 @@ var McpFakeStore = class {
|
|
|
1238
1384
|
add(sources, origin) {
|
|
1239
1385
|
const originData = snapshotInput(origin);
|
|
1240
1386
|
const kind = isPlainObject(originData) ? originData.kind : void 0;
|
|
1241
|
-
if (kind !== "file" && kind !== "code" && kind !== "control-api") throw new McpFakesAddError([badBlock({
|
|
1387
|
+
if (kind !== "file" && kind !== "code" && kind !== "control-api" && kind !== "record") throw new McpFakesAddError([badBlock({
|
|
1242
1388
|
source: null,
|
|
1243
1389
|
blockId: null
|
|
1244
|
-
}, "i", isPlainObject(originData) ? msg`origin.kind must be "file", "code"
|
|
1390
|
+
}, "i", isPlainObject(originData) ? msg`origin.kind must be "file", "code", "control-api" or "record", got ${shown(kind)}` : msg`origin must be an object { kind }, got ${shown(originData)}`)], []);
|
|
1245
1391
|
const runtimeNumber = kind === "file" ? null : this.nextRuntimeNumber(kind);
|
|
1246
1392
|
const runtimeSource = runtimeNumber === null ? null : `${kind}#${runtimeNumber}`;
|
|
1247
1393
|
const input = snapshotInput(sources, MCP_FAKES_MAX_DEPTH + 2);
|
|
@@ -1390,11 +1536,172 @@ var McpFakeStore = class {
|
|
|
1390
1536
|
this.consumed.clear();
|
|
1391
1537
|
this.untagged.clear();
|
|
1392
1538
|
this.evicted.clear();
|
|
1539
|
+
this.events.clear();
|
|
1540
|
+
this.reportOverflow.clear();
|
|
1541
|
+
this.untaggedEvents = [];
|
|
1542
|
+
this.untaggedOverflow = false;
|
|
1393
1543
|
return;
|
|
1394
1544
|
}
|
|
1395
1545
|
this.consumed.delete(testId);
|
|
1396
1546
|
this.evicted.delete(testId);
|
|
1397
|
-
|
|
1547
|
+
this.events.delete(testId);
|
|
1548
|
+
this.reportOverflow.delete(testId);
|
|
1549
|
+
if (testId === MCP_FAKES_DEFAULT_TEST_ID) {
|
|
1550
|
+
this.untagged.clear();
|
|
1551
|
+
this.untaggedEvents = [];
|
|
1552
|
+
this.untaggedOverflow = false;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* MR12: mark entries consumed for `testId` (null = untagged), as a claim
|
|
1557
|
+
* would. Ids not loaded on this mount are ignored, and so is a test id the
|
|
1558
|
+
* cap evicted (its claims fail as `evicted` until it is reset).
|
|
1559
|
+
*/
|
|
1560
|
+
markConsumed(testId, entryIds) {
|
|
1561
|
+
if (testId !== null && this.evicted.has(testId)) return;
|
|
1562
|
+
const known = entryIds.filter((entryId) => this.ids.has(entryId));
|
|
1563
|
+
if (known.length === 0) return;
|
|
1564
|
+
const state = this.stateFor(testId);
|
|
1565
|
+
for (const entryId of known) state.add(entryId);
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* AM7/FA1: the `list` of the most specific applicable tier that has one,
|
|
1569
|
+
* else null. Inside a tier, the first block in load order with a `list`.
|
|
1570
|
+
*/
|
|
1571
|
+
recordedList(identity) {
|
|
1572
|
+
const blocks = this.applicable(identity);
|
|
1573
|
+
for (const tier of TIER_ORDER) {
|
|
1574
|
+
const block = blocks.find((b) => b.tier === tier && b.list !== void 0);
|
|
1575
|
+
if (block?.list) return block.list;
|
|
1576
|
+
}
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
/** FA5 timing and the FA3/FA4 values of an answered entry (for MR15 framing). */
|
|
1580
|
+
replayOf(entryId) {
|
|
1581
|
+
for (const block of this.blocks) for (const tool of block.tools) {
|
|
1582
|
+
const entry = tool.calls.find((c) => c.id === entryId);
|
|
1583
|
+
if (entry) return {
|
|
1584
|
+
timing: block.timing,
|
|
1585
|
+
notifications: entry.notifications ?? NO_NOTIFICATIONS,
|
|
1586
|
+
durationMs: entry.durationMs
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
/** C7: true when any loaded call entry has a `notifications/message` (gates logging). */
|
|
1592
|
+
hasRecordedLogs() {
|
|
1593
|
+
return this.blocks.some((block) => block.tools.some((tool) => tool.calls.some((call) => call.notifications?.some((n) => n.method === "notifications/message"))));
|
|
1594
|
+
}
|
|
1595
|
+
/**
|
|
1596
|
+
* RP2: append one event to the identity's log, with the next process-wide
|
|
1597
|
+
* `seq` and its `args` bounded (`MCP_FAKES_MAX_EVENT_ARGS_BYTES`). A log
|
|
1598
|
+
* past `MCP_FAKES_MAX_EVENTS_PER_LOG` drops its oldest event and its report
|
|
1599
|
+
* says `evicted: true`; a new test id past the test-id cap evicts the
|
|
1600
|
+
* oldest test id's log the same way (C9). Never touches consumption state.
|
|
1601
|
+
*/
|
|
1602
|
+
logEvent(identity, event) {
|
|
1603
|
+
lastEventSeq += 1;
|
|
1604
|
+
const stored = Object.freeze({
|
|
1605
|
+
...event,
|
|
1606
|
+
args: boundedArgs(event.args),
|
|
1607
|
+
seq: lastEventSeq
|
|
1608
|
+
});
|
|
1609
|
+
const testId = identity.testId;
|
|
1610
|
+
if (testId === null) {
|
|
1611
|
+
this.untaggedEvents.push(stored);
|
|
1612
|
+
if (this.untaggedEvents.length > MCP_FAKES_MAX_EVENTS_PER_LOG) {
|
|
1613
|
+
this.untaggedEvents.shift();
|
|
1614
|
+
this.untaggedOverflow = true;
|
|
1615
|
+
}
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
let log = this.events.get(testId);
|
|
1619
|
+
if (!log) {
|
|
1620
|
+
log = [];
|
|
1621
|
+
this.events.set(testId, log);
|
|
1622
|
+
this.enforceEventCap();
|
|
1623
|
+
}
|
|
1624
|
+
log.push(stored);
|
|
1625
|
+
if (log.length > MCP_FAKES_MAX_EVENTS_PER_LOG) {
|
|
1626
|
+
log.shift();
|
|
1627
|
+
this.reportOverflow.add(testId);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* RP3 parts for this mount (report assembly is src/mcp-fakes-report.ts).
|
|
1632
|
+
* `served`, `failures` and `unfaked` come from the events whose context is
|
|
1633
|
+
* exactly `context`, in `seq` order; `unconsumed` (tiers `testId+context`
|
|
1634
|
+
* and `testId`) and `sharedUnconsumed` (tiers `context` and `shared`) are
|
|
1635
|
+
* the applicable entries of this store that `testId` has not consumed,
|
|
1636
|
+
* labelled with `mount`, the path the store's mock is served at. Every
|
|
1637
|
+
* block in the store answers there (`claim` does not read `block.mount`),
|
|
1638
|
+
* so none is left out by its own `mount` value: a mock that loaded its own
|
|
1639
|
+
* fakes (default mount "/mcp") and is mounted at another path still reports
|
|
1640
|
+
* them. Reads only: building a report changes no state.
|
|
1641
|
+
*/
|
|
1642
|
+
reportPart(testId, context, mount) {
|
|
1643
|
+
const part = {
|
|
1644
|
+
evicted: testId === null ? this.untaggedOverflow : this.evicted.has(testId) || this.reportOverflow.has(testId),
|
|
1645
|
+
served: [],
|
|
1646
|
+
unconsumed: [],
|
|
1647
|
+
unfaked: [],
|
|
1648
|
+
failures: [],
|
|
1649
|
+
sharedUnconsumed: []
|
|
1650
|
+
};
|
|
1651
|
+
const log = testId === null ? this.untaggedEvents : this.events.get(testId) ?? [];
|
|
1652
|
+
for (const event of log) {
|
|
1653
|
+
if (event.context !== context) continue;
|
|
1654
|
+
const { seq, tool, args } = event;
|
|
1655
|
+
if (event.outcome === "answered") part.served.push({
|
|
1656
|
+
entryId: event.entryId,
|
|
1657
|
+
mount: event.mount,
|
|
1658
|
+
tool,
|
|
1659
|
+
args,
|
|
1660
|
+
seq
|
|
1661
|
+
});
|
|
1662
|
+
else if (event.outcome === "unfaked") part.unfaked.push({
|
|
1663
|
+
mount: event.mount,
|
|
1664
|
+
tool,
|
|
1665
|
+
args,
|
|
1666
|
+
answeredBy: event.answeredBy
|
|
1667
|
+
});
|
|
1668
|
+
else part.failures.push({
|
|
1669
|
+
code: event.code,
|
|
1670
|
+
mount: event.mount,
|
|
1671
|
+
tool,
|
|
1672
|
+
args,
|
|
1673
|
+
seq
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
const used = this.stateOf(testId);
|
|
1677
|
+
for (const block of this.applicable({
|
|
1678
|
+
testId,
|
|
1679
|
+
context,
|
|
1680
|
+
undeclared: null
|
|
1681
|
+
})) {
|
|
1682
|
+
const own = block.tier === "testId+context" || block.tier === "testId";
|
|
1683
|
+
for (const tool of block.tools) for (const call of tool.calls) {
|
|
1684
|
+
if (used?.has(call.id)) continue;
|
|
1685
|
+
if (!own) part.sharedUnconsumed.push({
|
|
1686
|
+
entryId: call.id,
|
|
1687
|
+
mount,
|
|
1688
|
+
tool: tool.name
|
|
1689
|
+
});
|
|
1690
|
+
else if (call.args) part.unconsumed.push({
|
|
1691
|
+
entryId: call.id,
|
|
1692
|
+
mount,
|
|
1693
|
+
tool: tool.name,
|
|
1694
|
+
args: copyJson(call.args)
|
|
1695
|
+
});
|
|
1696
|
+
else part.unconsumed.push({
|
|
1697
|
+
entryId: call.id,
|
|
1698
|
+
mount,
|
|
1699
|
+
tool: tool.name,
|
|
1700
|
+
anyArgs: true
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return part;
|
|
1398
1705
|
}
|
|
1399
1706
|
/** Every entry id on this mount, in load order. */
|
|
1400
1707
|
allEntryIds() {
|
|
@@ -1581,9 +1888,25 @@ var McpFakeStore = class {
|
|
|
1581
1888
|
if (this.consumed.size <= this.maxTestIds) break;
|
|
1582
1889
|
this.consumed.delete(oldest);
|
|
1583
1890
|
this.evicted.add(oldest);
|
|
1891
|
+
if (this.events.delete(oldest)) this.reportOverflow.add(oldest);
|
|
1892
|
+
}
|
|
1893
|
+
this.enforceEventCap();
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* The test-id FIFO cap on event logs: evict the oldest logs until the cap
|
|
1897
|
+
* holds, marking each in the report-only `reportOverflow` (never `evicted`).
|
|
1898
|
+
*/
|
|
1899
|
+
enforceEventCap() {
|
|
1900
|
+
if (this.maxTestIds === 0) return;
|
|
1901
|
+
for (const oldest of this.events.keys()) {
|
|
1902
|
+
if (this.events.size <= this.maxTestIds) break;
|
|
1903
|
+
this.events.delete(oldest);
|
|
1904
|
+
this.reportOverflow.add(oldest);
|
|
1584
1905
|
}
|
|
1585
1906
|
}
|
|
1586
1907
|
};
|
|
1908
|
+
/** `replayOf` notifications of an entry that recorded none. */
|
|
1909
|
+
const NO_NOTIFICATIONS = Object.freeze([]);
|
|
1587
1910
|
/**
|
|
1588
1911
|
* The detail of a block-id collision (bad block (f)). The message prefix
|
|
1589
1912
|
* already names the block id, so the detail names the two sources.
|