Files
openclaw/extensions/browser/src/browser-control-state.test.ts
Peter Steinberger ea2b558e5e fix(browser): prevent stale profile resurrection (#104601)
* fix(browser): serialize profile lifecycle

* refactor(browser): tighten lifecycle cleanup

* chore(browser): keep release note in PR

* test(browser): keep temp fixtures inside plugin

* test(browser): use preferred temp root
2026-07-11 11:06:07 -07:00

144 lines
4.3 KiB
TypeScript

// Browser tests cover shared browser-control lifecycle serialization.
import type { Server } from "node:http";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { markBrowserRuntimeStopping } from "./browser/server-context.lifecycle.js";
const runtimeMocks = vi.hoisted(() => ({
stopBrowserRuntime: vi.fn(),
}));
vi.mock("./browser/runtime-lifecycle.js", () => ({
createBrowserRuntimeState: (params: {
server: Server | null;
port: number;
resolved: unknown;
}) => ({
server: params.server,
port: params.port,
resolved: params.resolved,
profiles: new Map(),
}),
stopBrowserRuntime: runtimeMocks.stopBrowserRuntime,
}));
const {
ensureBrowserControlRuntime,
getBrowserControlState,
stopBrowserControlRuntime,
withBrowserControlStart,
} = await import("./browser-control-state.js");
const resolved = { profiles: {}, controlPort: 18_791 } as never;
const onWarn = vi.fn();
function start(owner: "server" | "service", server: Server | null = null) {
return withBrowserControlStart(() =>
ensureBrowserControlRuntime({ server, port: 18_791, resolved, owner, onWarn }),
);
}
function stop(requestedBy: "server" | "service") {
return stopBrowserControlRuntime({ requestedBy, onWarn });
}
beforeEach(() => {
runtimeMocks.stopBrowserRuntime.mockReset().mockImplementation(async (params) => {
params.clearState();
});
});
describe("browser control lifecycle", () => {
it("allows a start queued after a no-state stop", async () => {
const stopping = stop("service");
const starting = start("service");
await expect(stopping).resolves.toBeNull();
await expect(starting).resolves.toBeTruthy();
await stop("service");
});
it("rejects a start requested after stop intent but before stop drains", async () => {
await start("service");
let releaseStop!: () => void;
const stopGate = new Promise<void>((resolve) => {
releaseStop = resolve;
});
runtimeMocks.stopBrowserRuntime.mockImplementationOnce(async (params) => {
await stopGate;
params.clearState();
});
const stopping = stop("service");
const starting = start("service");
releaseStop();
await stopping;
await expect(starting).rejects.toThrow("stopping");
expect(getBrowserControlState()).toBeNull();
});
it("retains a failed stop owner for an exact retry", async () => {
await start("service");
runtimeMocks.stopBrowserRuntime.mockImplementationOnce(async (params) => {
markBrowserRuntimeStopping(params.current);
throw new Error("cleanup failed");
});
await expect(stop("service")).rejects.toThrow("cleanup failed");
expect(getBrowserControlState()).toBeNull();
await expect(stop("service")).resolves.toBeTruthy();
await expect(start("service")).resolves.toBeTruthy();
await stop("service");
});
it("lets a foreground server adopt service state without a second runtime", async () => {
const serviceState = await start("service");
const server = {} as Server;
const serverState = await start("server", server);
expect(serverState).toBe(serviceState);
expect(serverState.server).toBe(server);
await stop("service");
expect(runtimeMocks.stopBrowserRuntime).not.toHaveBeenCalled();
await stop("server");
expect(runtimeMocks.stopBrowserRuntime).toHaveBeenCalledOnce();
});
it("allows a start queued after a foreground-owned service stop", async () => {
await start("service");
await start("server", {} as Server);
const stopping = stop("service");
const starting = start("service");
await expect(stopping).resolves.toBeNull();
await expect(starting).resolves.toBeTruthy();
await stop("server");
});
it("orders a queued stop after an in-progress cold start", async () => {
let releaseStart!: () => void;
const startGate = new Promise<void>((resolve) => {
releaseStart = resolve;
});
const starting = withBrowserControlStart(async () => {
await startGate;
return await ensureBrowserControlRuntime({
server: null,
port: 18_791,
resolved,
owner: "service",
onWarn,
});
});
const stopping = stop("service");
releaseStart();
await starting;
await stopping;
expect(runtimeMocks.stopBrowserRuntime).toHaveBeenCalledOnce();
expect(getBrowserControlState()).toBeNull();
});
});