From 9fa28928e576273afa2f6f60c4b855cf103a4d6e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 2 Aug 2026 16:10:26 -0700 Subject: [PATCH] refactor(browser): deduplicate profile hot-reload fixtures (#118220) --- ...server-context.hot-reload-profiles.test.ts | 645 ++++++------------ 1 file changed, 220 insertions(+), 425 deletions(-) diff --git a/extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts b/extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts index c4b822a09bf1..f353ad6aecd3 100644 --- a/extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts +++ b/extensions/browser/src/browser/server-context.hot-reload-profiles.test.ts @@ -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, +) { + 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(); 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(); }); });