import { afterEach, describe, expect, it, vi } from "vitest"; import { withFetchPreconnect } from "../test-utils/fetch-mock.js"; import "./server-context.chrome-test-harness.js"; import * as cdpModule from "./cdp.js"; import { InvalidBrowserNavigationUrlError } from "./navigation-guard.js"; import { createBrowserRouteContext } from "./server-context.js"; import { makeManagedTabsWithNew, makeState, originalFetch, } from "./server-context.remote-tab-ops.harness.js"; afterEach(() => { globalThis.fetch = originalFetch; vi.restoreAllMocks(); }); describe("browser server-context tab selection state", () => { it("updates lastTargetId when openTab is created via CDP", async () => { const createTargetViaCdp = vi .spyOn(cdpModule, "createTargetViaCdp") .mockResolvedValue({ targetId: "CREATED" }); const fetchMock = vi.fn(async (url: unknown) => { const u = String(url); if (!u.includes("/json/list")) { throw new Error(`unexpected fetch: ${u}`); } return { ok: true, json: async () => [ { id: "CREATED", title: "New Tab", url: "http://127.0.0.1:8080", webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/CREATED", type: "page", }, ], } as unknown as Response; }); global.fetch = withFetchPreconnect(fetchMock); const state = makeState("openclaw"); const ctx = createBrowserRouteContext({ getState: () => state }); const openclaw = ctx.forProfile("openclaw"); const opened = await openclaw.openTab("http://127.0.0.1:8080"); expect(opened.targetId).toBe("CREATED"); expect(state.profiles.get("openclaw")?.lastTargetId).toBe("CREATED"); expect(createTargetViaCdp).toHaveBeenCalledWith({ cdpUrl: "http://127.0.0.1:18800", url: "http://127.0.0.1:8080", ssrfPolicy: { allowPrivateNetwork: true }, }); }); it("closes excess managed tabs after opening a new tab", async () => { vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" }); const existingTabs = makeManagedTabsWithNew(); const fetchMock = vi.fn(async (url: unknown) => { const value = String(url); if (value.includes("/json/list")) { return { ok: true, json: async () => existingTabs } as unknown as Response; } if (value.includes("/json/close/OLD1")) { return { ok: true, json: async () => ({}) } as unknown as Response; } throw new Error(`unexpected fetch: ${value}`); }); global.fetch = withFetchPreconnect(fetchMock); const state = makeState("openclaw"); (state.profiles as Map).set("openclaw", { profile: { name: "openclaw" }, running: { pid: 1234, proc: { on: vi.fn() } }, lastTargetId: null, }); const ctx = createBrowserRouteContext({ getState: () => state }); const openclaw = ctx.forProfile("openclaw"); const opened = await openclaw.openTab("http://127.0.0.1:3009"); expect(opened.targetId).toBe("NEW"); await vi.waitFor(() => { expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining("/json/close/OLD1"), expect.any(Object), ); }); }); it("never closes the just-opened managed tab during cap cleanup", async () => { vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" }); const existingTabs = makeManagedTabsWithNew({ newFirst: true }); const fetchMock = vi.fn(async (url: unknown) => { const value = String(url); if (value.includes("/json/list")) { return { ok: true, json: async () => existingTabs } as unknown as Response; } if (value.includes("/json/close/OLD1")) { return { ok: true, json: async () => ({}) } as unknown as Response; } if (value.includes("/json/close/NEW")) { throw new Error("cleanup must not close NEW"); } throw new Error(`unexpected fetch: ${value}`); }); global.fetch = withFetchPreconnect(fetchMock); const state = makeState("openclaw"); (state.profiles as Map).set("openclaw", { profile: { name: "openclaw" }, running: { pid: 1234, proc: { on: vi.fn() } }, lastTargetId: null, }); const ctx = createBrowserRouteContext({ getState: () => state }); const openclaw = ctx.forProfile("openclaw"); const opened = await openclaw.openTab("http://127.0.0.1:3009"); expect(opened.targetId).toBe("NEW"); await vi.waitFor(() => { expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining("/json/close/OLD1"), expect.any(Object), ); }); expect(fetchMock).not.toHaveBeenCalledWith( expect.stringContaining("/json/close/NEW"), expect.anything(), ); }); it("does not fail tab open when managed-tab cleanup list fails", async () => { vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" }); let listCount = 0; const fetchMock = vi.fn(async (url: unknown) => { const value = String(url); if (value.includes("/json/list")) { listCount += 1; if (listCount === 1) { return { ok: true, json: async () => [ { id: "NEW", title: "New Tab", url: "http://127.0.0.1:3009", webSocketDebuggerUrl: "ws://127.0.0.1/devtools/page/NEW", type: "page", }, ], } as unknown as Response; } throw new Error("/json/list timeout"); } throw new Error(`unexpected fetch: ${value}`); }); global.fetch = withFetchPreconnect(fetchMock); const state = makeState("openclaw"); (state.profiles as Map).set("openclaw", { profile: { name: "openclaw" }, running: { pid: 1234, proc: { on: vi.fn() } }, lastTargetId: null, }); const ctx = createBrowserRouteContext({ getState: () => state }); const openclaw = ctx.forProfile("openclaw"); const opened = await openclaw.openTab("http://127.0.0.1:3009"); expect(opened.targetId).toBe("NEW"); }); it("does not run managed tab cleanup in attachOnly mode", async () => { vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" }); const existingTabs = makeManagedTabsWithNew(); const fetchMock = vi.fn(async (url: unknown) => { const value = String(url); if (value.includes("/json/list")) { return { ok: true, json: async () => existingTabs } as unknown as Response; } if (value.includes("/json/close/")) { throw new Error("should not close tabs in attachOnly mode"); } throw new Error(`unexpected fetch: ${value}`); }); global.fetch = withFetchPreconnect(fetchMock); const state = makeState("openclaw"); state.resolved.attachOnly = true; const ctx = createBrowserRouteContext({ getState: () => state }); const openclaw = ctx.forProfile("openclaw"); const opened = await openclaw.openTab("http://127.0.0.1:3009"); expect(opened.targetId).toBe("NEW"); expect(fetchMock).not.toHaveBeenCalledWith( expect.stringContaining("/json/close/"), expect.anything(), ); }); it("does not block openTab on slow best-effort cleanup closes", async () => { vi.spyOn(cdpModule, "createTargetViaCdp").mockResolvedValue({ targetId: "NEW" }); const existingTabs = makeManagedTabsWithNew(); const fetchMock = vi.fn(async (url: unknown) => { const value = String(url); if (value.includes("/json/list")) { return { ok: true, json: async () => existingTabs } as unknown as Response; } if (value.includes("/json/close/OLD1")) { return new Promise(() => {}); } throw new Error(`unexpected fetch: ${value}`); }); global.fetch = withFetchPreconnect(fetchMock); const state = makeState("openclaw"); (state.profiles as Map).set("openclaw", { profile: { name: "openclaw" }, running: { pid: 1234, proc: { on: vi.fn() } }, lastTargetId: null, }); const ctx = createBrowserRouteContext({ getState: () => state }); const openclaw = ctx.forProfile("openclaw"); const opened = await Promise.race([ openclaw.openTab("http://127.0.0.1:3009"), new Promise((_, reject) => setTimeout(() => reject(new Error("openTab timed out waiting for cleanup")), 300), ), ]); expect(opened.targetId).toBe("NEW"); }); it("blocks unsupported non-network URLs before any HTTP tab-open fallback", async () => { const fetchMock = vi.fn(async () => { throw new Error("unexpected fetch"); }); global.fetch = withFetchPreconnect(fetchMock); const state = makeState("openclaw"); const ctx = createBrowserRouteContext({ getState: () => state }); const openclaw = ctx.forProfile("openclaw"); await expect(openclaw.openTab("file:///etc/passwd")).rejects.toBeInstanceOf( InvalidBrowserNavigationUrlError, ); expect(fetchMock).not.toHaveBeenCalled(); }); });