import { describe, expect, it, vi } from "vitest"; import { SessionManager } from "@/session-manager/manager"; import { type AgentOverlayResetApi, type ChromeWindowsApi, handleTabBorrow, handleTabClose, handleTabCreate, handleTabList, handleTabReturn, handleTabSelect, NEW_TAB_DEFAULT_URL, returnBorrowedTab, type TabMutationApi, } from "../tabs"; 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 window ids"); return id; }), remove: vi.fn(async () => {}), ensureActiveTab: vi.fn(async () => 1), }; } function fakeChromeTabsApi(tabs: Partial[]) { return { query: vi.fn(async (_q: chrome.tabs.QueryInfo) => tabs.map((t, i) => ({ id: i + 1, ...t }) as chrome.tabs.Tab), ), }; } describe("handleTabList", () => { it("returns invalid_params for unknown scope values", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([10]) }); await sm.start("aa11"); const res = await handleTabList( sm, { session_id: "aa11", scope: "private" } as unknown as Parameters[1], fakeChromeTabsApi([]), ); expect(res).toMatchObject({ code: "invalid_params" }); }); it("default scope=all unions user and agent tabs when scope is omitted", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const tabs = fakeChromeTabsApi([ { windowId: 100, url: "chrome://newtab/", title: "Agent New Tab", active: true }, { windowId: 200, url: "https://example.com", title: "Example", active: true }, ]); const res = await handleTabList(sm, { session_id: "aa11" }, tabs); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.tabs).toHaveLength(2); expect(res.tabs.map((t) => t.scope).sort()).toEqual(["agent", "user"]); }); it("scope=user includes unclaimed tabs inside the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const tabs = fakeChromeTabsApi([ { windowId: 100, url: "chrome://newtab/", title: "Agent New Tab", active: true }, { windowId: 100, url: "https://user.example/", title: "Free user tab", active: false }, { windowId: 200, url: "https://example.com", title: "Example", active: true }, { windowId: 200, url: "https://github.com", title: "GitHub", active: false }, ]); const res = await handleTabList(sm, { session_id: "aa11", scope: "user" }, tabs); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.tabs).toHaveLength(3); expect(res.tabs.every((t) => t.scope === "user")).toBe(true); expect(res.tabs.map((t) => t.url)).toEqual([ "https://user.example/", "https://example.com", "https://github.com", ]); }); it("scope=agent returns only the requesting session's Agent Window tabs", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const tabs = fakeChromeTabsApi([ { windowId: 100, url: "chrome://newtab/", title: "Agent" }, { windowId: 200, url: "https://example.com" }, ]); const res = await handleTabList(sm, { session_id: "aa11", scope: "agent" }, tabs); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.tabs).toHaveLength(1); expect(res.tabs[0]).toMatchObject({ scope: "agent", url: "chrome://newtab/" }); }); it("hides other sessions' Agent Window tabs from scope=all (cross-session isolation)", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100, 200]) }); await sm.start("aa11"); // agent window 100 await sm.start("bb22"); // agent window 200 const tabs = fakeChromeTabsApi([ { windowId: 100, url: "chrome://newtab/" }, // aa11's agent window { windowId: 200, url: "https://internal-tools/" }, // bb22's agent window — must be hidden from aa11 { windowId: 300, url: "https://example.com" }, ]); const res = await handleTabList(sm, { session_id: "aa11", scope: "all" }, tabs); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.tabs.map((t) => t.url).sort()).toEqual(["chrome://newtab/", "https://example.com"]); }); }); // --------------------------------------------------------------------------- // M8 helpers // --------------------------------------------------------------------------- interface FakeTabState { tabs: Map; nextTabId: number; windowsClosed: Set; } function makeTabMutationApi(state: FakeTabState): { api: TabMutationApi; spies: { create: ReturnType; remove: ReturnType; update: ReturnType; get: ReturnType; move: ReturnType; }; } { const create = vi.fn(async (props: chrome.tabs.CreateProperties) => { const id = state.nextTabId++; const tab = { id, windowId: props.windowId ?? 1, url: props.url ?? "", pendingUrl: props.url, active: props.active ?? true, index: props.index ?? state.tabs.size, } as chrome.tabs.Tab; state.tabs.set(id, tab); return tab; }); const remove = vi.fn(async (tabId: number) => { state.tabs.delete(tabId); }); const update = vi.fn(async (tabId: number, props: chrome.tabs.UpdateProperties) => { const t = state.tabs.get(tabId); if (!t) throw new Error(`update: tab ${tabId} not found`); if (props.active !== undefined) (t as { active?: boolean }).active = props.active; return t; }); const get = vi.fn(async (tabId: number) => { const t = state.tabs.get(tabId); if (!t) throw new Error(`get: tab ${tabId} not found`); return t; }); const move = vi.fn(async (tabId: number, props: chrome.tabs.MoveProperties) => { const t = state.tabs.get(tabId); if (!t) throw new Error(`move: tab ${tabId} not found`); if (typeof props.windowId === "number") { if (state.windowsClosed.has(props.windowId)) { throw new Error(`move: window ${props.windowId} closed`); } (t as { windowId?: number }).windowId = props.windowId; } if (typeof props.index === "number") { (t as { index?: number }).index = props.index === -1 ? 999 : props.index; } return t; }); return { api: { create, remove, update, get, move }, spies: { create, remove, update, get, move }, }; } function makeWindowsApi( state: FakeTabState, opts?: { lastFocused?: number | null; createWindowId?: number }, ): { api: ChromeWindowsApi; spies: { get: ReturnType; lastFocused: ReturnType; create: ReturnType; remove: ReturnType; }; } { const get = vi.fn(async (windowId: number) => { if (state.windowsClosed.has(windowId)) { throw new Error(`window ${windowId} closed`); } return { id: windowId } as chrome.windows.Window; }); const lastFocused = vi.fn(async () => { if (opts?.lastFocused === null) { throw new Error("no last focused window"); } return { id: opts?.lastFocused ?? 500 } as chrome.windows.Window; }); const create = vi.fn(async (_props: chrome.windows.CreateData) => { const id = opts?.createWindowId ?? 999; return { id } as chrome.windows.Window; }); const remove = vi.fn(async (windowId: number) => { state.windowsClosed.add(windowId); }); return { api: { get, getLastFocused: lastFocused, create, remove }, spies: { get, lastFocused, create, remove }, }; } describe("handleTabCreate", () => { it("creates a tab inside the requesting session's Agent Window with the default URL", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map(), nextTabId: 10, windowsClosed: new Set() }; const { api, spies } = makeTabMutationApi(state); const res = await handleTabCreate(sm, { session_id: "aa11" }, { tabs: api }); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(spies.create).toHaveBeenCalledWith({ windowId: 100, url: NEW_TAB_DEFAULT_URL, active: true, }); expect(res).toMatchObject({ tab_id: 10, window_id: 100 }); expect(sm.get("aa11")?.agentCreatedTabs.has(10)).toBe(true); }); it("forwards explicit url + index + active=false", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map(), nextTabId: 10, windowsClosed: new Set() }; const { api, spies } = makeTabMutationApi(state); const res = await handleTabCreate( sm, { session_id: "aa11", url: "https://example.com/", active: false, index: 2, }, { tabs: api }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(spies.create).toHaveBeenCalledWith({ windowId: 100, url: "https://example.com/", active: false, index: 2, }); expect(res.url).toBe("https://example.com/"); }); it("rejects negative index", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map(), nextTabId: 10, windowsClosed: new Set() }; const { api } = makeTabMutationApi(state); const res = await handleTabCreate(sm, { session_id: "aa11", index: -1 }, { tabs: api }); expect(res).toMatchObject({ code: "invalid_params" }); }); it("does not claim any tab when chrome.tabs.create fails", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map(), nextTabId: 10, windowsClosed: new Set() }; const { api, spies } = makeTabMutationApi(state); spies.create.mockRejectedValueOnce(new Error("chrome.tabs.create failed")); const res = await handleTabCreate(sm, { session_id: "aa11" }, { tabs: api }); expect(res).toMatchObject({ code: "protocol_error" }); expect(sm.get("aa11")?.agentCreatedTabs).toEqual(new Set([1])); }); it("claims the returned id even when a user tab appears during tab creation", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); const state: FakeTabState = { tabs: new Map(), nextTabId: 10, windowsClosed: new Set() }; const { api, spies } = makeTabMutationApi(state); let resolveCreate!: (tab: chrome.tabs.Tab) => void; const createResult = new Promise((resolve) => { resolveCreate = resolve; }); spies.create.mockImplementationOnce(() => createResult); const pending = handleTabCreate(sm, { session_id: "aa11" }, { tabs: api }); // A user can create an unrelated same-window tab while Chrome is still // resolving the agent request. There is no pending slot for it to steal. expect(ctx.agentCreatedTabs.has(99)).toBe(false); resolveCreate({ id: 10, windowId: 100, active: true } as chrome.tabs.Tab); await expect(pending).resolves.toMatchObject({ tab_id: 10 }); expect(ctx.agentCreatedTabs.has(10)).toBe(true); expect(ctx.agentCreatedTabs.has(99)).toBe(false); }); }); describe("handleTabClose", () => { it("removes a tab inside the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.agentCreatedTabs.add(5); const state: FakeTabState = { tabs: new Map([[5, { id: 5, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); const res = await handleTabClose(sm, { session_id: "aa11", tab_id: 5 }, { tabs: api }); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.tab_id).toBe(5); expect(spies.remove).toHaveBeenCalledWith(5); }); it("refuses to close a free tab in the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map([[5, { id: 5, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); const res = await handleTabClose(sm, { session_id: "aa11", tab_id: 5 }, { tabs: api }); expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" }, }); expect(spies.remove).not.toHaveBeenCalled(); }); it("rejects closing a borrowed tab — caller must tab_return first", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(8, { tabId: 8, originalWindowId: 200, originalIndex: 3 }); const state: FakeTabState = { tabs: new Map([[8, { id: 8, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); const res = await handleTabClose(sm, { session_id: "aa11", tab_id: 8 }, { tabs: api }); expect(res).toMatchObject({ code: "invalid_params" }); expect(spies.remove).not.toHaveBeenCalled(); }); it("refuses to close a tab outside the Agent Window with permission_denied", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map([[9, { id: 9, windowId: 200 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const res = await handleTabClose(sm, { session_id: "aa11", tab_id: 9 }, { tabs: api }); expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" }, }); }); it("rejects when another session has borrowed the tab", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100, 200]) }); await sm.start("aa11"); const ctxB = await sm.start("bb22"); ctxB.borrowedTabs.set(7, { tabId: 7, originalWindowId: 300, originalIndex: 1 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 200 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const res = await handleTabClose(sm, { session_id: "aa11", tab_id: 7 }, { tabs: api }); expect(res).toMatchObject({ code: "permission_denied", data: { reason: "borrow_conflict" } }); }); }); describe("handleTabSelect", () => { it("activates a tab inside the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.agentCreatedTabs.add(3); const state: FakeTabState = { tabs: new Map([[3, { id: 3, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); const res = await handleTabSelect(sm, { session_id: "aa11", tab_id: 3 }, { tabs: api }); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res).toEqual({ tab_id: 3, window_id: 100 }); expect(spies.update).toHaveBeenCalledWith(3, { active: true }); }); it("rejects activating a tab in a different session's Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100, 200]) }); await sm.start("aa11"); await sm.start("bb22"); const state: FakeTabState = { tabs: new Map([[3, { id: 3, windowId: 200 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const res = await handleTabSelect(sm, { session_id: "aa11", tab_id: 3 }, { tabs: api }); expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" }, }); }); it("rejects a borrowed tab that is no longer in the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(3, { tabId: 3, originalWindowId: 200, originalIndex: 0 }); const state: FakeTabState = { tabs: new Map([[3, { id: 3, windowId: 200 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const res = await handleTabSelect(sm, { session_id: "aa11", tab_id: 3 }, { tabs: api }); expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" }, }); }); }); describe("handleTabBorrow", () => { it("moves the tab into the Agent Window and records the original position", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 200, index: 4 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); const res = await handleTabBorrow(sm, { session_id: "aa11", tab_id: 7 }, { tabs: api }); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res).toEqual({ tab_id: 7, original_window_id: 200, original_index: 4, agent_window_id: 100, }); expect(ctx.borrowedTabs.get(7)).toEqual({ tabId: 7, originalWindowId: 200, originalIndex: 4, }); expect(spies.move).toHaveBeenCalledWith(7, { windowId: 100, index: -1 }); expect(spies.update).toHaveBeenCalledWith(7, { active: true }); }); it("refuses to borrow a tab already inside the Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100, index: 0 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const res = await handleTabBorrow(sm, { session_id: "aa11", tab_id: 7 }, { tabs: api }); expect(res).toMatchObject({ code: "invalid_params" }); }); it("refuses to borrow a tab already held by another session", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100, 200]) }); await sm.start("aa11"); const ctxB = await sm.start("bb22"); ctxB.borrowedTabs.set(7, { tabId: 7, originalWindowId: 300, originalIndex: 1 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 200, index: 0 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const res = await handleTabBorrow(sm, { session_id: "aa11", tab_id: 7 }, { tabs: api }); expect(res).toMatchObject({ code: "permission_denied", data: { reason: "borrow_conflict" } }); }); it("allows only one concurrent borrow of the same tab across sessions", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100, 200]) }); await sm.start("aa11"); await sm.start("bb22"); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 300, index: 1 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const [a, b] = await Promise.all([ handleTabBorrow(sm, { session_id: "aa11", tab_id: 7 }, { tabs: api }), handleTabBorrow(sm, { session_id: "bb22", tab_id: 7 }, { tabs: api }), ]); const results = [a, b]; expect(results.filter((r) => !("code" in r))).toHaveLength(1); expect(results.filter((r) => "code" in r && r.code === "permission_denied")).toHaveLength(1); expect(sm.list().filter((ctx) => ctx.borrowedTabs.has(7))).toHaveLength(1); }); it("refuses to borrow a tab inside another session's Agent Window", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100, 200]) }); await sm.start("aa11"); await sm.start("bb22"); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 200, index: 0 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const res = await handleTabBorrow(sm, { session_id: "aa11", tab_id: 7 }, { tabs: api }); expect(res).toMatchObject({ code: "permission_denied", data: { reason: "agent_window_scope" }, }); }); it("returns cancelled when the approver declines", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 200, index: 0 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); const res = await handleTabBorrow( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, approveBorrow: async () => false }, ); expect(res).toMatchObject({ code: "cancelled" }); expect(ctx.borrowedTabs.has(7)).toBe(false); expect(spies.move).not.toHaveBeenCalled(); }); it("passes confirm to the approver and treats approver errors as cancellation", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 200, index: 0 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); const approveBorrow = vi.fn(async () => { throw new Error("confirmation dismissed"); }); const res = await handleTabBorrow( sm, { session_id: "aa11", tab_id: 7, confirm: false }, { tabs: api, approveBorrow }, ); expect(approveBorrow).toHaveBeenCalledWith({ sessionId: "aa11", tabId: 7, confirm: false }); expect(res).toMatchObject({ code: "cancelled" }); expect(spies.move).not.toHaveBeenCalled(); }); }); describe("handleTabReturn", () => { it("returns the tab to its original window/index and clears borrowedTabs", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100, index: 999 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); const { api: windowsApi } = makeWindowsApi(state); const res = await handleTabReturn( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, windows: windowsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.returned_to_window_id).toBe(200); expect(res.returned_to_index).toBe(4); expect(res.fallback).toBeUndefined(); expect(ctx.borrowedTabs.has(7)).toBe(false); expect(spies.move).toHaveBeenCalledWith(7, { windowId: 200, index: 4 }); }); it("resets agent overlays in a returned tab after a successful tab_return", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100, index: 999 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); const { api: windowsApi } = makeWindowsApi(state); const agentOverlayReset = { resetAgentOverlays: vi.fn(async () => {}), } satisfies AgentOverlayResetApi; const res = await handleTabReturn( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, windows: windowsApi, agentOverlayReset }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(agentOverlayReset.resetAgentOverlays).toHaveBeenCalledWith(7, "aa11"); }); it("falls back to the last focused normal window when the original is gone", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set([200]), }; const { api, spies } = makeTabMutationApi(state); const { api: windowsApi, spies: winSpies } = makeWindowsApi(state, { lastFocused: 500 }); const res = await handleTabReturn( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, windows: windowsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.returned_to_window_id).toBe(500); expect(res.fallback).toBe(true); expect(spies.move).toHaveBeenCalledWith(7, { windowId: 500, index: -1 }); expect(winSpies.create).not.toHaveBeenCalled(); }); it("creates a new window when no normal window remains", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set([200]), }; const { api } = makeTabMutationApi(state); const { api: windowsApi, spies: winSpies } = makeWindowsApi(state, { lastFocused: null, createWindowId: 777, }); const res = await handleTabReturn( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, windows: windowsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.returned_to_window_id).toBe(777); expect(res.fallback).toBe(true); expect(winSpies.create).toHaveBeenCalledOnce(); }); it("closes a newly created fallback window when cancellation wins before the move", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set([200]), }; const { api, spies } = makeTabMutationApi(state); const { api: windowsApi, spies: winSpies } = makeWindowsApi(state, { lastFocused: 100, createWindowId: 777, }); let resolveCreate: (window: chrome.windows.Window) => void = () => {}; winSpies.create.mockImplementationOnce( () => new Promise((resolve) => { resolveCreate = resolve; }), ); const controller = new AbortController(); const pending = handleTabReturn( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, windows: windowsApi, signal: controller.signal }, ); await vi.waitFor(() => expect(winSpies.create).toHaveBeenCalledOnce()); controller.abort(); resolveCreate({ id: 777 } as chrome.windows.Window); await expect(pending).resolves.toMatchObject({ code: "cancelled" }); expect(winSpies.remove).toHaveBeenCalledWith(777); expect(spies.move).not.toHaveBeenCalled(); expect(ctx.borrowedTabs.has(7)).toBe(true); }); it("surfaces the fallback window id when cancellation cleanup fails", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set([200]), }; const { api, spies } = makeTabMutationApi(state); const { api: windowsApi, spies: winSpies } = makeWindowsApi(state, { lastFocused: 100, createWindowId: 777, }); let resolveCreate: (window: chrome.windows.Window) => void = () => {}; winSpies.create.mockImplementationOnce( () => new Promise((resolve) => { resolveCreate = resolve; }), ); winSpies.remove.mockRejectedValueOnce(new Error("window removal denied")); const controller = new AbortController(); const pending = handleTabReturn( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, windows: windowsApi, signal: controller.signal }, ); await vi.waitFor(() => expect(winSpies.create).toHaveBeenCalledOnce()); controller.abort(); resolveCreate({ id: 777 } as chrome.windows.Window); await expect(pending).resolves.toMatchObject({ code: "protocol_error", data: { reason: "cleanup_failed", resource_type: "window", resource_id: 777 }, message: expect.stringMatching(/fallback window 777.*window removal denied/), }); expect(spies.move).not.toHaveBeenCalled(); expect(ctx.borrowedTabs.has(7)).toBe(true); }); it("falls back to a new window when getLastFocused only returns 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: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set([200]), }; const { api } = makeTabMutationApi(state); const { api: windowsApi, spies: winSpies } = makeWindowsApi(state, { lastFocused: 100, createWindowId: 777, }); const res = await handleTabReturn( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, windows: windowsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.returned_to_window_id).toBe(777); expect(winSpies.create).toHaveBeenCalledOnce(); }); it("does not fall back into another session's Agent Window (creates a new window instead)", async () => { // Regression: a returned borrowed tab whose original window is gone // must never be parked in *another* session's Agent Window. Agent // Windows are `type: "normal"`, so getLastFocused({windowTypes: // ["normal"]}) can return session bb22's window (200). Without the // isAgentWindowId guard the tab would be moved into window 200, // letting bb22 write to it and destroying it when bb22 stops. const sm = new SessionManager({ agentWindow: fakeAgentWindow([100, 200]) }); const ctx = await sm.start("aa11"); await sm.start("bb22"); // owns Agent Window 200 ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 300, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set([300]), }; const { api, spies } = makeTabMutationApi(state); const { api: windowsApi, spies: winSpies } = makeWindowsApi(state, { lastFocused: 200, // session bb22's Agent Window createWindowId: 777, }); const res = await handleTabReturn( sm, { session_id: "aa11", tab_id: 7 }, { tabs: api, windows: windowsApi }, ); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.returned_to_window_id).toBe(777); expect(res.fallback).toBe(true); expect(winSpies.create).toHaveBeenCalledOnce(); expect(spies.move).not.toHaveBeenCalledWith(7, expect.objectContaining({ windowId: 200 })); }); it("returnBorrowedTab honours an explicit isAgentWindowId predicate", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 300, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set([300]), }; const { api, spies } = makeTabMutationApi(state); const { api: windowsApi, spies: winSpies } = makeWindowsApi(state, { lastFocused: 555, createWindowId: 777, }); const res = await returnBorrowedTab(ctx, 7, { tabs: api, windows: windowsApi, isAgentWindowId: (windowId) => windowId === 555, }); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.toWindowId).toBe(777); expect(res.fallback).toBe(true); expect(winSpies.create).toHaveBeenCalledOnce(); expect(spies.move).not.toHaveBeenCalledWith(7, expect.objectContaining({ windowId: 555 })); }); it("returns not_found when the tab is not borrowed by this session", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); await sm.start("aa11"); const state: FakeTabState = { tabs: new Map(), nextTabId: 50, windowsClosed: new Set() }; const { api } = makeTabMutationApi(state); const { api: windowsApi } = makeWindowsApi(state); const res = await handleTabReturn( sm, { session_id: "aa11", tab_id: 9999 }, { tabs: api, windows: windowsApi }, ); expect(res).toMatchObject({ code: "not_found" }); }); it("returnBorrowedTab surfaces a chrome.tabs.move failure as cdp_failed", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api } = makeTabMutationApi(state); api.move = vi.fn(async () => { throw new Error("simulated move failure"); }); const { api: windowsApi } = makeWindowsApi(state); const res = await returnBorrowedTab(ctx, 7, { tabs: api, windows: windowsApi }); expect(res).toMatchObject({ code: "cdp_failed" }); }); it("falls back to a new window when moving back to the original position fails", async () => { const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) }); const ctx = await sm.start("aa11"); ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 4 }); const state: FakeTabState = { tabs: new Map([[7, { id: 7, windowId: 100 } as chrome.tabs.Tab]]), nextTabId: 50, windowsClosed: new Set(), }; const { api, spies } = makeTabMutationApi(state); spies.move.mockImplementationOnce(async () => { throw new Error("original index no longer valid"); }); const { api: windowsApi } = makeWindowsApi(state, { lastFocused: 100, createWindowId: 777, }); const res = await returnBorrowedTab(ctx, 7, { tabs: api, windows: windowsApi }); if ("code" in res) throw new Error(`unexpected error: ${JSON.stringify(res)}`); expect(res.toWindowId).toBe(777); expect(res.fallback).toBe(true); expect(spies.move).toHaveBeenLastCalledWith(7, { windowId: 777, index: 0 }); }); });