diff --git a/src/domain/javascript/javascriptSemanticQuerySchemas.ts b/src/domain/javascript/javascriptSemanticQuerySchemas.ts index a88adc706..6d548545f 100644 --- a/src/domain/javascript/javascriptSemanticQuerySchemas.ts +++ b/src/domain/javascript/javascriptSemanticQuerySchemas.ts @@ -41,7 +41,7 @@ export const javaScriptSemanticQuerySeedSchema = z.discriminatedUnion("kind", [ }), z.strictObject({ kind: z.literal("property"), - name: z.string().min(1), + name: z.string(), }), z.strictObject({ kind: z.literal("endpoint"), diff --git a/tests/boundary/javascriptSemanticTraceEvidence.test.ts b/tests/boundary/javascriptSemanticTraceEvidence.test.ts index 91cb7585f..25316c648 100644 --- a/tests/boundary/javascriptSemanticTraceEvidence.test.ts +++ b/tests/boundary/javascriptSemanticTraceEvidence.test.ts @@ -197,6 +197,78 @@ it("excludes local exports from writes-only traces through CLI and MCP", async ( } }); +it("traces exact empty property names through CLI and MCP", async () => { + const root = await createTestTempDirectory("rea-semantic-empty-property-"); + await writeFile( + join(root, "main.js"), + "const routes = {'': 'HOME', x: 'NAMED'}; const root = routes['']; const named = routes.x;\n", + ); + const cli = createCli({}); + const analyzed = await serveCli(cli, [ + "analyze-javascript-application", + root, + "--json", + ]); + expect(analyzed.exitCode).toBe(0); + const application = parseEvidence(JSON.parse(analyzed.stdout)); + const { semantic_graph: graph } = + javascriptApplicationAnalysisResultSchema.parse( + application.normalized_result, + ); + const harness = await createApplicationMcpHarness(); + try { + for (const name of ["", "x"]) { + const slots = graph.nodes.filter( + (node) => + node.kind === "property-slot" && node.properties.name === name, + ); + expect(slots).toHaveLength(1); + const slot = slots[0]; + if (slot === undefined) + throw new Error("Expected retained property slot"); + const exact = await assertSuccessfulTraceParity( + cli, + harness, + application, + { + seed: { kind: "semantic-node", node_id: slot.node_id }, + direction: "forward-influence", + }, + ); + const selected = await assertSuccessfulTraceParity( + cli, + harness, + application, + { + seed: { kind: "property", name }, + direction: "forward-influence", + }, + ); + expect(selected.seed_node_ids).toEqual([slot.node_id]); + expect(selected.nodes).toEqual(exact.nodes); + expect(selected.relations).toEqual(exact.relations); + expect(selected.nodes).toHaveLength(2); + expect(selected.relations).toHaveLength(1); + expect(selected.coverage.status).toBe("partial"); + } + const absent = await assertSuccessfulTraceParity( + cli, + harness, + application, + { + seed: { kind: "property", name: "missing" }, + direction: "forward-influence", + }, + ); + expect(absent.seed_node_ids).toEqual([]); + expect(absent.nodes).toEqual([]); + expect(absent.relations).toEqual([]); + expect(absent.coverage.status).toBe("partial"); + } finally { + await harness.close(); + } +}); + const assertSuccessfulTraceParity = async ( cli: ReturnType, harness: Awaited>, @@ -213,6 +285,10 @@ const assertSuccessfulTraceParity = async ( JSON.stringify(arguments_), "--json", ]); + expect(cliResponse.exitCode, cliResponse.stdout).toBe(0); + expect(JSON.parse(cliResponse.stdout), cliResponse.stdout).not.toHaveProperty( + "error", + ); const cliEvidence = parseEvidence(JSON.parse(cliResponse.stdout)); const cliResult = javaScriptSemanticTraceResultSchema.parse( cliEvidence.normalized_result, @@ -274,7 +350,6 @@ const assertTraceContextsResolve = ( expect([...referencedIds].toSorted()).toEqual( [...contexts.keys()].toSorted(), ); - expect(contexts.size).toBeGreaterThan(0); }; const serveCli = async (