From 2d11fd92d233f65dd0f3904583e09078a5a3342c Mon Sep 17 00:00:00 2001 From: ShuaixinHuang <1836656514@qq.com> Date: Tue, 3 Nov 2026 08:54:39 +0800 Subject: [PATCH] fix(javascript): reconcile Node targets with entry assets (#8670) * fix(javascript): reconcile Node targets with entry assets * test(inspector): read reconciled evidence payload * Update tests/boundary/inspector/javascriptRuntimeNodeTarget.test.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * test(inspector): read the workflow evidence payload * Update tests/boundary/inspector/javascriptRuntimeNodeTarget.test.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * test(javascript): restore Evidence workflow result access --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- docs/javascript-runtime-reconciliation.md | 4 + .../javascript/javascriptRuntimeLoadState.ts | 5 +- .../javascriptRuntimeReconciliationRuntime.ts | 5 +- ...ascriptNodeTargetReconciliationCli.test.ts | 52 +++++++++++++ .../javascriptRuntimeNodeTarget.test.ts | 37 ++++++++++ tests/fixtures/nodeRuntimeReconciliation.ts | 73 +++++++++++++++++++ 6 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 tests/acceptance/applications/javascriptNodeTargetReconciliationCli.test.ts create mode 100644 tests/boundary/inspector/javascriptRuntimeNodeTarget.test.ts create mode 100644 tests/fixtures/nodeRuntimeReconciliation.ts diff --git a/docs/javascript-runtime-reconciliation.md b/docs/javascript-runtime-reconciliation.md index 85bfbff428..16b8fdc1e0 100644 --- a/docs/javascript-runtime-reconciliation.md +++ b/docs/javascript-runtime-reconciliation.md @@ -93,6 +93,10 @@ coverage. It does not print captured source text. Runtime targets, frames, scripts, and workers become capture-scoped graph nodes. REA evaluates compatible static candidates conservatively: +V8 Inspector targets whose protocol type is `node` use JavaScript asset/module +candidates for their verified entry location. A target-only correspondence does +not mark that script loaded; load-state evidence requires a script observation. + 1. A captured-source SHA-256 plus one matching mapped location yields a content-and-location match. 2. When no static candidate exists at a mapped location, one unique exact diff --git a/src/domain/javascript/javascriptRuntimeLoadState.ts b/src/domain/javascript/javascriptRuntimeLoadState.ts index 275de19d37..e49d60c8f7 100644 --- a/src/domain/javascript/javascriptRuntimeLoadState.ts +++ b/src/domain/javascript/javascriptRuntimeLoadState.ts @@ -57,7 +57,10 @@ const classifyLayer = ( const direct = new Map(); const ambiguous = new Set(); const incomplete = new Set(); - for (const match of matches) + // A target's entry location establishes correspondence, not script presence. + for (const match of matches.filter( + ({ entity_kind }) => entity_kind === "script", + )) if ( match.status === "matched" && match.static_layer_id === layer.layerId && diff --git a/src/domain/javascript/javascriptRuntimeReconciliationRuntime.ts b/src/domain/javascript/javascriptRuntimeReconciliationRuntime.ts index b0be2de5bd..613d47175c 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationRuntime.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationRuntime.ts @@ -94,7 +94,10 @@ const targetEntity = ( frameKey: null, scriptKey: null, location, - staticKind: "renderer", + staticKind: + capture.kind === "v8-inspector" && target.type === "node" + ? "javascript" + : "renderer", sourceSha256: null, label: target.title, properties: { diff --git a/tests/acceptance/applications/javascriptNodeTargetReconciliationCli.test.ts b/tests/acceptance/applications/javascriptNodeTargetReconciliationCli.test.ts new file mode 100644 index 0000000000..357bc04959 --- /dev/null +++ b/tests/acceptance/applications/javascriptNodeTargetReconciliationCli.test.ts @@ -0,0 +1,52 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { javascriptRuntimeReconciliationResultSchema } from "../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; +import { nodeReconciliationFixture } from "../../fixtures/nodeRuntimeReconciliation.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest.for(["CLI", "stdio MCP"] as const)( + "reconciles synthetic Node target entry Evidence through %s", + async (surface, { cli }) => { + const input = nodeReconciliationFixture(false); + let document: unknown; + if (surface === "CLI") { + const root = await createTestTempDirectory( + "rea-node-reconciliation-cli-", + ); + const path = join(root, "input.json"); + await writeFile(path, JSON.stringify(input)); + const response = await cli.run({ + arguments: ["reconcile-javascript-runtime", path, "--format", "json"], + }); + expect(response.exitCode, response.stderr).toBe(0); + document = response.json; + } else { + const call = await cli.sharedMcpCall!(); + const response = await call("reconcile_javascript_runtime", input); + expect(response.isError, JSON.stringify(response)).not.toBe(true); + document = response.structuredContent; + } + const result = javascriptRuntimeReconciliationResultSchema.parse( + (document as { normalized_result: unknown }).normalized_result, + ); + expect(result.reconciliations).toEqual([ + expect.objectContaining({ + entity_kind: "target", + status: "matched", + reason: "unique-location-match", + confidence: "medium", + candidate_static_count: 1, + }), + ]); + expect(result.summary).toMatchObject({ + runtime_scripts: 0, + static_loaded: 0, + static_not_observed: 0, + }); + expect( + result.graph.edges.filter(({ relation }) => relation === "observed_as"), + ).toHaveLength(1); + }, +); diff --git a/tests/boundary/inspector/javascriptRuntimeNodeTarget.test.ts b/tests/boundary/inspector/javascriptRuntimeNodeTarget.test.ts new file mode 100644 index 0000000000..c56840195b --- /dev/null +++ b/tests/boundary/inspector/javascriptRuntimeNodeTarget.test.ts @@ -0,0 +1,37 @@ +import { expect, test } from "vitest"; +import { reconcileJavaScriptRuntimeEvidence } from "../../../src/application/javascript/JavaScriptRuntimeReconciliationService.js"; +import { javascriptRuntimeReconciliationResultSchema } from "../../../src/domain/javascript/javascriptRuntimeReconciliationSchemas.js"; +import { nodeReconciliationFixture } from "../../fixtures/nodeRuntimeReconciliation.js"; + +test.each([false, true])( + "matches a synthetic Node entry target without inventing script presence: %s", + (scriptObserved) => { + const reconciled = reconcileJavaScriptRuntimeEvidence( + nodeReconciliationFixture(scriptObserved), + ); + if (!reconciled.ok) throw reconciled.error; + const result = javascriptRuntimeReconciliationResultSchema.parse( + reconciled.value.evidence.normalized_result, + ); + const target = result.reconciliations.find( + ({ entity_kind }) => entity_kind === "target", + ); + expect(target).toMatchObject({ + status: "matched", + basis: "artifact-path", + confidence: "medium", + reason: "unique-location-match", + candidate_static_count: 1, + }); + expect( + result.graph.nodes.find( + ({ node_id }) => node_id === target?.static_node_id, + )?.kind, + ).toBe("javascript-asset"); + expect(result.summary).toMatchObject({ + runtime_scripts: scriptObserved ? 1 : 0, + static_loaded: scriptObserved ? 1 : 0, + static_not_observed: 0, + }); + }, +); diff --git a/tests/fixtures/nodeRuntimeReconciliation.ts b/tests/fixtures/nodeRuntimeReconciliation.ts new file mode 100644 index 0000000000..cf6e75ba23 --- /dev/null +++ b/tests/fixtures/nodeRuntimeReconciliation.ts @@ -0,0 +1,73 @@ +import { createJavaScriptRuntimeObservationEvidence } from "../../src/application/javascript/JavaScriptRuntimeObservationEvidence.js"; +import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; +import { javascriptRuntimeObservationSchema } from "../../src/domain/javascript/javascriptRuntimeObservation.js"; +import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../../src/inspector/providerIdentity.js"; + +/** Synthetic, schema-validated capture; this is not a live Inspector observation. */ +export const nodeReconciliationFixture = (scriptObserved: boolean) => { + const location = { + kind: "file", + file_path: "/Applications/Example.app/Contents/Resources/app/renderer.js", + }; + const runtime = javascriptRuntimeObservationSchema.parse({ + runtime: { + product: "node.js/v24.22.1", + protocol_version: "1.3", + v8_version: null, + }, + target: { + target_id: "synthetic-node", + protocol_type: "node", + attached: null, + location, + runtime_kind: "unknown", + runtime_kind_authority: "not-declared", + }, + capture: { + observation_ms: 100, + events_observed: scriptObserved ? 1 : 0, + events_retained: scriptObserved ? 1 : 0, + events_dropped: 0, + metadata_bytes_retained: 0, + truncated: false, + truncation_reasons: [], + }, + scripts: { + items: scriptObserved + ? [ + { + script_key: `v8_script_${"4".repeat(64)}`, + location, + execution_context_key: null, + cdp_hash: null, + length: null, + is_module: null, + status: "observed-loaded", + }, + ] + : [], + observed_total: scriptObserved ? 1 : 0, + excluded: { unsupported_location: 0, invalid_protocol_value: 0 }, + }, + execution_contexts: [], + directly_observed: [], + unavailable_without_instrumentation: [], + unknowns: ["Synthetic bounded capture."], + limitations: ["Synthetic fixture, not a live runtime."], + }); + return { + static_layers: [{ analysis: JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE }], + runtime_observations: [ + createJavaScriptRuntimeObservationEvidence( + "observe_javascript_runtime", + { + inspector_endpoint: "http://127.0.0.1:9229", + target_id: "synthetic-node", + observation_ms: 100, + }, + runtime, + V8_INSPECTOR_PROVIDER_IDENTITY, + ), + ], + }; +};