fix(runtime): retain Inspector and Electron cleanup ownership

This commit is contained in:
morluto committed 2026-10-19 14:03:28 +08:00
1 parent 7f4e7d5577
commit 87ab054ea3
23 files changed
+1901 -321

No files matched your search

@@ -205,3 +205,42 @@ it("retains completed target facts when cancellation arrives before Evidence pub
});
}
});
it.each(["list", "inspect"] as const)(
"preserves a pre-abort reason before Electron %s provider work",
async (kind) => {
const reason = new Error(`stop Electron ${kind}`);
const controller = new AbortController();
controller.abort(reason);
let providerCalls = 0;
const provider: ElectronObservationPort = {
identity: () => identity,
listTargets: async () => {
providerCalls += 1;
return err(new AnalysisCancelledError("list_electron_targets"));
},
inspectPage: async () => {
providerCalls += 1;
return err(new AnalysisCancelledError("inspect_electron_page"));
},
};
const result =
kind === "list"
? await listElectronTargets(
provider,
{ cdp_endpoint: input.cdp_endpoint },
{ signal: controller.signal },
)
: await inspectElectronPage(provider, input, {
signal: controller.signal,
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
}
expect(providerCalls).toBe(0);
},
);
@@ -29,7 +29,11 @@ export const listElectronTargets = async (
const ready = requireProvider(provider, "list_electron_targets");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("list_electron_targets"));
return err(
new AnalysisCancelledError("list_electron_targets", {
cause: options.signal.reason,
}),
);
const result = await ready.value.listTargets(input, options);
if (!result.ok) return result;
const report = electronTargetListSchema.safeParse(result.value);
@@ -61,7 +65,11 @@ export const inspectElectronPage = async (
const ready = requireProvider(provider, "inspect_electron_page");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("inspect_electron_page"));
return err(
new AnalysisCancelledError("inspect_electron_page", {
cause: options.signal.reason,
}),
);
const result = await ready.value.inspectPage(input, options);
if (!result.ok) {
const partial = result.error.partialObservation;
@@ -13,6 +13,8 @@ import type { Result } from "../../domain/result.js";
/** Provider-neutral boundary for passive Node/Electron Inspector observation. */
export interface JavaScriptRuntimeObservationPort {
/** Retry retained Inspector and associated file cleanup during shutdown. */
close?(): Promise<void>;
identity(): ProviderIdentity;
listTargets(
input: ListJavaScriptRuntimeTargetsInput,
@@ -271,3 +271,49 @@ it("reports non-JSON Inspector output before late cancellation", async () => {
error: { _tag: "AnalysisOutputError", partialObservation: undefined },
});
});
it.each(["list", "observe"] as const)(
"preserves a pre-abort reason before Inspector %s provider work",
async (kind) => {
const reason = new Error(`stop Inspector ${kind}`);
const controller = new AbortController();
controller.abort(reason);
let providerCalls = 0;
const provider: JavaScriptRuntimeObservationPort = {
identity: () => ({ id: "test", name: "Test", version: "1" }),
listTargets: async () => {
providerCalls += 1;
return err(
new AnalysisCancelledError("list_javascript_runtime_targets"),
);
},
observe: async () => {
providerCalls += 1;
return err(new AnalysisCancelledError("observe_javascript_runtime"));
},
};
const result =
kind === "list"
? await listJavaScriptRuntimeTargets(
provider,
{ inspector_endpoint: "http://127.0.0.1:9229" },
{ signal: controller.signal },
)
: await observeJavaScriptRuntime(
provider,
{
inspector_endpoint: "http://127.0.0.1:9229",
target_id: "target",
observation_ms: 0,
},
{ signal: controller.signal },
);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
}
expect(providerCalls).toBe(0);
},
);
@@ -28,7 +28,11 @@ export const listJavaScriptRuntimeTargets = async (
const ready = requireProvider(provider, "list_javascript_runtime_targets");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("list_javascript_runtime_targets"));
return err(
new AnalysisCancelledError("list_javascript_runtime_targets", {
cause: options.signal.reason,
}),
);
const result = await ready.value.listTargets(input, options);
if (!result.ok) return result;
const report = javascriptRuntimeTargetListSchema.safeParse(result.value);
@@ -58,7 +62,11 @@ export const observeJavaScriptRuntime = async (
const ready = requireProvider(provider, "observe_javascript_runtime");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("observe_javascript_runtime"));
return err(
new AnalysisCancelledError("observe_javascript_runtime", {
cause: options.signal.reason,
}),
);
const result = await ready.value.observe(input, options);
if (!result.ok) {
const partial = result.error.partialObservation;