mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Tool cancellation is now cooperative end-to-end: the queue stays busy until compensation completes, cancellation is checked cooperatively across the pipeline, the fallback window is compensated too, and cleanup_failed is surfaced explicitly. Includes a gated delayed-compensation E2E test.
180 lines
6.9 KiB
TypeScript
180 lines
6.9 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import type { AgentWindowApi, AgentWindowCreateOptions } from "../agent-window";
|
|
import { SessionManager } from "../manager";
|
|
|
|
function fakeAgentWindow(): AgentWindowApi & {
|
|
createMock: ReturnType<typeof vi.fn>;
|
|
removeMock: ReturnType<typeof vi.fn>;
|
|
ensureActiveTabMock: ReturnType<typeof vi.fn>;
|
|
} {
|
|
let nextId = 100;
|
|
const createMock = vi.fn(async (_url: string, _opts?: AgentWindowCreateOptions) => {
|
|
const id = nextId++;
|
|
return id;
|
|
});
|
|
const removeMock = vi.fn(async (_id: number) => {});
|
|
const ensureActiveTabMock = vi.fn(async (_windowId: number, _url: string) => {});
|
|
return {
|
|
create: createMock,
|
|
remove: removeMock,
|
|
ensureActiveTab: ensureActiveTabMock,
|
|
createMock,
|
|
removeMock,
|
|
ensureActiveTabMock,
|
|
};
|
|
}
|
|
|
|
describe("SessionManager", () => {
|
|
it("creates an Agent Window when starting a session", async () => {
|
|
const aw = fakeAgentWindow();
|
|
const sm = new SessionManager({ agentWindow: aw, now: () => 1700000000000 });
|
|
const ctx = await sm.start("aa11");
|
|
expect(aw.createMock).toHaveBeenCalledOnce();
|
|
expect(aw.createMock).toHaveBeenCalledWith("about:blank", {});
|
|
expect(aw.ensureActiveTabMock).toHaveBeenCalledOnce();
|
|
expect(aw.ensureActiveTabMock).toHaveBeenCalledWith(100, "about:blank");
|
|
expect(ctx.sessionId).toBe("aa11");
|
|
expect(ctx.agentWindowId).toBe(100);
|
|
expect(ctx.createdAtMs).toBe(1700000000000);
|
|
expect(ctx.refStore.isEmpty()).toBe(true);
|
|
expect(ctx.borrowedTabs.size).toBe(0);
|
|
});
|
|
it("forwards an optional window size when starting a session", async () => {
|
|
const aw = fakeAgentWindow();
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
const ctx = await sm.start("aa11", { size: { width: 1280, height: 800 } });
|
|
expect(aw.createMock).toHaveBeenCalledWith("about:blank", {
|
|
size: { width: 1280, height: 800 },
|
|
});
|
|
expect(ctx.agentWindowId).toBe(100);
|
|
});
|
|
|
|
it("forwards an explicit unfocused start to the Agent Window", async () => {
|
|
const aw = fakeAgentWindow();
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
|
|
await sm.start("aa11", { focused: false });
|
|
|
|
expect(aw.createMock).toHaveBeenCalledWith("about:blank", { focused: false });
|
|
});
|
|
|
|
it("indexes the session by sessionId and agent window id", async () => {
|
|
const aw = fakeAgentWindow();
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
const ctx = await sm.start("aa11");
|
|
expect(sm.has("aa11")).toBe(true);
|
|
expect(sm.get("aa11")).toBe(ctx);
|
|
expect(sm.findByWindowId(ctx.agentWindowId)).toBe(ctx);
|
|
expect(sm.findByWindowId(99999)).toBeNull();
|
|
expect(sm.list().length).toBe(1);
|
|
});
|
|
|
|
it("rejects starting the same session twice", async () => {
|
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow() });
|
|
await sm.start("aa11");
|
|
await expect(sm.start("aa11")).rejects.toThrow(/already exists/);
|
|
});
|
|
|
|
it("removes a newly created Agent Window when startup is aborted", async () => {
|
|
const aw = fakeAgentWindow();
|
|
let resolveCreate: (windowId: number) => void = () => {};
|
|
aw.createMock.mockImplementationOnce(
|
|
() =>
|
|
new Promise<number>((resolve) => {
|
|
resolveCreate = resolve;
|
|
}),
|
|
);
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
const controller = new AbortController();
|
|
const pending = sm.start("aa11", { signal: controller.signal });
|
|
|
|
controller.abort();
|
|
resolveCreate(777);
|
|
|
|
await expect(pending).rejects.toMatchObject({ name: "AbortError" });
|
|
expect(aw.removeMock).toHaveBeenCalledWith(777);
|
|
expect(sm.has("aa11")).toBe(false);
|
|
});
|
|
|
|
it("removes an incomplete Agent Window when active-tab setup fails", async () => {
|
|
const aw = fakeAgentWindow();
|
|
aw.ensureActiveTabMock.mockRejectedValueOnce(new Error("tab setup failed"));
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
|
|
await expect(sm.start("aa11")).rejects.toThrow("tab setup failed");
|
|
|
|
expect(aw.removeMock).toHaveBeenCalledWith(100);
|
|
expect(sm.has("aa11")).toBe(false);
|
|
});
|
|
|
|
it("surfaces the orphan Agent Window id when startup cleanup fails", async () => {
|
|
const aw = fakeAgentWindow();
|
|
aw.ensureActiveTabMock.mockRejectedValueOnce(new Error("tab setup failed"));
|
|
aw.removeMock.mockRejectedValueOnce(new Error("window removal denied"));
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
|
|
await expect(sm.start("aa11")).rejects.toMatchObject({
|
|
name: "SessionStartCleanupError",
|
|
windowId: 100,
|
|
message: expect.stringMatching(/cleanup of Agent Window 100 failed.*window removal denied/),
|
|
});
|
|
expect(sm.has("aa11")).toBe(false);
|
|
});
|
|
|
|
it("stop() closes the Agent Window and forgets the session", async () => {
|
|
const aw = fakeAgentWindow();
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
const ctx = await sm.start("aa11");
|
|
const removed = await sm.stop("aa11");
|
|
expect(removed).toBe(ctx);
|
|
expect(aw.removeMock).toHaveBeenCalledWith(ctx.agentWindowId);
|
|
expect(sm.has("aa11")).toBe(false);
|
|
expect(sm.findByWindowId(ctx.agentWindowId)).toBeNull();
|
|
});
|
|
|
|
it("stop({ dropOnly: true }) skips the chrome.windows.remove call", async () => {
|
|
const aw = fakeAgentWindow();
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
await sm.start("aa11");
|
|
await sm.stop("aa11", { dropOnly: true });
|
|
expect(aw.removeMock).not.toHaveBeenCalled();
|
|
expect(sm.has("aa11")).toBe(false);
|
|
});
|
|
|
|
it("stopAll() drops every session and returns their ids", async () => {
|
|
const aw = fakeAgentWindow();
|
|
const sm = new SessionManager({ agentWindow: aw });
|
|
await sm.start("aa11");
|
|
await sm.start("bb22");
|
|
const dropped = await sm.stopAll();
|
|
expect(dropped.sort()).toEqual(["aa11", "bb22"]);
|
|
expect(sm.list()).toEqual([]);
|
|
});
|
|
|
|
describe("findBorrowingSession", () => {
|
|
it("returns null when no session has borrowed the tab", async () => {
|
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow() });
|
|
await sm.start("aa11");
|
|
expect(sm.findBorrowingSession(42, "aa11")).toBeNull();
|
|
expect(sm.findBorrowingSession(42, null)).toBeNull();
|
|
});
|
|
|
|
it("ignores borrows held by the calling session itself", async () => {
|
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow() });
|
|
const ctx = await sm.start("aa11");
|
|
ctx.borrowedTabs.set(42, { tabId: 42, originalWindowId: 7, originalIndex: 3 });
|
|
expect(sm.findBorrowingSession(42, "aa11")).toBeNull();
|
|
});
|
|
|
|
it("reports the borrowing session id when a different session holds the tab", async () => {
|
|
const sm = new SessionManager({ agentWindow: fakeAgentWindow() });
|
|
const a = await sm.start("aa11");
|
|
await sm.start("bb22");
|
|
a.borrowedTabs.set(42, { tabId: 42, originalWindowId: 7, originalIndex: 3 });
|
|
expect(sm.findBorrowingSession(42, "bb22")).toBe("aa11");
|
|
// currentSessionId=null asks "is anyone borrowing this tab?"
|
|
expect(sm.findBorrowingSession(42, null)).toBe("aa11");
|
|
});
|
|
});
|
|
});
|