From 71a024c789a3d4626b85965a0b511a577a027801 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:22:07 +0800 Subject: [PATCH] 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. --- ...avascriptFeatureSeed.array_members.test.ts | 30 +--- .../javascript/javascriptFeatureTrace.ts | 16 +- .../javascriptFeatureTraceSchemas.ts | 3 - ...scriptFeatureTraceSeedSelectionCli.test.ts | 138 +++++++++++------- 4 files changed, 96 insertions(+), 91 deletions(-) diff --git a/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts b/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts index f8a57c53ea..f0616b718f 100644 --- a/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts +++ b/src/domain/javascript/javascriptFeatureSeed.array_members.test.ts @@ -293,34 +293,8 @@ describe("feature seed selection controls", () => { ); }); -describe("public feature trace from contextBridge source members", () => { - it.each(["openProject", "missingProject"])( - "normalizes optional seed defaults before deriving trace identity for %s", - (value) => { - const input = { - sourceEvidenceId, - graph: contextBridgeGraph(), - nativeEvidence: [], - direction: "both" as const, - }; - const omitted = traceApplicationFeature({ - ...input, - seed: { kind: "api", value }, - }); - const explicit = traceApplicationFeature({ - ...input, - seed: { - kind: "api", - value, - match: null, - case_sensitive: false, - }, - }); - expect(omitted).toEqual(explicit); - }, - ); - - it("traces one exact member seed through the public feature workflow", () => { +describe("feature trace projection from contextBridge source members", () => { + it("traces one exact member seed while preserving projection facts", () => { const mode = { value: "openProject", match: "exact" as const, diff --git a/src/domain/javascript/javascriptFeatureTrace.ts b/src/domain/javascript/javascriptFeatureTrace.ts index e4109b85d4..d136bf784b 100644 --- a/src/domain/javascript/javascriptFeatureTrace.ts +++ b/src/domain/javascript/javascriptFeatureTrace.ts @@ -12,8 +12,7 @@ import type { } from "./javascriptApplicationGraphSchemas.js"; import { applicationFeatureTraceResultSchema, - applicationFeatureSeedSchema, - type ApplicationFeatureSeedInput, + type ApplicationFeatureSeed, type ApplicationFeatureTraceResult, type TraceApplicationFeatureInput, } from "./javascriptFeatureTraceSchemas.js"; @@ -32,23 +31,18 @@ export interface ApplicationFeatureTraceProjectionInput { readonly sourceEvidenceId: string; readonly graph: JavaScriptApplicationGraph; readonly nativeEvidence: readonly Evidence[]; - readonly seed: ApplicationFeatureSeedInput; + readonly seed: ApplicationFeatureSeed; readonly direction: TraceApplicationFeatureInput["direction"]; } /** Trace a literal feature through its complete reachable, authority-preserving JAG. */ export const traceApplicationFeature = ( - source: ApplicationFeatureTraceProjectionInput, + input: ApplicationFeatureTraceProjectionInput, ): ApplicationFeatureTraceResult => { - const input = { - ...source, - seed: applicationFeatureSeedSchema.parse(source.seed), - }; - const allSeedMatches = findApplicationFeatureSeeds( + const seedMatches = findApplicationFeatureSeeds( input.graph.nodes, input.seed, ); - const seedMatches = allSeedMatches; if (seedMatches.length === 0) return noMatchResult(input); const traversal = traverseApplicationFeature( input.graph, @@ -86,7 +80,7 @@ export const traceApplicationFeature = ( coverage: { status: traceCoverageStatus(input.graph), source_graph_status: input.graph.coverage.status, - total_seed_matches: allSeedMatches.length, + total_seed_matches: seedMatches.length, }, evidence_links: evidenceLinks, limitations: traceLimitations(input.graph), diff --git a/src/domain/javascript/javascriptFeatureTraceSchemas.ts b/src/domain/javascript/javascriptFeatureTraceSchemas.ts index e7eba7e5b8..2de4df99c8 100644 --- a/src/domain/javascript/javascriptFeatureTraceSchemas.ts +++ b/src/domain/javascript/javascriptFeatureTraceSchemas.ts @@ -191,9 +191,6 @@ export type TraceApplicationFeatureInput = z.infer< export type ApplicationFeatureSeed = z.output< typeof applicationFeatureSeedSchema >; -export type ApplicationFeatureSeedInput = z.input< - typeof applicationFeatureSeedSchema ->; export type ApplicationFeatureTraceResult = z.infer< typeof applicationFeatureTraceResultSchema >; diff --git a/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts b/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts index a0438556d7..40591eff0b 100644 --- a/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts +++ b/tests/acceptance/applications/javascriptFeatureTraceSeedSelectionCli.test.ts @@ -1,7 +1,5 @@ -import { Client } from "@modelcontextprotocol/client"; -import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { writeFile } from "node:fs/promises"; -import { join, resolve } from "node:path"; +import { join } from "node:path"; import { expect } from "vitest"; import { z } from "zod"; @@ -12,7 +10,11 @@ import { } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { cliTest, type TestCli } from "../../support/cli/cliFixture.js"; +import { + cliTest, + type SharedMcpCall, + type TestCli, +} from "../../support/cli/cliFixture.js"; type FeatureSeed = z.input; type Trace = z.infer; @@ -155,44 +157,49 @@ const analyzeFixture = async (cli: TestCli, root: string) => { const traceThroughCli = async ( cli: TestCli, application: unknown, - scenario: SeedScenario, -): Promise => { + 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", - JSON.stringify({ - application, - seed: scenario.seed, - direction: "both", - }), - "--json", - ], + 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 applicationFeatureTraceResultSchema.parse(evidence.normalized_result); + return { + evidence, + result: applicationFeatureTraceResultSchema.parse( + evidence.normalized_result, + ), + }; }; const traceThroughMcp = async ( - client: Client, + call: SharedMcpCall, application: unknown, - scenario: SeedScenario, -): Promise => { - const response = await client.callTool({ - name: "trace_application_feature", - arguments: { - application, - seed: scenario.seed, - direction: "both", - }, + 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 applicationFeatureTraceResultSchema.parse(evidence.normalized_result); + return { + evidence, + result: applicationFeatureTraceResultSchema.parse( + evidence.normalized_result, + ), + }; }; const expectTrace = (trace: Trace, scenario: SeedScenario): void => { @@ -229,28 +236,61 @@ cliTest( `const notificationRoute = { path: "/accounts/preferences/notifications" };\nconst tinyRoute = { path: "/tiny" };\nmodule.exports = { notificationRoute, tinyRoute };\n`, ); const { evidence, result } = await analyzeFixture(cli, root); - const client = new Client({ name: "feature-seed-selection", version: "1" }); - const transport = new StdioClientTransport({ - command: process.execPath, - args: [resolve("scripts/rea.mjs"), "mcp"], - cwd: process.cwd(), - env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" }, - stderr: "pipe", - }); - transport.stderr?.on("data", () => undefined); - try { - await client.connect(transport); - for (const scenario of createSeedScenarios(result.graph)) { - const cliTrace = await traceThroughCli(cli, evidence, scenario); - const mcpTrace = await traceThroughMcp(client, evidence, scenario); - expectTrace(cliTrace, scenario); - expectTrace(mcpTrace, scenario); - expect(mcpTrace.seed_matches).toEqual(cliTrace.seed_matches); - expect(mcpTrace.coverage).toEqual(cliTrace.coverage); - } - } finally { - await client.close(); - await transport.close(); + 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"); + } } }, );