From 09b188bb0daca1fdf62915e99aafcadfbbce66c6 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:13:23 +0800 Subject: [PATCH] fix(inspector): retain captures through authorization cleanup failures --- docs/javascript-runtime-observation.md | 4 ++ src/browser/ElectronFileScope.cleanup.test.ts | 48 +++++++++++++++++++ src/inspector/V8InspectorProvider.ts | 38 ++++----------- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/docs/javascript-runtime-observation.md b/docs/javascript-runtime-observation.md index 36f319399b..debf248726 100644 --- a/docs/javascript-runtime-observation.md +++ b/docs/javascript-runtime-observation.md @@ -147,6 +147,10 @@ Inspector does not authenticate the operating-system PID or distinguish an Electron role, so Evidence records the role authority as `caller-declared-unverified`. +If final script-location authorization fails, collected script and context facts +remain available in the typed failure with explicitly unverified locations. +Descriptor cleanup status and its retry owner remain independent of that capture. + Electron main, preload, and renderer behavior can therefore be observed only as separate Inspector targets. The provider does not infer that two targets belong to the same Electron application. diff --git a/src/browser/ElectronFileScope.cleanup.test.ts b/src/browser/ElectronFileScope.cleanup.test.ts index f4cd21c36d..a38618a983 100644 --- a/src/browser/ElectronFileScope.cleanup.test.ts +++ b/src/browser/ElectronFileScope.cleanup.test.ts @@ -73,6 +73,10 @@ vi.mock("node:fs/promises", async (importOriginal) => { import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { CdpElectronProvider } from "./CdpElectronProvider.js"; +import { V8InspectorProvider } from "../inspector/V8InspectorProvider.js"; +import { observeJavaScriptRuntime } from "../application/javascript/JavaScriptRuntimeObservationService.js"; +import { startFakeV8Inspector } from "../../tests/fixtures/inspector/fakeV8Inspector.js"; +import { prependAsarJsonWhitespace } from "../../tests/fixtures/asarHeader.js"; import { authorizedElectronFile, mapElectronFileAuthorizationFailure, @@ -120,6 +124,50 @@ const asarPage = async (): Promise => { return pathToFileURL(join(archive, "index.html")).href; }; +it("retains Inspector scripts and contexts when final ASAR authorization cannot close its descriptor", async () => { + const scriptUrl = await asarPage(); + const archive = handleState.archive; + if (archive === undefined) throw new Error("Missing fixture ASAR"); + const inspector = await startFakeV8Inspector({ + targetUrl: "https://example.test/entry.js", + scriptUrls: [scriptUrl], + }); + const provider = new V8InspectorProvider(); + const input = { + inspector_endpoint: inspector.endpoint, + target_id: inspector.targetId, + observation_ms: 0, + }; + try { + const control = await observeJavaScriptRuntime(provider, input); + expect(control.ok).toBe(true); + if (!control.ok) throw control.error; + expect(control.value.normalized_result).toMatchObject({ + scripts: { observed_total: 1 }, + execution_contexts: [{ name: "main" }], + }); + await prependAsarJsonWhitespace(archive, " "); + handleState.failCloseCount = 1; + const result = await observeJavaScriptRuntime(provider, input); + expect(result.ok).toBe(false); + if (result.ok) throw new Error("Expected descriptor close failure"); + expect(result.error.cleanupIncomplete).toBe(true); + expect(result.error.partialObservation).toMatchObject({ + scripts: { + observed_total: 1, + items: [{ location: { kind: "unresolved", reported_url: scriptUrl } }], + }, + execution_contexts: [{ name: "main" }], + }); + await provider.close(); + expect(result.error.cleanupIncomplete).toBe(false); + } finally { + handleState.failCloseCount = 0; + await provider.close(); + await inspector.close(); + } +}); + it("retains a failed ASAR close through denial and retries before reopening", async () => { const location = await asarPage(); const missing = pathToFileURL( diff --git a/src/inspector/V8InspectorProvider.ts b/src/inspector/V8InspectorProvider.ts index 7a72aaac7b..01e3383a32 100644 --- a/src/inspector/V8InspectorProvider.ts +++ b/src/inspector/V8InspectorProvider.ts @@ -410,13 +410,11 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { }); outcome = ok(javascriptRuntimeObservationSchema.parse(result)); } catch (cause: unknown) { - let error = providerError(cause, "observe_javascript_runtime"); + const error = providerError(cause, "observe_javascript_runtime"); if ( state !== undefined && discovery !== undefined && - target !== undefined && - (cause instanceof BrowserObservationError || - cause instanceof AnalysisCancelledError) + target !== undefined ) { try { const partialObservation = await finalizeInspectorCapture({ @@ -427,35 +425,19 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort { state, locationMode: "reported", }); - if (cause instanceof AnalysisCancelledError) { - cause.retainPartialObservation(partialObservation); - error = cause; - } else { - error = new BrowserObservationError( - "observe_javascript_runtime", - cause.reason, - { - cause: error, - ...(error.userMessage === undefined - ? {} - : { detail: error.userMessage }), - partialObservation, - }, - ); - } + error.retainPartialObservation(partialObservation); } catch (projectionFailure: unknown) { - if (!(cause instanceof AnalysisCancelledError)) - error = new ProviderAdapterError( + error.retainSecondaryFailure( + new ProviderAdapterError( V8_INSPECTOR_PROVIDER_IDENTITY.id, "observe_javascript_runtime", { - cause: new AggregateError( - [error, projectionFailure], - "Inspector failure and partial-observation projection both failed", - ), - userMessage: `${error.message} Partial observations could not be projected.`, + cause: projectionFailure, + userMessage: + "Inspector partial observations could not be projected.", }, - ); + ), + ); } } outcome = err(error);