mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
Initial public release
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AGENT_WINDOW_HOME, chromeAgentWindowApi } from "../agent-window";
|
||||
|
||||
describe("chromeAgentWindowApi.ensureActiveTab", () => {
|
||||
const query = vi.fn();
|
||||
const update = vi.fn();
|
||||
const create = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("chrome", {
|
||||
tabs: { query, update, create },
|
||||
});
|
||||
query.mockReset();
|
||||
update.mockReset();
|
||||
create.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("activates an existing tab when the window already has one", async () => {
|
||||
query.mockResolvedValue([{ id: 7, active: false }]);
|
||||
update.mockResolvedValue({});
|
||||
|
||||
await chromeAgentWindowApi.ensureActiveTab(100, AGENT_WINDOW_HOME);
|
||||
|
||||
expect(query).toHaveBeenCalledWith({ windowId: 100 });
|
||||
expect(update).toHaveBeenCalledWith(7, { active: true });
|
||||
expect(create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates about:blank when the Agent Window has no tabs", async () => {
|
||||
query.mockResolvedValue([]);
|
||||
create.mockResolvedValue({ id: 8 });
|
||||
|
||||
await chromeAgentWindowApi.ensureActiveTab(100, AGENT_WINDOW_HOME);
|
||||
|
||||
expect(create).toHaveBeenCalledWith({
|
||||
windowId: 100,
|
||||
url: AGENT_WINDOW_HOME,
|
||||
active: true,
|
||||
});
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionStateHandler, FrameHandler, Transport } from "@/transport/transport";
|
||||
import type { ConnectionState, ProtocolFrame } from "@/transport/types";
|
||||
import { attachSessionEventHandler, type WindowRemovedListener } from "../event-handler";
|
||||
import { SessionManager } from "../manager";
|
||||
|
||||
function fakeWindowEvents() {
|
||||
const listeners = new Set<(windowId: number) => void>();
|
||||
const api: WindowRemovedListener = {
|
||||
addListener: (cb) => listeners.add(cb),
|
||||
removeListener: (cb) => listeners.delete(cb),
|
||||
};
|
||||
return {
|
||||
api,
|
||||
emit(id: number) {
|
||||
for (const l of listeners) l(id);
|
||||
},
|
||||
listenerCount: () => listeners.size,
|
||||
};
|
||||
}
|
||||
|
||||
function fakeTransport(): Transport & { sent: ProtocolFrame[] } {
|
||||
const sent: ProtocolFrame[] = [];
|
||||
const t: Transport & { sent: ProtocolFrame[] } = {
|
||||
state: "connected" as ConnectionState,
|
||||
sent,
|
||||
connect: () => Promise.resolve(),
|
||||
disconnect: () => Promise.resolve(),
|
||||
send: (msg) => sent.push(msg),
|
||||
onMessage: (_h: FrameHandler) => ({ dispose: () => {} }),
|
||||
onConnectionStateChange: (_h: ConnectionStateHandler) => ({ dispose: () => {} }),
|
||||
};
|
||||
return t;
|
||||
}
|
||||
|
||||
describe("attachSessionEventHandler", () => {
|
||||
it("drops the local session and emits session.window_closed when the agent window closes", async () => {
|
||||
const manager = new SessionManager({
|
||||
agentWindow: {
|
||||
create: vi.fn(async () => 4242),
|
||||
remove: vi.fn(async () => {}),
|
||||
ensureActiveTab: vi.fn(async () => {}),
|
||||
},
|
||||
});
|
||||
await manager.start("aa11");
|
||||
const transport = fakeTransport();
|
||||
const events = fakeWindowEvents();
|
||||
const cdp = { detachSession: vi.fn(async () => {}) };
|
||||
|
||||
attachSessionEventHandler({
|
||||
manager,
|
||||
transport,
|
||||
windowEvents: events.api,
|
||||
cdp,
|
||||
});
|
||||
expect(events.listenerCount()).toBe(1);
|
||||
|
||||
events.emit(4242);
|
||||
await vi.waitUntil(() => transport.sent.length > 0);
|
||||
|
||||
expect(cdp.detachSession).toHaveBeenCalledWith("aa11");
|
||||
expect(manager.has("aa11")).toBe(false);
|
||||
expect(transport.sent).toEqual([
|
||||
{
|
||||
event: "session.window_closed",
|
||||
payload: { session_id: "aa11", reason: "user_closed_window" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reports borrowed tabs as return failures when the Agent Window was already closed", async () => {
|
||||
const manager = new SessionManager({
|
||||
agentWindow: {
|
||||
create: vi.fn(async () => 4242),
|
||||
remove: vi.fn(async () => {}),
|
||||
ensureActiveTab: vi.fn(async () => {}),
|
||||
},
|
||||
});
|
||||
const ctx = await manager.start("aa11");
|
||||
ctx.borrowedTabs.set(7, { tabId: 7, originalWindowId: 200, originalIndex: 0 });
|
||||
const transport = fakeTransport();
|
||||
const events = fakeWindowEvents();
|
||||
|
||||
attachSessionEventHandler({
|
||||
manager,
|
||||
transport,
|
||||
windowEvents: events.api,
|
||||
});
|
||||
|
||||
events.emit(4242);
|
||||
for (let i = 0; i < 4; i += 1) await Promise.resolve();
|
||||
|
||||
expect(transport.sent).toEqual([
|
||||
{
|
||||
event: "session.window_closed",
|
||||
payload: {
|
||||
session_id: "aa11",
|
||||
reason: "user_closed_window",
|
||||
return_failures: [
|
||||
{
|
||||
tab_id: 7,
|
||||
code: "cdp_failed",
|
||||
message: "Agent Window was closed before borrowed tab could be returned",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores non-agent windows", async () => {
|
||||
const manager = new SessionManager({
|
||||
agentWindow: {
|
||||
create: vi.fn(async () => 1),
|
||||
remove: vi.fn(),
|
||||
ensureActiveTab: vi.fn(async () => {}),
|
||||
},
|
||||
});
|
||||
const transport = fakeTransport();
|
||||
const events = fakeWindowEvents();
|
||||
attachSessionEventHandler({ manager, transport, windowEvents: events.api });
|
||||
events.emit(9999);
|
||||
await Promise.resolve();
|
||||
expect(transport.sent).toEqual([]);
|
||||
});
|
||||
|
||||
it("dispose() removes the listener", () => {
|
||||
const manager = new SessionManager({
|
||||
agentWindow: {
|
||||
create: vi.fn(async () => 1),
|
||||
remove: vi.fn(),
|
||||
ensureActiveTab: vi.fn(async () => {}),
|
||||
},
|
||||
});
|
||||
const transport = fakeTransport();
|
||||
const events = fakeWindowEvents();
|
||||
const handle = attachSessionEventHandler({
|
||||
manager,
|
||||
transport,
|
||||
windowEvents: events.api,
|
||||
});
|
||||
expect(events.listenerCount()).toBe(1);
|
||||
handle.dispose();
|
||||
expect(events.listenerCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { AgentWindowApi } 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) => {
|
||||
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("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("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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { RefStore } from "../ref-store";
|
||||
|
||||
describe("RefStore", () => {
|
||||
it("stores and resolves ref → backendNodeId", () => {
|
||||
const s = new RefStore();
|
||||
s.set("e1", 42, { tabId: 7 });
|
||||
expect(s.resolve("@e1")).toBe(42);
|
||||
expect(s.resolve("e1")).toBe(42);
|
||||
expect(s.resolve("@e1", { tabId: 7 })).toBe(42);
|
||||
expect(s.resolve("@e1", { tabId: 8 })).toBeNull();
|
||||
expect(s.resolveEntry("@e1")).toMatchObject({
|
||||
backendNodeId: 42,
|
||||
tabId: 7,
|
||||
generation: 0,
|
||||
});
|
||||
expect(s.size()).toBe(1);
|
||||
expect(s.isEmpty()).toBe(false);
|
||||
});
|
||||
|
||||
it("replace() clears prior entries", () => {
|
||||
const s = new RefStore();
|
||||
s.set("@e1", 1);
|
||||
s.set("@e2", 2);
|
||||
s.replace([
|
||||
["e10", { backendNodeId: 10, tabId: 1 }],
|
||||
["@e11", { backendNodeId: 11, tabId: 1 }],
|
||||
]);
|
||||
expect(s.resolve("@e1")).toBeNull();
|
||||
expect(s.resolve("@e10")).toBe(10);
|
||||
expect(s.resolve("@e10", { tabId: 1 })).toBe(10);
|
||||
expect(s.resolve("@e10", { tabId: 2 })).toBeNull();
|
||||
expect(s.resolve("@e11")).toBe(11);
|
||||
expect(s.size()).toBe(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user