Files
rea/tests/acceptance/applications/javascriptEndpointPayload.test.ts
7ad75bdc80 fix: restore JavaScript analysis and current regression checks (#3080)
Restore source-position-aware ambient aliases, scalar env/argv reads, candidate
endpoint worker data, Chrome DOMSnapshot empty strings, and accumulated Ghidra
LaunchSupport diagnostics. Align regression fixtures with declared current
contracts, while preserving identity, provenance, cancellation, and cleanup
checks. Assert workflow caveats independently at both established locations.

Validation: original five source failures reproduced on main; 307 focused tests
passed with one platform skip, 22 fixture follow-up tests and 22 workflow/MCP
tests passed; full static and docs checks passed. Real public http-server CLI
analyses preserve identical artifact identity, statistics, and 581-token selected
output. Real Chromium confirms restored DOM form destinations; isolated real
service-worker verification passes on main and the change. The complete local
browser run retained an unclassified later marker timeout; current-head CI
provides complete real-browser and recovery lane results. Native IDA/Ghidra
execution and complete local suite execution are not claimed.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:17:54 +08:00

266 lines
9.0 KiB
TypeScript

import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { applicationVersionComparisonResultSchema } from "../../../src/domain/javascript/javascriptApplicationVersionComparisonSchemas.js";
import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascript/javascriptSemanticTraceSchemas.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"keeps full endpoint evidence usable without copying large values into metadata",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-endpoint-payload-");
const endpoint = `https://example.invalid/${"part/ğ".repeat(160_000)}`;
const digestLookalike = `value-sha256:${createHash("sha256").update(JSON.stringify(endpoint)).digest("hex")}`;
// UTF-8 replacement would conflate these distinct JavaScript strings.
const surrogate = "\ud800".repeat(100);
const replacement = "\ufffd".repeat(100);
const values = [
endpoint,
digestLookalike,
"/ok",
"/ok",
surrogate,
replacement,
];
const source = values
.map((value) => `fetch(${JSON.stringify(value)});`)
.join("\n");
const sourcePath = join(root, "main.js");
await writeFile(sourcePath, 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 = new Client({ name: "endpoint-payload-e2e", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "" },
stderr: "pipe",
});
transport.stderr?.on("data", () => undefined);
try {
await client.connect(transport);
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");
const textEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(JSON.parse(text.text));
expect(textEvidence.evidence_id).toBe(evidence.evidence_id);
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
const endpoints = result.graph.nodes.filter(
(node) => node.kind === "endpoint",
);
const nodeFor = (value: string) => {
const node = endpoints.find((candidate) =>
candidate.observations.some(
({ properties }) => properties.value === value,
),
);
if (node === undefined) throw new Error("Complete endpoint was lost");
return node;
};
const endpointNode = nodeFor(endpoint);
expect(
new Set(
[endpoint, digestLookalike, surrogate, replacement].map(
(value) => nodeFor(value).node_id,
),
).size,
).toBe(4);
const repeatedNode = nodeFor("/ok");
expect(
endpoints.filter((node) =>
node.observations.some(
({ properties }) => properties.value === "/ok",
),
),
).toHaveLength(1);
expect(repeatedNode.observations).toHaveLength(2);
expect(repeatedNode.identity).toMatchObject({
key: JSON.stringify(["literal", "/ok"]),
});
expect(
repeatedNode.observations.every(({ label }) => label === "/ok"),
).toBe(true);
expect(endpointNode.identity).toMatchObject({
artifact_sha256: createHash("sha256").update(source).digest("hex"),
});
expect(endpointNode.observations[0]?.evidence.location).toMatchObject({
available: true,
value: { kind: "source-range", source: "main.js" },
});
const requests = result.semantic_graph.nodes.filter(
(node) =>
node.kind === "request" && node.properties.endpoint === endpoint,
);
expect(requests).toHaveLength(1);
await verifyEndpointWorkflows({
client,
evidenceId: evidence.evidence_id,
endpoint,
endpointNodeId: endpointNode.node_id,
requestNodeIds: requests.map(({ node_id }) => node_id),
root,
source,
sourcePath,
});
await client.ping();
} finally {
await client.close();
await transport.close();
}
},
120_000,
);
interface EndpointWorkflowInput {
readonly client: Client;
readonly evidenceId: string;
readonly endpoint: string;
readonly endpointNodeId: string;
readonly requestNodeIds: readonly string[];
readonly root: string;
readonly source: string;
readonly sourcePath: string;
}
const verifyEndpointWorkflows = async ({
client,
evidenceId,
endpoint,
endpointNodeId,
requestNodeIds,
root,
source,
sourcePath,
}: EndpointWorkflowInput): Promise<void> => {
const application = {
kind: "retained-evidence",
evidence_id: evidenceId,
};
const featureResponse = await client.callTool({
name: "trace_application_feature",
arguments: {
application,
seed: {
kind: "string",
value: endpoint,
match: "exact",
case_sensitive: true,
},
},
});
expect(featureResponse.isError).not.toBe(true);
const featureEvidence = toolContract(
"trace_application_feature",
).outputSchema.parse(featureResponse.structuredContent);
const feature = applicationFeatureTraceResultSchema.parse(
featureEvidence.normalized_result,
);
expect(feature.seed_matches.map(({ node_id }) => node_id)).toContain(
endpointNodeId,
);
const traceResponse = await client.callTool({
name: "trace_javascript_semantics",
arguments: {
application,
query: {
seed: { kind: "endpoint", value: endpoint },
direction: "backward-provenance",
},
},
});
expect(traceResponse.isError).not.toBe(true);
const traceEvidence = toolContract(
"trace_javascript_semantics",
).outputSchema.parse(traceResponse.structuredContent);
const trace = javaScriptSemanticTraceResultSchema.parse(
traceEvidence.normalized_result,
);
expect(trace.seed_node_ids).toEqual(requestNodeIds);
expect(
trace.nodes.some((node) => node.properties.endpoint === endpoint),
).toBe(true);
await writeFile(sourcePath, `// next artifact version\n${source}`);
const nextResponse = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root, format: "directory" },
});
expect(nextResponse.isError).not.toBe(true);
const nextEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(nextResponse.structuredContent);
const next = javascriptApplicationAnalysisResultSchema.parse(
nextEvidence.normalized_result,
);
const nextEndpoint = next.graph.nodes.find(
(node) =>
node.kind === "endpoint" &&
node.observations.some(({ properties }) => properties.value === endpoint),
);
if (nextEndpoint === undefined) throw new Error("Next endpoint was lost");
const comparisonResponse = await client.callTool({
name: "compare_application_versions",
arguments: {
left: application,
right: {
kind: "retained-evidence",
evidence_id: nextEvidence.evidence_id,
},
},
});
expect(comparisonResponse.isError).not.toBe(true);
const comparisonEvidence = toolContract(
"compare_application_versions",
).outputSchema.parse(comparisonResponse.structuredContent);
const comparison = applicationVersionComparisonResultSchema.parse(
comparisonEvidence.normalized_result,
);
expect(comparison.items).toContainEqual(
expect.objectContaining({
node_kind: "endpoint",
left_node_id: endpointNodeId,
right_node_id: nextEndpoint.node_id,
match: expect.objectContaining({
status: "matched",
basis: "semantic-key",
}),
}),
);
};