1import {
2 DEFAULT_CLIENT_SETTINGS,
3 type PreviewSessionSnapshot,
4 type ScopedThreadRef,
5} from "@t3tools/contracts";
6import * as Cause from "effect/Cause";
7import { AsyncResult } from "effect/reactivity";
8import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
9
10import { type ClosedViewEntry, useClosedViewStore } from "./closedViewStore";
11import { __setClientSettingsForTests } from "./hooks/useSettings";
12import { readThreadPreviewState, resetPreviewStateForTests } from "./previewStateStore";
13import {
14 planNextReopen,
15 pullRequestsSearchForRestore,
16 type ReopenOwnerState,
17 reopenClosedView,
18} from "./reopenClosedView";
19import {
20 selectThreadRightPanelState,
21 type ThreadRightPanelState,
22 useRightPanelStore,
23} from "./rightPanelStore";
24
25const threadRef = {
26 environmentId: "local" as ScopedThreadRef["environmentId"],
27 threadId: "thread-1" as ScopedThreadRef["threadId"],
28};
29const snapshot: PreviewSessionSnapshot = {
30 threadId: threadRef.threadId,
31 tabId: "old-tab",
32 navStatus: { _tag: "Success", url: "https://example.com", title: "Example" },
33 canGoBack: false,
34 canGoForward: false,
35 viewport: { _tag: "freeform", width: 1200, height: 800 },
36 profileId: "work",
37 updatedAt: "2026-09-22T12:00:00.000Z",
38};
39
40beforeEach(() => {
41 __setClientSettingsForTests(DEFAULT_CLIENT_SETTINGS);
42 resetPreviewStateForTests();
43 useClosedViewStore.setState({ entries: [] });
44 useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} });
45});
46
47describe("reopenClosedView", () => {
48 it("restores a file tab at its line without creating a browser session", async () => {
49 const openPreview = vi.fn();
50 const reopened = await reopenClosedView(
51 {
52 kind: "panel-tab",
53 threadRef,
54 surface: {
55 kind: "file",
56 id: "file:src/app.ts",
57 relativePath: "src/app.ts",
58 revealLine: 18,
59 revealRequestId: 1,
60 },
61 },
62 { openPreview, workspaceAvailable: true },
63 );
64 expect(reopened).toBe(true);
65 const state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef);
66 expect(state.isOpen).toBe(true);
67 expect(state.surfaces).toMatchObject([{ id: "file:src/app.ts", revealLine: 18 }]);
68 expect(openPreview).not.toHaveBeenCalled();
69 });
70
71 it("does not reopen workspace tabs without an available project", async () => {
72 const options = {
73 openPreview: vi.fn(),
74 workspaceAvailable: false,
75 };
76 for (const surface of [
77 { kind: "files", id: "files" },
78 {
79 kind: "file",
80 id: "file:src/app.ts",
81 relativePath: "src/app.ts",
82 revealLine: null,
83 revealRequestId: 0,
84 },
85 ] as const) {
86 expect(await reopenClosedView({ kind: "panel-tab", threadRef, surface }, options)).toBe(
87 false,
88 );
89 }
90 expect(
91 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces,
92 ).toEqual([]);
93 });
94
95 it("recreates a browser tab with saved URL, viewport and profile, then selects its new ID", async () => {
96 const openPreview = vi.fn(async () => AsyncResult.success({ ...snapshot, tabId: "new-tab" }));
97 const result = await reopenClosedView(
98 { kind: "browser", threadRef, snapshot },
99 { openPreview, workspaceAvailable: false },
100 );
101 expect(result).toBe(true);
102 expect(openPreview).toHaveBeenCalledWith({
103 environmentId: threadRef.environmentId,
104 input: {
105 threadId: threadRef.threadId,
106 url: "https://example.com",
107 viewport: snapshot.viewport,
108 profileId: "work",
109 },
110 });
111 expect(readThreadPreviewState(threadRef).snapshot?.tabId).toBe("new-tab");
112 expect(
113 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef)
114 .activeSurfaceId,
115 ).toBe("browser:new-tab");
116 });
117
118 it("leaves the panel unchanged when a browser resource fails to reopen", async () => {
119 const result = await reopenClosedView(
120 { kind: "browser", threadRef, snapshot },
121 {
122 openPreview: async () => AsyncResult.failure(Cause.fail(new Error("offline"))),
123 workspaceAvailable: false,
124 },
125 );
126 expect(result).toBe(false);
127 expect(
128 selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, threadRef).surfaces,
129 ).toEqual([]);
130 });
131});
132
133describe("planNextReopen", () => {
134 const entries: ClosedViewEntry[] = [
135 { id: "a", kind: "panel-tab", threadRef, surface: { kind: "diff", id: "diff" } },
136 { id: "b", kind: "panel-tab", threadRef, surface: { kind: "files", id: "files" } },
137 ];
138 const known: ReopenOwnerState = {
139 environmentKnown: true,
140 catalogReady: true,
141 ownerExists: true,
142 shellLive: true,
143 panel: { isOpen: false, activeSurfaceId: null, surfaces: [] },
144 };
145 const openDiff: ThreadRightPanelState = {
146 isOpen: true,
147 activeSurfaceId: "diff",
148 surfaces: [{ kind: "diff", id: "diff" }],
149 };
150
151 it.each([
152 ["restores the newest entry", [known, known], [], "a"],
153 [
154 "drops a removed environment once the catalog is ready",
155 [{ ...known, environmentKnown: false }, known],
156 ["a"],
157 "b",
158 ],
159 [
160 "waits for the catalog before dropping",
161 [{ ...known, environmentKnown: false, catalogReady: false }, known],
162 [],
163 null,
164 ],
165 [
166 "skips but keeps a thread deleted from a live shell",
167 [{ ...known, ownerExists: false }, known],
168 [],
169 "b",
170 ],
171 [
172 "waits when a cached shell lacks the thread",
173 [{ ...known, ownerExists: false, shellLive: false }, known],
174 [],
175 null,
176 ],
177 ["drops a tab that is already open", [{ ...known, panel: openDiff }, known], ["a"], "b"],
178 [
179 "restores a tab whose panel is hidden",
180 [{ ...known, panel: { ...openDiff, isOpen: false } }],
181 [],
182 "a",
183 ],
184 ] as const)("%s", (_name, owners, dropped, restored) => {
185 const plan = planNextReopen(
186 entries.slice(0, owners.length),
187 (e) => owners[entries.indexOf(e)]!,
188 );
189 expect(plan.drop.map((e) => e.id)).toEqual(dropped);
190 expect(plan.restore?.id ?? null).toBe(restored);
191 });
192});
193
194describe("pullRequestsSearchForRestore", () => {
195 it("replaces the selection and keeps list filters", () => {
196 const search = pullRequestsSearchForRestore(
197 {
198 involvement: "reviewing",
199 state: "open",
200 q: "mine",
201 host: "filter.example.com",
202 selectedHost: "old",
203 number: 1,
204 },
205 {
206 kind: "pull-request",
207 id: "pull-request:example",
208 repository: "owner/repo",
209 number: 42,
210 projectId: "project-1",
211 environmentId: "remote",
212 host: "github.example.com",
213 },
214 );
215 expect(search).toEqual({
216 involvement: "reviewing",
217 state: "open",
218 q: "mine",
219 host: "filter.example.com",
220 repository: "owner/repo",
221 number: 42,
222 selectedProjectId: "project-1",
223 selectedHost: "github.example.com",
224 selectedEnvironmentId: "remote",
225 });
226 });
227
228 it("clears a stale selection and fills default filters when nothing is selected", () => {
229 expect(pullRequestsSearchForRestore({ repository: "old/repo", number: 7 }, null)).toEqual({
230 involvement: "all",
231 state: "open",
232 });
233 });
234});