Files
openclaw/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts
2026-03-03 00:15:00 +00:00

109 lines
3.7 KiB
TypeScript

import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { EventEmitter } from "node:events";
import { afterEach, describe, expect, it, vi } from "vitest";
import "./server-context.chrome-test-harness.js";
import * as chromeModule from "./chrome.js";
import type { RunningChrome } from "./chrome.js";
import type { BrowserServerState } from "./server-context.js";
import { createBrowserRouteContext } from "./server-context.js";
function makeBrowserState(): BrowserServerState {
return {
// oxlint-disable-next-line typescript/no-explicit-any
server: null as any,
port: 0,
resolved: {
enabled: true,
controlPort: 18791,
cdpProtocol: "http",
cdpHost: "127.0.0.1",
cdpIsLoopback: true,
cdpPortRangeStart: 18800,
cdpPortRangeEnd: 18810,
evaluateEnabled: false,
remoteCdpTimeoutMs: 1500,
remoteCdpHandshakeTimeoutMs: 3000,
extraArgs: [],
color: "#FF4500",
headless: true,
noSandbox: false,
attachOnly: false,
ssrfPolicy: { allowPrivateNetwork: true },
defaultProfile: "openclaw",
profiles: {
openclaw: { cdpPort: 18800, color: "#FF4500" },
},
},
profiles: new Map(),
};
}
function mockLaunchedChrome(
launchOpenClawChrome: { mockResolvedValue: (value: RunningChrome) => unknown },
pid: number,
) {
const proc = new EventEmitter() as unknown as ChildProcessWithoutNullStreams;
launchOpenClawChrome.mockResolvedValue({
pid,
exe: { kind: "chromium", path: "/usr/bin/chromium" },
userDataDir: "/tmp/openclaw-test",
cdpPort: 18800,
startedAt: Date.now(),
proc,
});
}
function setupEnsureBrowserAvailableHarness() {
vi.useFakeTimers();
const launchOpenClawChrome = vi.mocked(chromeModule.launchOpenClawChrome);
const stopOpenClawChrome = vi.mocked(chromeModule.stopOpenClawChrome);
const isChromeReachable = vi.mocked(chromeModule.isChromeReachable);
const isChromeCdpReady = vi.mocked(chromeModule.isChromeCdpReady);
isChromeReachable.mockResolvedValue(false);
const state = makeBrowserState();
const ctx = createBrowserRouteContext({ getState: () => state });
const profile = ctx.forProfile("openclaw");
return { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile };
}
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
vi.restoreAllMocks();
});
describe("browser server-context ensureBrowserAvailable", () => {
it("waits for CDP readiness after launching to avoid follow-up PortInUseError races (#21149)", async () => {
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile } =
setupEnsureBrowserAvailableHarness();
isChromeCdpReady.mockResolvedValueOnce(false).mockResolvedValue(true);
mockLaunchedChrome(launchOpenClawChrome, 123);
const promise = profile.ensureBrowserAvailable();
await vi.advanceTimersByTimeAsync(100);
await expect(promise).resolves.toBeUndefined();
expect(launchOpenClawChrome).toHaveBeenCalledTimes(1);
expect(isChromeCdpReady).toHaveBeenCalled();
expect(stopOpenClawChrome).not.toHaveBeenCalled();
});
it("stops launched chrome when CDP readiness never arrives", async () => {
const { launchOpenClawChrome, stopOpenClawChrome, isChromeCdpReady, profile } =
setupEnsureBrowserAvailableHarness();
isChromeCdpReady.mockResolvedValue(false);
mockLaunchedChrome(launchOpenClawChrome, 321);
const promise = profile.ensureBrowserAvailable();
const rejected = expect(promise).rejects.toThrow("not reachable after start");
await vi.advanceTimersByTimeAsync(8100);
await rejected;
expect(launchOpenClawChrome).toHaveBeenCalledTimes(1);
expect(stopOpenClawChrome).toHaveBeenCalledTimes(1);
});
});