Fixes #3933. Part of #4027. Normalize exact literals and configuration defaults through the shared primitive owner. Preserve tagged limits through member/destructuring projection and record encountered bounds in per-analysis state independently of later selected values. Remove the retained-value rescan and duplicate default reader. Preserve source Evidence, bounded/null values, aggregate wire capacity and complete endpoint lookup identities for query/trace/comparison. Validated 66 focused and 932 semantic tests on the restored identical runtime; final public selection 7/7 adds an augmented large-endpoint acceptance (67 distinct focused cases across invocations). Full static/docs and fresh real Obsidian controls passed. Compact measurements and rejected attempts remain recorded. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
138 lines
5.0 KiB
TypeScript
138 lines
5.0 KiB
TypeScript
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { createHash } from "node:crypto";
|
|
import { readFile, writeFile } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { afterAll, beforeAll, expect } from "vitest";
|
|
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
const client = new Client({ name: "literal-budget-boundary", 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",
|
|
});
|
|
beforeAll(async () => {
|
|
await client.connect(transport);
|
|
transport.stderr?.on("data", () => undefined);
|
|
});
|
|
afterAll(async () => {
|
|
await Promise.all([client.close(), transport.close()]);
|
|
});
|
|
|
|
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 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 client.ping();
|
|
},
|
|
);
|
|
}
|