1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 EMPTY_TERMINAL_BUFFER_STATE,
5 type KnownTerminalSession,
6} from "@t3tools/client-runtime/state/terminal";
7import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts";
8
9import { getTerminalLabel } from "@t3tools/shared/terminalLabels";
10
11import {
12 buildTerminalMenuSessions,
13 nextOpenTerminalId,
14 previousLiveTerminalId,
15 resolveProjectScriptTerminalId,
16 type TerminalMenuSession,
17} from "./terminalMenu";
18
19function makeMenuSession(input: {
20 readonly terminalId: string;
21 readonly status: TerminalMenuSession["status"];
22}): TerminalMenuSession {
23 return {
24 terminalId: input.terminalId,
25 cwd: null,
26 status: input.status,
27 hasRunningSubprocess: false,
28 displayLabel: getTerminalLabel(input.terminalId),
29 updatedAt: null,
30 };
31}
32
33function makeKnownSession(input: {
34 readonly terminalId: string;
35 readonly status: KnownTerminalSession["state"]["status"];
36 readonly cwd?: string | null;
37 readonly updatedAt?: string | null;
38}): KnownTerminalSession {
39 return {
40 target: {
41 environmentId: EnvironmentId.make("env-1"),
42 threadId: ThreadId.make("thread-1"),
43 terminalId: input.terminalId,
44 },
45 state: {
46 summary: input.cwd
47 ? {
48 threadId: "thread-1",
49 terminalId: input.terminalId,
50 cwd: input.cwd,
51 worktreePath: input.cwd,
52 status: input.status === "closed" ? "error" : input.status,
53 pid: input.status === "running" ? 123 : null,
54 exitCode: null,
55 exitSignal: null,
56 hasRunningSubprocess: false,
57 label: getTerminalLabel(input.terminalId),
58 updatedAt: input.updatedAt ?? "2026-04-15T20:00:00.000Z",
59 }
60 : null,
61 output: EMPTY_TERMINAL_BUFFER_STATE.output,
62 status: input.status,
63 error: null,
64 hasRunningSubprocess: false,
65 updatedAt: input.updatedAt ?? "2026-04-15T20:00:00.000Z",
66 version: 1,
67 lifecycleVersion: 1,
68 },
69 };
70}
71
72describe("buildTerminalMenuSessions", () => {
73 it("only lists server-known sessions that are running or starting (plus current)", () => {
74 expect(
75 buildTerminalMenuSessions({
76 knownSessions: [
77 makeKnownSession({
78 terminalId: "term-3",
79 status: "running",
80 cwd: "/workspace/feature",
81 updatedAt: "2026-04-15T20:05:00.000Z",
82 }),
83 makeKnownSession({
84 terminalId: "term-2",
85 status: "exited",
86 cwd: "/workspace/exited",
87 updatedAt: "2026-04-15T20:06:00.000Z",
88 }),
89 ],
90 workspaceRoot: "/workspace/root",
91 }),
92 ).toEqual([
93 {
94 terminalId: "term-3",
95 cwd: "/workspace/feature",
96 status: "running",
97 hasRunningSubprocess: false,
98 displayLabel: "Terminal 3",
99 updatedAt: "2026-04-15T20:05:00.000Z",
100 },
101 ]);
102 });
103
104 it("keeps the current terminal visible even if it is no longer running", () => {
105 expect(
106 buildTerminalMenuSessions({
107 knownSessions: [],
108 workspaceRoot: "/workspace/root",
109 currentSession: {
110 terminalId: "term-4",
111 cwd: "/workspace/exited",
112 status: "exited",
113 hasRunningSubprocess: false,
114 displayLabel: "Terminal 4",
115 updatedAt: "2026-04-15T20:07:00.000Z",
116 },
117 }),
118 ).toEqual([
119 {
120 terminalId: "term-4",
121 cwd: "/workspace/exited",
122 status: "exited",
123 hasRunningSubprocess: false,
124 displayLabel: "Terminal 4",
125 updatedAt: "2026-04-15T20:07:00.000Z",
126 },
127 ]);
128 });
129});
130
131describe("nextOpenTerminalId", () => {
132 it("allocates separate terminals on repeated visits without readable metadata", () => {
133 const first = nextOpenTerminalId({
134 listedTerminalIds: [],
135 uniqueSuffix: "783c91cc-a413-47c7-8312-c2a5a1f05e40",
136 });
137 const nextVisit = nextOpenTerminalId({
138 listedTerminalIds: [],
139 uniqueSuffix: "102315fc-ceef-45c4-b978-c4d4947d3c26",
140 });
141 expect(first).not.toBe(DEFAULT_TERMINAL_ID);
142 expect(nextVisit).not.toBe(first);
143 });
144
145 it("matches nextTerminalId when not on a terminal route", () => {
146 expect(nextOpenTerminalId({ listedTerminalIds: [] })).toBe(DEFAULT_TERMINAL_ID);
147 expect(nextOpenTerminalId({ listedTerminalIds: [DEFAULT_TERMINAL_ID] })).toBe("term-2");
148 });
149
150 it("avoids the mounted primary tab when the session list is still empty", () => {
151 expect(
152 nextOpenTerminalId({
153 listedTerminalIds: [],
154 activeRouteTerminalId: DEFAULT_TERMINAL_ID,
155 }),
156 ).toBe("term-2");
157 });
158
159 it("does not double-count when the route id is already listed", () => {
160 expect(
161 nextOpenTerminalId({
162 listedTerminalIds: [DEFAULT_TERMINAL_ID],
163 activeRouteTerminalId: DEFAULT_TERMINAL_ID,
164 }),
165 ).toBe("term-2");
166 });
167});
168
169describe("previousLiveTerminalId", () => {
170 it("returns null when no other live session remains", () => {
171 expect(
172 previousLiveTerminalId({
173 sessions: [
174 makeMenuSession({ terminalId: "term-2", status: "exited" }),
175 makeMenuSession({ terminalId: "term-3", status: "closed" }),
176 ],
177 exitedTerminalId: "term-2",
178 }),
179 ).toBe(null);
180 });
181
182 it("prefers the nearest live session below the exited id", () => {
183 expect(
184 previousLiveTerminalId({
185 sessions: [
186 makeMenuSession({ terminalId: DEFAULT_TERMINAL_ID, status: "running" }),
187 makeMenuSession({ terminalId: "term-2", status: "running" }),
188 makeMenuSession({ terminalId: "term-3", status: "exited" }),
189 makeMenuSession({ terminalId: "term-4", status: "running" }),
190 ],
191 exitedTerminalId: "term-3",
192 }),
193 ).toBe("term-2");
194 });
195
196 it("falls back to the nearest live session above when the exited id was lowest", () => {
197 expect(
198 previousLiveTerminalId({
199 sessions: [
200 makeMenuSession({ terminalId: DEFAULT_TERMINAL_ID, status: "exited" }),
201 makeMenuSession({ terminalId: "term-2", status: "starting" }),
202 makeMenuSession({ terminalId: "term-4", status: "running" }),
203 ],
204 exitedTerminalId: DEFAULT_TERMINAL_ID,
205 }),
206 ).toBe("term-2");
207 });
208
209 it("ignores dead sessions when picking the fallback", () => {
210 expect(
211 previousLiveTerminalId({
212 sessions: [
213 makeMenuSession({ terminalId: DEFAULT_TERMINAL_ID, status: "running" }),
214 makeMenuSession({ terminalId: "term-2", status: "exited" }),
215 makeMenuSession({ terminalId: "term-3", status: "exited" }),
216 ],
217 exitedTerminalId: "term-3",
218 }),
219 ).toBe(DEFAULT_TERMINAL_ID);
220 });
221});
222
223describe("resolveProjectScriptTerminalId", () => {
224 it("never targets an unseen default terminal when metadata is unavailable", () => {
225 const terminalId = resolveProjectScriptTerminalId({
226 existingTerminalIds: [],
227 hasRunningTerminal: false,
228 uniqueSuffix: "783c91cc-a413-47c7-8312-c2a5a1f05e40",
229 });
230 expect(terminalId).not.toBe(DEFAULT_TERMINAL_ID);
231 expect(getTerminalLabel(terminalId)).toBe("Terminal 1");
232 });
233
234 it("reuses the default shell when no terminal is running", () => {
235 expect(
236 resolveProjectScriptTerminalId({
237 existingTerminalIds: [DEFAULT_TERMINAL_ID],
238 hasRunningTerminal: false,
239 }),
240 ).toBe(DEFAULT_TERMINAL_ID);
241 });
242
243 it("opens a new terminal when a shell is already running", () => {
244 expect(
245 resolveProjectScriptTerminalId({
246 existingTerminalIds: [DEFAULT_TERMINAL_ID, "term-2", "term-4"],
247 hasRunningTerminal: true,
248 }),
249 ).toBe("term-3");
250 });
251});