mirror of
https://github.com/morluto/rea.git
synced 2026-10-22 05:42:31 -05:00
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)
176 lines
5.6 KiB
JavaScript
176 lines
5.6 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { fileURLToPath } from "node:url";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { closeVerifierResources } from "./verifier-cleanup.mjs";
|
|
|
|
// REA registers process-static tool contracts. Bind their parsed definitions to
|
|
// the SDK's negotiated connection identity, including when a Client reconnects.
|
|
const toolBindings = new WeakMap();
|
|
|
|
const advertisedTool = async (client, name) => {
|
|
const connection = client.getServerVersion();
|
|
assert.ok(connection, "MCP must be initialized before binding tool schemas");
|
|
let binding = toolBindings.get(client);
|
|
if (binding?.connection !== connection) {
|
|
binding = {
|
|
connection,
|
|
definitions: client.listTools().then(({ tools }) => {
|
|
const definitions = new Map();
|
|
for (const tool of tools) {
|
|
assert.ok(
|
|
!definitions.has(tool.name),
|
|
`Duplicate MCP tool: ${tool.name}`,
|
|
);
|
|
definitions.set(tool.name, tool);
|
|
}
|
|
return definitions;
|
|
}),
|
|
};
|
|
toolBindings.set(client, binding);
|
|
}
|
|
let definitions;
|
|
try {
|
|
definitions = await binding.definitions;
|
|
assert.equal(
|
|
client.getServerVersion(),
|
|
connection,
|
|
"MCP connection changed while binding tool schemas",
|
|
);
|
|
} catch (cause) {
|
|
if (toolBindings.get(client) === binding) toolBindings.delete(client);
|
|
throw cause;
|
|
}
|
|
const tool = definitions.get(name);
|
|
assert.ok(tool, `MCP catalog did not advertise ${name}`);
|
|
return tool;
|
|
};
|
|
|
|
/** Call an advertised tool with SDK output validation of its actual schema. */
|
|
export async function artifactMcpCall(client, parameters, options = {}) {
|
|
options.signal?.throwIfAborted();
|
|
assert.equal(
|
|
options.toolDefinition,
|
|
undefined,
|
|
"Artifact verification owns the advertised schema; pass request options only",
|
|
);
|
|
const toolDefinition = await advertisedTool(client, parameters.name);
|
|
assert.notEqual(
|
|
toolDefinition.outputSchema,
|
|
undefined,
|
|
`MCP tool omitted its output schema: ${parameters.name}`,
|
|
);
|
|
options.signal?.throwIfAborted();
|
|
return client.callTool(parameters, { ...options, toolDefinition });
|
|
}
|
|
|
|
const environment = () => {
|
|
const compileCache =
|
|
process.env.NODE_COMPILE_CACHE ??
|
|
process.env.REA_ARTIFACT_NODE_COMPILE_CACHE;
|
|
return {
|
|
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
...(compileCache &&
|
|
process.env.NODE_V8_COVERAGE === undefined &&
|
|
process.env.NODE_DISABLE_COMPILE_CACHE !== "1"
|
|
? { NODE_COMPILE_CACHE: compileCache }
|
|
: {}),
|
|
REA_LOG_LEVEL: "silent",
|
|
REA_ANALYSIS_PROVIDER: "auto",
|
|
// Artifact workflows have no deep-provider prerequisite or GUI authority.
|
|
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
|
};
|
|
};
|
|
|
|
/** Execute the compiled CLI with production providers and no test adapters. */
|
|
export async function artifactCli(
|
|
command,
|
|
target,
|
|
arguments_ = [],
|
|
options = {},
|
|
) {
|
|
return (await artifactCliEvidence(command, target, arguments_, options))
|
|
.normalized_result;
|
|
}
|
|
|
|
/** Execute the compiled CLI and return its complete Evidence record. */
|
|
export async function artifactCliEvidence(
|
|
command,
|
|
target,
|
|
arguments_ = [],
|
|
{ timeoutMs = 60000 } = {},
|
|
) {
|
|
const { stdout } = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
fileURLToPath(new URL("../rea.mjs", import.meta.url)),
|
|
command,
|
|
target,
|
|
...arguments_,
|
|
"--json",
|
|
],
|
|
{ env: environment(), timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 },
|
|
);
|
|
const evidence = JSON.parse(stdout);
|
|
assert.equal(evidence.error, undefined, stdout);
|
|
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
|
|
return evidence;
|
|
}
|
|
|
|
const withOpenedArtifactMcp = async (target, openTool, verify) => {
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
|
|
env: environment(),
|
|
stderr: "pipe",
|
|
});
|
|
const client = new Client({ name: "artifact-real-e2e", version: "1" });
|
|
let failure;
|
|
try {
|
|
await client.connect(transport);
|
|
const opened = await artifactMcpCall(client, {
|
|
name: openTool,
|
|
arguments: { path: target },
|
|
});
|
|
assert.notEqual(opened.isError, true, JSON.stringify(opened));
|
|
if (openTool === "open_bundle_resources")
|
|
assert.equal(opened.structuredContent?.result?.caller_path, target);
|
|
await verify(client);
|
|
} catch (cause) {
|
|
failure = cause;
|
|
throw cause;
|
|
} finally {
|
|
toolBindings.delete(client);
|
|
await closeVerifierResources(
|
|
[() => client.close(), () => transport.close()],
|
|
failure,
|
|
);
|
|
}
|
|
};
|
|
|
|
/** Run a real stdio MCP subprocess with an explicitly opened binary target. */
|
|
export async function withArtifactMcp(target, verify) {
|
|
return withOpenedArtifactMcp(target, "open_binary", verify);
|
|
}
|
|
|
|
/** Run a real stdio MCP subprocess with an explicitly opened bundle resource root. */
|
|
export async function withBundleResourcesMcp(target, verify) {
|
|
return withOpenedArtifactMcp(target, "open_bundle_resources", verify);
|
|
}
|
|
|
|
/** Require a successful structured MCP tool result rather than its text rendering. */
|
|
export async function artifactMcpResult(client, name, arguments_ = {}) {
|
|
const result = await artifactMcpCall(client, {
|
|
name,
|
|
arguments: arguments_,
|
|
});
|
|
assert.notEqual(result.isError, true, JSON.stringify(result));
|
|
assert.ok(
|
|
result.structuredContent?.normalized_result,
|
|
"MCP omitted structured result",
|
|
);
|
|
return result.structuredContent.normalized_result;
|
|
}
|