import type { AdapterContext, ComputerRef } from "@rakazo/adapter-kit"; import { afterEach, describe, expect, it, vi } from "vitest"; import { createBrowserProvider, resolveBrowserProviderKind } from "./browser-provider-factory.js"; import { ComputerBrowserProvider } from "./computer-browser.js"; import { FakeBrowserProvider } from "./fake-browser.js"; import { pageBrowserSessionKey } from "./page-browser-session.js"; const baseContext: AdapterContext = { operationId: "1", traceId: "1", spaceId: "w", userId: "u", signal: new AbortController().signal, }; const pages = { "https://example.test/a": { title: "A", html: `A `, }, }; describe("browser provider factory", () => { const previous = process.env.BROWSER_PROVIDER; afterEach(() => { if (previous === undefined) delete process.env.BROWSER_PROVIDER; else process.env.BROWSER_PROVIDER = previous; }); it("defaults production kind to computer, not fake", () => { delete process.env.BROWSER_PROVIDER; expect(resolveBrowserProviderKind({})).toBe("computer"); expect(resolveBrowserProviderKind({ BROWSER_PROVIDER: "" })).toBe("computer"); expect(createBrowserProvider().describe().id).toBe("computer"); // API/worker pass undefined kind with a sandbox; still computer. expect(createBrowserProvider(undefined, {}).describe().id).toBe("computer"); }); it("selects fake or emulator only when BROWSER_PROVIDER asks for them", () => { expect(resolveBrowserProviderKind({ BROWSER_PROVIDER: "fake" })).toBe("fake"); expect(createBrowserProvider("fake").describe().id).toBe("fake"); expect(resolveBrowserProviderKind({ BROWSER_PROVIDER: "emulator" })).toBe("emulator"); expect(createBrowserProvider("emulator").describe().id).toBe("emulator"); }); }); describe("computer browser provider", () => { it("falls back to computer_act when live Chrome is unavailable", async () => { const provider = new ComputerBrowserProvider({ pages }); const computer: ComputerRef = { id: "team-1", botId: "home-key", kind: "docker", providerRef: "team-1", }; const result = await provider.navigate( computer, { url: "https://example.test/a" }, { ...baseContext, botId: "bot-a" }, ); expect(result).toMatchObject({ fallback: "computer_act", error: expect.stringMatching(/not attached|computer_act|live page browser/i), }); }); it("drives the live page path when the CDP helper succeeds", async () => { const liveDriver = vi.fn(async (_c, request, _ctx) => { const { command } = request; if (command === "navigate") { return { ok: true, url: String(request.url), title: "Live" }; } if (command === "snapshot") { return { ok: true, url: "https://example.test/live", title: "Live", tree: '- textbox "Secret" [e1] value="live-secret"', elements: [{ ref: "e1", role: "textbox", name: "Secret", value: "live-secret" }], }; } return { ok: true, completed: 1, url: "https://example.test/live", title: "Live", tree: '- textbox "Secret" [e1] value="typed"', elements: [{ ref: "e1", role: "textbox", name: "Secret", value: "typed" }], }; }); const provider = new ComputerBrowserProvider({ pages, liveDriver }); const computer: ComputerRef = { id: "team-1", botId: "home-key", kind: "docker", providerRef: "team-1", }; const context = { ...baseContext, botId: "bot-a" }; const nav = await provider.navigate(computer, { url: "https://example.test/live" }, context); expect(nav).toEqual({ url: "https://example.test/live", title: "Live" }); const snap = await provider.snapshot(computer, {}, context); expect(snap.fallback).toBeUndefined(); expect(snap.elements[0]?.value).toBe("live-secret"); const act = await provider.act( computer, { actions: [{ kind: "fill", ref: "e1", text: "typed" }] }, context, ); expect(act.ok).toBe(true); expect(act.completed).toBe(1); expect(liveDriver).toHaveBeenCalled(); expect(liveDriver.mock.calls[0]?.[1]).toEqual({ command: "navigate", url: "https://example.test/live", }); }); it("returns computer_act when the live driver reports failure", async () => { const provider = new ComputerBrowserProvider({ pages, liveDriver: async () => ({ ok: false, error: "chrome not listening", fallback: "computer_act", }), }); const computer: ComputerRef = { id: "c1", botId: "home-key", kind: "docker", providerRef: "c1", }; const result = await provider.snapshot(computer, {}, { ...baseContext, botId: "bot-a" }); expect(result.fallback).toBe("computer_act"); expect(result.error).toMatch(/chrome not listening/i); }); it("uses in-process pages for fake computers", async () => { const provider = new ComputerBrowserProvider({ pages }); const computer: ComputerRef = { id: "c1", botId: "bot-a", kind: "fake", providerRef: "c1", }; const result = await provider.navigate( computer, { url: "https://example.test/a" }, baseContext, ); expect(result.fallback).toBeUndefined(); expect(result.title).toBe("A"); }); }); describe("page browser session isolation", () => { it("keeps page state when the same bot reacquires after a lease fence renewal", async () => { const browser = new FakeBrowserProvider({ pages }); const computer: ComputerRef = { id: "team-computer", botId: "team-home", kind: "fake", providerRef: "team-computer", }; const first = { ...baseContext, botId: "bot-a", screenLeaseId: "run-1:1", runId: "run-1" }; const resumed = { ...baseContext, botId: "bot-a", screenLeaseId: "run-1:8", runId: "run-1" }; expect(pageBrowserSessionKey(computer, first)).toBe(pageBrowserSessionKey(computer, resumed)); expect(pageBrowserSessionKey(computer, first)).toBe("team-computer::bot-a"); await browser.navigate(computer, { url: "https://example.test/a" }, first); const snap = await browser.snapshot(computer, {}, resumed); expect(snap.url).toBe("https://example.test/a"); expect(snap.elements.find((el) => el.name.includes("Secret"))?.value).toBe("from-bot-a"); }); it("keeps Team bots on the same computer in separate sessions", async () => { const browser = new FakeBrowserProvider({ pages }); // Production Team computers share computer.botId (home key) across bots. const shared: ComputerRef = { id: "team-computer", botId: "team-home", kind: "fake", providerRef: "team-computer", }; const contextA = { ...baseContext, botId: "bot-a" }; const contextB = { ...baseContext, botId: "bot-b" }; expect(pageBrowserSessionKey(shared, contextA)).not.toBe( pageBrowserSessionKey(shared, contextB), ); await browser.navigate(shared, { url: "https://example.test/a" }, contextA); const snapA = await browser.snapshot(shared, {}, contextA); const secret = snapA.elements.find((el) => el.name.includes("Secret")); expect(secret?.value).toBe("from-bot-a"); // Bot B has not navigated; its session must not see bot A's page. const snapB = await browser.snapshot(shared, {}, contextB); expect(snapB.url).toBe("about:blank"); expect(snapB.elements.find((el) => el.name.includes("Secret"))).toBeUndefined(); }); }); describe("page browser safety", () => { const computer: ComputerRef = { id: "computer", botId: "team-home", kind: "docker", providerRef: "computer", }; it("does not advertise unavailable providers", () => { expect(new ComputerBrowserProvider().describe().capabilities.page).toBe(false); }); it.each([{}, { ok: true }])( "rejects malformed live results instead of claiming success: %j", async (payload) => { const provider = new ComputerBrowserProvider({ liveDriver: async () => payload }); expect( await provider.act(computer, { actions: [{ kind: "click", ref: "e1" }] }, baseContext), ).toMatchObject({ ok: false, uncertain: true, completed: 0 }); }, ); it("preserves partial progress and uncertain outcomes", async () => { const provider = new ComputerBrowserProvider({ liveDriver: async () => ({ ok: false, completed: 1, uncertain: true }), }); expect( await provider.act( computer, { actions: [ { kind: "click", ref: "e1" }, { kind: "click", ref: "e2" }, ], }, baseContext, ), ).toMatchObject({ ok: false, uncertain: true, completed: 1 }); }); it("propagates cancellation instead of suggesting another action", async () => { const signal = AbortSignal.abort(); const driver = vi.fn(async () => ({ ok: true })); const provider = new ComputerBrowserProvider({ liveDriver: driver }); await expect(provider.snapshot(computer, {}, { ...baseContext, signal })).rejects.toThrow(); expect(driver).not.toHaveBeenCalled(); }); });