mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
refactor(browser): deduplicate profile hot-reload fixtures (#118220)
This commit is contained in:
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user