import { scopeThreadRef, scopedThreadKey } from "@t3tools/client-runtime/environment";import { ThreadId } from "@t3tools/contracts";import { beforeEach, describe, expect, it } from "vite-plus/test"; import { migratePersistedTerminalUiStateStoreState, selectThreadTerminalUiState, useTerminalUiStateStore,} from "./terminalUiStateStore";import { DEFAULT_THREAD_TERMINAL_ID } from "./types"; const THREAD_ID = ThreadId.make("thread-1");const THREAD_REF = scopeThreadRef("environment-a" as never, THREAD_ID);const OTHER_THREAD_REF = scopeThreadRef("environment-b" as never, THREAD_ID); describe("terminalUiStateStore actions", () => { beforeEach(() => { useTerminalUiStateStore.persist.clearStorage(); useTerminalUiStateStore.setState({ terminalUiStateByThreadKey: {}, suppressedTerminalIdsByThreadKey: {}, }); }); it("returns an empty default terminal UI state for unknown threads", () => { const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState).toEqual({ terminalOpen: false, terminalHeight: 280, terminalIds: [], activeTerminalId: "", terminalGroups: [], activeTerminalGroupId: "", }); }); it("opens and splits terminals into the active group", () => { const store = useTerminalUiStateStore.getState(); store.setTerminalOpen(THREAD_REF, true); store.splitTerminal(THREAD_REF, "terminal-2"); const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState.terminalOpen).toBe(true); expect(terminalUiState.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "terminal-2"]); expect(terminalUiState.activeTerminalId).toBe("terminal-2"); expect(terminalUiState.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "terminal-2"], }, ]); }); it("stacks vertically split terminals in the active group", () => { const store = useTerminalUiStateStore.getState(); store.setTerminalOpen(THREAD_REF, true); store.splitTerminalVertical(THREAD_REF, "terminal-2"); const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState.terminalGroups).toEqual([ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "terminal-2"], splitDirection: "vertical", }, ]); }); it("materializes the default terminal when opening an empty drawer", () => { useTerminalUiStateStore.getState().setTerminalOpen(THREAD_REF, true); const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState).toEqual({ terminalOpen: true, terminalHeight: 280, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], activeTerminalId: DEFAULT_THREAD_TERMINAL_ID, terminalGroups: [ { id: `group-${DEFAULT_THREAD_TERMINAL_ID}`, terminalIds: [DEFAULT_THREAD_TERMINAL_ID], }, ], activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`, }); }); it("caps splits at four terminals per group", () => { const store = useTerminalUiStateStore.getState(); store.splitTerminal(THREAD_REF, "terminal-2"); store.splitTerminal(THREAD_REF, "terminal-3"); store.splitTerminal(THREAD_REF, "terminal-4"); store.splitTerminal(THREAD_REF, "terminal-5"); store.splitTerminal(THREAD_REF, "terminal-6"); const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState.terminalIds).toEqual([ "terminal-2", "terminal-3", "terminal-4", "terminal-5", ]); expect(terminalUiState.terminalGroups).toEqual([ { id: "group-terminal-2", terminalIds: ["terminal-2", "terminal-3", "terminal-4", "terminal-5"], }, ]); }); it("creates new terminals in a separate group", () => { useTerminalUiStateStore.getState().newTerminal(THREAD_REF, "terminal-2"); const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState.terminalIds).toEqual(["terminal-2"]); expect(terminalUiState.activeTerminalId).toBe("terminal-2"); expect(terminalUiState.activeTerminalGroupId).toBe("group-terminal-2"); expect(terminalUiState.terminalGroups).toEqual([ { id: "group-terminal-2", terminalIds: ["terminal-2"] }, ]); }); it("ensures unknown server terminals are registered, opened, and activated", () => { const store = useTerminalUiStateStore.getState(); store.ensureTerminal(THREAD_REF, "setup-setup", { open: true, active: true }); const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState.terminalOpen).toBe(true); expect(terminalUiState.terminalIds).toEqual(["setup-setup"]); expect(terminalUiState.activeTerminalId).toBe("setup-setup"); expect(terminalUiState.terminalGroups).toEqual([ { id: "group-setup-setup", terminalIds: ["setup-setup"] }, ]); }); it("keeps state isolated per environment when raw thread ids collide", () => { const store = useTerminalUiStateStore.getState(); store.setTerminalOpen(THREAD_REF, true); store.newTerminal(OTHER_THREAD_REF, "env-b-terminal"); expect( selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ).terminalOpen, ).toBe(true); expect( selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, OTHER_THREAD_REF, ).terminalIds, ).toEqual(["env-b-terminal"]); }); it("drops persisted entries whose thread keys are not valid scoped keys", () => { const migrated = migratePersistedTerminalUiStateStoreState( { terminalStateByThreadKey: { [scopedThreadKey(THREAD_REF)]: { terminalOpen: true, terminalHeight: 320, terminalIds: ["term-1"], activeTerminalId: "term-1", terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }], activeTerminalGroupId: "group-term-1", }, "legacy-thread-id": { terminalOpen: true, terminalHeight: 320, terminalIds: ["term-1"], activeTerminalId: "term-1", terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }], activeTerminalGroupId: "group-term-1", }, }, }, 2, ); expect(migrated).toEqual({ terminalUiStateByThreadKey: { [scopedThreadKey(THREAD_REF)]: { terminalOpen: true, terminalHeight: 320, terminalIds: ["term-1"], activeTerminalId: "term-1", terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }], activeTerminalGroupId: "group-term-1", }, }, }); }); it("resets to default and clears persisted entry when closing the last terminal", () => { const store = useTerminalUiStateStore.getState(); store.newTerminal(THREAD_REF, "terminal-only"); store.closeTerminal(THREAD_REF, "terminal-only"); expect( useTerminalUiStateStore.getState().terminalUiStateByThreadKey[scopedThreadKey(THREAD_REF)], ).toBeUndefined(); expect( selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ).terminalIds, ).toEqual([]); }); it("keeps a valid active terminal after closing an active split terminal", () => { const store = useTerminalUiStateStore.getState(); store.splitTerminal(THREAD_REF, "terminal-2"); store.splitTerminal(THREAD_REF, "terminal-3"); store.closeTerminal(THREAD_REF, "terminal-3"); const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState.activeTerminalId).toBe("terminal-2"); expect(terminalUiState.terminalIds).toEqual(["terminal-2"]); expect(terminalUiState.terminalGroups).toEqual([ { id: "group-terminal-2", terminalIds: ["terminal-2"] }, ]); }); it("reconciles terminal ids from an external ordered list", () => { const store = useTerminalUiStateStore.getState(); store.setTerminalOpen(THREAD_REF, true); store.reconcileTerminalIds(THREAD_REF, ["term-a", "term-b"]); const terminalUiState = selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ); expect(terminalUiState.terminalIds).toEqual(["term-a", "term-b"]); expect(terminalUiState.activeTerminalId).toBe("term-a"); expect(terminalUiState.terminalGroups).toEqual([ { id: "group-term-a", terminalIds: ["term-a"] }, { id: "group-term-b", terminalIds: ["term-b"] }, ]); }); it("does not import a closed panel terminal from stale metadata", () => { const store = useTerminalUiStateStore.getState(); store.newTerminal(THREAD_REF, "term-2"); store.closeTerminal(THREAD_REF, "term-1"); store.reconcileTerminalIds(THREAD_REF, ["term-1", "term-2"]); expect( selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ).terminalIds, ).toEqual(["term-2"]); store.newTerminal(THREAD_REF, "term-1"); expect( selectThreadTerminalUiState( useTerminalUiStateStore.getState().terminalUiStateByThreadKey, THREAD_REF, ).terminalIds, ).toEqual(["term-2", "term-1"]); }); it("is a no-op when clearing terminal UI state for a thread with no state", () => { const store = useTerminalUiStateStore.getState(); const before = useTerminalUiStateStore.getState(); store.clearTerminalUiState(THREAD_REF); expect(useTerminalUiStateStore.getState()).toBe(before); });});
import { scopeThreadRef, scopedThreadKey } from "@t3tools/client-runtime/environment";
import { ThreadId } from "@t3tools/contracts";
import { beforeEach, describe, expect, it } from "vite-plus/test";
import {
migratePersistedTerminalUiStateStoreState,
selectThreadTerminalUiState,
useTerminalUiStateStore,
} from "./terminalUiStateStore";
import { DEFAULT_THREAD_TERMINAL_ID } from "./types";
const THREAD_ID = ThreadId.make("thread-1");
const THREAD_REF = scopeThreadRef("environment-a" as never, THREAD_ID);
const OTHER_THREAD_REF = scopeThreadRef("environment-b" as never, THREAD_ID);
describe("terminalUiStateStore actions", () => {
beforeEach(() => {
useTerminalUiStateStore.persist.clearStorage();
useTerminalUiStateStore.setState({
terminalUiStateByThreadKey: {},
suppressedTerminalIdsByThreadKey: {},
});
});
it("returns an empty default terminal UI state for unknown threads", () => {
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState).toEqual({
terminalOpen: false,
terminalHeight: 280,
terminalIds: [],
activeTerminalId: "",
terminalGroups: [],
activeTerminalGroupId: "",
});
});
it("opens and splits terminals into the active group", () => {
const store = useTerminalUiStateStore.getState();
store.setTerminalOpen(THREAD_REF, true);
store.splitTerminal(THREAD_REF, "terminal-2");
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState.terminalOpen).toBe(true);
expect(terminalUiState.terminalIds).toEqual([DEFAULT_THREAD_TERMINAL_ID, "terminal-2"]);
expect(terminalUiState.activeTerminalId).toBe("terminal-2");
expect(terminalUiState.terminalGroups).toEqual([
{
id: `group-${DEFAULT_THREAD_TERMINAL_ID}`,
terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "terminal-2"],
},
]);
});
it("stacks vertically split terminals in the active group", () => {
const store = useTerminalUiStateStore.getState();
store.setTerminalOpen(THREAD_REF, true);
store.splitTerminalVertical(THREAD_REF, "terminal-2");
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState.terminalGroups).toEqual([
{
id: `group-${DEFAULT_THREAD_TERMINAL_ID}`,
terminalIds: [DEFAULT_THREAD_TERMINAL_ID, "terminal-2"],
splitDirection: "vertical",
},
]);
});
it("materializes the default terminal when opening an empty drawer", () => {
useTerminalUiStateStore.getState().setTerminalOpen(THREAD_REF, true);
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState).toEqual({
terminalOpen: true,
terminalHeight: 280,
terminalIds: [DEFAULT_THREAD_TERMINAL_ID],
activeTerminalId: DEFAULT_THREAD_TERMINAL_ID,
terminalGroups: [
{
id: `group-${DEFAULT_THREAD_TERMINAL_ID}`,
terminalIds: [DEFAULT_THREAD_TERMINAL_ID],
},
],
activeTerminalGroupId: `group-${DEFAULT_THREAD_TERMINAL_ID}`,
});
});
it("caps splits at four terminals per group", () => {
const store = useTerminalUiStateStore.getState();
store.splitTerminal(THREAD_REF, "terminal-2");
store.splitTerminal(THREAD_REF, "terminal-3");
store.splitTerminal(THREAD_REF, "terminal-4");
store.splitTerminal(THREAD_REF, "terminal-5");
store.splitTerminal(THREAD_REF, "terminal-6");
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState.terminalIds).toEqual([
"terminal-2",
"terminal-3",
"terminal-4",
"terminal-5",
]);
expect(terminalUiState.terminalGroups).toEqual([
{
id: "group-terminal-2",
terminalIds: ["terminal-2", "terminal-3", "terminal-4", "terminal-5"],
},
]);
});
it("creates new terminals in a separate group", () => {
useTerminalUiStateStore.getState().newTerminal(THREAD_REF, "terminal-2");
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState.terminalIds).toEqual(["terminal-2"]);
expect(terminalUiState.activeTerminalId).toBe("terminal-2");
expect(terminalUiState.activeTerminalGroupId).toBe("group-terminal-2");
expect(terminalUiState.terminalGroups).toEqual([
{ id: "group-terminal-2", terminalIds: ["terminal-2"] },
]);
});
it("ensures unknown server terminals are registered, opened, and activated", () => {
const store = useTerminalUiStateStore.getState();
store.ensureTerminal(THREAD_REF, "setup-setup", { open: true, active: true });
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState.terminalOpen).toBe(true);
expect(terminalUiState.terminalIds).toEqual(["setup-setup"]);
expect(terminalUiState.activeTerminalId).toBe("setup-setup");
expect(terminalUiState.terminalGroups).toEqual([
{ id: "group-setup-setup", terminalIds: ["setup-setup"] },
]);
});
it("keeps state isolated per environment when raw thread ids collide", () => {
const store = useTerminalUiStateStore.getState();
store.setTerminalOpen(THREAD_REF, true);
store.newTerminal(OTHER_THREAD_REF, "env-b-terminal");
expect(
selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
).terminalOpen,
).toBe(true);
expect(
selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
OTHER_THREAD_REF,
).terminalIds,
).toEqual(["env-b-terminal"]);
});
it("drops persisted entries whose thread keys are not valid scoped keys", () => {
const migrated = migratePersistedTerminalUiStateStoreState(
{
terminalStateByThreadKey: {
[scopedThreadKey(THREAD_REF)]: {
terminalOpen: true,
terminalHeight: 320,
terminalIds: ["term-1"],
activeTerminalId: "term-1",
terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }],
activeTerminalGroupId: "group-term-1",
},
"legacy-thread-id": {
terminalOpen: true,
terminalHeight: 320,
terminalIds: ["term-1"],
activeTerminalId: "term-1",
terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }],
activeTerminalGroupId: "group-term-1",
},
},
},
2,
);
expect(migrated).toEqual({
terminalUiStateByThreadKey: {
[scopedThreadKey(THREAD_REF)]: {
terminalOpen: true,
terminalHeight: 320,
terminalIds: ["term-1"],
activeTerminalId: "term-1",
terminalGroups: [{ id: "group-term-1", terminalIds: ["term-1"] }],
activeTerminalGroupId: "group-term-1",
},
},
});
});
it("resets to default and clears persisted entry when closing the last terminal", () => {
const store = useTerminalUiStateStore.getState();
store.newTerminal(THREAD_REF, "terminal-only");
store.closeTerminal(THREAD_REF, "terminal-only");
expect(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey[scopedThreadKey(THREAD_REF)],
).toBeUndefined();
expect(
selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
).terminalIds,
).toEqual([]);
});
it("keeps a valid active terminal after closing an active split terminal", () => {
const store = useTerminalUiStateStore.getState();
store.splitTerminal(THREAD_REF, "terminal-2");
store.splitTerminal(THREAD_REF, "terminal-3");
store.closeTerminal(THREAD_REF, "terminal-3");
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState.activeTerminalId).toBe("terminal-2");
expect(terminalUiState.terminalIds).toEqual(["terminal-2"]);
expect(terminalUiState.terminalGroups).toEqual([
{ id: "group-terminal-2", terminalIds: ["terminal-2"] },
]);
});
it("reconciles terminal ids from an external ordered list", () => {
const store = useTerminalUiStateStore.getState();
store.setTerminalOpen(THREAD_REF, true);
store.reconcileTerminalIds(THREAD_REF, ["term-a", "term-b"]);
const terminalUiState = selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
);
expect(terminalUiState.terminalIds).toEqual(["term-a", "term-b"]);
expect(terminalUiState.activeTerminalId).toBe("term-a");
expect(terminalUiState.terminalGroups).toEqual([
{ id: "group-term-a", terminalIds: ["term-a"] },
{ id: "group-term-b", terminalIds: ["term-b"] },
]);
});
it("does not import a closed panel terminal from stale metadata", () => {
const store = useTerminalUiStateStore.getState();
store.newTerminal(THREAD_REF, "term-2");
store.closeTerminal(THREAD_REF, "term-1");
store.reconcileTerminalIds(THREAD_REF, ["term-1", "term-2"]);
expect(
selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
).terminalIds,
).toEqual(["term-2"]);
store.newTerminal(THREAD_REF, "term-1");
expect(
selectThreadTerminalUiState(
useTerminalUiStateStore.getState().terminalUiStateByThreadKey,
THREAD_REF,
).terminalIds,
).toEqual(["term-2", "term-1"]);
});
it("is a no-op when clearing terminal UI state for a thread with no state", () => {
const store = useTerminalUiStateStore.getState();
const before = useTerminalUiStateStore.getState();
store.clearTerminalUiState(THREAD_REF);
expect(useTerminalUiStateStore.getState()).toBe(before);
});
});