1import { describe, expect, it, vi } from "vite-plus/test";
2import {
3 applyTerminalAttachStreamEvent,
4 combineTerminalSessionState,
5 EMPTY_TERMINAL_BUFFER_STATE,
6 INITIAL_TERMINAL_OUTPUT_CURSOR,
7 nextTerminalAttachSeedState,
8} from "@t3tools/client-runtime/state/terminal";
9
10import {
11 shouldHandleTerminalExit,
12 synchronizeTerminalOutput,
13 synchronizeTerminalSession,
14} from "./ThreadTerminalDrawer";
15
16describe("terminal selection actions", () => {
17 it("handles an exit that lands while the terminal surface is still loading", () => {
18 expect(shouldHandleTerminalExit("exited", "running", false, 1)).toBe(true);
19 expect(shouldHandleTerminalExit("exited", "exited", false, 1)).toBe(false);
20 expect(shouldHandleTerminalExit("closed", "running", true, 1)).toBe(false);
21 });
22});
23
24it("retains visible output and selection until a replacement subscription receives its snapshot", () => {
25 const terminal = {
26 resetAndWrite: vi.fn(),
27 write: vi.fn(),
28 getSelection: vi.fn(() => "selected"),
29 getSelectionCellCount: vi.fn(() => 8),
30 clearSelection: vi.fn(),
31 };
32 const snapshot = {
33 threadId: "thread",
34 terminalId: "term-1",
35 cwd: "/repo",
36 worktreePath: null,
37 status: "running" as const,
38 pid: 123,
39 history: "host output",
40 exitCode: null,
41 exitSignal: null,
42 label: "Terminal 1",
43 updatedAt: "2026-09-05T00:00:00.000Z",
44 };
45 const attached = applyTerminalAttachStreamEvent(nextTerminalAttachSeedState(), {
46 type: "snapshot",
47 snapshot,
48 });
49 const cursor = synchronizeTerminalOutput(terminal, attached, INITIAL_TERMINAL_OUTPUT_CURSOR);
50 expect(terminal.resetAndWrite).toHaveBeenLastCalledWith("host output");
51 terminal.resetAndWrite.mockClear();
52 terminal.clearSelection.mockClear();
53
54 const pending = combineTerminalSessionState(
55 { ...snapshot, hasRunningSubprocess: false },
56 EMPTY_TERMINAL_BUFFER_STATE,
57 );
58 expect(pending.status).toBe("running");
59 expect(synchronizeTerminalOutput(terminal, pending, cursor)).toBe(cursor);
60 expect(terminal.resetAndWrite).not.toHaveBeenCalled();
61 expect(terminal.write).not.toHaveBeenCalled();
62 expect(terminal.clearSelection).not.toHaveBeenCalled();
63
64 const observed = applyTerminalAttachStreamEvent(nextTerminalAttachSeedState(), {
65 type: "snapshot",
66 snapshot: { ...snapshot, history: "host output\nobserved output" },
67 });
68 const observedCursor = synchronizeTerminalOutput(terminal, observed, cursor);
69 expect(terminal.resetAndWrite).toHaveBeenLastCalledWith("host output\nobserved output");
70
71 const cleared = applyTerminalAttachStreamEvent(observed, {
72 type: "cleared",
73 threadId: "thread",
74 terminalId: "term-1",
75 });
76 synchronizeTerminalOutput(terminal, cleared, observedCursor);
77 expect(terminal.resetAndWrite).toHaveBeenLastCalledWith("");
78});
79
80it("reports an attach failure that leaves the output unchanged", () => {
81 const terminal = {
82 resetAndWrite: vi.fn(),
83 write: vi.fn(),
84 getSelection: vi.fn(() => "selected"),
85 getSelectionCellCount: vi.fn(() => 8),
86 clearSelection: vi.fn(),
87 };
88 const previous = { ...EMPTY_TERMINAL_BUFFER_STATE, error: null };
89 const failed = { ...previous, error: "Terminal attach failed" };
90
91 expect(
92 synchronizeTerminalSession(terminal, previous, failed, INITIAL_TERMINAL_OUTPUT_CURSOR),
93 ).toBe(INITIAL_TERMINAL_OUTPUT_CURSOR);
94 expect(terminal.write).toHaveBeenCalledWith("\r\n[terminal] Terminal attach failed\r\n");
95 expect(terminal.resetAndWrite).not.toHaveBeenCalled();
96 expect(terminal.getSelection).not.toHaveBeenCalled();
97 expect(terminal.getSelectionCellCount).not.toHaveBeenCalled();
98 expect(terminal.clearSelection).not.toHaveBeenCalled();
99});