fix(extension): preserve existing tab scope semantics

This commit is contained in:
drakezhang
2026-09-04 13:14:26 +08:00
parent 0dc11b30ea
commit 386ef91c24
17 changed files with 92 additions and 90 deletions
@@ -458,6 +458,16 @@ describe("ToolDispatcher", () => {
});
it("invokes onBrowserControlResumed for browser-control tools but not passive reads", async () => {
vi.stubGlobal("chrome", {
tabs: {
create: vi.fn(async (props: chrome.tabs.CreateProperties) => ({
id: 7,
windowId: props.windowId,
url: props.url,
active: props.active,
})),
},
});
const { transport, sent, deliver } = fakeTransport();
const sessions = new SessionManager({
agentWindow: {
@@ -467,7 +477,13 @@ describe("ToolDispatcher", () => {
},
});
const onBrowserControlResumed = vi.fn();
const dispatcher = new ToolDispatcher({ transport, sessions, onBrowserControlResumed });
const onAgentTabClaimed = vi.fn();
const dispatcher = new ToolDispatcher({
transport,
sessions,
onBrowserControlResumed,
onAgentTabClaimed,
});
dispatcher.start();
deliver(makeRequest("tool.session_start", { session_id: "aa11" }));
@@ -484,6 +500,7 @@ describe("ToolDispatcher", () => {
});
await flushMicrotasks();
expect(onBrowserControlResumed).toHaveBeenCalledWith("aa11");
expect(onAgentTabClaimed).toHaveBeenCalledWith(7, 1);
});
it("reasserts remembered hover before follow-up work and releases only after actions", async () => {
@@ -20,7 +20,7 @@ function fakeAgentWindow(ids: number[]) {
return id;
}),
remove: vi.fn(async () => {}),
ensureActiveTab: vi.fn(async () => 7),
ensureActiveTab: vi.fn(async () => 1),
};
}
@@ -197,7 +197,6 @@ describe("handleEmulate", () => {
it("targets an explicit tab_id", async () => {
const sm = await makeManager();
sm.get("aa11")?.agentCreatedTabs.add(9);
const deps = makeDeps();
const result = await handleEmulate(
sm,
@@ -277,7 +276,6 @@ describe("handleEmulate", () => {
it("keeps emulation state per tab", async () => {
const sm = await makeManager();
sm.get("aa11")?.agentCreatedTabs.add(9);
const deps = makeDeps();
await handleEmulate(sm, { session_id: "aa11", overrides: fullOverrides }, deps);
await handleEmulate(
@@ -12,7 +12,7 @@ function fakeAgentWindow(ids: number[]) {
return id;
}),
remove: vi.fn(async () => {}),
ensureActiveTab: vi.fn(async () => 4),
ensureActiveTab: vi.fn(async () => 1),
};
}
@@ -52,16 +52,10 @@ function installHelpLifecycleChrome() {
function fakeManager(sessionId: string, agentWindowId: number, tabId: number) {
const refStore = new RefStore();
const session = {
sessionId,
agentWindowId,
refStore,
borrowedTabs: new Map(),
agentCreatedTabs: new Set([tabId]),
};
const mgr = {
get: (id: string) => (id === sessionId ? session : null),
findByWindowId: (wid: number) => (wid === agentWindowId ? session : null),
get: (id: string) =>
id === sessionId ? { sessionId, agentWindowId, refStore, borrowedTabs: new Map() } : null,
findByWindowId: (wid: number) => (wid === agentWindowId ? { sessionId } : null),
} as unknown as SessionManager;
return mgr;
}
@@ -363,7 +357,6 @@ describe("handleRequestHelp", () => {
agentWindowId: 99,
refStore,
borrowedTabs: new Map(),
agentCreatedTabs: new Set([5]),
}
: null,
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
@@ -420,7 +413,6 @@ describe("handleRequestHelp", () => {
}),
},
borrowedTabs: new Map(),
agentCreatedTabs: new Set([5]),
}
: null,
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
@@ -506,7 +498,6 @@ describe("handleRequestHelp", () => {
agentWindowId: 99,
refStore,
borrowedTabs: new Map(),
agentCreatedTabs: new Set([5]),
}
: null,
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
@@ -21,7 +21,7 @@ function fakeAgentWindow(ids: number[]) {
return id;
}),
remove: vi.fn(async () => {}),
ensureActiveTab: vi.fn(async () => 4),
ensureActiveTab: vi.fn(async () => 1),
};
}
@@ -113,7 +113,6 @@ describe("handleClick", () => {
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
const ctx = await sm.start("aa11");
ctx.refStore.set("e3", 1234, { tabId: 4 });
ctx.agentCreatedTabs.add(5);
const fake = makeFakeCdp({});
const res = await handleClick(
sm,
@@ -21,7 +21,7 @@ function fakeAgentWindow(ids: number[]) {
return id;
}),
remove: vi.fn(async () => {}),
ensureActiveTab: vi.fn(async () => 4),
ensureActiveTab: vi.fn(async () => 1),
};
}
@@ -28,7 +28,7 @@ function fakeAgentWindow(ids: number[]) {
return id;
}),
remove: vi.fn(async () => {}),
ensureActiveTab: vi.fn(async () => 4),
ensureActiveTab: vi.fn(async () => 1),
};
}
@@ -84,7 +84,6 @@ function fakeManager() {
agentWindowId: AGENT_WINDOW_ID,
refStore: { resolve: () => null, replace: () => {} },
borrowedTabs: new Map(),
agentCreatedTabs: new Set([TAB_ID]),
}
: null,
findByWindowId: (windowId: number) =>
@@ -301,8 +301,6 @@ describe("handleSessionStop window release (issue #57)", () => {
const ctx = await sm.start("aa11");
ctx.agentCreatedTabs.add(11);
ctx.agentCreatedTabs.add(12);
// home tab id from ensureActiveTab override
ctx.agentCreatedTabs.add(10); // home tab
// user tab 99 (created via Chrome UI, NOT in agentCreatedTabs)
const state: FakeState = {
tabs: new Map([
@@ -340,7 +338,6 @@ describe("handleSessionStop window release (issue #57)", () => {
const sm = new SessionManager({ agentWindow: aw });
const ctx = await sm.start("aa11");
ctx.agentCreatedTabs.add(11);
ctx.agentCreatedTabs.add(10); // home tab
const state: FakeState = {
tabs: new Map([
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
@@ -369,7 +366,6 @@ describe("handleSessionStop window release (issue #57)", () => {
const sm = new SessionManager({ agentWindow: aw });
const ctx = await sm.start("aa11");
ctx.agentCreatedTabs.add(11); // no longer in state → remove throws
ctx.agentCreatedTabs.add(10); // home tab
const state: FakeState = {
tabs: new Map([[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab]]),
windowsClosed: new Set(),
@@ -399,7 +395,6 @@ describe("handleSessionStop window release (issue #57)", () => {
const sm = new SessionManager({ agentWindow: aw });
const ctx = await sm.start("aa11");
ctx.agentCreatedTabs.add(11);
ctx.agentCreatedTabs.add(10); // home tab
const state: FakeState = {
tabs: new Map([
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
@@ -427,7 +422,6 @@ describe("handleSessionStop window release (issue #57)", () => {
const sm = new SessionManager({ agentWindow: aw });
const ctx = await sm.start("aa11");
ctx.agentCreatedTabs.add(11);
ctx.agentCreatedTabs.add(10); // home tab
const state: FakeState = {
tabs: new Map([[11, { id: 11, windowId: agentWindowId } as chrome.tabs.Tab]]),
windowsClosed: new Set(),
@@ -473,7 +467,6 @@ describe("handleSessionStop window release (issue #57)", () => {
const sm = new SessionManager({ agentWindow: aw });
const ctx = await sm.start("aa11");
ctx.agentCreatedTabs.add(11); // Step 4 remove() will throw for this tab
ctx.agentCreatedTabs.add(10); // home tab
const state: FakeState = {
tabs: new Map([
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
@@ -60,24 +60,19 @@ describe("handleTabList", () => {
expect(res.tabs.map((t) => t.scope).sort()).toEqual(["agent", "user"]);
});
it("scope=user includes unclaimed tabs inside the Agent Window", async () => {
it("scope=user excludes the requesting session's 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).toHaveLength(2);
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",
]);
expect(res.tabs.map((t) => t.url)).toEqual(["https://example.com", "https://github.com"]);
});
it("scope=agent returns only the requesting session's Agent Window tabs", async () => {
@@ -302,8 +297,7 @@ describe("handleTabCreate", () => {
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);
await sm.start("aa11");
const state: FakeTabState = {
tabs: new Map([[5, { id: 5, windowId: 100 } as chrome.tabs.Tab]]),
nextTabId: 50,
@@ -316,23 +310,6 @@ describe("handleTabClose", () => {
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");
@@ -383,8 +360,7 @@ describe("handleTabClose", () => {
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);
await sm.start("aa11");
const state: FakeTabState = {
tabs: new Map([[3, { id: 3, windowId: 100 } as chrome.tabs.Tab]]),
nextTabId: 50,
@@ -12,7 +12,7 @@ function fakeAgentWindow(ids: number[]) {
return id;
}),
remove: vi.fn(async () => {}),
ensureActiveTab: vi.fn(async () => 4),
ensureActiveTab: vi.fn(async () => 1),
};
}