1import { EnvironmentId, ThreadId, type TerminalSummary } from "@t3tools/contracts";
2import { act, createElement, useEffect } from "react";
3import { createRoot, type Root } from "react-dom/client";
4import { afterEach, beforeEach, expect, it, vi } from "vite-plus/test";
5
6import { useKnownTerminalSessions } from "./terminalSessions";
7
8const state = vi.hoisted(() => ({
9 canRead: true,
10 data: null as ReadonlyArray<TerminalSummary> | null,
11 error: null as string | null,
12}));
13
14vi.mock("./session", () => ({ useEnvironmentScope: () => state.canRead }));
15vi.mock("./terminal", () => ({ terminalEnvironment: { metadata: () => ({}) } }));
16vi.mock("./query", () => ({
17 useEnvironmentQuery: (atom: unknown) =>
18 atom === null ? { data: null, error: null } : { data: state.data, error: state.error },
19}));
20
21let root: Root;
22let current: ReturnType<typeof useKnownTerminalSessions>;
23const target = {
24 environmentId: EnvironmentId.make("secondary"),
25 threadId: ThreadId.make("thread"),
26};
27const terminal: TerminalSummary = {
28 threadId: "thread",
29 terminalId: "term-1",
30 cwd: "/repo",
31 worktreePath: null,
32 status: "running",
33 pid: 123,
34 exitCode: null,
35 exitSignal: null,
36 hasRunningSubprocess: true,
37 label: "Busy process",
38 updatedAt: "2026-09-05T00:00:00.000Z",
39};
40
41function Probe() {
42 const sessions = useKnownTerminalSessions(target);
43 useEffect(() => {
44 current = sessions;
45 }, [sessions]);
46 return null;
47}
48
49function render() {
50 return act(() => root.render(createElement(Probe)));
51}
52
53beforeEach(() => {
54 state.canRead = true;
55 state.data = null;
56 state.error = null;
57 const document = { nodeType: 9, addEventListener() {}, removeEventListener() {} };
58 const container = {
59 nodeType: 1,
60 tagName: "DIV",
61 namespaceURI: "http://www.w3.org/1999/xhtml",
62 ownerDocument: document,
63 addEventListener() {},
64 removeEventListener() {},
65 };
66 vi.stubGlobal("document", document);
67 vi.stubGlobal("window", { document, HTMLIFrameElement: EventTarget });
68 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
69 root = createRoot(container as unknown as HTMLElement);
70});
71
72afterEach(async () => {
73 await act(() => root.unmount());
74 vi.unstubAllGlobals();
75});
76
77it("distinguishes missing metadata from an authoritative empty list across scope changes", async () => {
78 await render();
79 expect(current).toBeNull();
80
81 state.data = [terminal];
82 await render();
83 expect(current?.[0]).toMatchObject({
84 target: { environmentId: "secondary", terminalId: "term-1" },
85 state: { hasRunningSubprocess: true },
86 });
87
88 state.canRead = false;
89 await render();
90 expect(current).toBeNull();
91
92 state.canRead = true;
93 state.data = [];
94 await render();
95 expect(current).toEqual([]);
96});
97
98it("does not reuse cached terminal metadata after a subscription failure", async () => {
99 state.data = [terminal];
100 await render();
101 state.error = "Access denied";
102 await render();
103 expect(current).toBeNull();
104});