diff --git a/CHANGELOG.md b/CHANGELOG.md index 269e5dfa4f2..6ab5434dfbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai - Browser/Extension relay stale tabs: evict stale cached targets from `/json/list` when extension targets are destroyed/crashed or commands fail with missing target/session errors. (#6175) Thanks @vincentkoc. - CLI/Browser start timeout: honor `openclaw browser --timeout start` and stop by removing the fixed 15000ms override so slower Chrome startups can use caller-provided timeouts. (#22412, #23427) Thanks @vincentkoc. - Browser/CDP startup diagnostics: include Chrome stderr output and a Linux no-sandbox hint in startup timeout errors so failed launches are easier to diagnose. (#29312) Thanks @veast. +- Browser/CDP startup readiness: wait for CDP websocket readiness after launching Chrome and cleanly stop/reset when readiness never arrives, reducing follow-up `PortInUseError` races after `browser start`/`open`. (#29538) Thanks @AaronWander. - Docker/Image health checks: add Dockerfile `HEALTHCHECK` that probes gateway `GET /healthz` so container runtimes can mark unhealthy instances without requiring auth credentials in the probe command. (#11478) Thanks @U-C4N and @vincentkoc. - Docker/Sandbox bootstrap hardening: make `OPENCLAW_SANDBOX` opt-in parsing explicit (`1|true|yes|on`), support custom Docker socket paths via `OPENCLAW_DOCKER_SOCKET`, defer docker.sock exposure until sandbox prerequisites pass, and reset/roll back persisted sandbox mode to `off` when setup is skipped or partially fails to avoid stale broken sandbox state. (#29974) Thanks @jamtujest and @vincentkoc. - Daemon/systemd checks in containers: treat missing `systemctl` invocations (including `spawn systemctl ENOENT`/`EACCES`) as unavailable service state during `is-enabled` checks, preventing container flows from failing with `Gateway service check failed` before install/status handling can continue. (#26089) Thanks @sahilsatralkar and @vincentkoc. diff --git a/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts b/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts index b16017e521b..78f08d822da 100644 --- a/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts +++ b/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts @@ -27,6 +27,8 @@ function makeBrowserState(): BrowserServerState { cdpProtocol: "http", cdpHost: "127.0.0.1", cdpIsLoopback: true, + cdpPortRangeStart: null, + cdpPortRangeEnd: null, evaluateEnabled: false, remoteCdpTimeoutMs: 1500, remoteCdpHandshakeTimeoutMs: 3000, @@ -47,6 +49,7 @@ function makeBrowserState(): BrowserServerState { afterEach(() => { vi.useRealTimers(); + vi.clearAllMocks(); vi.restoreAllMocks(); }); @@ -84,4 +87,38 @@ describe("browser server-context ensureBrowserAvailable", () => { expect(isChromeCdpReady).toHaveBeenCalled(); expect(stopOpenClawChrome).not.toHaveBeenCalled(); }); + + it("stops launched chrome when CDP readiness never arrives", async () => { + 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); + isChromeCdpReady.mockResolvedValue(false); + + const proc = new EventEmitter() as unknown as ChildProcessWithoutNullStreams; + launchOpenClawChrome.mockResolvedValue({ + pid: 321, + exe: { kind: "chromium", path: "/usr/bin/chromium" }, + userDataDir: "/tmp/openclaw-test", + cdpPort: 18800, + startedAt: Date.now(), + proc, + }); + + const state = makeBrowserState(); + const ctx = createBrowserRouteContext({ getState: () => state }); + const profile = ctx.forProfile("openclaw"); + + 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); + }); });