mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-12 07:09:19 +03:00
221 lines
7.8 KiB
TypeScript
221 lines
7.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
EMPTY_WORKBENCH_STATE,
|
|
MAX_WORKBENCH_PANES,
|
|
addWorkbenchPane,
|
|
attachWorkbenchPane,
|
|
createWorkbenchTab,
|
|
detachWorkbenchPane,
|
|
dissolveWorkbenchTab,
|
|
normalizeWorkbenchState,
|
|
orderWorkbenchTabs,
|
|
reconcileWorkbench,
|
|
renameWorkbenchTab,
|
|
setWorkbenchLayout,
|
|
setWorkbenchPaneLayoutOrder,
|
|
setWorkbenchSplitRatios,
|
|
workbenchTab,
|
|
workbenchTabForPane,
|
|
} from "@/components/workbench/workbench-model";
|
|
|
|
describe("workbench model", () => {
|
|
it("derives standalone panes without persisting virtual tabs", () => {
|
|
const match = workbenchTabForPane(EMPTY_WORKBENCH_STATE, "pane-a");
|
|
|
|
expect(match.tabKey).not.toBe("pane-a");
|
|
expect(match.tab).toEqual({
|
|
explicit: false,
|
|
title: null,
|
|
paneKeys: ["pane-a"],
|
|
layoutPaneKeys: ["pane-a"],
|
|
layout: "columns",
|
|
splitRatios: [],
|
|
});
|
|
expect(EMPTY_WORKBENCH_STATE.tabs).toEqual({});
|
|
});
|
|
|
|
it("persists only a visible singleton group", () => {
|
|
const state = createWorkbenchTab(EMPTY_WORKBENCH_STATE, "pane-a");
|
|
const match = workbenchTabForPane(state, "pane-a");
|
|
|
|
expect(workbenchTab(state, match.tabKey)).toMatchObject({
|
|
explicit: true,
|
|
paneKeys: ["pane-a"],
|
|
});
|
|
expect(detachWorkbenchPane(state, match.tabKey, "pane-a").tabs).toEqual({});
|
|
});
|
|
|
|
it("materializes a group when a pane is added", () => {
|
|
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
|
|
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
|
|
state = setWorkbenchLayout(state, tabKey, "main-stack");
|
|
state = renameWorkbenchTab(state, tabKey, "Research");
|
|
|
|
expect(workbenchTab(state, tabKey)).toEqual({
|
|
explicit: false,
|
|
title: "Research",
|
|
paneKeys: ["pane-a", "pane-b"],
|
|
layoutPaneKeys: ["pane-a", "pane-b"],
|
|
layout: "main-stack",
|
|
splitRatios: [],
|
|
});
|
|
});
|
|
|
|
it("detaches a pane without persisting its standalone projection", () => {
|
|
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
|
|
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
|
|
state = createWorkbenchTab(state, "pane-a");
|
|
state = addWorkbenchPane(state, "pane-a", "pane-c");
|
|
state = detachWorkbenchPane(state, tabKey, "pane-a");
|
|
|
|
expect(workbenchTab(state, tabKey)?.paneKeys).toEqual(["pane-b", "pane-c"]);
|
|
expect(workbenchTabForPane(state, "pane-a").tab.paneKeys).toEqual(["pane-a"]);
|
|
expect(Object.values(state.tabs).some((tab) => tab.paneKeys.includes("pane-a"))).toBe(false);
|
|
});
|
|
|
|
it("dissolves a group into derived standalone panes", () => {
|
|
const grouped = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
|
|
const tabKey = workbenchTabForPane(grouped, "pane-a").tabKey;
|
|
const state = dissolveWorkbenchTab(grouped, tabKey);
|
|
|
|
expect(state.tabs).toEqual({});
|
|
expect(workbenchTabForPane(state, "pane-a").tab.paneKeys).toEqual(["pane-a"]);
|
|
expect(workbenchTabForPane(state, "pane-b").tab.paneKeys).toEqual(["pane-b"]);
|
|
});
|
|
|
|
it("moves panes symmetrically and removes an implicit singleton source", () => {
|
|
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
|
|
const sourceTabKey = workbenchTabForPane(state, "pane-a").tabKey;
|
|
state = createWorkbenchTab(state, "pane-c");
|
|
const targetTabKey = workbenchTabForPane(state, "pane-c").tabKey;
|
|
|
|
state = attachWorkbenchPane(state, targetTabKey, "pane-a");
|
|
expect(workbenchTab(state, sourceTabKey)).toBeNull();
|
|
expect(workbenchTab(state, targetTabKey)?.paneKeys).toEqual(["pane-c", "pane-a"]);
|
|
|
|
state = attachWorkbenchPane(state, targetTabKey, "pane-b");
|
|
expect(workbenchTab(state, targetTabKey)?.paneKeys).toEqual([
|
|
"pane-c",
|
|
"pane-a",
|
|
"pane-b",
|
|
]);
|
|
});
|
|
|
|
it("keeps membership independent from workspace pane order", () => {
|
|
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
|
|
state = addWorkbenchPane(state, "pane-a", "pane-c");
|
|
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
|
|
|
|
const [ordered] = orderWorkbenchTabs(
|
|
state,
|
|
["pane-c", "pane-a", "pane-b"],
|
|
new Map(),
|
|
);
|
|
expect(workbenchTab(state, tabKey)?.paneKeys).toEqual(["pane-a", "pane-b", "pane-c"]);
|
|
expect(ordered.paneKeys).toEqual(["pane-c", "pane-a", "pane-b"]);
|
|
|
|
state = setWorkbenchPaneLayoutOrder(state, tabKey, ["pane-b", "pane-c", "pane-a"]);
|
|
expect(workbenchTab(state, tabKey)?.layoutPaneKeys).toEqual([
|
|
"pane-b",
|
|
"pane-c",
|
|
"pane-a",
|
|
]);
|
|
});
|
|
|
|
it("stores resize ratios and resets them when geometry changes", () => {
|
|
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
|
|
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
|
|
state = setWorkbenchSplitRatios(state, tabKey, [0.35]);
|
|
|
|
expect(workbenchTab(state, tabKey)?.splitRatios).toEqual([0.35]);
|
|
state = setWorkbenchLayout(state, tabKey, "rows");
|
|
expect(workbenchTab(state, tabKey)?.splitRatios).toEqual([]);
|
|
});
|
|
|
|
it("keeps groups contiguous and ranks them by their latest pane", () => {
|
|
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-c");
|
|
state = addWorkbenchPane(state, "pane-b", "pane-d");
|
|
const alphaTabKey = workbenchTabForPane(state, "pane-a").tabKey;
|
|
const betaTabKey = workbenchTabForPane(state, "pane-b").tabKey;
|
|
|
|
const tabs = orderWorkbenchTabs(
|
|
state,
|
|
["pane-d", "pane-c", "pane-b", "pane-a", "pane-e"],
|
|
new Map([
|
|
["pane-a", "2026-08-01T10:00:00Z"],
|
|
["pane-b", "2026-08-03T10:00:00Z"],
|
|
["pane-c", "2026-08-05T10:00:00Z"],
|
|
["pane-d", "2026-08-04T10:00:00Z"],
|
|
["pane-e", "2026-08-02T10:00:00Z"],
|
|
]),
|
|
);
|
|
|
|
expect(tabs.map(({ tabKey, paneKeys }) => ({ tabKey, paneKeys }))).toEqual([
|
|
{ tabKey: alphaTabKey, paneKeys: ["pane-c", "pane-a"] },
|
|
{ tabKey: betaTabKey, paneKeys: ["pane-d", "pane-b"] },
|
|
{ tabKey: workbenchTabForPane(state, "pane-e").tabKey, paneKeys: ["pane-e"] },
|
|
]);
|
|
expect(Object.keys(state.tabs)).toHaveLength(2);
|
|
});
|
|
|
|
it("caps a group at four panes", () => {
|
|
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-1");
|
|
for (let index = 2; index <= MAX_WORKBENCH_PANES; index += 1) {
|
|
state = addWorkbenchPane(state, "pane-a", `pane-${index}`);
|
|
}
|
|
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
|
|
expect(workbenchTab(state, tabKey)?.paneKeys).toEqual([
|
|
"pane-a",
|
|
"pane-1",
|
|
"pane-2",
|
|
"pane-3",
|
|
]);
|
|
});
|
|
|
|
it("repairs persisted groups without materializing missing sessions", () => {
|
|
const state = normalizeWorkbenchState({
|
|
version: 1,
|
|
tabs: {
|
|
alpha: {
|
|
title: "Alpha",
|
|
paneKeys: ["pane-a", "pane-b", "pane-b", 9],
|
|
layout: "unknown",
|
|
},
|
|
duplicate: {
|
|
paneKeys: ["pane-b", "deleted"],
|
|
layout: "grid",
|
|
},
|
|
invisible: {
|
|
paneKeys: ["pane-c"],
|
|
layout: "columns",
|
|
},
|
|
},
|
|
});
|
|
const reconciled = reconcileWorkbench(
|
|
state,
|
|
new Set(["pane-a", "pane-b", "pane-c"]),
|
|
);
|
|
|
|
expect(workbenchTab(reconciled, "alpha")).toEqual({
|
|
explicit: false,
|
|
title: "Alpha",
|
|
paneKeys: ["pane-a", "pane-b"],
|
|
layoutPaneKeys: ["pane-a", "pane-b"],
|
|
layout: "columns",
|
|
splitRatios: [],
|
|
});
|
|
expect(Object.keys(reconciled.tabs)).toEqual(["alpha"]);
|
|
expect(workbenchTabForPane(reconciled, "pane-c").tab.paneKeys).toEqual(["pane-c"]);
|
|
});
|
|
|
|
it("keeps persisted state sparse with thousands of standalone sessions", () => {
|
|
const sessionKeys = Array.from({ length: 2_000 }, (_, index) => `pane-${index}`);
|
|
const reconciled = reconcileWorkbench(EMPTY_WORKBENCH_STATE, new Set(sessionKeys));
|
|
const ordered = orderWorkbenchTabs(reconciled, sessionKeys, new Map());
|
|
|
|
expect(reconciled.tabs).toEqual({});
|
|
expect(ordered).toHaveLength(2_000);
|
|
});
|
|
});
|