import { describe, expect, it, vi } from "vitest"; import { SessionManager } from "@/session-manager/manager"; import type { CdpRunner } from "@/tools/shared"; import { handleEvaluate } from "../evaluate"; function fakeAgentWindow(ids: number[]) { let i = 0; return { create: vi.fn(async () => { const id = ids[i++]; if (id === undefined) throw new Error("ran out of fake ids"); return id; }), remove: vi.fn(async () => {}), ensureActiveTab: vi.fn(async () => 1), }; } interface FakeCdpOptions { windowId?: number; evaluate?: (params: unknown) => unknown; onDialogOpening?: () => void; dialogBlocking?: boolean; } function makeFakeCdp(opts: FakeCdpOptions = {}) { const sent: Array<{ tabId: number; method: string; params?: object }> = []; let dialogBlocking = opts.dialogBlocking ?? false; const dialogRecords: Array<{ tab_id: number; type: "alert"; message: string; handled: "accepted"; sequence: number; }> = []; let sequence = 0; const sendImpl = async (tabId: number, method: string, params?: object) => { sent.push({ tabId, method, params }); if (method === "Runtime.evaluate") { if (dialogBlocking) { opts.onDialogOpening?.(); dialogBlocking = false; sequence += 1; dialogRecords.push({ tab_id: tabId, type: "alert", message: "blocked", handled: "accepted", sequence, }); if (opts.evaluate) return opts.evaluate(params); return { result: { type: "number", value: 2 } }; } if (opts.evaluate) return opts.evaluate(params); return { result: { type: "number", value: 2 } }; } throw new Error(`unexpected CDP call ${method}`); }; const send = vi.fn(sendImpl); const cdp: CdpRunner = { send: send as unknown as ( tabId: number, method: string, params?: object, ) => Promise, trackSessionTab: vi.fn(), dialogCursor: vi.fn(() => 0), dialogsSince: vi.fn((_tabId: number, cursor: number) => dialogRecords.filter((d) => d.sequence > cursor), ), }; const windowId = opts.windowId ?? 100; const tabsApi = { get: vi.fn(async (tabId: number) => ({ id: tabId, windowId, active: true }) as chrome.tabs.Tab), query: vi.fn(async () => [{ id: 4, windowId, active: true } as chrome.tabs.Tab]), }; return { cdp, tabsApi, sent }; } describe("handleEvaluate", () => { it("rejects empty expression as invalid_params", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const fake = makeFakeCdp(); const res = await handleEvaluate( sm, { session_id: "aa11", expression: "" }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); expect(res).toMatchObject({ code: "invalid_params" }); }); it("evaluates 1+1 and returns ok with value 2", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const fake = makeFakeCdp({ evaluate: () => ({ result: { type: "number", value: 2 } }), }); const res = await handleEvaluate( sm, { session_id: "aa11", expression: "1+1" }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.ok).toBe(true); expect(res.value).toBe(2); expect(res.tab_id).toBe(4); expect(res.error).toBeUndefined(); }); it("forwards await_promise/return_by_value with their defaults filled in", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); let observed: Record | undefined; const fake = makeFakeCdp({ evaluate: (params) => { observed = params as Record; return { result: { type: "string", value: "ok" } }; }, }); await handleEvaluate( sm, { session_id: "aa11", expression: "Promise.resolve('ok')" }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); expect(observed).toMatchObject({ expression: "Promise.resolve('ok')", awaitPromise: true, returnByValue: true, throwOnSideEffect: false, }); }); it("respects explicit await_promise=false and return_by_value=false overrides", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); let observed: Record | undefined; const fake = makeFakeCdp({ evaluate: (params) => { observed = params as Record; return { result: { type: "string", value: "kept" } }; }, }); await handleEvaluate( sm, { session_id: "aa11", expression: "'kept'", await_promise: false, return_by_value: false, }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); expect(observed).toMatchObject({ awaitPromise: false, returnByValue: false, }); }); it("returns a JSON-safe RemoteObject descriptor when return_by_value=false", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const fake = makeFakeCdp({ evaluate: () => ({ result: { type: "object", subtype: "promise", className: "Promise", objectId: "remote-object-1", description: "Promise", }, }), }); const res = await handleEvaluate( sm, { session_id: "aa11", expression: "Promise.resolve(1)", await_promise: false, return_by_value: false, }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.ok).toBe(true); expect(res.value).toEqual({ type: "object", subtype: "promise", className: "Promise", objectId: "remote-object-1", description: "Promise", }); }); it("returns ok=false with error text when the expression throws", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const fake = makeFakeCdp({ evaluate: () => ({ result: { type: "object", subtype: "error" }, exceptionDetails: { text: "Uncaught Error", lineNumber: 2, columnNumber: 8, exception: { description: "Error: boom\n at :1:7", }, }, }), }); const res = await handleEvaluate( sm, { session_id: "aa11", expression: 'throw new Error("boom")' }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.ok).toBe(false); expect(res.value).toBeUndefined(); expect(res.error?.text).toContain("boom"); expect(res.error?.line).toBe(3); expect(res.error?.column).toBe(8); }); it("stringifies unserializableValue (Infinity / NaN / BigInt)", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const fake = makeFakeCdp({ evaluate: () => ({ result: { type: "number", unserializableValue: "Infinity" }, }), }); const res = await handleEvaluate( sm, { session_id: "aa11", expression: "1/0" }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.ok).toBe(true); expect(res.value).toBe("Infinity"); }); it("allows evaluating against a borrowed tab inside the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 0 }); const fake = makeFakeCdp(); fake.tabsApi.get = vi.fn( async () => ({ id: 7, windowId: 100, active: true }) as chrome.tabs.Tab, ); const res = await handleEvaluate( sm, { session_id: "aa11", expression: "1+1", tab_id: 7 }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.ok).toBe(true); }); it("returns dialogs observed during evaluate", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const fake = makeFakeCdp({ dialogBlocking: true }); const res = await handleEvaluate( sm, { session_id: "aa11", expression: "alert('hi'); 1+1" }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.ok).toBe(true); expect(res.dialogs).toHaveLength(1); expect(res.dialogs?.[0]?.message).toBe("blocked"); expect(res.dialogs?.[0]?.type).toBe("alert"); }); it("rejects tabs outside the Agent Window with permission_denied", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const fake = makeFakeCdp(); fake.tabsApi.get = vi.fn( async () => ({ id: 11, windowId: 200, active: true }) as chrome.tabs.Tab, ); const res = await handleEvaluate( sm, { session_id: "aa11", expression: "1+1", tab_id: 11 }, { cdp: fake.cdp, tabsApi: fake.tabsApi }, ); expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" }, }); }); });