1import { RegistryContext } from "@effect/atom-react";
2import {
3 DEFAULT_SERVER_SETTINGS,
4 EnvironmentId,
5 ProviderInstanceId,
6 type ServerSettings,
7 type ServerSettingsPatch,
8} from "@t3tools/contracts";
9import * as Cause from "effect/Cause";
10import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
11import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
12import { act } from "react";
13import { create, type ReactTestRenderer } from "react-test-renderer";
14import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
15
16const state = vi.hoisted(() => ({
17 settings: new Map<EnvironmentId, Atom.Writable<ServerSettings | null>>(),
18 persist: vi.fn(),
19}));
20
21vi.mock("~/state/server", () => ({
22 serverEnvironment: {
23 settingsValueAtom: (id: EnvironmentId) => state.settings.get(id)!,
24 updateSettings: Symbol("updateSettings"),
25 },
26 primaryServerSettingsAtom: undefined,
27}));
28vi.mock("~/state/use-atom-command", () => ({ useAtomCommand: () => state.persist }));
29vi.mock("~/localApi", () => ({
30 ensureLocalApi: () => ({ persistence: { getClientSettings: async () => null } }),
31}));
32
33import { useToggleEnvironmentModelFavorite } from "./useSettings";
34
35const primaryId = EnvironmentId.make("primary");
36const remoteId = EnvironmentId.make("remote");
37const provider = ProviderInstanceId.make("codex_work");
38const model = "gpt-6.1-sol";
39const initialSettings = {
40 ...DEFAULT_SERVER_SETTINGS,
41 favorites: [],
42 providerModelPreferences: {},
43} satisfies ServerSettings;
44let registry: AtomRegistry.AtomRegistry;
45let renderer: ReactTestRenderer | undefined;
46let durable: Map<EnvironmentId, ServerSettings>;
47let writes: Array<{
48 environmentId: EnvironmentId;
49 patch: ServerSettingsPatch;
50 finish: (success?: boolean) => void;
51}>;
52
53function FavoriteEditor({ environmentId }: { environmentId: EnvironmentId }) {
54 const toggle = useToggleEnvironmentModelFavorite(environmentId);
55 return <button onClick={() => toggle(provider, model)}>Toggle favorite</button>;
56}
57
58function editor(environmentId: EnvironmentId) {
59 return (
60 <RegistryContext.Provider value={registry}>
61 <FavoriteEditor environmentId={environmentId} />
62 </RegistryContext.Provider>
63 );
64}
65
66async function mount(environmentId = primaryId) {
67 await act(() => {
68 renderer = create(editor(environmentId));
69 });
70}
71
72function click(): Promise<void> {
73 return renderer!.root.findByType("button").props.onClick();
74}
75
76beforeEach(() => {
77 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
78 registry = AtomRegistry.make();
79 state.settings.clear();
80 durable = new Map();
81 writes = [];
82 for (const id of [primaryId, remoteId]) {
83 state.settings.set(id, Atom.make<ServerSettings | null>(initialSettings));
84 durable.set(id, initialSettings);
85 }
86 state.persist.mockReset().mockImplementation(
87 ({
88 environmentId,
89 input,
90 }: {
91 environmentId: EnvironmentId;
92 input: { patch: ServerSettingsPatch };
93 }) =>
94 new Promise((resolve) => {
95 writes.push({
96 environmentId,
97 patch: input.patch,
98 finish: (success = true) => {
99 if (!success) {
100 resolve(AsyncResult.failure(Cause.fail(new Error("save failed"))));
101 return;
102 }
103 const settings = applyServerSettingsPatch(durable.get(environmentId)!, input.patch);
104 durable.set(environmentId, settings);
105 resolve(AsyncResult.success(settings));
106 },
107 });
108 }),
109 );
110});
111
112afterEach(async () => {
113 await act(() => renderer?.unmount());
114 renderer = undefined;
115 registry.dispose();
116 vi.unstubAllGlobals();
117});
118
119describe("environment favorite toggles", () => {
120 it.each([false, true])(
121 "toggles twice before any server echo when initially favorited=%s",
122 async (initiallyFavorited) => {
123 const favorites = initiallyFavorited ? [{ provider, model }] : [];
124 const settings = { ...initialSettings, favorites };
125 registry.set(state.settings.get(primaryId)!, settings);
126 durable.set(primaryId, settings);
127 await mount();
128 const first = click();
129 const second = click();
130 // Preparing the patch is asynchronous even when migration has already completed.
131 await act(async () => {});
132 expect(writes.map((write) => write.patch.setModelFavorites?.[0]?.favorite)).toEqual([
133 !initiallyFavorited,
134 initiallyFavorited,
135 ]);
136 writes[0]!.finish();
137 await first;
138 writes[1]!.finish();
139 await second;
140 expect(durable.get(primaryId)!.favorites).toEqual(favorites);
141 },
142 );
143
144 it("keeps the newest pending intent when an earlier write completes", async () => {
145 await mount();
146 const first = click();
147 const second = click();
148 await act(async () => {});
149 writes[0]!.finish();
150 await first;
151 const third = click();
152 await act(async () => {});
153 expect(writes.map((write) => write.patch.setModelFavorites?.[0]?.favorite)).toEqual([
154 true,
155 false,
156 true,
157 ]);
158 writes[1]!.finish();
159 await second;
160 writes[2]!.finish();
161 await third;
162 expect(durable.get(primaryId)!.favorites).toEqual([{ provider, model }]);
163 });
164
165 it("clears failed intent so a retry still adds the favorite", async () => {
166 await mount();
167 const failed = click();
168 await act(async () => {});
169 writes[0]!.finish(false);
170 await failed;
171 const retry = click();
172 await act(async () => {});
173 writes[1]!.finish();
174 await retry;
175 expect(durable.get(primaryId)!.favorites).toEqual([{ provider, model }]);
176 });
177
178 it("toggles the acknowledged choice when the RPC reply precedes its server echo", async () => {
179 await mount();
180 const first = click();
181 await act(async () => {});
182 writes[0]!.finish();
183 await first;
184 expect(registry.get(state.settings.get(primaryId)!)!.favorites).toEqual([]);
185 const second = click();
186 await act(async () => {});
187 writes[1]!.finish();
188 await second;
189 expect(durable.get(primaryId)!.favorites).toEqual([]);
190 });
191
192 it("uses a later remote change instead of a cached acknowledged choice", async () => {
193 await mount();
194 const first = click();
195 await act(async () => {});
196 writes[0]!.finish();
197 await first;
198 // Another client removed the favorite after this client's add was acknowledged.
199 const remoteSettings = { ...initialSettings, favorites: [] };
200 durable.set(primaryId, remoteSettings);
201 await act(() => registry.set(state.settings.get(primaryId)!, remoteSettings));
202 const second = click();
203 await act(async () => {});
204 writes[1]!.finish();
205 await second;
206 expect(durable.get(primaryId)!.favorites).toEqual([{ provider, model }]);
207 });
208
209 it("preserves an acknowledged add when a later remove fails before its echo", async () => {
210 await mount();
211 const first = click();
212 await act(async () => {});
213 writes[0]!.finish();
214 await first;
215 const failed = click();
216 await act(async () => {});
217 writes[1]!.finish(false);
218 await failed;
219 const retry = click();
220 await act(async () => {});
221 writes[2]!.finish();
222 await retry;
223 expect(durable.get(primaryId)!.favorites).toEqual([]);
224 });
225
226 it("does not reuse another environment's pending intent for the same model", async () => {
227 await mount();
228 const primary = click();
229 await act(() => renderer!.update(editor(remoteId)));
230 const remote = click();
231 await act(async () => {});
232 writes[0]!.finish();
233 await primary;
234 writes[1]!.finish();
235 await remote;
236 expect(durable.get(primaryId)!.favorites).toEqual([{ provider, model }]);
237 expect(durable.get(remoteId)!.favorites).toEqual([{ provider, model }]);
238 });
239
240 it("reads the latest server state even when the click callback predates its echo", async () => {
241 await mount();
242 const previousClick = renderer!.root.findByType("button").props.onClick as () => Promise<void>;
243 const updated = { ...initialSettings, favorites: [{ provider, model }] };
244 durable.set(primaryId, updated);
245 await act(() => registry.set(state.settings.get(primaryId)!, updated));
246 const clicked = previousClick();
247 await act(async () => {});
248 writes[0]!.finish();
249 await clicked;
250 expect(durable.get(primaryId)!.favorites).toEqual([]);
251 });
252});