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 {
10 AVAILABLE_CONNECTION_STATE,
11 type SupervisorConnectionState,
12} from "@t3tools/client-runtime/connection";
13import * as Cause from "effect/Cause";
14import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
15import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
16import type { ReactNode } from "react";
17import { describe, expect, it, vi } from "vite-plus/test";
18
19import { act, render, type Screen } from "../../test/render";
20
21const state = vi.hoisted(() => ({
22 catalog: null as unknown as Atom.Writable<{
23 readonly isReady: boolean;
24 readonly entries: ReadonlyMap<string, unknown>;
25 }>,
26 settings: new Map<string, unknown>(),
27 connections: new Map<
28 EnvironmentId,
29 Atom.Writable<AsyncResult.AsyncResult<SupervisorConnectionState>>
30 >(),
31 persist: vi.fn(),
32}));
33
34vi.mock(import("~/connection/catalog"), async (original) => {
35 const actual = await original();
36 const { Atom } = await import("effect/reactivity");
37 state.catalog = Atom.keepAlive(
38 Atom.make<{ readonly isReady: boolean; readonly entries: ReadonlyMap<string, unknown> }>({
39 isReady: true,
40 entries: new Map(),
41 }),
42 );
43 return {
44 ...actual,
45 environmentCatalog: {
46 ...actual.environmentCatalog,
47 catalogValueAtom: state.catalog as never,
48 stateAtom: ((id: EnvironmentId) => state.connections.get(id)) as never,
49 },
50 };
51});
52vi.mock("~/localApi", () => ({
53 ensureLocalApi: () => ({
54 persistence: {
55 getClientSettings: async () => ({ favorites: [{ provider, model }] }),
56 },
57 }),
58}));
59
60vi.mock(import("~/state/server"), async (original) => {
61 const actual = await original();
62 return {
63 ...actual,
64 serverEnvironment: {
65 ...actual.serverEnvironment,
66 settingsValueAtom: ((id: EnvironmentId) => state.settings.get(id)) as never,
67 updateSettings: {
68 ...actual.serverEnvironment.updateSettings,
69 run: (_registry: unknown, input: unknown) => state.persist(input),
70 } as never,
71 },
72 };
73});
74vi.mock(import("~/state/use-atom-command"), async (original) => ({
75 ...(await original()),
76 useAtomCommand: (() => state.persist) as never,
77}));
78
79import { appAtomRegistry } from "~/rpc/atomRegistry";
80import {
81 ensureClientSettingsHydrated,
82 startModelPreferencesMigration,
83 useToggleEnvironmentModelFavorite,
84} from "./useSettings";
85
86const primaryId = EnvironmentId.make("primary");
87const remoteId = EnvironmentId.make("remote");
88const provider = ProviderInstanceId.make("codex_work");
89const model = "gpt-6.1-sol";
90const initialSettings = {
91 ...DEFAULT_SERVER_SETTINGS,
92 favorites: [],
93 providerModelPreferences: {},
94} satisfies ServerSettings;
95interface Write {
96 environmentId: EnvironmentId;
97 patch: ServerSettingsPatch;
98 finish: (success?: boolean) => void;
99}
100
101const settingsAtom = (id: EnvironmentId) =>
102 state.settings.get(id) as Atom.Writable<ServerSettings | null>;
103
104function FavoriteEditor({
105 environmentId,
106 toggles,
107 onClicked,
108}: {
109 environmentId: EnvironmentId;
110 toggles: Array<() => Promise<void>>;
111 onClicked: (pending: Promise<void>) => void;
112}) {
113 const toggle = useToggleEnvironmentModelFavorite(environmentId);
114 const onClick = () => toggle(provider, model);
115 toggles.push(onClick);
116 return <button onClick={() => onClicked(onClick())}>Toggle favorite</button>;
117}
118
119async function setup() {
120 await using stack = new AsyncDisposableStack();
121 const registry = stack.adopt(AtomRegistry.make(), (registry) => registry.dispose());
122 state.settings.clear();
123 state.connections.clear();
124 const durable = new Map<EnvironmentId, ServerSettings>();
125 const toggles: Array<() => Promise<void>> = [];
126 const writes: Write[] = [];
127 let pendingClick: Promise<void> | undefined;
128 for (const id of [primaryId, remoteId]) {
129 state.settings.set(id, Atom.make<ServerSettings | null>(initialSettings));
130 durable.set(id, initialSettings);
131 }
132 state.persist.mockReset().mockImplementation(
133 ({
134 environmentId,
135 input,
136 }: {
137 environmentId: EnvironmentId;
138 input: { patch: ServerSettingsPatch };
139 }) =>
140 new Promise((resolve) => {
141 writes.push({
142 environmentId,
143 patch: input.patch,
144 finish: (success = true) => {
145 if (!success) {
146 resolve(AsyncResult.failure(Cause.fail(new Error("save failed"))));
147 return;
148 }
149 const settings = applyServerSettingsPatch(durable.get(environmentId)!, input.patch);
150 durable.set(environmentId, settings);
151 resolve(AsyncResult.success(settings));
152 },
153 });
154 }),
155 );
156
157 const environment = stack.move();
158 let screen: Screen | undefined;
159 const show = async (ui: ReactNode) => {
160 const tree = <RegistryContext.Provider value={registry}>{ui}</RegistryContext.Provider>;
161 if (screen) await screen.rerender(tree);
162 else screen = environment.use(await render(tree));
163 };
164
165 return {
166 registry,
167 durable,
168 toggles,
169 writes,
170 show,
171 mount: (environmentId = primaryId) =>
172 show(
173 <FavoriteEditor
174 environmentId={environmentId}
175 toggles={toggles}
176 onClicked={(pending) => {
177 pendingClick = pending;
178 }}
179 />,
180 ),
181 click(): Promise<void> {
182 screen!.container.querySelector("button")!.click();
183 return pendingClick!;
184 },
185 [Symbol.asyncDispose]: () => environment.disposeAsync(),
186 };
187}
188
189describe("environment favorite toggles", () => {
190 it.each([false, true])(
191 "toggles twice before any server echo when initially favorited=%s",
192 async (initiallyFavorited) => {
193 await using fixture = await setup();
194 const { registry, durable, writes, mount, click } = fixture;
195 const favorites = initiallyFavorited ? [{ provider, model }] : [];
196 const settings = { ...initialSettings, favorites };
197 registry.set(settingsAtom(primaryId), settings);
198 durable.set(primaryId, settings);
199 await mount();
200 const first = click();
201 const second = click();
202 await act(async () => {});
203 expect(writes.map((write) => write.patch.setModelFavorites?.[0]?.favorite)).toEqual([
204 !initiallyFavorited,
205 initiallyFavorited,
206 ]);
207 writes[0]!.finish();
208 await first;
209 writes[1]!.finish();
210 await second;
211 expect(durable.get(primaryId)!.favorites).toEqual(favorites);
212 },
213 );
214
215 it("preserves an acknowledged add when a later remove fails before its echo", async () => {
216 await using fixture = await setup();
217 const { durable, writes, mount, click } = fixture;
218 await mount();
219 const first = click();
220 await act(async () => {});
221 writes[0]!.finish();
222 await first;
223 const failed = click();
224 await act(async () => {});
225 writes[1]!.finish(false);
226 await failed;
227 const retry = click();
228 await act(async () => {});
229 writes[2]!.finish();
230 await retry;
231 expect(durable.get(primaryId)!.favorites).toEqual([]);
232 });
233
234 it("does not reuse another environment's pending intent for the same model", async () => {
235 await using fixture = await setup();
236 const { durable, writes, mount, click } = fixture;
237 await mount();
238 const primary = click();
239 await mount(remoteId);
240 const remote = click();
241 await act(async () => {});
242 writes[0]!.finish();
243 await primary;
244 writes[1]!.finish();
245 await remote;
246 expect(durable.get(primaryId)!.favorites).toEqual([{ provider, model }]);
247 expect(durable.get(remoteId)!.favorites).toEqual([{ provider, model }]);
248 });
249
250 it("reads the latest server state even when the click callback predates its echo", async () => {
251 await using fixture = await setup();
252 const { registry, durable, toggles, writes, mount } = fixture;
253 await mount();
254 const previousClick = toggles.at(-1)!;
255 const updated = { ...initialSettings, favorites: [{ provider, model }] };
256 durable.set(primaryId, updated);
257 await act(() => registry.set(settingsAtom(primaryId), updated));
258 const clicked = previousClick();
259 await act(async () => {});
260 writes[0]!.finish();
261 await clicked;
262 expect(durable.get(primaryId)!.favorites).toEqual([]);
263 });
264});
265
266describe("legacy model preferences migration", () => {
267 it.each(["succeeds", "fails"] as const)(
268 "waits for connection and retries only after reconnect when the first save %s",
269 async (outcome) => {
270 await using _fixture = await setup();
271 const environmentId = EnvironmentId.make(`migration-${outcome}`);
272 const otherId = EnvironmentId.make(`other-${outcome}`);
273 for (const id of [environmentId, otherId]) {
274 state.settings.set(
275 id,
276 Atom.keepAlive(Atom.make<ServerSettings | null>(DEFAULT_SERVER_SETTINGS)),
277 );
278 state.connections.set(
279 id,
280 Atom.keepAlive(
281 Atom.make<AsyncResult.AsyncResult<SupervisorConnectionState>>(
282 AsyncResult.success(AVAILABLE_CONNECTION_STATE),
283 ),
284 ),
285 );
286 }
287 let finishFirstSave = () => {};
288 const firstSave = new Promise((resolve) => {
289 finishFirstSave = () =>
290 resolve(
291 outcome === "succeeds"
292 ? AsyncResult.success(DEFAULT_SERVER_SETTINGS)
293 : AsyncResult.failure(Cause.fail(new Error("save failed"))),
294 );
295 });
296 state.persist
297 .mockReturnValueOnce(firstSave)
298 .mockResolvedValue(AsyncResult.success(DEFAULT_SERVER_SETTINGS));
299 const connect = (id: EnvironmentId, generation: number) =>
300 appAtomRegistry.set(
301 state.connections.get(id)!,
302 AsyncResult.success({
303 ...AVAILABLE_CONNECTION_STATE,
304 desired: true,
305 generation,
306 phase: "connected",
307 }),
308 );
309 const pushSettings = (id = environmentId) =>
310 appAtomRegistry.set(settingsAtom(id), { ...DEFAULT_SERVER_SETTINGS });
311 const migration = (id: EnvironmentId) => ({
312 environmentId: id,
313 input: { patch: { migrateModelPreferences: { favorites: [{ provider, model }] } } },
314 });
315
316 appAtomRegistry.set(state.catalog, {
317 isReady: true,
318 entries: new Map([
319 [environmentId, {}],
320 [otherId, {}],
321 ]),
322 });
323 using _migration = { [Symbol.dispose]: startModelPreferencesMigration() };
324 await ensureClientSettingsHydrated();
325 pushSettings();
326 expect(state.persist).not.toHaveBeenCalled();
327
328 connect(environmentId, 1);
329 expect(state.persist).toHaveBeenCalledExactlyOnceWith(migration(environmentId));
330 pushSettings();
331 expect(state.persist).toHaveBeenCalledTimes(1);
332 finishFirstSave();
333 await firstSave;
334 pushSettings();
335 pushSettings();
336 expect(state.persist).toHaveBeenCalledTimes(1);
337
338 connect(otherId, 7);
339 expect(state.persist).toHaveBeenLastCalledWith(migration(otherId));
340 pushSettings(otherId);
341 expect(state.persist).toHaveBeenCalledTimes(2);
342
343 appAtomRegistry.set(
344 state.connections.get(environmentId)!,
345 AsyncResult.success({ ...AVAILABLE_CONNECTION_STATE, generation: 1 }),
346 );
347 pushSettings();
348 expect(state.persist).toHaveBeenCalledTimes(2);
349 connect(environmentId, 2);
350 pushSettings();
351 expect(state.persist.mock.calls).toEqual([
352 [migration(environmentId)],
353 [migration(otherId)],
354 [migration(environmentId)],
355 ]);
356 },
357 );
358});