Spaces:
Paused
Paused
| import fs from "node:fs"; | |
| import fsp from "node:fs/promises"; | |
| import os from "node:os"; | |
| import path from "node:path"; | |
| import { afterEach, describe, expect, it, vi } from "vitest"; | |
| import { | |
| decorateOpenClawProfile, | |
| ensureProfileCleanExit, | |
| findChromeExecutableMac, | |
| findChromeExecutableWindows, | |
| isChromeReachable, | |
| resolveBrowserExecutableForPlatform, | |
| stopOpenClawChrome, | |
| } from "./chrome.js"; | |
| import { | |
| DEFAULT_OPENCLAW_BROWSER_COLOR, | |
| DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME, | |
| } from "./constants.js"; | |
| async function readJson(filePath: string): Promise<Record<string, unknown>> { | |
| const raw = await fsp.readFile(filePath, "utf-8"); | |
| return JSON.parse(raw) as Record<string, unknown>; | |
| } | |
| describe("browser chrome profile decoration", () => { | |
| afterEach(() => { | |
| vi.unstubAllGlobals(); | |
| vi.restoreAllMocks(); | |
| }); | |
| it("writes expected name + signed ARGB seed to Chrome prefs", async () => { | |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); | |
| try { | |
| decorateOpenClawProfile(userDataDir, { color: DEFAULT_OPENCLAW_BROWSER_COLOR }); | |
| const expectedSignedArgb = ((0xff << 24) | 0xff4500) >> 0; | |
| const localState = await readJson(path.join(userDataDir, "Local State")); | |
| const profile = localState.profile as Record<string, unknown>; | |
| const infoCache = profile.info_cache as Record<string, unknown>; | |
| const def = infoCache.Default as Record<string, unknown>; | |
| expect(def.name).toBe(DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME); | |
| expect(def.shortcut_name).toBe(DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME); | |
| expect(def.profile_color_seed).toBe(expectedSignedArgb); | |
| expect(def.profile_highlight_color).toBe(expectedSignedArgb); | |
| expect(def.default_avatar_fill_color).toBe(expectedSignedArgb); | |
| expect(def.default_avatar_stroke_color).toBe(expectedSignedArgb); | |
| const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); | |
| const browser = prefs.browser as Record<string, unknown>; | |
| const theme = browser.theme as Record<string, unknown>; | |
| const autogenerated = prefs.autogenerated as Record<string, unknown>; | |
| const autogeneratedTheme = autogenerated.theme as Record<string, unknown>; | |
| expect(theme.user_color2).toBe(expectedSignedArgb); | |
| expect(autogeneratedTheme.color).toBe(expectedSignedArgb); | |
| const marker = await fsp.readFile( | |
| path.join(userDataDir, ".openclaw-profile-decorated"), | |
| "utf-8", | |
| ); | |
| expect(marker.trim()).toMatch(/^\d+$/); | |
| } finally { | |
| await fsp.rm(userDataDir, { recursive: true, force: true }); | |
| } | |
| }); | |
| it("best-effort writes name when color is invalid", async () => { | |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); | |
| try { | |
| decorateOpenClawProfile(userDataDir, { color: "lobster-orange" }); | |
| const localState = await readJson(path.join(userDataDir, "Local State")); | |
| const profile = localState.profile as Record<string, unknown>; | |
| const infoCache = profile.info_cache as Record<string, unknown>; | |
| const def = infoCache.Default as Record<string, unknown>; | |
| expect(def.name).toBe(DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME); | |
| expect(def.profile_color_seed).toBeUndefined(); | |
| } finally { | |
| await fsp.rm(userDataDir, { recursive: true, force: true }); | |
| } | |
| }); | |
| it("recovers from missing/invalid preference files", async () => { | |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); | |
| try { | |
| await fsp.mkdir(path.join(userDataDir, "Default"), { recursive: true }); | |
| await fsp.writeFile(path.join(userDataDir, "Local State"), "{", "utf-8"); // invalid JSON | |
| await fsp.writeFile( | |
| path.join(userDataDir, "Default", "Preferences"), | |
| "[]", // valid JSON but wrong shape | |
| "utf-8", | |
| ); | |
| decorateOpenClawProfile(userDataDir, { color: DEFAULT_OPENCLAW_BROWSER_COLOR }); | |
| const localState = await readJson(path.join(userDataDir, "Local State")); | |
| expect(typeof localState.profile).toBe("object"); | |
| const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); | |
| expect(typeof prefs.profile).toBe("object"); | |
| } finally { | |
| await fsp.rm(userDataDir, { recursive: true, force: true }); | |
| } | |
| }); | |
| it("writes clean exit prefs to avoid restore prompts", async () => { | |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); | |
| try { | |
| ensureProfileCleanExit(userDataDir); | |
| const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); | |
| expect(prefs.exit_type).toBe("Normal"); | |
| expect(prefs.exited_cleanly).toBe(true); | |
| } finally { | |
| await fsp.rm(userDataDir, { recursive: true, force: true }); | |
| } | |
| }); | |
| it("is idempotent when rerun on an existing profile", async () => { | |
| const userDataDir = await fsp.mkdtemp(path.join(os.tmpdir(), "openclaw-chrome-test-")); | |
| try { | |
| decorateOpenClawProfile(userDataDir, { color: DEFAULT_OPENCLAW_BROWSER_COLOR }); | |
| decorateOpenClawProfile(userDataDir, { color: DEFAULT_OPENCLAW_BROWSER_COLOR }); | |
| const prefs = await readJson(path.join(userDataDir, "Default", "Preferences")); | |
| const profile = prefs.profile as Record<string, unknown>; | |
| expect(profile.name).toBe(DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME); | |
| } finally { | |
| await fsp.rm(userDataDir, { recursive: true, force: true }); | |
| } | |
| }); | |
| }); | |
| describe("browser chrome helpers", () => { | |
| afterEach(() => { | |
| vi.unstubAllEnvs(); | |
| vi.unstubAllGlobals(); | |
| vi.restoreAllMocks(); | |
| }); | |
| it("picks the first existing Chrome candidate on macOS", () => { | |
| const exists = vi | |
| .spyOn(fs, "existsSync") | |
| .mockImplementation((p) => | |
| String(p).includes("Google Chrome.app/Contents/MacOS/Google Chrome"), | |
| ); | |
| const exe = findChromeExecutableMac(); | |
| expect(exe?.kind).toBe("chrome"); | |
| expect(exe?.path).toMatch(/Google Chrome\.app/); | |
| exists.mockRestore(); | |
| }); | |
| it("returns null when no Chrome candidate exists", () => { | |
| const exists = vi.spyOn(fs, "existsSync").mockReturnValue(false); | |
| expect(findChromeExecutableMac()).toBeNull(); | |
| exists.mockRestore(); | |
| }); | |
| it("picks the first existing Chrome candidate on Windows", () => { | |
| vi.stubEnv("LOCALAPPDATA", "C:\\Users\\Test\\AppData\\Local"); | |
| const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => { | |
| const pathStr = String(p); | |
| return ( | |
| pathStr.includes("Google\\Chrome\\Application\\chrome.exe") || | |
| pathStr.includes("BraveSoftware\\Brave-Browser\\Application\\brave.exe") || | |
| pathStr.includes("Microsoft\\Edge\\Application\\msedge.exe") | |
| ); | |
| }); | |
| const exe = findChromeExecutableWindows(); | |
| expect(exe?.kind).toBe("chrome"); | |
| expect(exe?.path).toMatch(/chrome\.exe$/); | |
| exists.mockRestore(); | |
| }); | |
| it("finds Chrome in Program Files on Windows", () => { | |
| const marker = path.win32.join("Program Files", "Google", "Chrome"); | |
| const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => String(p).includes(marker)); | |
| const exe = findChromeExecutableWindows(); | |
| expect(exe?.kind).toBe("chrome"); | |
| expect(exe?.path).toMatch(/chrome\.exe$/); | |
| exists.mockRestore(); | |
| }); | |
| it("returns null when no Chrome candidate exists on Windows", () => { | |
| const exists = vi.spyOn(fs, "existsSync").mockReturnValue(false); | |
| expect(findChromeExecutableWindows()).toBeNull(); | |
| exists.mockRestore(); | |
| }); | |
| it("resolves Windows executables without LOCALAPPDATA", () => { | |
| vi.stubEnv("LOCALAPPDATA", ""); | |
| vi.stubEnv("ProgramFiles", "C:\\Program Files"); | |
| vi.stubEnv("ProgramFiles(x86)", "C:\\Program Files (x86)"); | |
| const marker = path.win32.join( | |
| "Program Files", | |
| "Google", | |
| "Chrome", | |
| "Application", | |
| "chrome.exe", | |
| ); | |
| const exists = vi.spyOn(fs, "existsSync").mockImplementation((p) => String(p).includes(marker)); | |
| const exe = resolveBrowserExecutableForPlatform( | |
| {} as Parameters<typeof resolveBrowserExecutableForPlatform>[0], | |
| "win32", | |
| ); | |
| expect(exe?.kind).toBe("chrome"); | |
| expect(exe?.path).toMatch(/chrome\.exe$/); | |
| exists.mockRestore(); | |
| }); | |
| it("reports reachability based on /json/version", async () => { | |
| vi.stubGlobal( | |
| "fetch", | |
| vi.fn().mockResolvedValue({ | |
| ok: true, | |
| json: async () => ({ webSocketDebuggerUrl: "ws://127.0.0.1/devtools" }), | |
| } as unknown as Response), | |
| ); | |
| await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(true); | |
| vi.stubGlobal( | |
| "fetch", | |
| vi.fn().mockResolvedValue({ | |
| ok: false, | |
| json: async () => ({}), | |
| } as unknown as Response), | |
| ); | |
| await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false); | |
| vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("boom"))); | |
| await expect(isChromeReachable("http://127.0.0.1:12345", 50)).resolves.toBe(false); | |
| }); | |
| it("stopOpenClawChrome no-ops when process is already killed", async () => { | |
| const proc = { killed: true, exitCode: null, kill: vi.fn() }; | |
| await stopOpenClawChrome( | |
| { | |
| proc, | |
| cdpPort: 12345, | |
| } as unknown as Parameters<typeof stopOpenClawChrome>[0], | |
| 10, | |
| ); | |
| expect(proc.kill).not.toHaveBeenCalled(); | |
| }); | |
| it("stopOpenClawChrome sends SIGTERM and returns once CDP is down", async () => { | |
| vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("down"))); | |
| const proc = { killed: false, exitCode: null, kill: vi.fn() }; | |
| await stopOpenClawChrome( | |
| { | |
| proc, | |
| cdpPort: 12345, | |
| } as unknown as Parameters<typeof stopOpenClawChrome>[0], | |
| 10, | |
| ); | |
| expect(proc.kill).toHaveBeenCalledWith("SIGTERM"); | |
| }); | |
| }); | |