Files
openwork/apps/app/tests/workspace-routes.test.ts

296 lines
11 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
classifyRouteSessionReadError,
mergeRouteWorkspaces,
readRouteSessionsWithRetry,
refreshRouteWorkspaceListState,
stabilizeRouteWorkspaceOrder,
} from "../src/react-app/shell/route-workspaces";
import { mapRouteWorkspaceLoads } from "../src/react-app/shell/route-refresh-control";
import {
mergeWorkspaceRouteSession,
preserveWorkspaceRouteSession,
removeWorkspaceRouteSession,
sessionIdForLegacyWorkspaceInference,
globalExtensionsRoute,
workspaceExtensionsRoute,
workspaceSettingsRoute,
} from "../src/react-app/shell/workspace-routes";
describe("workspace surface routes", () => {
test("keeps Extensions outside Settings and preserves deep links", () => {
expect(workspaceSettingsRoute(" workspace/a ", "extensions")).toBe(
"/workspace/workspace%2Fa/settings/extensions",
);
expect(workspaceExtensionsRoute(" workspace/a ")).toBe(
"/workspace/workspace%2Fa/extensions",
);
expect(workspaceExtensionsRoute(" workspace/a ", "/skills/")).toBe(
"/workspace/workspace%2Fa/extensions/skills",
);
expect(globalExtensionsRoute("mcps")).toBe("/extensions/mcps");
});
});
describe("workspace route session inference", () => {
test("modern workspace routes do not contribute a refresh session id", () => {
expect(sessionIdForLegacyWorkspaceInference("workspace-a", "session-a")).toBeNull();
expect(sessionIdForLegacyWorkspaceInference("workspace-a", "session-b")).toBeNull();
expect(sessionIdForLegacyWorkspaceInference(" workspace-a ", " session-c ")).toBeNull();
});
test("legacy session routes contribute a trimmed refresh session id", () => {
expect(sessionIdForLegacyWorkspaceInference(null, " session-a ")).toBe("session-a");
expect(sessionIdForLegacyWorkspaceInference("", "session-b")).toBe("session-b");
expect(sessionIdForLegacyWorkspaceInference(" ", " ")).toBeNull();
});
});
describe("workspace route session hydration", () => {
test("adds an out-of-window routed session without duplicating it", () => {
const listed = [{ id: "session-200", title: "Recent" }];
const hydrated = { id: "session-010", title: "Deep link" };
expect(mergeWorkspaceRouteSession(listed, hydrated)).toEqual([hydrated, ...listed]);
expect(mergeWorkspaceRouteSession([hydrated, ...listed], { ...hydrated, title: "Updated" })).toEqual([
{ id: "session-010", title: "Updated" },
...listed,
]);
});
test("preserves the active hydrated session across capped list refreshes", () => {
const hydrated = { id: "session-010", title: "Deep link" };
const current = [hydrated, { id: "session-200", title: "Recent" }];
const refreshed = [{ id: "session-201", title: "Newest" }];
expect(preserveWorkspaceRouteSession(refreshed, current, hydrated.id)).toEqual([hydrated, ...refreshed]);
expect(preserveWorkspaceRouteSession([hydrated, ...refreshed], current, hydrated.id)).toEqual([
hydrated,
...refreshed,
]);
});
test("removes a previous out-of-window overlay before another is added", () => {
const listed = [{ id: "session-200", title: "Recent" }];
const first = mergeWorkspaceRouteSession(listed, { id: "session-010", title: "First" });
const restored = removeWorkspaceRouteSession(first, "session-010");
const second = mergeWorkspaceRouteSession(restored, { id: "session-009", title: "Second" });
expect(restored).toEqual(listed);
expect(second).toHaveLength(listed.length + 1);
expect(second.map((session) => session.id)).toEqual(["session-009", "session-200"]);
});
});
describe("workspace route session read errors", () => {
test("distinguishes missing, retryable, and terminal failures", () => {
expect(classifyRouteSessionReadError(Object.assign(new Error("missing"), { status: 404, code: "session_not_found" }))).toBe("not-found");
expect(classifyRouteSessionReadError(Object.assign(new Error("workspace missing"), { status: 404, code: "workspace_not_found" }))).toBe("error");
expect(classifyRouteSessionReadError(Object.assign(new Error("upstream"), { status: 502 }))).toBe("retryable");
expect(classifyRouteSessionReadError(Object.assign(new Error("engine starting"), { status: 400, code: "opencode_unconfigured" }))).toBe("retryable");
expect(classifyRouteSessionReadError(new Error("request timed out"))).toBe("retryable");
expect(classifyRouteSessionReadError(Object.assign(new Error("forbidden"), { status: 403 }))).toBe("error");
});
test("retries a bounded runtime startup gap and returns the recovered sessions", async () => {
let attempts = 0;
const waits: number[] = [];
const sessions = await readRouteSessionsWithRetry({
load: async () => {
attempts += 1;
if (attempts < 3) {
throw Object.assign(new Error("engine starting"), {
status: 400,
code: "opencode_unconfigured",
});
}
return ["session-1"];
},
retryDelaysMs: [250, 750, 1_500],
wait: async (delayMs) => { waits.push(delayMs); },
});
expect(sessions).toEqual(["session-1"]);
expect(attempts).toBe(3);
expect(waits).toEqual([250, 750]);
});
test("does not retry a terminal authorization failure", async () => {
let attempts = 0;
await expect(readRouteSessionsWithRetry({
load: async () => {
attempts += 1;
throw Object.assign(new Error("forbidden"), { status: 403 });
},
retryDelaysMs: [250, 750],
wait: async () => undefined,
})).rejects.toMatchObject({ status: 403 });
expect(attempts).toBe(1);
});
});
describe("workspace route session load budget", () => {
test("loads workspaces in bounded batches while preserving order", async () => {
let active = 0;
let peak = 0;
const release: Array<() => void> = [];
const workspaces = Array.from({ length: 10 }, (_, index) => index);
const resultPromise = mapRouteWorkspaceLoads(workspaces, async (workspace) => {
active += 1;
peak = Math.max(peak, active);
await new Promise<void>((resolve) => release.push(resolve));
active -= 1;
return `workspace-${workspace}`;
});
await Bun.sleep(0);
expect(active).toBe(4);
release.splice(0).forEach((resolve) => resolve());
await Bun.sleep(0);
expect(active).toBe(4);
release.splice(0).forEach((resolve) => resolve());
await Bun.sleep(0);
expect(active).toBe(2);
release.splice(0).forEach((resolve) => resolve());
expect(await resultPromise).toEqual(workspaces.map((workspace) => `workspace-${workspace}`));
expect(peak).toBe(4);
});
});
describe("workspace route list merging", () => {
const previouslyKnownWorkspaces = [{
id: "workspace-known",
name: "Known workspace",
path: "/tmp/known",
workspaceType: "local",
displayNameResolved: "Known workspace",
}];
const desktopWorkspaces = [{
id: "workspace-desktop",
name: "Desktop workspace",
path: "/tmp/openwork",
workspaceType: "local",
displayNameResolved: "Desktop workspace",
}];
test("keeps desktop workspaces when the server workspace list is not an array", () => {
expect(mergeRouteWorkspaces({ items: undefined }, desktopWorkspaces)).toEqual(desktopWorkspaces);
});
test("preserves known workspaces when the route list payload is missing items", async () => {
await expect(refreshRouteWorkspaceListState({
load: async () => ({}),
desktopWorkspaces,
previousWorkspaces: previouslyKnownWorkspaces,
orderIds: [],
})).resolves.toMatchObject({
error: null,
usable: false,
workspaces: previouslyKnownWorkspaces,
});
});
test("preserves known workspaces when the route list payload has undefined items", async () => {
await expect(refreshRouteWorkspaceListState({
load: async () => ({ items: undefined }),
desktopWorkspaces,
previousWorkspaces: previouslyKnownWorkspaces,
orderIds: [],
})).resolves.toMatchObject({
error: null,
usable: false,
workspaces: previouslyKnownWorkspaces,
});
});
test("preserves known workspaces when the route list request rejects", async () => {
const result = await refreshRouteWorkspaceListState({
load: async () => {
throw new Error("workspace list unavailable");
},
desktopWorkspaces,
previousWorkspaces: previouslyKnownWorkspaces,
orderIds: [],
});
expect(result.usable).toBe(false);
expect(result.workspaces).toEqual(previouslyKnownWorkspaces);
expect(result.error).toBeInstanceOf(Error);
});
test("retries transient workspace-list failures during startup", async () => {
let attempts = 0;
const waits: number[] = [];
const result = await refreshRouteWorkspaceListState({
load: async () => {
attempts += 1;
if (attempts < 3) throw new Error("Failed to fetch");
return { items: previouslyKnownWorkspaces, activeId: previouslyKnownWorkspaces[0]?.id };
},
desktopWorkspaces,
previousWorkspaces: [],
orderIds: [],
retryDelaysMs: [250, 750, 1_500],
wait: async (delayMs) => {
waits.push(delayMs);
},
});
expect(attempts).toBe(3);
expect(waits).toEqual([250, 750]);
expect(result.error).toBeNull();
expect(result.usable).toBe(true);
expect(result.workspaces.map((workspace) => workspace.id)).toEqual([
"workspace-known",
"workspace-desktop",
]);
});
test("captures the first visible order so active-workspace refreshes cannot reshuffle the sidebar", () => {
const alpha = { ...previouslyKnownWorkspaces[0], id: "workspace-alpha" };
const beta = { ...desktopWorkspaces[0], id: "workspace-beta" };
const initial = stabilizeRouteWorkspaceOrder([alpha, beta], []);
const afterBetaActivation = stabilizeRouteWorkspaceOrder([beta, alpha], initial.orderIds);
expect(initial.orderIds).toEqual(["workspace-alpha", "workspace-beta"]);
expect(afterBetaActivation.workspaces.map((workspace) => workspace.id)).toEqual([
"workspace-alpha",
"workspace-beta",
]);
});
test("appends newly discovered workspaces without displacing the saved order", () => {
const alpha = { ...previouslyKnownWorkspaces[0], id: "workspace-alpha" };
const beta = { ...desktopWorkspaces[0], id: "workspace-beta" };
const gamma = { ...desktopWorkspaces[0], id: "workspace-gamma" };
const stable = stabilizeRouteWorkspaceOrder(
[beta, gamma, alpha],
["workspace-alpha", "workspace-beta"],
);
expect(stable.orderIds).toEqual([
"workspace-alpha",
"workspace-beta",
"workspace-gamma",
]);
expect(stable.workspaces.map((workspace) => workspace.id)).toEqual(stable.orderIds);
});
test("keeps temporarily absent workspace positions for the next successful refresh", () => {
const alpha = { ...previouslyKnownWorkspaces[0], id: "workspace-alpha" };
const beta = { ...desktopWorkspaces[0], id: "workspace-beta" };
const duringGap = stabilizeRouteWorkspaceOrder([beta], ["workspace-alpha", "workspace-beta"]);
const recovered = stabilizeRouteWorkspaceOrder([beta, alpha], duringGap.orderIds);
expect(duringGap.orderIds).toEqual(["workspace-alpha", "workspace-beta"]);
expect(duringGap.workspaces.map((workspace) => workspace.id)).toEqual(["workspace-beta"]);
expect(recovered.workspaces.map((workspace) => workspace.id)).toEqual([
"workspace-alpha",
"workspace-beta",
]);
});
});