Bind artifact lifecycle and analysis calls to their advertised named SDK output schemas, sharing discovery per connection and rebinding on reconnect. Reject schema overrides and ambiguous catalogs, preserve request options, and retain primary failures while attempting all owned cleanup. Add real-stdio boundary regressions for malformed output, discovery reuse, reconnection, schema ambiguity, override rejection and timeout preservation. Assisted-by: Reimu (gensokyo-skills:reimu-incident-triage) Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing)
210 lines
5.8 KiB
JavaScript
210 lines
5.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { fileURLToPath } from "node:url";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
|
|
import * as artifact from "../../scripts/lib/artifact-e2e.mjs";
|
|
import { closeVerifierResources } from "../../scripts/lib/verifier-cleanup.mjs";
|
|
|
|
const [mode, target] = process.argv.slice(2);
|
|
assert.ok(
|
|
[
|
|
"healthy",
|
|
"corrupt",
|
|
"reuse",
|
|
"concurrent",
|
|
"reconnect",
|
|
"missing-schema",
|
|
"duplicate-first",
|
|
"duplicate-last",
|
|
"override",
|
|
"request-options",
|
|
].includes(mode),
|
|
);
|
|
assert.equal(typeof target, "string");
|
|
|
|
// Isolate the fault to this child client. The server, resource admission and
|
|
// advertised schema are real; alter only the reply before tool validation.
|
|
const request = Client.prototype.request;
|
|
let catalogRequests = 0;
|
|
let corruptedReplies = 0;
|
|
let toolCalls = 0;
|
|
let connection = 0;
|
|
const toolTimeouts = [];
|
|
Client.prototype.request = async function (input, options) {
|
|
if (input.method === "tools/list") catalogRequests++;
|
|
if (input.method === "tools/call") {
|
|
toolCalls++;
|
|
toolTimeouts.push(options?.timeout ?? null);
|
|
}
|
|
const result = await request.call(this, input, options);
|
|
if (
|
|
mode === "override" &&
|
|
input.method === "tools/call" &&
|
|
input.params.name === "binary_session"
|
|
) {
|
|
corruptedReplies++;
|
|
return { ...result, structuredContent: { result: 777 } };
|
|
}
|
|
if (input.method === "tools/list") {
|
|
if (mode === "missing-schema")
|
|
return {
|
|
...result,
|
|
tools: result.tools.map((tool) => {
|
|
if (tool.name !== "open_bundle_resources") return tool;
|
|
const { outputSchema: _schema, ...withoutSchema } = tool;
|
|
return withoutSchema;
|
|
}),
|
|
};
|
|
if (mode === "duplicate-first" || mode === "duplicate-last") {
|
|
const listed = result.tools.find(
|
|
(tool) => tool.name === "open_bundle_resources",
|
|
);
|
|
assert.ok(listed);
|
|
const duplicate = {
|
|
...listed,
|
|
outputSchema: { type: "object", required: ["__rea_wrong_schema"] },
|
|
};
|
|
return {
|
|
...result,
|
|
tools:
|
|
mode === "duplicate-first"
|
|
? [duplicate, ...result.tools]
|
|
: [...result.tools, duplicate],
|
|
};
|
|
}
|
|
if (mode === "reconnect" && connection === 2)
|
|
return {
|
|
...result,
|
|
tools: result.tools.map((tool) =>
|
|
tool.name === "binary_session"
|
|
? {
|
|
...tool,
|
|
outputSchema: {
|
|
type: "object",
|
|
required: ["__rea_second_connection"],
|
|
},
|
|
}
|
|
: tool,
|
|
),
|
|
};
|
|
}
|
|
if (
|
|
mode !== "corrupt" ||
|
|
input.method !== "tools/call" ||
|
|
input.params.name !== "open_bundle_resources"
|
|
)
|
|
return result;
|
|
assert.equal(
|
|
typeof result.structuredContent?.result?.canonical_path,
|
|
"string",
|
|
"The real reply omitted the field selected for fault injection",
|
|
);
|
|
corruptedReplies++;
|
|
return {
|
|
...result,
|
|
structuredContent: {
|
|
...result.structuredContent,
|
|
result: { ...result.structuredContent.result, canonical_path: 777 },
|
|
},
|
|
};
|
|
};
|
|
|
|
const status = async (client) => {
|
|
const result = await artifact.artifactMcpCall(client, {
|
|
name: "binary_session",
|
|
arguments: {},
|
|
});
|
|
assert.notEqual(result.isError, true, JSON.stringify(result));
|
|
};
|
|
|
|
async function standaloneConnections() {
|
|
const client = new Client({ name: "artifact-binding-probe", version: "1" });
|
|
let firstVersion;
|
|
for (
|
|
connection = 1;
|
|
connection <= (mode === "reconnect" ? 2 : 1);
|
|
connection++
|
|
) {
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [
|
|
fileURLToPath(new URL("../../scripts/rea.mjs", import.meta.url)),
|
|
"mcp",
|
|
],
|
|
env: {
|
|
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
REA_LOG_LEVEL: "silent",
|
|
},
|
|
stderr: "pipe",
|
|
});
|
|
let failure;
|
|
try {
|
|
await client.connect(transport);
|
|
const version = client.getServerVersion();
|
|
assert.ok(version);
|
|
if (firstVersion !== undefined) {
|
|
assert.notEqual(version, firstVersion);
|
|
assert.equal(version.name, firstVersion.name);
|
|
assert.equal(version.version, firstVersion.version);
|
|
}
|
|
firstVersion = version;
|
|
if (mode === "concurrent")
|
|
await Promise.all([status(client), status(client)]);
|
|
else await status(client);
|
|
} catch (cause) {
|
|
failure = cause;
|
|
throw cause;
|
|
} finally {
|
|
await closeVerifierResources(
|
|
[() => client.close(), () => transport.close()],
|
|
failure,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let error;
|
|
try {
|
|
if (mode === "concurrent" || mode === "reconnect")
|
|
await standaloneConnections();
|
|
else
|
|
await artifact.withBundleResourcesMcp(target, async (client) => {
|
|
if (mode === "reuse") await Promise.all([status(client), status(client)]);
|
|
if (mode === "override")
|
|
await artifact.artifactMcpCall(
|
|
client,
|
|
{ name: "binary_session", arguments: {} },
|
|
{
|
|
toolDefinition: {
|
|
name: "binary_session",
|
|
inputSchema: { type: "object" },
|
|
outputSchema: { type: "object" },
|
|
},
|
|
},
|
|
);
|
|
if (mode === "request-options")
|
|
await artifact.artifactMcpCall(
|
|
client,
|
|
{ name: "binary_session", arguments: {} },
|
|
{ timeout: 12_345 },
|
|
);
|
|
});
|
|
} catch (cause) {
|
|
error = {
|
|
name: cause instanceof Error ? cause.name : "unknown",
|
|
message: cause instanceof Error ? cause.message : String(cause),
|
|
code: cause?.code ?? null,
|
|
};
|
|
}
|
|
process.stdout.write(
|
|
JSON.stringify({
|
|
accepted: error === undefined,
|
|
catalogRequests,
|
|
corruptedReplies,
|
|
toolCalls,
|
|
toolTimeouts,
|
|
error: error ?? null,
|
|
}) + "\n",
|
|
);
|