Files
NetMesh/application/state/sessionWorkspaceDetach.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

336 lines
10 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import type { TerminalSession, Workspace } from "../../domain/models";
import {
applyCloseSessionToSessions,
closeSessionsState,
closeSessionWorkspaceLayoutState,
detachSessionFromWorkspaceState,
replaceDissolvedWorkspaceTabOrder,
resolveActiveTabAfterCloseSession,
} from "./sessionWorkspaceDetach";
const session = (id: string, workspaceId = "ws-1"): TerminalSession => ({
id,
hostId: id,
hostLabel: id,
status: "connected",
workspaceId,
});
const workspace = (sessionIds: string[]): Workspace => ({
id: "ws-1",
title: "Workspace",
focusedSessionId: sessionIds[0],
focusSessionOrder: sessionIds,
root: sessionIds.length === 1
? { id: "pane-1", type: "pane", sessionId: sessionIds[0] }
: {
id: "split-1",
type: "split",
direction: "vertical",
children: sessionIds.map((sessionId, index) => ({
id: `pane-${index + 1}`,
type: "pane" as const,
sessionId,
})),
sizes: sessionIds.map(() => 1),
},
});
test("detach dissolves the original workspace when one session remains", () => {
const result = detachSessionFromWorkspaceState({
sessions: [session("s1"), session("s2")],
workspaces: [workspace(["s1", "s2"])],
sessionId: "s1",
});
assert.equal(result.changed, true);
assert.equal(result.activeTabId, "s1");
assert.deepEqual(result.sessions.map((s) => [s.id, s.workspaceId]), [
["s1", undefined],
["s2", undefined],
]);
assert.equal(result.workspaces.length, 0);
assert.equal(result.dissolvedWorkspaceId, "ws-1");
assert.deepEqual(result.replacementTabIds, ["s1", "s2"]);
});
test("detach preserves the other sessions in a multi-pane workspace", () => {
const result = detachSessionFromWorkspaceState({
sessions: [session("s1"), session("s2"), session("s3")],
workspaces: [workspace(["s1", "s2", "s3"])],
sessionId: "s2",
});
assert.equal(result.changed, true);
assert.deepEqual(result.sessions.map((s) => [s.id, s.workspaceId]), [
["s1", "ws-1"],
["s2", undefined],
["s3", "ws-1"],
]);
assert.deepEqual(result.workspaces[0].focusSessionOrder, ["s1", "s3"]);
assert.equal(result.workspaces[0].focusedSessionId, "s1");
assert.deepEqual(
result.workspaces[0].root.type === "split"
? result.workspaces[0].root.children.map((child) => child.type === "pane" ? child.sessionId : null)
: [],
["s1", "s3"],
);
});
test("dissolved workspace replacement preserves its tab position", () => {
assert.deepEqual(
replaceDissolvedWorkspaceTabOrder(["log-1", "ws-1", "session-3"], "ws-1", ["s1", "s2"]),
["log-1", "s1", "s2", "session-3"],
);
});
test("dissolved workspace replacement removes duplicate replacement ids", () => {
assert.deepEqual(
replaceDissolvedWorkspaceTabOrder(["s1", "ws-1", "session-3"], "ws-1", ["s1", "s2"]),
["s1", "s2", "session-3"],
);
});
test("dissolved workspace replacement is idempotent", () => {
const once = replaceDissolvedWorkspaceTabOrder(["log-1", "ws-1", "session-3"], "ws-1", ["s1", "s2"]);
assert.deepEqual(
replaceDissolvedWorkspaceTabOrder(once, "ws-1", ["s1", "s2"]),
once,
);
});
test("single remaining session preserves dissolved workspace tab position", () => {
assert.deepEqual(
replaceDissolvedWorkspaceTabOrder(["log-1", "ws-1", "session-3"], "ws-1", ["s2"]),
["log-1", "s2", "session-3"],
);
});
test("closing a workspace session dissolves the workspace when one terminal remains", () => {
const result = closeSessionWorkspaceLayoutState([workspace(["s1", "s2"])], "ws-1", "s1");
assert.equal(result.dissolvedWorkspaceId, "ws-1");
assert.equal(result.lastRemainingSessionId, "s2");
assert.deepEqual(result.workspaces, []);
assert.deepEqual(
replaceDissolvedWorkspaceTabOrder(
["log-1", result.dissolvedWorkspaceId!, "session-3"],
result.dissolvedWorkspaceId,
result.lastRemainingSessionId ? [result.lastRemainingSessionId] : undefined,
),
["log-1", "s2", "session-3"],
);
});
test("closing the focused session hands focus to a remaining session (#3046)", () => {
const result = closeSessionWorkspaceLayoutState(
[workspace(["s1", "s2", "s3", "s4"])],
"ws-1",
"s1",
);
assert.equal(result.workspaces.length, 1);
assert.equal(result.workspaces[0].focusedSessionId, "s2");
assert.deepEqual(result.workspaces[0].focusSessionOrder, ["s2", "s3", "s4"]);
});
test("closing a non-focused session keeps the focused session", () => {
const result = closeSessionWorkspaceLayoutState(
[workspace(["s1", "s2", "s3", "s4"])],
"ws-1",
"s3",
);
assert.equal(result.workspaces.length, 1);
assert.equal(result.workspaces[0].focusedSessionId, "s1");
assert.deepEqual(result.workspaces[0].focusSessionOrder, ["s1", "s2", "s4"]);
});
test("closing one split pane keeps the other terminal as an orphan tab", () => {
const layoutResult = closeSessionWorkspaceLayoutState(
[workspace(["s1", "s2"])],
"ws-1",
"s1",
);
const nextSessions = applyCloseSessionToSessions(
[session("s1"), session("s2")],
"s1",
layoutResult,
);
assert.deepEqual(nextSessions.map((s) => [s.id, s.workspaceId]), [
["s2", undefined],
]);
assert.equal(
resolveActiveTabAfterCloseSession({
currentActiveTabId: "ws-1",
closedSessionId: "s1",
workspaceId: "ws-1",
layoutResult,
remainingSessions: nextSessions,
preferredTabId: "s-outside",
}),
"s2",
);
});
test("closing the last workspace session does not invent a surviving terminal", () => {
const layoutResult = closeSessionWorkspaceLayoutState(
[workspace(["s1"])],
"ws-1",
"s1",
);
const nextSessions = applyCloseSessionToSessions(
[session("s1")],
"s1",
layoutResult,
);
assert.deepEqual(nextSessions, []);
assert.equal(layoutResult.removedWorkspaceId, "ws-1");
assert.equal(layoutResult.lastRemainingSessionId, undefined);
assert.equal(
resolveActiveTabAfterCloseSession({
currentActiveTabId: "ws-1",
closedSessionId: "s1",
workspaceId: "ws-1",
layoutResult,
remainingSessions: nextSessions,
}),
"vault",
);
});
test("closing the last visible solo tab does not fall back to a hidden MCP session", () => {
const visibleSession: TerminalSession = { ...session("s1", undefined) };
const hiddenSession: TerminalSession = { ...session("s2", undefined), hiddenFromTabs: true };
const layoutResult = closeSessionWorkspaceLayoutState([], undefined, "s1");
const remainingSessions = [hiddenSession];
assert.equal(
resolveActiveTabAfterCloseSession({
currentActiveTabId: visibleSession.id,
closedSessionId: "s1",
workspaceId: undefined,
layoutResult,
remainingSessions,
}),
"vault",
);
});
test("closing a temporary solo tab restores the previously focused tab", () => {
const standalone = (id: string, status: TerminalSession["status"] = "connected"): TerminalSession => ({
...session(id),
workspaceId: undefined,
status,
});
const remainingSessions = [
standalone("s1", "disconnected"),
standalone("s2"),
standalone("s3"),
standalone("s4"),
standalone("s5", "disconnected"),
standalone("s6"),
];
const layoutResult = closeSessionWorkspaceLayoutState([], undefined, "s7");
assert.equal(
resolveActiveTabAfterCloseSession({
currentActiveTabId: "s7",
closedSessionId: "s7",
workspaceId: undefined,
layoutResult,
remainingSessions,
preferredTabId: "s5",
}),
"s5",
);
});
test("closing a temporary solo tab ignores a preferred tab that no longer exists", () => {
const standalone = (id: string): TerminalSession => ({
...session(id),
workspaceId: undefined,
});
const remainingSessions = [standalone("s1"), standalone("s2"), standalone("s6")];
const layoutResult = closeSessionWorkspaceLayoutState([], undefined, "s7");
assert.equal(
resolveActiveTabAfterCloseSession({
currentActiveTabId: "s7",
closedSessionId: "s7",
workspaceId: undefined,
layoutResult,
remainingSessions,
preferredTabId: "s5",
}),
"s6",
);
});
test("closeSessionsState restores the previous tab instead of the newest remaining tab", () => {
const standaloneSession = (id: string): TerminalSession => ({
...session(id),
workspaceId: undefined,
});
const result = closeSessionsState({
sessions: [
standaloneSession("s1"),
standaloneSession("s2"),
standaloneSession("s5"),
standaloneSession("s6"),
standaloneSession("s7"),
],
workspaces: [],
sessionIds: ["s7"],
currentActiveTabId: "s7",
preferredTabId: "s5",
tabOrder: ["s1", "s2", "s5", "s6", "s7"],
});
assert.equal(result.activeTabId, "s5");
assert.deepEqual(result.sessions.map((candidate) => candidate.id), ["s1", "s2", "s5", "s6"]);
});
test("closing several standalone sessions removes the whole batch", () => {
const standaloneSession = (id: string): TerminalSession => ({
...session(id),
workspaceId: undefined,
});
const result = closeSessionsState({
sessions: [standaloneSession("s1"), standaloneSession("s2"), standaloneSession("s3")],
workspaces: [],
sessionIds: ["s1", "s2", "s3"],
currentActiveTabId: "s1",
tabOrder: ["s1", "s2", "s3"],
});
assert.deepEqual(result.sessions, []);
assert.deepEqual(result.workspaces, []);
assert.equal(result.activeTabId, "vault");
});
test("closing a subset of standalone sessions keeps tabs outside the batch", () => {
const standaloneSession = (id: string): TerminalSession => ({
...session(id),
workspaceId: undefined,
});
const result = closeSessionsState({
sessions: [standaloneSession("s1"), standaloneSession("s2"), standaloneSession("s3")],
workspaces: [],
sessionIds: ["s2", "s3"],
currentActiveTabId: "s1",
tabOrder: ["s1", "s2", "s3"],
});
assert.deepEqual(result.sessions.map((candidate) => candidate.id), ["s1"]);
assert.equal(result.activeTabId, undefined);
});