Files
openwork/apps/app/tests/route-refresh-control.test.ts

320 lines
11 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
commitRouteWorkspaceSelection,
createLatestWorkspaceCommitter,
createRouteWorkspaceSelectionCommitter,
createRouteRefreshLifecycle,
planRouteConnectionGap,
planRouteWorkspaceLoads,
} from "../src/react-app/shell/route-refresh-control";
describe("createLatestWorkspaceCommitter", () => {
test("coalesces an in-flight switching burst to the last route", async () => {
const committed: string[] = [];
let releaseFirst: () => void = () => undefined;
const firstBlocked = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const committer = createLatestWorkspaceCommitter(async (workspaceId) => {
committed.push(workspaceId);
if (workspaceId === "ws_1") await firstBlocked;
});
committer.request("ws_1");
committer.request("ws_2");
committer.request("ws_3");
expect(committed).toEqual(["ws_1"]);
releaseFirst();
await committer.settled();
expect(committed).toEqual(["ws_1", "ws_3"]);
});
test("does not repeat the active commit when the final route returns to it", async () => {
const committed: string[] = [];
let releaseFirst: () => void = () => undefined;
const firstBlocked = new Promise<void>((resolve) => {
releaseFirst = resolve;
});
const committer = createLatestWorkspaceCommitter(async (workspaceId) => {
committed.push(workspaceId);
if (committed.length === 1) await firstBlocked;
});
committer.request("ws_1");
committer.request("ws_2");
committer.request("ws_1");
releaseFirst();
await committer.settled();
expect(committed).toEqual(["ws_1"]);
});
});
describe("route workspace selection handoff", () => {
test("keeps the Settings destination behind an in-flight session commit", async () => {
const committed: string[] = [];
let releaseSessionCommit: () => void = () => undefined;
const sessionCommitBlocked = new Promise<void>((resolve) => {
releaseSessionCommit = resolve;
});
const committer = createRouteWorkspaceSelectionCommitter();
committer.request("ws_1", async (workspaceId) => {
committed.push(`session:${workspaceId}:start`);
await sessionCommitBlocked;
committed.push(`session:${workspaceId}:end`);
});
committer.request("ws_2", async (workspaceId) => {
committed.push(`session:${workspaceId}`);
});
committer.request("ws_2", async (workspaceId) => {
committed.push(`settings:${workspaceId}`);
});
expect(committed).toEqual(["session:ws_1:start"]);
releaseSessionCommit();
await committer.settled();
expect(committed).toEqual([
"session:ws_1:start",
"session:ws_1:end",
"settings:ws_2",
]);
});
test("retries the same destination with the newer route callback after a failed commit", async () => {
const committed: string[] = [];
let releaseFailedCommit: () => void = () => undefined;
const failedCommitBlocked = new Promise<void>((resolve) => {
releaseFailedCommit = resolve;
});
const committer = createRouteWorkspaceSelectionCommitter();
committer.request("ws_2", async () => {
committed.push("session:ws_2");
await failedCommitBlocked;
throw new Error("stale connection");
});
committer.request("ws_2", async () => {
committed.push("settings:ws_2");
});
releaseFailedCommit();
await committer.settled();
expect(committed).toEqual(["session:ws_2", "settings:ws_2"]);
});
});
describe("commitRouteWorkspaceSelection", () => {
test("serializes desktop mutations before server activation", async () => {
const calls: string[] = [];
await commitRouteWorkspaceSelection({
workspaceId: " ws_2 ",
desktopRuntime: true,
setDesktopSelected: async (workspaceId) => { calls.push(`selected:${workspaceId}`); },
setDesktopRuntimeActive: async (workspaceId) => { calls.push(`runtime:${workspaceId}`); },
activateWorkspace: async (workspaceId) => { calls.push(`server:${workspaceId}`); },
});
expect(calls).toEqual(["selected:ws_2", "runtime:ws_2", "server:ws_2"]);
});
test("continues after a desktop persistence failure", async () => {
const calls: string[] = [];
await commitRouteWorkspaceSelection({
workspaceId: "ws_2",
desktopRuntime: true,
setDesktopSelected: async () => { throw new Error("write failed"); },
setDesktopRuntimeActive: async (workspaceId) => { calls.push(`runtime:${workspaceId}`); },
activateWorkspace: async (workspaceId) => { calls.push(`server:${workspaceId}`); },
});
expect(calls).toEqual(["runtime:ws_2", "server:ws_2"]);
});
});
describe("planRouteWorkspaceLoads", () => {
test("loads every unloaded workspace with the selected workspace first", () => {
expect(planRouteWorkspaceLoads(
["ws_1", "ws_2", "ws_3", "ws_4"],
"ws_3",
new Set(["ws_1"]),
)).toEqual(["ws_3", "ws_2", "ws_4"]);
});
test("still loads other unloaded workspaces when the selected one is loaded", () => {
expect(planRouteWorkspaceLoads(["ws_1", "ws_2"], "ws_2", new Set(["ws_2"]))).toEqual(["ws_1"]);
});
test("returns nothing when every workspace session index is loaded", () => {
expect(planRouteWorkspaceLoads(["ws_1", "ws_2"], "ws_1", new Set(["ws_1", "ws_2"]))).toEqual([]);
});
test("ignores a selected workspace that is not in the list", () => {
expect(planRouteWorkspaceLoads(["ws_1"], "ws_missing", new Set())).toEqual(["ws_1"]);
});
});
describe("createRouteRefreshLifecycle", () => {
test("dedupes refreshes while one is in flight", () => {
const lifecycle = createRouteRefreshLifecycle();
const first = lifecycle.begin();
expect(first).not.toBeNull();
expect(lifecycle.begin()).toBeNull();
expect(first?.finish()).toBe(true);
expect(lifecycle.isInFlight()).toBe(false);
expect(lifecycle.begin()).not.toBeNull();
});
test("supersede cancels the in-flight attempt instead of racing it", () => {
const lifecycle = createRouteRefreshLifecycle();
const stale = lifecycle.begin();
const fresh = lifecycle.begin({ supersede: true });
expect(stale?.isCurrent()).toBe(false);
expect(fresh?.isCurrent()).toBe(true);
// The superseded attempt no longer owns the in-flight slot, so its
// completion cannot re-open the route mid-refresh.
expect(stale?.finish()).toBe(false);
expect(lifecycle.isInFlight()).toBe(true);
expect(fresh?.finish()).toBe(true);
expect(lifecycle.isInFlight()).toBe(false);
});
});
describe("planRouteConnectionGap", () => {
test("desktop gaps retain route state and hold the boot overlay", () => {
expect(planRouteConnectionGap({ desktopRuntime: true })).toEqual({
retainExistingState: true,
markRouteReady: false,
});
});
test("web treats a missing connection as a real disconnected state", () => {
expect(planRouteConnectionGap({ desktopRuntime: false })).toEqual({
retainExistingState: false,
markRouteReady: true,
});
});
});
// Mirrors refreshRouteState's control flow: begin → resolve connection →
// staleness check → gap plan or apply → finish, with route readiness reported
// only by the attempt that still owns the refresh. On a desktop gap the
// connection is quarantined (no request may carry the previous bearer token
// to the freed loopback port) while session display state is retained.
type SimulatedRouteState = {
baseUrl: string;
token: string;
sessions: string[];
routeReady: boolean;
};
function createSimulatedRoute(initial: SimulatedRouteState) {
const lifecycle = createRouteRefreshLifecycle();
const state = { ...initial, sessions: [...initial.sessions] };
const refresh = async (
resolveConnection: () => Promise<{ baseUrl: string; token: string } | null>,
options?: { supersede?: boolean },
) => {
const attempt = lifecycle.begin(options);
if (!attempt) return;
let routeReadyAfterRefresh = true;
try {
const resolution = await resolveConnection();
if (!attempt.isCurrent()) return;
if (!resolution) {
const gapPlan = planRouteConnectionGap({ desktopRuntime: true });
routeReadyAfterRefresh = gapPlan.markRouteReady;
// The live connection is quarantined in every gap.
state.baseUrl = "";
state.token = "";
if (!gapPlan.retainExistingState) {
state.sessions = [];
}
return;
}
state.baseUrl = resolution.baseUrl;
state.token = resolution.token;
} finally {
attempt.finish();
if (attempt.isCurrent() && routeReadyAfterRefresh) {
state.routeReady = true;
}
}
};
return { state, refresh };
}
describe("desktop restart refresh sequence", () => {
test("an empty resolution quarantines the connection, retains sessions, and a later one recovers", async () => {
const route = createSimulatedRoute({
baseUrl: "http://127.0.0.1:4100",
token: "tok_before_restart",
sessions: ["task-1", "task-2"],
routeReady: false,
});
// Local server is restarting: the first resolution has no base URL/token.
await route.refresh(async () => null);
// No live endpoint or bearer token survives the gap — the freed loopback
// port must never receive the previous credentials…
expect(route.state.baseUrl).toBe("");
expect(route.state.token).toBe("");
// …while the session display state is retained for the user.
expect(route.state.sessions).toEqual(["task-1", "task-2"]);
// The boot overlay must not be released by the gap refresh.
expect(route.state.routeReady).toBe(false);
// The restarted server published fresh connection info.
await route.refresh(async () => ({ baseUrl: "http://127.0.0.1:4187", token: "tok_after_restart" }));
expect(route.state.baseUrl).toBe("http://127.0.0.1:4187");
expect(route.state.token).toBe("tok_after_restart");
expect(route.state.sessions).toEqual(["task-1", "task-2"]);
expect(route.state.routeReady).toBe(true);
});
test("a stale slow refresh cannot overwrite the recovered connection", async () => {
const route = createSimulatedRoute({
baseUrl: "",
token: "",
sessions: ["task-1"],
routeReady: false,
});
let releaseStaleResolution: (value: null) => void = () => {};
const staleResolution = new Promise<null>((resolve) => {
releaseStaleResolution = resolve;
});
// A refresh starts against the restarting server and hangs on resolution.
const staleRun = route.refresh(() => staleResolution);
// The republished server settings supersede it with a fresh refresh.
await route.refresh(
async () => ({ baseUrl: "http://127.0.0.1:4187", token: "tok_recovered" }),
{ supersede: true },
);
expect(route.state.baseUrl).toBe("http://127.0.0.1:4187");
expect(route.state.routeReady).toBe(true);
// The stale attempt finally resolves empty — after the recovery. Its
// completion must change nothing.
releaseStaleResolution(null);
await staleRun;
expect(route.state.baseUrl).toBe("http://127.0.0.1:4187");
expect(route.state.token).toBe("tok_recovered");
expect(route.state.sessions).toEqual(["task-1"]);
expect(route.state.routeReady).toBe(true);
});
});