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; type Trace = z.infer; type SeedScenario = { readonly name: string; readonly seed: FeatureSeed; readonly expected: { readonly nodeId: string; readonly basis: "node-id" | "property"; } | null; }; const createSeedScenarios = ( graph: z.infer["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, ) => { 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, ) => { 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"); } } }, );