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.
123 lines
4.5 KiB
TypeScript
123 lines
4.5 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { readFile, 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 { fileLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
const mcp = fileLocalToolsMcp();
|
|
|
|
const large = "x".repeat(200_000);
|
|
for (const scenario of [
|
|
{ name: "quoted literal", expression: JSON.stringify(large), limited: true },
|
|
{ name: "cooked template", expression: `\`${large}\``, limited: true },
|
|
{
|
|
name: "limited member receiver",
|
|
expression: `(${JSON.stringify(large)}).field`,
|
|
limited: true,
|
|
},
|
|
{
|
|
name: "configuration-only default",
|
|
expression: `process.env.MODE ?? ${JSON.stringify(large)}`,
|
|
bare: true,
|
|
limited: true,
|
|
},
|
|
{
|
|
name: "equivalent addition",
|
|
expression: `${JSON.stringify(large.slice(0, 100_000))} + ${JSON.stringify(large.slice(100_000))}`,
|
|
limited: true,
|
|
},
|
|
{ name: "small exact control", expression: '"small"', limited: false },
|
|
]) {
|
|
cliTest(
|
|
`preserves normalized string-byte coverage for ${scenario.name}`,
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-literal-budget-");
|
|
const path = join(root, "main.js");
|
|
const source =
|
|
"bare" in scenario
|
|
? `${scenario.expression};\n`
|
|
: `export const payload = ${scenario.expression};\n`;
|
|
await writeFile(path, source);
|
|
const digest = createHash("sha256").update(source).digest("hex");
|
|
const cliResponse = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
timeoutMs: 60_000,
|
|
});
|
|
expect(cliResponse.exitCode).toBe(0);
|
|
const cliEvidence = toolContract(
|
|
"analyze_javascript_application",
|
|
).outputSchema.parse(cliResponse.json);
|
|
const response = await mcp.client.callTool({
|
|
name: "analyze_javascript_application",
|
|
arguments: { input_path: root },
|
|
});
|
|
expect(response.isError).not.toBe(true);
|
|
const evidence = toolContract(
|
|
"analyze_javascript_application",
|
|
).outputSchema.parse(response.structuredContent);
|
|
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
|
|
|
|
for (const candidate of [cliEvidence, evidence]) {
|
|
const analysis = javascriptApplicationAnalysisResultSchema.parse(
|
|
candidate.normalized_result,
|
|
);
|
|
const stringLengths = analysis.semantic_graph.nodes.flatMap(
|
|
({ properties }) =>
|
|
typeof properties.value === "string"
|
|
? [properties.value.length]
|
|
: [],
|
|
);
|
|
expect(stringLengths).not.toContain(large.length);
|
|
if (scenario.limited) {
|
|
expect(analysis.graph.coverage.status).toBe("partial");
|
|
expect(analysis.graph.coverage.limits).toContainEqual(
|
|
expect.objectContaining({
|
|
name: "javascript_semantic_primitive_bytes",
|
|
unit: "bytes",
|
|
value: 1_048_576,
|
|
}),
|
|
);
|
|
expect(analysis.semantic_graph.unknowns).toContainEqual(
|
|
expect.objectContaining({
|
|
reason: "resource-limit",
|
|
detail: expect.stringMatching(
|
|
/primitive string-byte budget exceeded/i,
|
|
),
|
|
}),
|
|
);
|
|
} else {
|
|
expect(stringLengths).toContain(5);
|
|
expect(analysis.graph.coverage.limits).toEqual([]);
|
|
}
|
|
if ("bare" in scenario) {
|
|
const node = analysis.semantic_graph.nodes.find(
|
|
({ kind, properties }) =>
|
|
kind === "config-source" && properties.source_kind === "default",
|
|
);
|
|
expect(node?.properties).toMatchObject({
|
|
value: null,
|
|
value_resource_limit: "primitive-bytes",
|
|
});
|
|
expect(analysis.semantic_graph.unknowns).toContainEqual(
|
|
expect.objectContaining({
|
|
node_id: node?.node_id,
|
|
reason: "resource-limit",
|
|
}),
|
|
);
|
|
}
|
|
expect(
|
|
analysis.graph.nodes.find((node) => node.kind === "javascript-asset")
|
|
?.identity,
|
|
).toMatchObject({ sha256: digest });
|
|
}
|
|
expect(await readFile(path, "utf8")).toBe(source);
|
|
await mcp.client.ping();
|
|
},
|
|
);
|
|
}
|