refactor(browser): deduplicate profile hot-reload fixtures (#118220)

This commit is contained in:
Peter Steinberger
2026-08-02 16:10:26 -07:00
committed by GitHub
parent 2cc0a98519
commit 9fa28928e5
@@ -8,7 +8,7 @@ import {
getOrCreateProfileRuntime,
isProfileGenerationCurrent,
} from "./server-context.lifecycle.js";
import type { BrowserServerState } from "./server-context.types.js";
import type { BrowserServerState, ProfileRuntimeState } from "./server-context.types.js";
type TestProfileConfig = {
cdpPort?: number;
@@ -129,8 +129,67 @@ function runtimeState(
return runtime;
}
function createTestProfileRuntimeState(profile: ResolvedBrowserProfile) {
return runtimeState(profile, null, null);
function createBrowserState() {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map(),
};
return { cfg, state };
}
function createProfileFixture(
options: {
name?: string;
config?: TestProfileConfig;
running?: RunningChrome | null;
lastTargetId?: string | null;
} = {},
) {
const name = options.name ?? "openclaw";
if (options.config) {
mockState.cfgProfiles[name] = options.config;
mockState.cachedConfig = null;
}
const { state } = createBrowserState();
const profile = requireValue(resolveProfile(state.resolved, name), `${name} profile missing`);
const runtime = runtimeState(profile, options.running ?? null, options.lastTargetId ?? null);
state.profiles.set(name, runtime);
return { state, profile, runtime };
}
function refreshProfiles(state: BrowserServerState) {
refreshResolvedBrowserConfigFromDisk({ current: state, refreshConfigFromDisk: true });
}
function updateProfile(
state: BrowserServerState,
name: string,
config: TestProfileConfig,
clearCachedConfig = false,
) {
mockState.cfgProfiles[name] = config;
if (clearCachedConfig) {
mockState.cachedConfig = null;
}
refreshProfiles(state);
}
function enqueueCurrentProfileStart(
state: BrowserServerState,
runtime: ProfileRuntimeState,
run: (signal: AbortSignal, generation: number) => Promise<void>,
) {
return enqueueProfileStart({
state,
runtime,
configRevision: getProfileLifecycle(runtime).configRevision,
key: "default",
run,
});
}
describe("server-context hot-reload profiles", () => {
@@ -143,25 +202,14 @@ describe("server-context hot-reload profiles", () => {
mockState.cfgProfiles = {
openclaw: { cdpPort: 18800, color: "#FF4500" },
};
mockState.cachedConfig = null; // Clear simulated cache
mockState.cachedConfig = null;
});
it("forProfile hot-reloads newly added profiles from config", () => {
// Start with only openclaw profile
// 1. Prime the cache by calling getRuntimeConfig() first
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const { cfg, state } = createBrowserState();
// Verify cache is primed (without desktop)
expect(cfg.browser?.profiles?.desktop).toBeUndefined();
const state = {
server: null,
port: 18791,
resolved,
profiles: new Map(),
};
// Initially, "desktop" profile should not exist
expect(
resolveBrowserProfileWithHotReload({
current: state,
@@ -170,14 +218,11 @@ describe("server-context hot-reload profiles", () => {
}),
).toBeNull();
// 2. Simulate adding a new profile to config (like user editing openclaw.json)
mockState.cfgProfiles.desktop = { cdpUrl: "http://127.0.0.1:9222", color: "#0066CC" };
// 3. Verify without clearConfigCache, getRuntimeConfig() still returns stale cached value
const staleCfg = getRuntimeConfig();
expect(staleCfg.browser?.profiles?.desktop).toBeUndefined(); // Cache is stale!
expect(staleCfg.browser?.profiles?.desktop).toBeUndefined();
// 4. Hot-reload uses the refresh source without flushing the global getRuntimeConfig cache.
const profile = resolveBrowserProfileWithHotReload({
current: state,
refreshConfigFromDisk: true,
@@ -186,24 +231,14 @@ describe("server-context hot-reload profiles", () => {
expect(profile?.name).toBe("desktop");
expect(profile?.cdpUrl).toBe("http://127.0.0.1:9222");
// 5. Verify the new profile was merged into the cached state
expect(state.resolved.profiles).toHaveProperty("desktop");
// 6. Verify GLOBAL cache was NOT cleared - subsequent simple getRuntimeConfig() still sees STALE value
// This confirms the fix: we read fresh config for the specific profile lookup without flushing the global cache
const stillStaleCfg = getRuntimeConfig();
expect(stillStaleCfg.browser?.profiles?.desktop).toBeUndefined();
});
it("forProfile still throws for profiles that don't exist in fresh config", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
server: null,
port: 18791,
resolved,
profiles: new Map(),
};
const { state } = createBrowserState();
// Profile that doesn't exist anywhere should still throw
expect(
@@ -218,40 +253,19 @@ describe("server-context hot-reload profiles", () => {
it.each(["constructor", "prototype"] as const)(
"treats removed %s profiles as absent during hot reload",
(profileName) => {
mockState.cfgProfiles = {
[profileName]: { cdpPort: 18801, color: "#0066CC" },
};
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const profile = requireValue(
resolveProfile(resolved, profileName),
`${profileName} profile missing`,
);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([
[
profileName,
{
profile,
running: { pid: 123 } as never,
lastTargetId: "tab-1",
},
],
]),
};
mockState.cfgProfiles = {};
const { state, runtime } = createProfileFixture({
name: profileName,
config: { cdpPort: 18801, color: "#0066CC" },
running: { pid: 123 } as never,
lastTargetId: "tab-1",
});
mockState.cfgProfiles = {};
mockState.cachedConfig = null;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
refreshProfiles(state);
expect(resolveProfile(state.resolved, profileName)).toBeNull();
const runtime = requireValue(state.profiles.get(profileName), "runtime missing");
const actor = getProfileLifecycle(runtime);
expect(actor.terminal).toBe("config-removed");
expect(actor.transitionReason).toBe("profile removed from config");
@@ -259,14 +273,7 @@ describe("server-context hot-reload profiles", () => {
);
it("forProfile refreshes existing profile config after getRuntimeConfig cache updates", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
server: null,
port: 18791,
resolved,
profiles: new Map(),
};
const { state } = createBrowserState();
mockState.cfgProfiles.openclaw = { cdpPort: 19999, color: "#FF4500" };
mockState.cachedConfig = null;
@@ -281,51 +288,23 @@ describe("server-context hot-reload profiles", () => {
});
it("listProfiles refreshes config before enumerating profiles", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const state = {
server: null,
port: 18791,
resolved,
profiles: new Map(),
};
const { state } = createBrowserState();
mockState.cfgProfiles.desktop = { cdpPort: 19999, color: "#0066CC" };
mockState.cachedConfig = null;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
refreshProfiles(state);
expect(Object.keys(state.resolved.profiles)).toContain("desktop");
});
it("captures the old profile before adopting changed invariants", async () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = requireValue(
resolveProfile(resolved, "openclaw"),
"openclaw profile missing",
);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([
["openclaw", runtimeState(openclawProfile, { pid: 123 } as never, "tab-1")],
]),
};
mockState.cfgProfiles.openclaw = { cdpPort: 19999, color: "#FF4500" };
mockState.cachedConfig = null;
const oldCdpUrl = openclawProfile.cdpUrl;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
const { state, profile, runtime } = createProfileFixture({
running: { pid: 123 } as never,
lastTargetId: "tab-1",
});
const oldCdpUrl = profile.cdpUrl;
updateProfile(state, "openclaw", { cdpPort: 19999, color: "#FF4500" }, true);
const runtime = requireValue(state.profiles.get("openclaw"), "openclaw runtime missing");
expect(runtime.profile.cdpPort).toBe(19999);
expect(runtime.lastTargetId).toBeNull();
expect(getProfileLifecycle(runtime).transitionReason).toContain("cdpPort");
@@ -339,122 +318,71 @@ describe("server-context hot-reload profiles", () => {
});
it("marks local managed runtime state for reconcile when profile headless changes", () => {
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = requireValue(
resolveProfile(resolved, "openclaw"),
"openclaw profile missing",
);
expect(openclawProfile.headless).toBe(true);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([
["openclaw", runtimeState(openclawProfile, { pid: 123 } as never, "tab-1")],
]),
};
mockState.cfgProfiles.openclaw = {
cdpPort: 18800,
color: "#FF4500",
headless: false,
};
mockState.cachedConfig = null;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
const { state, profile, runtime } = createProfileFixture({
running: { pid: 123 } as never,
lastTargetId: "tab-1",
});
expect(profile.headless).toBe(true);
updateProfile(state, "openclaw", { cdpPort: 18800, color: "#FF4500", headless: false }, true);
const runtime = requireValue(state.profiles.get("openclaw"), "openclaw runtime missing");
expect(runtime.profile.headless).toBe(false);
expect(runtime.lastTargetId).toBeNull();
expect(getProfileLifecycle(runtime).transitionReason).toContain("headless");
});
it("marks local managed runtime state for reconcile when profile executablePath changes", () => {
mockState.cfgProfiles.openclaw = {
cdpPort: 18800,
color: "#FF4500",
executablePath: "/usr/bin/chrome-old",
};
mockState.cachedConfig = null;
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const openclawProfile = requireValue(
resolveProfile(resolved, "openclaw"),
"openclaw profile missing",
);
expect(openclawProfile.executablePath).toBe("/usr/bin/chrome-old");
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([
["openclaw", runtimeState(openclawProfile, { pid: 123 } as never, "tab-1")],
]),
};
mockState.cfgProfiles.openclaw = {
cdpPort: 18800,
color: "#FF4500",
executablePath: "/usr/bin/chrome-new",
};
mockState.cachedConfig = null;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
const { state, profile, runtime } = createProfileFixture({
config: {
cdpPort: 18800,
color: "#FF4500",
executablePath: "/usr/bin/chrome-old",
},
running: { pid: 123 } as never,
lastTargetId: "tab-1",
});
expect(profile.executablePath).toBe("/usr/bin/chrome-old");
updateProfile(
state,
"openclaw",
{ cdpPort: 18800, color: "#FF4500", executablePath: "/usr/bin/chrome-new" },
true,
);
const runtime = requireValue(state.profiles.get("openclaw"), "openclaw runtime missing");
expect(runtime.profile.executablePath).toBe("/usr/bin/chrome-new");
expect(runtime.lastTargetId).toBeNull();
expect(getProfileLifecycle(runtime).transitionReason).toContain("executablePath");
});
it("does not reconcile existing-session runtime when only headless changes", () => {
mockState.cfgProfiles.remote = {
cdpUrl: "http://127.0.0.1:9222",
color: "#0066CC",
headless: true,
driver: "existing-session",
};
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const remoteProfile = requireValue(
resolveProfile(resolved, "remote"),
"remote profile missing",
);
expect(remoteProfile.driver).toBe("existing-session");
expect(remoteProfile.attachOnly).toBe(true);
expect(remoteProfile.headless).toBe(true);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([
["remote", runtimeState(remoteProfile, { pid: 456 } as never, "tab-remote")],
]),
};
mockState.cfgProfiles.remote = {
cdpUrl: "http://127.0.0.1:9222",
color: "#0066CC",
headless: false,
driver: "existing-session",
};
mockState.cachedConfig = null;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
const { state, profile, runtime } = createProfileFixture({
name: "remote",
config: {
cdpUrl: "http://127.0.0.1:9222",
color: "#0066CC",
headless: true,
driver: "existing-session",
},
running: { pid: 456 } as never,
lastTargetId: "tab-remote",
});
expect(profile.driver).toBe("existing-session");
expect(profile.attachOnly).toBe(true);
expect(profile.headless).toBe(true);
updateProfile(
state,
"remote",
{
cdpUrl: "http://127.0.0.1:9222",
color: "#0066CC",
headless: false,
driver: "existing-session",
},
true,
);
const runtime = requireValue(state.profiles.get("remote"), "remote runtime missing");
expect(runtime.profile.driver).toBe("existing-session");
expect(runtime.profile.headless).toBe(false);
expect(runtime.lastTargetId).toBe("tab-remote");
@@ -462,45 +390,28 @@ describe("server-context hot-reload profiles", () => {
});
it("does not reconcile remote cdp runtime when only headless changes", () => {
mockState.cfgProfiles.remote = {
cdpUrl: "http://10.0.0.42:9222",
color: "#0066CC",
headless: true,
};
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const remoteProfile = requireValue(
resolveProfile(resolved, "remote"),
"remote profile missing",
);
expect(remoteProfile.driver).toBe("openclaw");
expect(remoteProfile.attachOnly).toBe(false);
expect(remoteProfile.cdpIsLoopback).toBe(false);
expect(remoteProfile.headless).toBe(true);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([
["remote", runtimeState(remoteProfile, { pid: 789 } as never, "tab-remote-cdp")],
]),
};
mockState.cfgProfiles.remote = {
cdpUrl: "http://10.0.0.42:9222",
color: "#0066CC",
headless: false,
};
mockState.cachedConfig = null;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
const { state, profile, runtime } = createProfileFixture({
name: "remote",
config: {
cdpUrl: "http://10.0.0.42:9222",
color: "#0066CC",
headless: true,
},
running: { pid: 789 } as never,
lastTargetId: "tab-remote-cdp",
});
expect(profile.driver).toBe("openclaw");
expect(profile.attachOnly).toBe(false);
expect(profile.cdpIsLoopback).toBe(false);
expect(profile.headless).toBe(true);
updateProfile(
state,
"remote",
{ cdpUrl: "http://10.0.0.42:9222", color: "#0066CC", headless: false },
true,
);
const runtime = requireValue(state.profiles.get("remote"), "remote runtime missing");
expect(runtime.profile.driver).toBe("openclaw");
expect(runtime.profile.cdpIsLoopback).toBe(false);
expect(runtime.profile.headless).toBe(false);
@@ -509,32 +420,21 @@ describe("server-context hot-reload profiles", () => {
});
it("reconciles existing-session command and structural argument changes", () => {
mockState.cfgProfiles.work = {
cdpUrl: "http://127.0.0.1:9222",
color: "#0066CC",
driver: "existing-session",
mcpCommand: "/old/mcp",
mcpArgs: ["--one"],
};
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const work = requireValue(resolveProfile(resolved, "work"), "work profile missing");
const runtime = createTestProfileRuntimeState(work);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([["work", runtime]]),
};
const { state, runtime } = createProfileFixture({
name: "work",
config: {
cdpUrl: "http://127.0.0.1:9222",
color: "#0066CC",
driver: "existing-session",
mcpCommand: "/old/mcp",
mcpArgs: ["--one"],
},
});
mockState.cfgProfiles.work = {
updateProfile(state, "work", {
...mockState.cfgProfiles.work,
mcpCommand: "/new/mcp",
mcpArgs: ["--one", "--two"],
};
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
expect(getProfileLifecycle(runtime).transitionReason).toContain("mcpCommand");
@@ -543,41 +443,25 @@ describe("server-context hot-reload profiles", () => {
});
it("invalidates a pending A start before adopting B", async () => {
mockState.cfgProfiles.work = { cdpPort: 18801, color: "#0066CC" };
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const workA = requireValue(resolveProfile(resolved, "work"), "work A missing");
const runtime = createTestProfileRuntimeState(workA);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([["work", runtime]]),
};
const { state, profile, runtime } = createProfileFixture({
name: "work",
config: { cdpPort: 18801, color: "#0066CC" },
});
const launchA = deferred();
const launchAStarted = deferred();
const adopted: string[] = [];
const revisionA = getProfileLifecycle(runtime).configRevision;
const pendingA = enqueueProfileStart({
state,
runtime,
configRevision: revisionA,
key: "default",
run: async (signal, generation) => {
launchAStarted.resolve();
await launchA.promise;
if (
!isProfileGenerationCurrent({ state, runtime, configRevision: revisionA, generation })
) {
throw signal.reason ?? new Error("A was superseded");
}
adopted.push(workA.cdpUrl);
},
const pendingA = enqueueCurrentProfileStart(state, runtime, async (signal, generation) => {
launchAStarted.resolve();
await launchA.promise;
if (!isProfileGenerationCurrent({ state, runtime, configRevision: revisionA, generation })) {
throw signal.reason ?? new Error("A was superseded");
}
adopted.push(profile.cdpUrl);
});
await launchAStarted.promise;
mockState.cfgProfiles.work = { cdpPort: 18802, color: "#00AA00" };
refreshResolvedBrowserConfigFromDisk({ current: state, refreshConfigFromDisk: true });
updateProfile(state, "work", { cdpPort: 18802, color: "#00AA00" });
const workB = requireValue(resolveProfile(state.resolved, "work"), "work B missing");
expect(runtime.profile.cdpUrl).toBe(workB.cdpUrl);
@@ -585,14 +469,8 @@ describe("server-context hot-reload profiles", () => {
await expect(pendingA).rejects.toThrow(/profile invariants changed|superseded/i);
await getProfileLifecycle(runtime).tail;
await expect(
enqueueProfileStart({
state,
runtime,
configRevision: getProfileLifecycle(runtime).configRevision,
key: "default",
run: async () => {
adopted.push(runtime.profile.cdpUrl);
},
enqueueCurrentProfileStart(state, runtime, async () => {
adopted.push(runtime.profile.cdpUrl);
}),
).resolves.toBeUndefined();
@@ -600,17 +478,10 @@ describe("server-context hot-reload profiles", () => {
});
it("rapid A to B to C closes both stale endpoints and adopts only C", async () => {
mockState.cfgProfiles.work = { cdpPort: 18801, color: "#0066CC" };
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const workA = requireValue(resolveProfile(resolved, "work"), "work A missing");
const runtime = createTestProfileRuntimeState(workA);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([["work", runtime]]),
};
const { state, profile, runtime } = createProfileFixture({
name: "work",
config: { cdpPort: 18801, color: "#0066CC" },
});
const retired = new Set<string>();
lifecycleMocks.retirePlaywrightBrowserConnection.mockImplementation(({ cdpUrl }) => {
if (retired.has(cdpUrl)) {
@@ -620,99 +491,63 @@ describe("server-context hot-reload profiles", () => {
return true;
});
mockState.cfgProfiles.work = { cdpPort: 18802, color: "#00AA00" };
refreshResolvedBrowserConfigFromDisk({ current: state, refreshConfigFromDisk: true });
updateProfile(state, "work", { cdpPort: 18802, color: "#00AA00" });
const workB = requireValue(resolveProfile(state.resolved, "work"), "work B missing");
const adopted: string[] = [];
const pendingB = enqueueProfileStart({
state,
runtime,
configRevision: getProfileLifecycle(runtime).configRevision,
key: "default",
run: async () => {
adopted.push(workB.cdpUrl);
},
const pendingB = enqueueCurrentProfileStart(state, runtime, async () => {
adopted.push(workB.cdpUrl);
});
mockState.cfgProfiles.work = { cdpPort: 18803, color: "#AA00AA" };
refreshResolvedBrowserConfigFromDisk({ current: state, refreshConfigFromDisk: true });
updateProfile(state, "work", { cdpPort: 18803, color: "#AA00AA" });
const workC = requireValue(resolveProfile(state.resolved, "work"), "work C missing");
expect(runtime.profile.cdpUrl).toBe(workC.cdpUrl);
await expect(pendingB).rejects.toThrow(/profile config changed|superseded/i);
await getProfileLifecycle(runtime).tail;
await expect(
enqueueProfileStart({
state,
runtime,
configRevision: getProfileLifecycle(runtime).configRevision,
key: "default",
run: async () => {
adopted.push(runtime.profile.cdpUrl);
},
enqueueCurrentProfileStart(state, runtime, async () => {
adopted.push(runtime.profile.cdpUrl);
}),
).resolves.toBeUndefined();
expect(lifecycleMocks.closePlaywrightBrowserConnection.mock.calls).toEqual([
[{ cdpUrl: workA.cdpUrl }],
[{ cdpUrl: profile.cdpUrl }],
[{ cdpUrl: workB.cdpUrl }],
]);
expect(adopted).toEqual([workC.cdpUrl]);
});
it("keeps a removed-name tombstone until a pending start cleans its late handle", async () => {
mockState.cfgProfiles.work = { cdpPort: 18801, color: "#0066CC" };
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const workA = requireValue(resolveProfile(resolved, "work"), "work profile missing");
const oldRuntime = createTestProfileRuntimeState(workA);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([["work", oldRuntime]]),
};
const {
state,
profile,
runtime: oldRuntime,
} = createProfileFixture({
name: "work",
config: { cdpPort: 18801, color: "#0066CC" },
});
expect(oldRuntime.running).toBeNull();
const lateRunning = { pid: 321 } as RunningChrome;
const launch = deferred();
const launchStarted = deferred();
const pendingStart = enqueueProfileStart({
state,
runtime: oldRuntime,
configRevision: getProfileLifecycle(oldRuntime).configRevision,
key: "default",
run: async () => {
launchStarted.resolve();
await launch.promise;
getProfileLifecycle(oldRuntime).handles.add(lateRunning);
},
const pendingStart = enqueueCurrentProfileStart(state, oldRuntime, async () => {
launchStarted.resolve();
await launch.promise;
getProfileLifecycle(oldRuntime).handles.add(lateRunning);
});
await launchStarted.promise;
delete mockState.cfgProfiles.work;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
refreshProfiles(state);
expect(getProfileLifecycle(oldRuntime).terminal).toBe("config-removed");
expect(state.profiles.get("work")).toBe(oldRuntime);
mockState.cfgProfiles.work = { cdpPort: 18802, color: "#00AA00" };
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
updateProfile(state, "work", { cdpPort: 18802, color: "#00AA00" });
const workB = requireValue(resolveProfile(state.resolved, "work"), "work B missing");
expect(getOrCreateProfileRuntime(state, workB)).toBe(oldRuntime);
expect(() =>
enqueueProfileStart({
state,
runtime: oldRuntime,
configRevision: getProfileLifecycle(oldRuntime).configRevision,
key: "default",
run: async () => {},
}),
).toThrow(/config-removed/);
expect(() => enqueueCurrentProfileStart(state, oldRuntime, async () => {})).toThrow(
/config-removed/,
);
launch.resolve();
await expect(pendingStart).rejects.toThrow(/config-removed|lifecycle changed/i);
@@ -724,31 +559,18 @@ describe("server-context hot-reload profiles", () => {
const replacement = getOrCreateProfileRuntime(state, workB);
expect(replacement).not.toBe(oldRuntime);
await expect(
enqueueProfileStart({
state,
runtime: replacement,
configRevision: getProfileLifecycle(replacement).configRevision,
key: "default",
run: async () => {},
}),
enqueueCurrentProfileStart(state, replacement, async () => {}),
).resolves.toBeUndefined();
expect(lifecycleMocks.closePlaywrightBrowserConnection).toHaveBeenCalledWith({
cdpUrl: workA.cdpUrl,
cdpUrl: profile.cdpUrl,
});
});
it("retries a failed removal tombstone before admitting a same-name re-add", async () => {
mockState.cfgProfiles.work = { cdpPort: 18801, color: "#0066CC" };
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const workA = requireValue(resolveProfile(resolved, "work"), "work profile missing");
const oldRuntime = createTestProfileRuntimeState(workA);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([["work", oldRuntime]]),
};
const { state, runtime: oldRuntime } = createProfileFixture({
name: "work",
config: { cdpPort: 18801, color: "#0066CC" },
});
lifecycleMocks.closePlaywrightBrowserConnection
.mockRejectedValueOnce(new Error("close failed"))
.mockResolvedValue(undefined);
@@ -757,19 +579,12 @@ describe("server-context hot-reload profiles", () => {
.mockReturnValue(false);
delete mockState.cfgProfiles.work;
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
refreshProfiles(state);
await getProfileLifecycle(oldRuntime).tail;
expect(getProfileLifecycle(oldRuntime).blockedReason).toContain("cleanup failed");
expect(state.profiles.get("work")).toBe(oldRuntime);
mockState.cfgProfiles.work = { cdpPort: 18802, color: "#00AA00" };
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
updateProfile(state, "work", { cdpPort: 18802, color: "#00AA00" });
await getProfileLifecycle(oldRuntime).tail;
await Promise.resolve();
@@ -778,55 +593,35 @@ describe("server-context hot-reload profiles", () => {
});
it("retries failed invariant cleanup before admitting the updated profile", async () => {
mockState.cfgProfiles.work = { cdpPort: 18801, color: "#0066CC" };
const cfg = getRuntimeConfig();
const resolved = resolveBrowserConfig(cfg.browser, cfg);
const workA = requireValue(resolveProfile(resolved, "work"), "work profile missing");
const runtime = createTestProfileRuntimeState(workA);
const state: BrowserServerState = {
server: null,
port: 18791,
resolved,
profiles: new Map([["work", runtime]]),
};
const { state, profile, runtime } = createProfileFixture({
name: "work",
config: { cdpPort: 18801, color: "#0066CC" },
});
lifecycleMocks.closePlaywrightBrowserConnection
.mockRejectedValueOnce(new Error("close failed"))
.mockResolvedValue(undefined);
mockState.cfgProfiles.work = { cdpPort: 18802, color: "#00AA00" };
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
updateProfile(state, "work", { cdpPort: 18802, color: "#00AA00" });
await getProfileLifecycle(runtime).tail;
expect(getProfileLifecycle(runtime).blockedReason).toContain("cleanup failed");
refreshResolvedBrowserConfigFromDisk({
current: state,
refreshConfigFromDisk: true,
});
refreshProfiles(state);
await getProfileLifecycle(runtime).tail;
expect(getProfileLifecycle(runtime).blockedReason).toBeNull();
expect(runtime.profile.cdpPort).toBe(18802);
expect(lifecycleMocks.retirePlaywrightBrowserConnection).toHaveBeenCalledWith({
cdpUrl: workA.cdpUrl,
cdpUrl: profile.cdpUrl,
});
expect(lifecycleMocks.closePlaywrightBrowserConnection).toHaveBeenCalledTimes(2);
expect(lifecycleMocks.closePlaywrightBrowserConnection).toHaveBeenNthCalledWith(1, {
cdpUrl: workA.cdpUrl,
cdpUrl: profile.cdpUrl,
});
expect(lifecycleMocks.closePlaywrightBrowserConnection).toHaveBeenNthCalledWith(2, {
cdpUrl: workA.cdpUrl,
cdpUrl: profile.cdpUrl,
});
await expect(
enqueueProfileStart({
state,
runtime,
configRevision: getProfileLifecycle(runtime).configRevision,
key: "default",
run: async () => {},
}),
enqueueCurrentProfileStart(state, runtime, async () => {}),
).resolves.toBeUndefined();
});
});