1import { RegistryContext } from "@effect/atom-react";
2import {
3 AuthSettingsWriteScope,
4 DEFAULT_SERVER_SETTINGS,
5 EnvironmentId,
6 type AuthEnvironmentScope,
7 type AuthSessionState,
8 type ServerSettingsPatch,
9} from "@t3tools/contracts";
10import * as Cause from "effect/Cause";
11import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
12import { act } from "react";
13import { create, type ReactTestRenderer } from "react-test-renderer";
14import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
15
16type SessionResult = AsyncResult.AsyncResult<AuthSessionState, Error>;
17const state = vi.hoisted(() => ({
18 registry: null as AtomRegistry.AtomRegistry | null,
19 sessions: new Map<EnvironmentId, Atom.Writable<SessionResult>>(),
20 environments: [] as Array<{
21 environmentId: EnvironmentId;
22 label: string;
23 connection: { phase: "connected" | "disconnected" };
24 serverConfig: {
25 environment: { capabilities: { threadAutoSettlement: boolean } };
26 settings: typeof DEFAULT_SERVER_SETTINGS;
27 };
28 }>,
29 persist: vi.fn(),
30 toast: vi.fn(),
31}));
32
33vi.mock("~/connection/runtime", () => ({ connectionAtomRuntime: undefined }));
34vi.mock("@t3tools/client-runtime/state/session", () => ({
35 createEnvironmentSessionAtoms: () => ({
36 sessionStateAtom: (id: EnvironmentId) => state.sessions.get(id)!,
37 }),
38}));
39vi.mock("~/rpc/atomRegistry", () => ({
40 get appAtomRegistry() {
41 return state.registry;
42 },
43}));
44vi.mock("~/state/environments", () => ({
45 useEnvironments: () => ({ environments: state.environments }),
46 usePrimaryEnvironment: () =>
47 state.environments.find((environment) => environment.environmentId === primaryId) ?? null,
48}));
49vi.mock("~/state/server", () => ({
50 serverEnvironment: { updateSettings: Symbol("updateSettings") },
51 primaryServerSettingsAtom: undefined,
52}));
53vi.mock("~/state/use-atom-command", () => ({ useAtomCommand: () => state.persist }));
54vi.mock("~/components/ui/toast", () => ({ toastManager: { add: state.toast } }));
55vi.mock("~/themePalette", () => ({}));
56vi.mock("./useTheme", () => ({}));
57
58import { useUpdatePrimarySettings } from "./useSettings";
59
60const primaryId = EnvironmentId.make("primary");
61const remoteId = EnvironmentId.make("remote");
62const patch = { sidebarAutoSettleOnMerge: false } satisfies ServerSettingsPatch;
63const session = (scopes: ReadonlyArray<AuthEnvironmentScope>): AuthSessionState => ({
64 authenticated: true,
65 scopes,
66 auth: {
67 policy: "remote-reachable",
68 bootstrapMethods: ["one-time-token"],
69 sessionMethods: ["bearer-access-token"],
70 sessionCookieName: "t3_session",
71 },
72});
73let renderer: ReactTestRenderer | undefined;
74
75function SettingsEditor() {
76 const updateSettings = useUpdatePrimarySettings();
77 return <button onClick={() => updateSettings(patch)}>Save shared setting</button>;
78}
79
80function saveSharedSettings() {
81 renderer!.root.findByType("button").props.onClick();
82}
83
84async function mountEditor() {
85 await act(() => {
86 renderer = create(
87 <RegistryContext.Provider value={state.registry!}>
88 <SettingsEditor />
89 </RegistryContext.Provider>,
90 );
91 });
92}
93
94beforeEach(() => {
95 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
96 state.registry = AtomRegistry.make();
97 state.sessions.clear();
98 state.sessions.set(
99 primaryId,
100 Atom.make<SessionResult>(AsyncResult.success(session([AuthSettingsWriteScope]))),
101 );
102 state.sessions.set(remoteId, Atom.make<SessionResult>(AsyncResult.initial()));
103 state.environments = [primaryId, remoteId].map((environmentId) => ({
104 environmentId,
105 label: environmentId,
106 connection: { phase: "connected" },
107 serverConfig: {
108 environment: { capabilities: { threadAutoSettlement: true } },
109 settings: DEFAULT_SERVER_SETTINGS,
110 },
111 }));
112 state.persist.mockReset();
113 state.persist.mockResolvedValue(AsyncResult.success(DEFAULT_SERVER_SETTINGS));
114 state.toast.mockReset();
115});
116
117afterEach(async () => {
118 await act(() => renderer?.unmount());
119 renderer = undefined;
120 state.registry?.dispose();
121 vi.unstubAllGlobals();
122});
123
124describe("shared settings writes", () => {
125 it("waits for the remaining target before reporting a partial save", async () => {
126 let finishRemote = () => {};
127 const remote = new Promise((resolve) => {
128 finishRemote = () => resolve(AsyncResult.success(DEFAULT_SERVER_SETTINGS));
129 });
130 state.persist.mockResolvedValueOnce(
131 AsyncResult.failure(Cause.fail(new Error("Permission denied"))),
132 );
133 state.persist.mockReturnValueOnce(remote);
134 await mountEditor();
135 await act(async () => saveSharedSettings());
136 expect(state.toast).not.toHaveBeenCalled();
137 await act(async () => finishRemote());
138 expect(state.toast).toHaveBeenCalledExactlyOnceWith({
139 type: "error",
140 title: "Setting saved on some environments",
141 description: "Could not save on primary: Permission denied\nSaved on remote.",
142 });
143 });
144
145 it("keeps a single-target failure specific to that environment", async () => {
146 state.registry!.set(state.sessions.get(remoteId)!, AsyncResult.success(session([])));
147 state.persist.mockResolvedValueOnce(
148 AsyncResult.failure(Cause.fail(new Error("Permission denied"))),
149 );
150 await mountEditor();
151 await act(async () => saveSharedSettings());
152 expect(state.toast).toHaveBeenCalledExactlyOnceWith({
153 type: "error",
154 title: "Setting not saved",
155 description: "Could not save on primary: Permission denied",
156 });
157 });
158
159 it("names the failed environment when a server rejects a save after the grant check", async () => {
160 state.persist.mockResolvedValueOnce(AsyncResult.success(DEFAULT_SERVER_SETTINGS));
161 state.persist.mockResolvedValueOnce(
162 AsyncResult.failure(Cause.fail(new Error("Permission denied"))),
163 );
164 await mountEditor();
165 await act(async () => saveSharedSettings());
166 expect(state.toast).toHaveBeenCalledExactlyOnceWith({
167 type: "error",
168 title: "Setting saved on some environments",
169 description: "Could not save on remote: Permission denied\nSaved on primary.",
170 });
171 });
172
173 it("dispatches to the primary and connected remote before the remote grant finishes loading", async () => {
174 await mountEditor();
175 saveSharedSettings();
176
177 expect(state.persist.mock.calls).toEqual([
178 [{ environmentId: primaryId, input: { patch } }],
179 [{ environmentId: remoteId, input: { patch } }],
180 ]);
181 expect(state.registry!.get(state.sessions.get(remoteId)!)).toMatchObject({ _tag: "Initial" });
182 });
183
184 it.each([
185 ["denied", () => AsyncResult.success(session([]))],
186 ["denied while refreshing", () => AsyncResult.waiting(AsyncResult.success(session([])))],
187 [
188 "failed",
189 () => AsyncResult.failure<AuthSessionState, Error>(Cause.fail(new Error("session rejected"))),
190 ],
191 ] as const)("skips a remote whose grant is %s", async (_label, result) => {
192 state.registry!.set(state.sessions.get(remoteId)!, result());
193 await mountEditor();
194 saveSharedSettings();
195
196 expect(state.persist).toHaveBeenCalledExactlyOnceWith({
197 environmentId: primaryId,
198 input: { patch },
199 });
200 });
201
202 it.each(["disconnected", "unsupported"] as const)(
203 "skips a %s remote even with a cold grant",
204 async (condition) => {
205 const remote = state.environments[1]!;
206 if (condition === "disconnected") remote.connection.phase = "disconnected";
207 else remote.serverConfig.environment.capabilities.threadAutoSettlement = false;
208 await mountEditor();
209 saveSharedSettings();
210
211 expect(state.persist).toHaveBeenCalledExactlyOnceWith({
212 environmentId: primaryId,
213 input: { patch },
214 });
215 },
216 );
217
218 it.each(["primary", "hosted"] as const)(
219 "rechecks a cold remote grant that resolves to denied after the %s handler renders",
220 async (mode) => {
221 if (mode === "hosted") {
222 state.environments = state.environments.filter(
223 (environment) => environment.environmentId !== primaryId,
224 );
225 }
226 await mountEditor();
227 const previousUpdate = renderer!.root.findByType("button").props.onClick as () => void;
228 await act(() => {
229 state.registry!.set(state.sessions.get(remoteId)!, AsyncResult.success(session([])));
230 });
231 previousUpdate();
232
233 if (mode === "primary") {
234 expect(state.persist).toHaveBeenCalledExactlyOnceWith({
235 environmentId: primaryId,
236 input: { patch },
237 });
238 expect(state.toast).not.toHaveBeenCalled();
239 } else {
240 expect(state.persist).not.toHaveBeenCalled();
241 expect(state.toast).toHaveBeenCalledExactlyOnceWith({
242 type: "warning",
243 title: "Setting not saved",
244 description: "This connection lacks permission to change settings on remote.",
245 });
246 }
247 },
248 );
249});