mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 10:43:30 +00:00
* test(mcp): open stdio clients through the shared local-tools fixture Twelve files built their own StdioClientTransport and Client for `rea mcp`. Those clients never listed tools, so the SDK did not validate structured results against advertised output schemas, and each file repeated transport setup, stderr draining and close ordering. Use connectLocalToolsMcp, which lists tools, validates replies, silences logging, leaves deep providers unconfigured and owns cleanup. Per-test clients register test-scoped cleanup; module-level clients collect cleanup for afterAll. * test(mcp): keep failed file-scoped MCP cleanup owned The three module-level clients removed every cleanup callback before running them, so a failed cleanup lost its only handle. Add fileLocalToolsMcp, which connects before the file's tests and removes each cleanup only after it succeeds, and use it in those files.
206 lines
7.5 KiB
TypeScript
206 lines
7.5 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
cliTest(
|
|
"retains large event names and emitter-scoped listener ownership through CLI and stdio MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-event-payload-");
|
|
const name = "payload/ğ".repeat(80_000);
|
|
const lookalike = `value-sha256:${createHash("sha256").update(JSON.stringify(name)).digest("hex")}`;
|
|
const source = [
|
|
'import {EventEmitter} from "node:events";',
|
|
"const bus = new EventEmitter(); const other = new EventEmitter(); const handler = () => {};",
|
|
`bus.on(${JSON.stringify(name)}, handler);`,
|
|
`bus.emit(${JSON.stringify(name)});`,
|
|
`bus.off(${JSON.stringify(name)}, handler);`,
|
|
`bus.on(${JSON.stringify(lookalike)}, handler); bus.emit(${JSON.stringify(lookalike)});`,
|
|
'bus.on("ok", handler); bus.emit("ok"); other.on("ok", handler); other.emit("ok");',
|
|
"bus.on(getEventName(), handler);",
|
|
].join("\n");
|
|
await writeFile(join(root, "main.js"), source);
|
|
const cliResponse = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--artifact-format",
|
|
"directory",
|
|
"--json",
|
|
],
|
|
timeoutMs: 60_000,
|
|
});
|
|
expect(cliResponse.exitCode).toBe(0);
|
|
const cliEvidence = toolContract(
|
|
"analyze_javascript_application",
|
|
).outputSchema.parse(cliResponse.json);
|
|
const { client } = await connectLocalToolsMcp();
|
|
const response = await client.callTool({
|
|
name: "analyze_javascript_application",
|
|
arguments: { input_path: root, format: "directory" },
|
|
});
|
|
expect(response.isError).not.toBe(true);
|
|
const evidence = toolContract(
|
|
"analyze_javascript_application",
|
|
).outputSchema.parse(response.structuredContent);
|
|
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
|
|
const text = response.content.find((item) => item.type === "text");
|
|
if (text?.type !== "text") throw new Error("Missing MCP text result");
|
|
expect(
|
|
toolContract("analyze_javascript_application").outputSchema.parse(
|
|
JSON.parse(text.text),
|
|
).evidence_id,
|
|
).toBe(evidence.evidence_id);
|
|
const graph = javascriptApplicationAnalysisResultSchema.parse(
|
|
evidence.normalized_result,
|
|
).semantic_graph;
|
|
const events = graph.nodes.filter((node) => node.kind === "event");
|
|
const selected = events.filter(
|
|
(node) => node.properties.event_name === name,
|
|
);
|
|
expect(selected).toHaveLength(1);
|
|
const event = selected[0];
|
|
if (event === undefined) throw new Error("Complete event was lost");
|
|
const listeners = graph.nodes.filter(
|
|
(node) => node.kind === "listener" && node.properties.event_name === name,
|
|
);
|
|
expect(new Set(listeners.map((node) => node.properties.method))).toEqual(
|
|
new Set(["on", "off"]),
|
|
);
|
|
const listenerIds = new Set(listeners.map((node) => node.node_id));
|
|
expect(
|
|
graph.relations
|
|
.filter(
|
|
(relation) =>
|
|
relation.source_node_id === event.node_id &&
|
|
listenerIds.has(relation.target_node_id),
|
|
)
|
|
.map((relation) => relation.relation),
|
|
).toEqual(
|
|
expect.arrayContaining(["registers-listener", "removes-listener"]),
|
|
);
|
|
const lookalikeEvent = events.find(
|
|
(node) => node.properties.event_name === lookalike,
|
|
);
|
|
expect(lookalikeEvent).toBeDefined();
|
|
expect(lookalikeEvent?.node_id).not.toBe(event.node_id);
|
|
const shortEvents = events.filter(
|
|
(node) => node.properties.event_name === "ok",
|
|
);
|
|
expect(shortEvents).toHaveLength(2);
|
|
expect(
|
|
new Set(shortEvents.map((node) => node.properties.emitter_key)).size,
|
|
).toBe(2);
|
|
expect(
|
|
shortEvents.every(
|
|
(node) =>
|
|
node.label === "ok" && node.identity.role_key.endsWith("\0ok"),
|
|
),
|
|
).toBe(true);
|
|
const dynamic = events.find((node) => node.properties.event_name === null);
|
|
expect(dynamic?.label).toBeNull();
|
|
expect(
|
|
graph.unknowns.some(
|
|
(unknown) =>
|
|
unknown.node_id === dynamic?.node_id && unknown.family === "event",
|
|
),
|
|
).toBe(true);
|
|
const traceResponse = await client.callTool({
|
|
name: "trace_javascript_semantics",
|
|
arguments: {
|
|
application: {
|
|
kind: "retained-evidence",
|
|
evidence_id: evidence.evidence_id,
|
|
},
|
|
query: {
|
|
seed: { kind: "event", name },
|
|
direction: "forward-influence",
|
|
},
|
|
},
|
|
});
|
|
expect(traceResponse.isError).not.toBe(true);
|
|
const traceEvidence = toolContract(
|
|
"trace_javascript_semantics",
|
|
).outputSchema.parse(traceResponse.structuredContent);
|
|
const trace = javaScriptSemanticTraceResultSchema.parse(
|
|
traceEvidence.normalized_result,
|
|
);
|
|
expect(new Set(trace.seed_node_ids)).toEqual(
|
|
new Set([event.node_id, ...listenerIds]),
|
|
);
|
|
expect(
|
|
trace.nodes
|
|
.filter((node) => listenerIds.has(node.node_id))
|
|
.map((node) => node.properties.event_name),
|
|
).toEqual(listeners.map(() => name));
|
|
await client.ping();
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"traces a retained empty literal endpoint by its preserved value",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-empty-endpoint-seed-");
|
|
await writeFile(
|
|
join(root, "main.js"),
|
|
'export const load = async () => (await fetch("")).json();\n',
|
|
);
|
|
const analyzed = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--artifact-format",
|
|
"directory",
|
|
"--json",
|
|
],
|
|
timeoutMs: 60_000,
|
|
});
|
|
expect(analyzed.exitCode, analyzed.stderr).toBe(0);
|
|
const application = toolContract(
|
|
"analyze_javascript_application",
|
|
).outputSchema.parse(analyzed.json);
|
|
const graph = javascriptApplicationAnalysisResultSchema.parse(
|
|
application.normalized_result,
|
|
).semantic_graph;
|
|
// Analysis keeps the complete literal endpoint, here the empty string.
|
|
const retained = new Set(
|
|
graph.nodes
|
|
.filter(
|
|
(node) =>
|
|
(node.kind === "request" || node.kind === "response") &&
|
|
node.properties.endpoint === "",
|
|
)
|
|
.map((node) => node.node_id),
|
|
);
|
|
expect(retained.size).toBeGreaterThan(0);
|
|
const request = join(root, "endpoint-query.json");
|
|
await writeFile(
|
|
request,
|
|
JSON.stringify({
|
|
application,
|
|
query: {
|
|
seed: { kind: "endpoint", value: "" },
|
|
direction: "forward-influence",
|
|
},
|
|
}),
|
|
);
|
|
const traced = await cli.run({
|
|
arguments: ["trace-javascript-semantics", request, "--json"],
|
|
timeoutMs: 60_000,
|
|
});
|
|
expect(traced.exitCode, traced.stdout).toBe(0);
|
|
const trace = javaScriptSemanticTraceResultSchema.parse(
|
|
toolContract("trace_javascript_semantics").outputSchema.parse(traced.json)
|
|
.normalized_result,
|
|
);
|
|
expect(new Set(trace.seed_node_ids)).toEqual(retained);
|
|
},
|
|
);
|