mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(extension): claim agent tabs explicitly
This commit is contained in:
@@ -196,9 +196,8 @@ describe("ToolDispatcher", () => {
|
||||
},
|
||||
});
|
||||
await sessions.start("aa11");
|
||||
// Register tab 7 as agent-created (pending counter + onCreated classify).
|
||||
sessions.markAgentTabPending(4242);
|
||||
sessions.classifyNewTab(7, 4242);
|
||||
// Register the exact tab id returned by the agent's creation path.
|
||||
sessions.get("aa11")?.agentCreatedTabs.add(7);
|
||||
|
||||
const dispatcher = new ToolDispatcher({ transport, sessions });
|
||||
dispatcher.start();
|
||||
|
||||
@@ -20,7 +20,7 @@ function fakeAgentWindow(ids: number[]) {
|
||||
return id;
|
||||
}),
|
||||
remove: vi.fn(async () => {}),
|
||||
ensureActiveTab: vi.fn(async () => 1),
|
||||
ensureActiveTab: vi.fn(async () => 7),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ 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,
|
||||
@@ -276,6 +277,7 @@ 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 () => 1),
|
||||
ensureActiveTab: vi.fn(async () => 4),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -52,10 +52,16 @@ 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 ? { sessionId, agentWindowId, refStore, borrowedTabs: new Map() } : null,
|
||||
findByWindowId: (wid: number) => (wid === agentWindowId ? { sessionId } : null),
|
||||
get: (id: string) => (id === sessionId ? session : null),
|
||||
findByWindowId: (wid: number) => (wid === agentWindowId ? session : null),
|
||||
} as unknown as SessionManager;
|
||||
return mgr;
|
||||
}
|
||||
@@ -357,6 +363,7 @@ describe("handleRequestHelp", () => {
|
||||
agentWindowId: 99,
|
||||
refStore,
|
||||
borrowedTabs: new Map(),
|
||||
agentCreatedTabs: new Set([5]),
|
||||
}
|
||||
: null,
|
||||
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
|
||||
@@ -413,6 +420,7 @@ describe("handleRequestHelp", () => {
|
||||
}),
|
||||
},
|
||||
borrowedTabs: new Map(),
|
||||
agentCreatedTabs: new Set([5]),
|
||||
}
|
||||
: null,
|
||||
findByWindowId: (wid: number) => (wid === 99 ? { sessionId: "abcd" } : null),
|
||||
@@ -498,6 +506,7 @@ 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 () => 1),
|
||||
ensureActiveTab: vi.fn(async () => 4),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,6 +113,7 @@ 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 () => 1),
|
||||
ensureActiveTab: vi.fn(async () => 4),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ function fakeAgentWindow(ids: number[]) {
|
||||
return id;
|
||||
}),
|
||||
remove: vi.fn(async () => {}),
|
||||
ensureActiveTab: vi.fn(async () => 1),
|
||||
ensureActiveTab: vi.fn(async () => 4),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ function fakeManager() {
|
||||
agentWindowId: AGENT_WINDOW_ID,
|
||||
refStore: { resolve: () => null, replace: () => {} },
|
||||
borrowedTabs: new Map(),
|
||||
agentCreatedTabs: new Set([TAB_ID]),
|
||||
}
|
||||
: null,
|
||||
findByWindowId: (windowId: number) =>
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("handleSessionStop window release (issue #57)", () => {
|
||||
ctx.agentCreatedTabs.add(11);
|
||||
ctx.agentCreatedTabs.add(12);
|
||||
// home tab id from ensureActiveTab override
|
||||
ctx.homeTabId = 10;
|
||||
ctx.agentCreatedTabs.add(10); // home tab
|
||||
// user tab 99 (created via Chrome UI, NOT in agentCreatedTabs)
|
||||
const state: FakeState = {
|
||||
tabs: new Map([
|
||||
@@ -340,7 +340,7 @@ describe("handleSessionStop window release (issue #57)", () => {
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
const ctx = await sm.start("aa11");
|
||||
ctx.agentCreatedTabs.add(11);
|
||||
ctx.homeTabId = 10;
|
||||
ctx.agentCreatedTabs.add(10); // home tab
|
||||
const state: FakeState = {
|
||||
tabs: new Map([
|
||||
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
|
||||
@@ -369,7 +369,7 @@ 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.homeTabId = 10;
|
||||
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 +399,7 @@ describe("handleSessionStop window release (issue #57)", () => {
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
const ctx = await sm.start("aa11");
|
||||
ctx.agentCreatedTabs.add(11);
|
||||
ctx.homeTabId = 10;
|
||||
ctx.agentCreatedTabs.add(10); // home tab
|
||||
const state: FakeState = {
|
||||
tabs: new Map([
|
||||
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
|
||||
@@ -427,7 +427,7 @@ describe("handleSessionStop window release (issue #57)", () => {
|
||||
const sm = new SessionManager({ agentWindow: aw });
|
||||
const ctx = await sm.start("aa11");
|
||||
ctx.agentCreatedTabs.add(11);
|
||||
ctx.homeTabId = 10;
|
||||
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 +473,7 @@ 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.homeTabId = 10;
|
||||
ctx.agentCreatedTabs.add(10); // home tab
|
||||
const state: FakeState = {
|
||||
tabs: new Map([
|
||||
[10, { id: 10, windowId: agentWindowId } as chrome.tabs.Tab],
|
||||
|
||||
@@ -60,19 +60,24 @@ describe("handleTabList", () => {
|
||||
expect(res.tabs.map((t) => t.scope).sort()).toEqual(["agent", "user"]);
|
||||
});
|
||||
|
||||
it("scope=user excludes the requesting session's Agent Window", async () => {
|
||||
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(2);
|
||||
expect(res.tabs).toHaveLength(3);
|
||||
expect(res.tabs.every((t) => t.scope === "user")).toBe(true);
|
||||
expect(res.tabs.map((t) => t.url)).toEqual(["https://example.com", "https://github.com"]);
|
||||
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 () => {
|
||||
@@ -221,6 +226,7 @@ describe("handleTabCreate", () => {
|
||||
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 () => {
|
||||
@@ -257,7 +263,7 @@ describe("handleTabCreate", () => {
|
||||
expect(res).toMatchObject({ code: "invalid_params" });
|
||||
});
|
||||
|
||||
it("releases the pending agent-tab slot when chrome.tabs.create fails", async () => {
|
||||
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() };
|
||||
@@ -267,18 +273,37 @@ describe("handleTabCreate", () => {
|
||||
const res = await handleTabCreate(sm, { session_id: "aa11" }, { tabs: api });
|
||||
expect(res).toMatchObject({ code: "protocol_error" });
|
||||
|
||||
// No tab was opened, so no `onCreated` event will consume the pending
|
||||
// slot. Without the release the counter leaks and the next tab the user
|
||||
// opens is misclassified as agent-created — the exact confusion the
|
||||
// counter exists to prevent.
|
||||
expect(sm.classifyNewTab(42, 100)).toBe("user");
|
||||
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<chrome.tabs.Tab>((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]) });
|
||||
await sm.start("aa11");
|
||||
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,
|
||||
@@ -291,6 +316,23 @@ 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");
|
||||
@@ -341,7 +383,8 @@ describe("handleTabClose", () => {
|
||||
describe("handleTabSelect", () => {
|
||||
it("activates a tab inside the Agent Window", async () => {
|
||||
const sm = new SessionManager({ agentWindow: fakeAgentWindow([100]) });
|
||||
await sm.start("aa11");
|
||||
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,
|
||||
|
||||
@@ -12,7 +12,7 @@ function fakeAgentWindow(ids: number[]) {
|
||||
return id;
|
||||
}),
|
||||
remove: vi.fn(async () => {}),
|
||||
ensureActiveTab: vi.fn(async () => 1),
|
||||
ensureActiveTab: vi.fn(async () => 4),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user