Files
rea/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts
morluto 71a024c789 refactor(javascript): consume normalized feature seeds
Require parsed seed facts in the pure trace projection and leave raw request defaults at the shared workflow boundary. Remove the redundant seed parse, unused raw-seed alias and duplicate seed-match binding.

Move omitted-versus-explicit defaults and full Evidence identity checks to compiled CLI/MCP acceptance. Reuse the owned MCP fixture and request files in the tests. Matching, native handoff, empty-literal, coverage and immutability checks remain covered; public schemas and successful outputs are unchanged.
2026-10-23 10:22:07 +08:00

297 lines
9.4 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import {
applicationFeatureSeedSchema,
applicationFeatureTraceResultSchema,
} from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
cliTest,
type SharedMcpCall,
type TestCli,
} from "../../support/cli/cliFixture.js";
type FeatureSeed = z.input<typeof applicationFeatureSeedSchema>;
type Trace = z.infer<typeof applicationFeatureTraceResultSchema>;
type SeedScenario = {
readonly name: string;
readonly seed: FeatureSeed;
readonly expected: {
readonly nodeId: string;
readonly basis: "node-id" | "property";
} | null;
};
const createSeedScenarios = (
graph: z.infer<typeof javascriptApplicationAnalysisResultSchema>["graph"],
): SeedScenario[] => {
const notificationRoute = graph.nodes.find(
(node) =>
node.kind === "endpoint" &&
node.observations.some(
({ properties }) =>
properties.endpoint_kind === "route" &&
properties.value === "/accounts/preferences/notifications",
),
);
const tinyRoute = graph.nodes.find(
(node) =>
node.kind === "endpoint" &&
node.observations.some(
({ properties }) =>
properties.endpoint_kind === "route" && properties.value === "/tiny",
),
);
const labeledNode = graph.nodes.find((node) =>
node.observations.some(
({ label }) => label !== null && label !== node.node_id,
),
);
if (notificationRoute === undefined || tinyRoute === undefined)
throw new Error(
"Expected literal route endpoints in the application graph",
);
if (labeledNode === undefined)
throw new Error("Expected a labeled node in the application graph");
const label = labeledNode.observations.find(
({ label: value }) => value !== null && value !== labeledNode.node_id,
)?.label;
if (label === undefined || label === null)
throw new Error("Expected the selected graph node to have a display label");
const id = labeledNode.node_id;
return [
{
name: "node ID contains prefix",
seed: { kind: "node-id", value: id.slice(0, 24), match: "contains" },
expected: { nodeId: id, basis: "node-id" },
},
{
name: "node ID exact ignores case by default",
seed: { kind: "node-id", value: id.toUpperCase(), match: "exact" },
expected: { nodeId: id, basis: "node-id" },
},
{
name: "node ID exact can be case sensitive",
seed: {
kind: "node-id",
value: id.toUpperCase(),
match: "exact",
case_sensitive: true,
},
expected: null,
},
{
name: "node ID never falls back to a display label",
seed: { kind: "node-id", value: label, match: "exact" },
expected: null,
},
{
name: "generated route label exact",
seed: {
kind: "route",
value: "route endpoint",
match: "exact",
case_sensitive: true,
},
expected: null,
},
{
name: "generated route label substring",
seed: { kind: "route", value: "route", match: "contains" },
expected: null,
},
{
name: "preserved long route exact",
seed: {
kind: "route",
value: "/accounts/preferences/notifications",
match: "exact",
},
expected: { nodeId: notificationRoute.node_id, basis: "property" },
},
{
name: "preserved route substring",
seed: {
kind: "route",
value: "/accounts/preferences",
match: "contains",
},
expected: { nodeId: notificationRoute.node_id, basis: "property" },
},
{
name: "short preserved route exact",
seed: { kind: "route", value: "/tiny", match: "exact" },
expected: { nodeId: tinyRoute.node_id, basis: "property" },
},
];
};
const analyzeFixture = async (cli: TestCli, root: string) => {
const response = await cli.run({
arguments: [
"analyze-javascript-application",
root,
"--artifact-format",
"directory",
"--json",
],
});
expect(response.exitCode).toBe(0);
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(response.json);
return {
evidence,
result: javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
),
};
};
const traceThroughCli = async (
cli: TestCli,
application: unknown,
scenario: Pick<SeedScenario, "name" | "seed">,
) => {
const root = await createTestTempDirectory("rea-feature-seed-request-");
const input = join(root, "trace.json");
await writeFile(
input,
JSON.stringify({ application, seed: scenario.seed, direction: "both" }),
);
const response = await cli.run({
arguments: ["trace-application-feature", input, "--json"],
});
expect(response.exitCode, `${scenario.name} via CLI`).toBe(0);
const evidence = toolContract("trace_application_feature").outputSchema.parse(
response.json,
);
return {
evidence,
result: applicationFeatureTraceResultSchema.parse(
evidence.normalized_result,
),
};
};
const traceThroughMcp = async (
call: SharedMcpCall,
application: unknown,
scenario: Pick<SeedScenario, "name" | "seed">,
) => {
const response = await call("trace_application_feature", {
application,
seed: scenario.seed,
direction: "both",
});
expect(response.isError, `${scenario.name} via MCP`).not.toBe(true);
const evidence = toolContract("trace_application_feature").outputSchema.parse(
response.structuredContent,
);
return {
evidence,
result: applicationFeatureTraceResultSchema.parse(
evidence.normalized_result,
),
};
};
const expectTrace = (trace: Trace, scenario: SeedScenario): void => {
if (scenario.expected === null) {
expect(trace.seed_matches).toEqual([]);
expect(trace.summary.matched_seeds).toBe(0);
expect(trace.coverage.status).toBe("no-match");
return;
}
expect(trace.seed_matches).toHaveLength(1);
expect(trace.seed_matches[0]).toMatchObject({
node_id: scenario.expected.nodeId,
basis: scenario.expected.basis,
});
expect(trace.summary.matched_seeds).toBe(1);
expect(trace.coverage.status).toBe("complete-within-source");
if (scenario.seed.kind === "node-id")
expect(trace.seed_matches[0]).toMatchObject({
basis: "node-id",
field: "node_id",
});
else
expect(trace.seed_matches[0]?.field).toMatch(
/\.properties\.(value|path)$/u,
);
};
cliTest(
"matches feature seeds against node IDs and preserved route values through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-feature-seed-selection-");
await writeFile(
join(root, "routes.js"),
`const notificationRoute = { path: "/accounts/preferences/notifications" };\nconst tinyRoute = { path: "/tiny" };\nmodule.exports = { notificationRoute, tinyRoute };\n`,
);
const { evidence, result } = await analyzeFixture(cli, root);
const call = await cli.sharedMcpCall?.();
if (call === undefined) throw new Error("Expected file MCP session");
for (const scenario of createSeedScenarios(result.graph)) {
const { result: cliTrace } = await traceThroughCli(
cli,
evidence,
scenario,
);
const { result: mcpTrace } = await traceThroughMcp(
call,
evidence,
scenario,
);
expectTrace(cliTrace, scenario);
expectTrace(mcpTrace, scenario);
expect(mcpTrace.seed_matches).toEqual(cliTrace.seed_matches);
expect(mcpTrace.coverage).toEqual(cliTrace.coverage);
}
},
);
cliTest(
"normalizes API seed defaults before deriving matching and missing trace identities through CLI and MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-feature-seed-defaults-");
await writeFile(
join(root, "preload.js"),
`
const { contextBridge } = require("electron");
contextBridge.exposeInMainWorld("desktop", { openProject: () => true });
`,
);
const { evidence } = await analyzeFixture(cli, root);
const call = await cli.sharedMcpCall?.();
if (call === undefined) throw new Error("Expected file MCP session");
for (const value of ["openProject", "missingProject"]) {
const omitted = { name: value, seed: { kind: "api" as const, value } };
const explicit = {
...omitted,
seed: { ...omitted.seed, match: null, case_sensitive: false },
};
const omittedCli = await traceThroughCli(cli, evidence, omitted);
const explicitCli = await traceThroughCli(cli, evidence, explicit);
const omittedMcp = await traceThroughMcp(call, evidence, omitted);
const explicitMcp = await traceThroughMcp(call, evidence, explicit);
expect(omittedCli, value).toEqual(explicitCli);
expect(omittedMcp, value).toEqual(explicitMcp);
expect(omittedMcp, value).toEqual(omittedCli);
expect(omittedCli.result.seed).toEqual(explicit.seed);
if (value === "openProject")
expect(omittedCli.result.summary.matched_seeds).toBeGreaterThan(0);
else {
expect(omittedCli.result.summary.matched_seeds).toBe(0);
expect(omittedCli.result.coverage.status).toBe("no-match");
}
}
},
);