import { beforeEach, describe, expect, it, vi } from "vitest"; const { resolveProfileMock, ensureChromeExtensionRelayServerMock } = vi.hoisted(() => ({ resolveProfileMock: vi.fn(), ensureChromeExtensionRelayServerMock: vi.fn(), })); const { stopOpenClawChromeMock, stopChromeExtensionRelayServerMock } = vi.hoisted(() => ({ stopOpenClawChromeMock: vi.fn(async () => {}), stopChromeExtensionRelayServerMock: vi.fn(async () => true), })); const { createBrowserRouteContextMock, listKnownProfileNamesMock } = vi.hoisted(() => ({ createBrowserRouteContextMock: vi.fn(), listKnownProfileNamesMock: vi.fn(), })); vi.mock("./chrome.js", () => ({ stopOpenClawChrome: stopOpenClawChromeMock, })); vi.mock("./config.js", () => ({ resolveProfile: resolveProfileMock, })); vi.mock("./extension-relay.js", () => ({ ensureChromeExtensionRelayServer: ensureChromeExtensionRelayServerMock, stopChromeExtensionRelayServer: stopChromeExtensionRelayServerMock, })); vi.mock("./server-context.js", () => ({ createBrowserRouteContext: createBrowserRouteContextMock, listKnownProfileNames: listKnownProfileNamesMock, })); import { ensureExtensionRelayForProfiles, stopKnownBrowserProfiles } from "./server-lifecycle.js"; describe("ensureExtensionRelayForProfiles", () => { beforeEach(() => { resolveProfileMock.mockClear(); ensureChromeExtensionRelayServerMock.mockClear(); }); it("starts relay only for extension profiles", async () => { resolveProfileMock.mockImplementation((_resolved: unknown, name: string) => { if (name === "chrome") { return { driver: "extension", cdpUrl: "http://127.0.0.1:18888" }; } return { driver: "openclaw", cdpUrl: "http://127.0.0.1:18889" }; }); ensureChromeExtensionRelayServerMock.mockResolvedValue(undefined); await ensureExtensionRelayForProfiles({ resolved: { profiles: { chrome: {}, openclaw: {}, }, } as never, onWarn: vi.fn(), }); expect(ensureChromeExtensionRelayServerMock).toHaveBeenCalledTimes(1); expect(ensureChromeExtensionRelayServerMock).toHaveBeenCalledWith({ cdpUrl: "http://127.0.0.1:18888", }); }); it("reports relay startup errors", async () => { resolveProfileMock.mockReturnValue({ driver: "extension", cdpUrl: "http://127.0.0.1:18888" }); ensureChromeExtensionRelayServerMock.mockRejectedValue(new Error("boom")); const onWarn = vi.fn(); await ensureExtensionRelayForProfiles({ resolved: { profiles: { chrome: {} } } as never, onWarn, }); expect(onWarn).toHaveBeenCalledWith( 'Chrome extension relay init failed for profile "chrome": Error: boom', ); }); }); describe("stopKnownBrowserProfiles", () => { beforeEach(() => { createBrowserRouteContextMock.mockClear(); listKnownProfileNamesMock.mockClear(); stopOpenClawChromeMock.mockClear(); stopChromeExtensionRelayServerMock.mockClear(); }); it("stops all known profiles and ignores per-profile failures", async () => { listKnownProfileNamesMock.mockReturnValue(["openclaw", "chrome"]); const stopMap: Record> = { openclaw: vi.fn(async () => {}), chrome: vi.fn(async () => { throw new Error("profile stop failed"); }), }; createBrowserRouteContextMock.mockReturnValue({ forProfile: (name: string) => ({ stopRunningBrowser: stopMap[name], }), }); const onWarn = vi.fn(); const state = { resolved: { profiles: {} }, profiles: new Map() }; await stopKnownBrowserProfiles({ getState: () => state as never, onWarn, }); expect(stopMap.openclaw).toHaveBeenCalledTimes(1); expect(stopMap.chrome).toHaveBeenCalledTimes(1); expect(onWarn).not.toHaveBeenCalled(); }); it("stops tracked runtime browsers even when the profile no longer resolves", async () => { listKnownProfileNamesMock.mockReturnValue(["deleted-local", "deleted-extension"]); createBrowserRouteContextMock.mockReturnValue({ forProfile: vi.fn(() => { throw new Error("profile not found"); }), }); const localRuntime = { profile: { name: "deleted-local", driver: "openclaw", }, running: { pid: 42, cdpPort: 18888, }, }; const launchedBrowser = localRuntime.running; const extensionRuntime = { profile: { name: "deleted-extension", driver: "extension", cdpUrl: "http://127.0.0.1:19999", }, running: null, }; const profiles = new Map([ ["deleted-local", localRuntime], ["deleted-extension", extensionRuntime], ]); const state = { resolved: { profiles: {} }, profiles, }; await stopKnownBrowserProfiles({ getState: () => state as never, onWarn: vi.fn(), }); expect(stopOpenClawChromeMock).toHaveBeenCalledWith(launchedBrowser); expect(localRuntime.running).toBeNull(); expect(stopChromeExtensionRelayServerMock).toHaveBeenCalledWith({ cdpUrl: "http://127.0.0.1:19999", }); }); it("warns when profile enumeration fails", async () => { listKnownProfileNamesMock.mockImplementation(() => { throw new Error("oops"); }); createBrowserRouteContextMock.mockReturnValue({ forProfile: vi.fn(), }); const onWarn = vi.fn(); await stopKnownBrowserProfiles({ getState: () => ({ resolved: { profiles: {} }, profiles: new Map() }) as never, onWarn, }); expect(onWarn).toHaveBeenCalledWith("openclaw browser stop failed: Error: oops"); }); });