From 9742d83213ace111deb0f14bdd86061bc1b1aea1 Mon Sep 17 00:00:00 2001 From: Ljy-0827 Date: Wed, 19 Aug 2026 17:13:16 +0800 Subject: [PATCH] fix(recorder): bug fix --- apps/extension/src/entrypoints/background.ts | 48 ++++++------- .../src/tools/__tests__/dispatcher.test.ts | 67 +++++++++++++++++ apps/extension/src/tools/dispatcher.ts | 71 +++++++++---------- apps/extension/src/tools/record.ts | 4 ++ 4 files changed, 129 insertions(+), 61 deletions(-) diff --git a/apps/extension/src/entrypoints/background.ts b/apps/extension/src/entrypoints/background.ts index 8e819d7..4edd0a2 100644 --- a/apps/extension/src/entrypoints/background.ts +++ b/apps/extension/src/entrypoints/background.ts @@ -39,6 +39,7 @@ import { attachRecordFinishListener, attachRecordQueryListener, attachRecordStepListener, + type RecordRuntimeDeps, } from "@/tools/record"; import { detectBrowserMeta } from "@/transport/handshake"; import type { Transport } from "@/transport/transport"; @@ -146,10 +147,33 @@ export default defineBackground(() => { void sessionsLive.syncFromManager(); }, }); + const recordDeps = { + tabsApi: chrome.tabs, + cdp, + frameCoordinator: recordFrameCoordinator, + sendToTab: (tabId: number, msg: Parameters[1]) => + chrome.tabs.sendMessage(tabId, msg), + bypassOverlay: async (tabId: number, enabled: boolean) => { + try { + await chrome.tabs.sendMessage(tabId, { + type: OVERLAY_AUTOMATION_BYPASS, + enabled, + }); + } catch { + // Content script may be unavailable on restricted pages. + } + }, + } satisfies RecordRuntimeDeps; + recordFrameCoordinator.attach(); + attachRecordStepListener(recordDeps); + attachRecordFinishListener(recordDeps); + attachRecordQueryListener(recordDeps); + const dispatcher = new ToolDispatcher({ transport, sessions, cdp, + recording: recordDeps, onSessionsChanged: onOverlaySessionStateChanged, onBrowserControlResumed, approveBorrow: (ctx) => @@ -171,30 +195,6 @@ export default defineBackground(() => { }), }); dispatcher.start(); - const recordDeps = { - tabsApi: chrome.tabs, - cdp, - frameCoordinator: recordFrameCoordinator, - sendToTab: (tabId: number, msg: Parameters[1]) => - chrome.tabs.sendMessage(tabId, msg), - bypassOverlay: async (tabId: number, enabled: boolean) => { - try { - await chrome.tabs.sendMessage(tabId, { - type: OVERLAY_AUTOMATION_BYPASS, - enabled, - }); - } catch { - // Content script may be unavailable on restricted pages. - } - }, - }; - // Message listeners stay up (cheap; fire only for record message types). - // Tab / webNavigation observation attaches lazily while a recording is - // active — see ensureBrowserObservationListeners in tools/record.ts. - attachRecordStepListener(recordDeps); - attachRecordFinishListener(recordDeps); - attachRecordQueryListener(recordDeps); - recordFrameCoordinator.attach(); if (typeof chrome.notifications?.onClicked?.addListener === "function") { attachBorrowNotificationClickHandler({ onClicked: chrome.notifications.onClicked, diff --git a/apps/extension/src/tools/__tests__/dispatcher.test.ts b/apps/extension/src/tools/__tests__/dispatcher.test.ts index 2e1b921..4ed32e0 100644 --- a/apps/extension/src/tools/__tests__/dispatcher.test.ts +++ b/apps/extension/src/tools/__tests__/dispatcher.test.ts @@ -8,6 +8,10 @@ import type { RequestFrame, } from "@/transport/types"; import { ToolDispatcher } from "../dispatcher"; +import { + resetBrowserObservationForTests, + setBrowserObservationAttachForTests, +} from "../record"; type TestDispatcherCdp = NonNullable[0]["cdp"]>; @@ -44,9 +48,72 @@ function makeRequest(method: string, params: unknown): RequestFrame { describe("ToolDispatcher", () => { afterEach(() => { + resetBrowserObservationForTests(); vi.unstubAllGlobals(); }); + it("uses the configured recording runtime for start and stop", async () => { + const { transport, sent, deliver } = fakeTransport(); + const sessions = new SessionManager({ + agentWindow: { + create: vi.fn(async () => 4242), + remove: vi.fn(), + ensureActiveTab: vi.fn(async () => {}), + }, + }); + await sessions.start("aa11"); + const tab = { + id: 7, + windowId: 4242, + active: true, + status: "complete", + url: "https://example.com/start", + } as chrome.tabs.Tab; + const frameCoordinator = { + begin: vi.fn(), + armTab: vi.fn(async () => true), + sourceFor: vi.fn(() => null), + stop: vi.fn(async () => true), + cancel: vi.fn(), + }; + const sendToTab = vi.fn(async () => ({ ok: true })); + setBrowserObservationAttachForTests( + () => () => {}, + () => () => {}, + ); + const dispatcher = new ToolDispatcher({ + transport, + sessions, + recording: { + tabsApi: { + get: vi.fn(async () => tab), + query: vi.fn(async () => [tab]), + }, + frameCoordinator, + sendToTab, + }, + }); + dispatcher.start(); + + deliver( + makeRequest("tool.record_start", { + session_id: "aa11", + url: "https://example.com/start", + }), + ); + await vi.waitFor(() => expect(sent).toHaveLength(1)); + + expect(sent[0]).toMatchObject({ result: { tab_id: 7, recording: true } }); + expect(frameCoordinator.begin).toHaveBeenCalledOnce(); + expect(frameCoordinator.armTab).toHaveBeenCalledWith(expect.any(String), 7); + + deliver({ id: "r-2", method: "tool.record_stop", params: { session_id: "aa11" } }); + await vi.waitFor(() => expect(sent).toHaveLength(2)); + + expect(frameCoordinator.stop).toHaveBeenCalledOnce(); + expect(sent[1]).toMatchObject({ id: "r-2", result: { trace: { steps: [] } } }); + }); + it("routes tool.session_start to the SessionManager and replies with the window id", async () => { const { transport, sent, deliver } = fakeTransport(); const sessions = new SessionManager({ diff --git a/apps/extension/src/tools/dispatcher.ts b/apps/extension/src/tools/dispatcher.ts index 21be895..e0a5ac4 100644 --- a/apps/extension/src/tools/dispatcher.ts +++ b/apps/extension/src/tools/dispatcher.ts @@ -51,7 +51,12 @@ import { handleScreenshot, handleSnapshot, } from "./observation"; -import { handleRecordAwait, handleRecordStart, handleRecordStop } from "./record"; +import { + handleRecordAwait, + handleRecordStart, + handleRecordStop, + type RecordRuntimeDeps, +} from "./record"; import { handleSessionStart, handleSessionStop, @@ -99,6 +104,7 @@ export interface DispatcherDeps { transport: Transport; sessions: SessionManager; cdp?: DispatcherCdpRunner; + recording?: RecordRuntimeDeps; /** * Invoked whenever a dispatched RPC may have changed the live * session set (currently `tool.session_start` and @@ -134,6 +140,7 @@ export class ToolDispatcher { private readonly transport: Transport; private readonly sessions: SessionManager; private readonly cdp?: DispatcherCdpRunner; + private readonly recording?: RecordRuntimeDeps; private readonly onSessionsChanged?: () => void; private readonly onBrowserControlResumed?: (sessionId: string) => void; private readonly approveBorrow?: BorrowConfirmationApprover; @@ -153,6 +160,7 @@ export class ToolDispatcher { this.transport = deps.transport; this.sessions = deps.sessions; this.cdp = deps.cdp; + this.recording = deps.recording; this.onSessionsChanged = deps.onSessionsChanged; this.onBrowserControlResumed = deps.onBrowserControlResumed; this.approveBorrow = deps.approveBorrow; @@ -531,44 +539,26 @@ export class ToolDispatcher { signal, }); case "tool.record_start": - return handleRecordStart(this.sessions, req.params as RecordStartParams, { - tabsApi: chromeTabsApi, - sendToTab: (tabId, msg) => chrome.tabs.sendMessage(tabId, msg), - bypassOverlay: async (tabId, enabled) => { - try { - await chrome.tabs.sendMessage(tabId, { - type: OVERLAY_AUTOMATION_BYPASS, - enabled, - }); - } catch { - // Content script may be unavailable on restricted pages. - } - }, - ...(this.cdp ? { cdp: this.cdp } : {}), - signal, - }); + return this.recording + ? handleRecordStart(this.sessions, req.params as RecordStartParams, { + ...this.recording, + signal, + }) + : recordingRuntimeUnavailable(); case "tool.record_stop": - return handleRecordStop(this.sessions, req.params as RecordStopParams, { - tabsApi: chromeTabsApi, - sendToTab: (tabId, msg) => chrome.tabs.sendMessage(tabId, msg), - bypassOverlay: async (tabId, enabled) => { - try { - await chrome.tabs.sendMessage(tabId, { - type: OVERLAY_AUTOMATION_BYPASS, - enabled, - }); - } catch { - // Content script may be unavailable on restricted pages. - } - }, - signal, - }); + return this.recording + ? handleRecordStop(this.sessions, req.params as RecordStopParams, { + ...this.recording, + signal, + }) + : recordingRuntimeUnavailable(); case "tool.record_await": - return handleRecordAwait(this.sessions, req.params as RecordAwaitParams, { - tabsApi: chromeTabsApi, - sendToTab: (tabId, msg) => chrome.tabs.sendMessage(tabId, msg), - signal, - }); + return this.recording + ? handleRecordAwait(this.sessions, req.params as RecordAwaitParams, { + ...this.recording, + signal, + }) + : recordingRuntimeUnavailable(); default: return { code: "unknown_method", @@ -704,6 +694,13 @@ function isRpcError(v: unknown): v is RpcError { ); } +function recordingRuntimeUnavailable(): RpcError { + return { + code: "protocol_error", + message: "recording runtime is unavailable", + }; +} + function sessionIdForBrowserControlMethod(req: RequestFrame): string | null { switch (req.method) { case "tool.tab_create": diff --git a/apps/extension/src/tools/record.ts b/apps/extension/src/tools/record.ts index c99f4b7..3143046 100644 --- a/apps/extension/src/tools/record.ts +++ b/apps/extension/src/tools/record.ts @@ -292,6 +292,10 @@ export interface RecordDeps { signal?: AbortSignal; } +export type RecordRuntimeDeps = Omit & { + frameCoordinator: NonNullable; +}; + let defaultDeps: RecordDeps | null = null; function getDefaultDeps(): RecordDeps { if (!defaultDeps) {