fix(browser): preserve caller cancellation through CDP observations

This commit is contained in:
morluto committed 2026-10-19 14:00:01 +08:00
1 parent 9aca543f46
commit 7f4e7d5577
15 files changed
+678 -48

No files matched your search

@@ -197,3 +197,21 @@ it("retains completed bundle facts as bundle Evidence after cancellation", async
normalized_result: bundle,
});
});
it("preserves a prior caller reason without admitting browser work", async () => {
const reason = new AnalysisOutputError(
"inspect_web_page",
"caller supplied reason",
);
const result = await listBrowserTargets(
provider(async () => {
throw new Error("Cancelled inventory must not start");
}),
input,
{ signal: AbortSignal.abort(reason) },
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancelled inventory");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
});
+30 -6
View File
@@ -38,7 +38,11 @@ export const listBrowserTargets = async (
const ready = requireBrowser(browser, "list_browser_targets");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("list_browser_targets"));
return err(
new AnalysisCancelledError("list_browser_targets", {
cause: options.signal.reason,
}),
);
const result = await ready.value.listTargets(input, options);
return result.ok
? browserEvidence(
@@ -60,7 +64,11 @@ export const inspectWebPage = async (
const ready = requireBrowser(browser, "inspect_web_page");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("inspect_web_page"));
return err(
new AnalysisCancelledError("inspect_web_page", {
cause: options.signal.reason,
}),
);
const result = await ready.value.inspectPage(input, options);
if (!result.ok)
return retainBrowserObservation(
@@ -89,7 +97,11 @@ export const analyzeWebBundle = async (
const ready = requireBrowser(browser, "analyze_web_bundle");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("analyze_web_bundle"));
return err(
new AnalysisCancelledError("analyze_web_bundle", {
cause: options.signal.reason,
}),
);
const analyzed = await ready.value.analyzeBundle(input, options);
if (!analyzed.ok)
return retainBrowserObservation(
@@ -116,7 +128,11 @@ export const observeWebSession = async (
const ready = requireBrowser(browser, "observe_web_session");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("observe_web_session"));
return err(
new AnalysisCancelledError("observe_web_session", {
cause: options.signal.reason,
}),
);
const observed = await ready.value.observeSession(input, options);
return observed.ok
? browserEvidence(
@@ -138,7 +154,11 @@ export const discoverWebMcpTools = async (
const ready = requireBrowser(browser, "discover_webmcp_tools");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("discover_webmcp_tools"));
return err(
new AnalysisCancelledError("discover_webmcp_tools", {
cause: options.signal.reason,
}),
);
const discovered = await ready.value.discoverWebMcpTools(input, options);
return discovered.ok
? browserEvidence(
@@ -180,7 +200,11 @@ export const captureWebScreenshot = async (
const ready = requireBrowser(browser, "capture_web_screenshot");
if (!ready.ok) return ready;
if (options.signal?.aborted)
return err(new AnalysisCancelledError("capture_web_screenshot"));
return err(
new AnalysisCancelledError("capture_web_screenshot", {
cause: options.signal.reason,
}),
);
const captured = await ready.value.captureScreenshot(input, options);
return captured.ok
? browserEvidence(
+6 -1
View File
@@ -29,10 +29,15 @@ it("rejects malformed input and prior cancellation without calling a provider",
if (malformed.ok)
throw new Error("Invalid endpoint must fail before execution");
expect(malformed.error._tag).toBe("AnalysisInputError");
const cancelled = await service.inspect({}, { signal: AbortSignal.abort() });
const reason = { requested: "stop before provider admission" };
const cancelled = await service.inspect(
{},
{ signal: AbortSignal.abort(reason) },
);
if (cancelled.ok)
throw new Error("Prior cancellation must fail before inspection");
expect(cancelled.error._tag).toBe("AnalysisCancelledError");
expect(cancelled.error.cause).toBe(reason);
});
it("preserves actual provider failures and rejects changed target/selector identities", async () => {
+3 -1
View File
@@ -187,7 +187,9 @@ const parseInput = <T>(
options: ExecutionOptions | undefined,
): Result<T, AnalysisError> => {
if (options?.signal?.aborted)
return err(new AnalysisCancelledError(operation));
return err(
new AnalysisCancelledError(operation, { cause: options.signal.reason }),
);
const parsed = schema.safeParse(rawInput);
return parsed.success
? ok(parsed.data)