From 81d4f6b42aa7098ef575576a3125a89bbe7348c3 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 11 Nov 2026 17:15:08 +0800 Subject: [PATCH] fix(runtime): bind Inspector evidence to the selected request (#10039) --- ...avaScriptRuntimeObservationService.test.ts | 143 +++++++++++++----- .../JavaScriptRuntimeObservationService.ts | 33 ++++ 2 files changed, 142 insertions(+), 34 deletions(-) diff --git a/src/application/javascript/JavaScriptRuntimeObservationService.test.ts b/src/application/javascript/JavaScriptRuntimeObservationService.test.ts index e6be118839..6e92841437 100644 --- a/src/application/javascript/JavaScriptRuntimeObservationService.test.ts +++ b/src/application/javascript/JavaScriptRuntimeObservationService.test.ts @@ -22,6 +22,41 @@ import { expectPreemptedCancellation, } from "../../../tests/fixtures/ownerObservationExpectation.js"; +const runtimeObservation = javascriptRuntimeObservationSchema.parse({ + runtime: { + product: "node.js/v24", + protocol_version: "1.3", + v8_version: "13", + }, + target: { + target_id: "target", + protocol_type: "node", + attached: false, + location: { kind: "file", file_path: "/fixture.js" }, + runtime_kind: "node", + runtime_kind_authority: "caller-declared-unverified", + }, + capture: { + observation_ms: 0, + events_observed: 0, + events_retained: 0, + events_dropped: 0, + metadata_bytes_retained: 0, + truncated: false, + truncation_reasons: [], + }, + scripts: { + items: [], + observed_total: 0, + excluded: { unsupported_location: 0, invalid_protocol_value: 0 }, + }, + execution_contexts: [], + directly_observed: ["Inspector target attached and queried."], + unavailable_without_instrumentation: [], + unknowns: [], + limitations: [], +}); + const observeCancelled = async (partial: unknown) => { const reason = { requested: "stop before runtime observation completed" }; const failure = new AnalysisCancelledError("observe_javascript_runtime", { @@ -175,40 +210,7 @@ it("does not let late cancellation hide an invalid completed Inspector report", }); it("converts a raw runtime observation retained by cleanup failure", async () => { - const observation = javascriptRuntimeObservationSchema.parse({ - runtime: { - product: "node.js/v24", - protocol_version: "1.3", - v8_version: "13", - }, - target: { - target_id: "target", - protocol_type: "node", - attached: false, - location: { kind: "file", file_path: "/fixture.js" }, - runtime_kind: "node", - runtime_kind_authority: "caller-declared-unverified", - }, - capture: { - observation_ms: 0, - events_observed: 0, - events_retained: 0, - events_dropped: 0, - metadata_bytes_retained: 0, - truncated: false, - truncation_reasons: [], - }, - scripts: { - items: [], - observed_total: 0, - excluded: { unsupported_location: 0, invalid_protocol_value: 0 }, - }, - execution_contexts: [], - directly_observed: ["Inspector target attached and queried."], - unavailable_without_instrumentation: [], - unknowns: [], - limitations: [], - }); + const observation = runtimeObservation; const failure = new BrowserObservationError( "observe_javascript_runtime", "cleanup_failed", @@ -239,6 +241,7 @@ it("converts a raw runtime observation retained by cleanup failure", async () => { inspector_endpoint: "http://127.0.0.1:9229", target_id: "target", + runtime_kind: "node", observation_ms: 0, }, ); @@ -349,3 +352,75 @@ it.each([{ scripts: [undefined] }, { elapsed_ms: Number.NaN }])( ); }, ); + +it.each([ + [ + "target", + { + ...runtimeObservation, + target: { ...runtimeObservation.target, target_id: "other-target" }, + }, + ], + [ + "window", + { + ...runtimeObservation, + capture: { ...runtimeObservation.capture, observation_ms: 1 }, + }, + ], + [ + "declared kind", + { + ...runtimeObservation, + target: { + ...runtimeObservation.target, + runtime_kind: "unknown" as const, + }, + }, + ], + [ + "kind authority", + { + ...runtimeObservation, + target: { + ...runtimeObservation.target, + runtime_kind_authority: "not-declared" as const, + }, + }, + ], +])( + "does not attest a provider's changed %s as the selected runtime observation", + async (_name, report) => { + const input = { + inspector_endpoint: "http://127.0.0.1:9229", + target_id: "target", + observation_ms: 0, + runtime_kind: "node" as const, + }; + const provider: JavaScriptRuntimeObservationPort = { + identity: () => ({ id: "test", name: "Test", version: "1" }), + listTargets: async () => { + throw new Error("unused"); + }, + observe: async () => ok(report), + }; + const completed = await observeJavaScriptRuntime(provider, input); + expect(completed).toMatchObject({ + ok: false, + error: { _tag: "AnalysisOutputError", partialObservation: report }, + }); + const primary = new AnalysisCancelledError("observe_javascript_runtime", { + partialObservation: report, + }); + const failed = await observeJavaScriptRuntime( + { ...provider, observe: async () => err(primary) }, + input, + ); + expect(failed).toMatchObject({ ok: false, error: primary }); + expect(primary.partialObservation).toEqual(report); + expect(primary.partialObservation).not.toHaveProperty("evidence_id"); + expect(primary.secondaryFailures).toMatchObject([ + { _tag: "AnalysisOutputError" }, + ]); + }, +); diff --git a/src/application/javascript/JavaScriptRuntimeObservationService.ts b/src/application/javascript/JavaScriptRuntimeObservationService.ts index 0eedaff93b..54b86d39c8 100644 --- a/src/application/javascript/JavaScriptRuntimeObservationService.ts +++ b/src/application/javascript/JavaScriptRuntimeObservationService.ts @@ -11,6 +11,7 @@ import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { ListJavaScriptRuntimeTargetsInput, ObserveJavaScriptRuntimeInput, + JavaScriptRuntimeObservation, } from "../../domain/javascript/javascriptRuntimeObservation.js"; import { err, ok, type Result } from "../../domain/result.js"; import { @@ -80,6 +81,17 @@ export const observeJavaScriptRuntime = async ( ) { const report = javascriptRuntimeObservationSchema.safeParse(partial); if (report.success) { + const mismatch = runtimeSelectionFailure(input, report.data); + if (mismatch !== undefined) { + result.error.retainSecondaryFailure(mismatch); + return err( + keepIncompleteObservation( + result.error, + "observe_javascript_runtime", + "Inspector retained a non-JSON incomplete observation.", + ), + ); + } result.error.partialObservation = createJavaScriptRuntimeObservationEvidence( "observe_javascript_runtime", @@ -107,6 +119,9 @@ export const observeJavaScriptRuntime = async ( { cause: report.error }, ), ); + const mismatch = runtimeSelectionFailure(input, report.data); + if (mismatch !== undefined) + return err(mismatch.retainPartialObservation(report.data)); return runtimeEvidence( "observe_javascript_runtime", input, @@ -116,6 +131,24 @@ export const observeJavaScriptRuntime = async ( ); }; +/** Shape-valid observations still must identify the caller-selected request. */ +const runtimeSelectionFailure = ( + input: ObserveJavaScriptRuntimeInput, + report: JavaScriptRuntimeObservation, +): AnalysisOutputError | undefined => + report.target.target_id !== input.target_id || + report.capture.observation_ms !== input.observation_ms || + report.target.runtime_kind !== (input.runtime_kind ?? "unknown") || + report.target.runtime_kind_authority !== + (input.runtime_kind === undefined + ? "not-declared" + : "caller-declared-unverified") + ? new AnalysisOutputError( + "observe_javascript_runtime", + "Inspector changed the selected target, observation window, or caller-declared runtime kind.", + ) + : undefined; + const requireProvider = ( provider: JavaScriptRuntimeObservationPort | undefined, operation: "list_javascript_runtime_targets" | "observe_javascript_runtime",