diff --git a/extensions/browser/src/browser/pw-session.dialogs.test.ts b/extensions/browser/src/browser/pw-session.dialogs.test.ts index 66750ebd41a..8ae1ece4da1 100644 --- a/extensions/browser/src/browser/pw-session.dialogs.test.ts +++ b/extensions/browser/src/browser/pw-session.dialogs.test.ts @@ -99,6 +99,28 @@ describe("observed browser dialogs", () => { observed.cleanup(); }); + it("uses the default arm-next-dialog timeout for non-finite timeoutMs", async () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const { page, emit } = createPageHarness(); + ensurePageState(page); + const dialog = createDialog({ type: "alert", message: "Still armed" }); + const observed = createObservedDialogAbortSignalForPage({ page }); + + armObservedDialogResponseOnPage({ page, accept: false, timeoutMs: Number.NaN }); + await vi.advanceTimersByTimeAsync(119_999); + emit("dialog", dialog); + await Promise.resolve(); + + expect(observed.signal.aborted).toBe(false); + expect(dialog.dismiss).toHaveBeenCalledOnce(); + expect(getObservedBrowserStateForPage(page).dialogs.pending).toEqual([]); + expect(getObservedBrowserStateForPage(page).dialogs.recent).toMatchObject([ + { id: "d1", type: "alert", closedBy: "armed" }, + ]); + observed.cleanup(); + }); + it("aborts in-flight actions while keeping unarmed dialogs pending", async () => { const { page, emit } = createPageHarness(); ensurePageState(page); diff --git a/extensions/browser/src/browser/pw-session.ts b/extensions/browser/src/browser/pw-session.ts index e061c652dfd..cc4e5eae7f8 100644 --- a/extensions/browser/src/browser/pw-session.ts +++ b/extensions/browser/src/browser/pw-session.ts @@ -1,5 +1,6 @@ import crypto from "node:crypto"; import path from "node:path"; +import { parseFiniteNumber } from "openclaw/plugin-sdk/number-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime"; import type { Browser, @@ -182,6 +183,11 @@ const connectingByCdpUrl = new Map>(); const blockedTargetsByCdpUrl = new Set(); const blockedPageRefsByCdpUrl = new Map>(); +function resolveObservedDialogTimeoutMs(timeoutMs: number | undefined): number { + const parsed = parseFiniteNumber(timeoutMs); + return Math.max(1, Math.floor(parsed ?? OBSERVED_DIALOG_TIMEOUT_MS)); +} + function normalizeCdpUrl(raw: string) { return raw.replace(/\/$/, ""); } @@ -784,7 +790,7 @@ export function armObservedDialogResponseOnPage(opts: { }): void { const state = ensurePageState(opts.page); clearArmedDialogResponse(state); - const timeoutMs = Math.max(1, Math.floor(opts.timeoutMs ?? OBSERVED_DIALOG_TIMEOUT_MS)); + const timeoutMs = resolveObservedDialogTimeoutMs(opts.timeoutMs); const response: ArmedDialogResponse = { accept: opts.accept, expiresAt: Date.now() + timeoutMs,