Skip to content
Source
Sign inSign up
Open History to see the last commit

ProviderSettingsPanel.environment.browser.test.tsx

623 lines21.8 KB

1import { RegistryContext } from "@effect/atom-react";
2import {
3 DEFAULT_UNIFIED_SETTINGS,
4 EnvironmentId,
5 ProviderDriverKind,
6 ProviderInstanceId,
7 type ProviderAuthState,
8 type ServerProvider,
9 type UnifiedSettings,
10} from "@t3tools/contracts";
11import * as Cause from "effect/Cause";
12import * as Duration from "effect/Duration";
13import { AsyncResult, type Atom } from "effect/reactivity";
14import type { ReactNode } from "react";
15import { describe, expect, it, vi } from "vite-plus/test";
16import { page, userEvent } from "vite-plus/test/browser";
17​
18import "~/index.css";
19import { act, render } from "../../../test/render";
20​
21const state = vi.hoisted(() => ({
22 providers: null as unknown as Atom.Writable<ReadonlyArray<ServerProvider>>,
23 providerAuthQuery: null as unknown,
24 providerAuth: null as ProviderAuthState | null,
25 settings: null as unknown as UnifiedSettings,
26 canManageProviders: true,
27 canWriteSettings: true,
28 searchTargetId: null as string | null,
29 readEnvironmentIds: [] as string[],
30 updateEnvironmentIds: [] as string[],
31 mutationEnvironmentIds: [] as string[],
32 refresh: vi.fn(),
33 updateProvider: vi.fn(),
34 uninstall: vi.fn(),
35 acceptUrlAuth: vi.fn(),
36 updateSettings: vi.fn(),
37 updateModelPreferences: vi.fn(),
38 toggleFavorite: vi.fn(),
39 mutateProviderInstance: vi.fn(),
40 confirm: vi.fn<() => Promise<boolean>>(),
41}));
42​
43vi.mock(import("../../state/server"), async (original) => {
44 const actual = await original();
45 const { Atom } = await import("effect/reactivity");
46 const providers = Atom.make<ReadonlyArray<ServerProvider>>([]).pipe(Atom.keepAlive);
47 state.providers = providers;
48 const config = Atom.make(null);
49 const providerAuth = Atom.make(null);
50 state.providerAuthQuery = providerAuth;
51 const commandLabels = {
52 refreshProviders: "refresh",
53 updateProvider: "update",
54 uninstallAcpRegistryManagedBinary: "uninstall",
55 acceptAcpRegistryUrlAuth: "accept-url-auth",
56 } as const;
57 const labelled = Object.fromEntries(
58 Object.entries(commandLabels).map(([key, label]) => [
59 key,
60 { ...actual.serverEnvironment[key as keyof typeof commandLabels], label },
61 ]),
62 );
63 return {
64 ...actual,
65 providersIgnoringCheckedAtAtom: (() => providers) as never,
66 serverEnvironment: {
67 ...actual.serverEnvironment,
68 ...labelled,
69 configValueAtom: (() => config) as never,
70 providerAuthState: (() => providerAuth) as never,
71 providersValueAtom: (() => providers) as never,
72 } as never,
73 };
74});
75vi.mock(import("../../state/use-atom-command"), async (original) => ({
76 ...(await original()),
77 useAtomCommand: ((command: { label?: string }) =>
78 ({
79 refresh: state.refresh,
80 update: state.updateProvider,
81 uninstall: state.uninstall,
82 "accept-url-auth": state.acceptUrlAuth,
83 })[command.label ?? ""] ?? vi.fn()) as never,
84}));
85vi.mock(import("~/localApi"), async (original) => {
86 const api = { dialogs: { confirm: state.confirm } };
87 return {
88 ...(await original()),
89 readLocalApi: (() => api) as never,
90 ensureLocalApi: (() => api) as never,
91 };
92});
93vi.mock(import("../../state/query"), async (original) => ({
94 ...(await original()),
95 useEnvironmentQuery: ((query: unknown) => ({
96 data: query === state.providerAuthQuery ? state.providerAuth : null,
97 error: null,
98 isPending: false,
99 refresh: vi.fn(),
100 })) as never,
101}));
102vi.mock(import("../../hooks/useSettings"), async (original) => ({
103 ...(await original()),
104 useEnvironmentSettings: ((environmentId: string) => {
105 state.readEnvironmentIds.push(environmentId);
106 return state.settings;
107 }) as never,
108 useUpdateEnvironmentSettings: ((environmentId: string) => {
109 state.updateEnvironmentIds.push(environmentId);
110 return state.updateSettings;
111 }) as never,
112 useUpdateEnvironmentModelPreferences: (() => state.updateModelPreferences) as never,
113 useToggleEnvironmentModelFavorite: (() => state.toggleFavorite) as never,
114 usePersistEnvironmentProviderInstanceMutation: ((environmentId: string) => {
115 state.mutationEnvironmentIds.push(environmentId);
116 return state.mutateProviderInstance;
117 }) as never,
118}));
119vi.mock(import("../../state/session"), async (original) => {
120 const scope = (environmentId: string, requested: string) =>
121 environmentId === "remote-device" &&
122 (requested === "providers:manage"
123 ? state.canManageProviders
124 : requested === "settings:write"
125 ? state.canWriteSettings
126 : requested === "orchestration:read");
127 return {
128 ...(await original()),
129 useEnvironmentScope: scope as never,
130 readEnvironmentScope: scope as never,
131 };
132});
133vi.mock(import("../../state/entities"), async (original) => ({
134 ...(await original()),
135 useProjects: (() => []) as never,
136}));
137vi.mock(import("./settingsLayout"), async (original) => ({
138 ...(await original()),
139 useSettingsSearchTargetId: () => state.searchTargetId,
140 SettingsRow: (({
141 id,
142 title,
143 description,
144 status,
145 control,
146 children,
147 }: {
148 id?: string;
149 title: ReactNode;
150 description?: ReactNode;
151 status?: ReactNode;
152 control?: ReactNode;
153 children?: ReactNode;
154 }) => (
155 <div id={id}>
156 <h4>{title}</h4>
157 {description}
158 {status}
159 {control}
160 {children}
161 </div>
162 )) as never,
163}));
164vi.mock(import("./UsageProviderSettings"), async (original) => ({
165 ...(await original()),
166 UsageProviderSettings: (() => null) as never,
167}));
168vi.mock(import("../ProviderUpdatesAction"), async (original) => ({
169 ...(await original()),
170 ProviderUpdatesAction: (() => null) as never,
171}));
172​
173import { appAtomRegistry } from "~/rpc/atomRegistry";
174import { EnvironmentProviderSettings } from "./ProviderSettingsPanel";
175​
176const environmentId = EnvironmentId.make("remote-device");
177const codexId = ProviderInstanceId.make("codex");
178const customId = ProviderInstanceId.make("codex_work");
179​
180function provider(): ServerProvider {
181 return {
182 instanceId: codexId,
183 driver: ProviderDriverKind.make("codex"),
184 enabled: true,
185 installed: true,
186 version: "1.0.0",
187 status: "ready",
188 auth: { status: "authenticated" },
189 checkedAt: "2026-07-24T12:00:00.000Z",
190 models: [{ slug: "chosen", name: "Chosen", isCustom: false, capabilities: null }],
191 slashCommands: [],
192 skills: [],
193 versionAdvisory: {
194 status: "behind_latest",
195 currentVersion: "1.0.0",
196 latestVersion: "1.1.0",
197 updateCommand: "pnpm add -g @openai/codex@latest",
198 canUpdate: true,
199 checkedAt: "2026-07-24T12:00:00.000Z",
200 message: "Update available.",
201 },
202 };
204​
205const setProviders = (providers: ReadonlyArray<ServerProvider>) =>
206 act(async () => appAtomRegistry.set(state.providers, providers));
207​
208function panel(options?: {
209 readonly readOnly?: boolean;
210 readonly targetInstanceId?: ProviderInstanceId;
211}) {
212 return (
213 <RegistryContext.Provider value={appAtomRegistry}>
214 <div className="@container/providers">
215 <EnvironmentProviderSettings
216 environmentId={environmentId}
217 environmentLabel="Remote device"
218 {...(options?.readOnly === undefined ? {} : { readOnly: options.readOnly })}
219 {...(options?.targetInstanceId === undefined
220 ? {}
221 : { targetInstanceId: options.targetInstanceId })}
222 />
223 </div>
224 </RegistryContext.Provider>
225 );
227​
228// The section header and the end of the provider list both offer Add provider.
229​
230async function setup() {
231 await page.viewport(1280, 900);
232 state.settings = DEFAULT_UNIFIED_SETTINGS;
233 state.canManageProviders = true;
234 state.canWriteSettings = true;
235 state.searchTargetId = null;
236 state.providerAuth = null;
237 state.readEnvironmentIds = [];
238 state.updateEnvironmentIds = [];
239 state.mutationEnvironmentIds = [];
240 state.updateSettings.mockReset();
241 state.updateModelPreferences.mockReset();
242 state.toggleFavorite.mockReset();
243 state.mutateProviderInstance.mockReset().mockResolvedValue(AsyncResult.success({}));
244 state.confirm.mockReset().mockResolvedValue(false);
245 state.refresh.mockReset().mockResolvedValue(AsyncResult.success(undefined));
246 state.updateProvider.mockReset().mockResolvedValue(AsyncResult.success(undefined));
247 state.uninstall.mockReset().mockResolvedValue(AsyncResult.success({}));
248 state.acceptUrlAuth.mockReset().mockResolvedValue(AsyncResult.success({ accepted: true }));
249 await setProviders([]);
251​
252describe("EnvironmentProviderSettings routing", () => {
253 it("does not substitute another account when the requested instance was removed", async () => {
254 await setup();
255 await setProviders([provider()]);
256 await using _screen = await render(panel({ targetInstanceId: customId }));
257 await expect
258 .element(page.getByText("This provider instance is no longer available on this device."))
259 .toBeVisible();
260 expect(state.updateSettings).not.toHaveBeenCalled();
261 });
262​
263 it("updates one provider instance without sending a stale whole map", async () => {
264 await setup();
265 state.settings = {
266 ...DEFAULT_UNIFIED_SETTINGS,
267 providerInstances: {
268 [customId]: {
269 driver: ProviderDriverKind.make("codex"),
270 enabled: true,
271 displayName: "Work",
272 },
273 },
274 };
275 await using _screen = await render(panel());
276 await userEvent.click(page.getByRole("switch", { name: "Enable Work" }).first());
277​
278 expect(state.mutateProviderInstance).toHaveBeenCalledWith(
279 {
280 operation: "upsert",
281 instanceId: customId,
282 instance: { driver: ProviderDriverKind.make("codex"), enabled: false, displayName: "Work" },
283 },
284 {},
285 );
286 });
287​
288 it.each([false, true])(
289 "builds a custom model save on the last one before its config echo (replied %s)",
290 async (replied) => {
291 await setup();
292 const first = { slug: "model-a", name: "Original A" };
293 const second = { slug: "model-b", name: "Original B" };
294 const stored = (customModels: ReadonlyArray<{ slug: string; name: string }>) => ({
295 ...DEFAULT_UNIFIED_SETTINGS,
296 providerInstances: {
297 [customId]: { driver: ProviderDriverKind.make("codex"), config: { customModels } },
298 },
299 });
300 state.settings = stored([first, second]);
301 const replies: Array<() => void> = [];
302 state.mutateProviderInstance.mockImplementation(
303 () => new Promise((resolve) => replies.push(() => resolve(AsyncResult.success({})))),
304 );
305 await using screen = await render(panel({ targetInstanceId: customId }));
306 const rename = async (slug: string, name: string) => {
307 await userEvent.click(screen.getByRole("button", { name: `Edit ${slug}`, exact: true }));
308 await userEvent.fill(
309 screen.getByRole("textbox", { name: "Display name", exact: true }),
310 name,
311 );
312 await userEvent.click(screen.getByRole("button", { name: "Save", exact: true }));
313 };
314​
315 await rename("model-a", "Entered A");
316 if (replied) await act(() => replies[0]!());
317 await rename("model-b", "Entered B");
318​
319 expect(state.mutateProviderInstance.mock.calls[1]?.[0]).toEqual({
320 operation: "upsert",
321 instanceId: customId,
322 instance: {
323 driver: ProviderDriverKind.make("codex"),
324 config: {
325 customModels: [
326 { ...first, name: "Entered A" },
327 { ...second, name: "Entered B" },
328 ],
329 },
330 },
331 });
332​
333 // The first save's echo lands after both replied, ahead of the second's.
334 await act(() => replies.forEach((reply) => reply()));
335 state.settings = stored([{ ...first, name: "Entered A" }, second]);
336 await screen.rerender(panel({ targetInstanceId: customId }));
337 await rename("model-a", "Again A");
338 expect(state.mutateProviderInstance.mock.calls[2]?.[0].instance.config.customModels).toEqual([
339 { ...first, name: "Again A" },
340 { ...second, name: "Entered B" },
341 ]);
342 },
343 );
344​
345 it("preserves a failed model draft for retry without including it in another edit", async () => {
346 await setup();
347 const first = { slug: "model-a", name: "Original A" };
348 const second = { slug: "model-b", name: "Original B" };
349 state.settings = {
350 ...DEFAULT_UNIFIED_SETTINGS,
351 providerInstances: {
352 [customId]: {
353 driver: ProviderDriverKind.make("codex"),
354 config: { customModels: [first, second] },
355 },
356 },
357 };
358 state.mutateProviderInstance
359 .mockResolvedValueOnce(AsyncResult.failure(Cause.fail(new Error("Save failed"))))
360 .mockResolvedValueOnce(AsyncResult.failure(Cause.fail(new Error("Still offline"))));
361 await using screen = await render(panel({ targetInstanceId: customId }));
362 await screen.getByRole("button", { name: "Edit model-a", exact: true }).click();
363 const displayName = screen.getByRole("textbox", { name: "Display name", exact: true });
364 await userEvent.fill(displayName, "Entered A");
365 const save = screen.getByRole("button", { name: "Save", exact: true });
366 await save.click();
367 await expect.element(save).toBeEnabled();
368 await expect.element(displayName).toHaveValue("Entered A");
369 await save.click();
370 await expect.element(save).toBeEnabled();
371 expect(state.mutateProviderInstance.mock.calls[1]?.[0].instance.config.customModels).toEqual([
372 { ...first, name: "Entered A" },
373 second,
374 ]);
375 await screen.getByRole("button", { name: "Cancel", exact: true }).click();
376 await screen.getByRole("button", { name: "Edit model-b", exact: true }).click();
377 await userEvent.fill(displayName, "Entered B");
378 await save.click();
379 await expect.element(displayName).not.toBeInTheDocument();
380 expect(state.mutateProviderInstance.mock.calls[2]?.[0].instance.config.customModels).toEqual([
381 first,
382 { ...second, name: "Entered B" },
383 ]);
384 });
385​
386 it("routes explicit ACP browser authentication consent to the selected environment", async () => {
387 await setup();
388 const instanceId = ProviderInstanceId.make("acpRegistry_antigravity");
389 const action = {
390 elicitationId: "google-login-1",
391 url: "https://accounts.google.com/login",
392 message: "Continue with Google",
393 };
394 state.settings = {
395 ...DEFAULT_UNIFIED_SETTINGS,
396 providerInstances: {
397 [instanceId]: {
398 driver: ProviderDriverKind.make("acpRegistry"),
399 enabled: true,
400 config: { agentId: "antigravity" },
401 },
402 },
403 };
404 await setProviders([
405 {
406 ...provider(),
407 instanceId,
408 driver: ProviderDriverKind.make("acpRegistry"),
409 auth: { status: "unauthenticated", action },
410 },
411 ]);
412 await using _screen = await render(panel({ targetInstanceId: instanceId }));
413 const link = page.getByRole("link", { name: "Continue authentication" });
414 link.element().addEventListener("click", (event) => event.preventDefault());
415 await userEvent.click(link);
416​
417 expect(state.acceptUrlAuth).toHaveBeenCalledWith({
418 environmentId,
419 input: { instanceId, elicitationId: action.elicitationId },
420 });
421 });
422​
423 it("lets a signed-out managed Codex account switch to the existing CLI", async () => {
424 await setup();
425 const codex = ProviderDriverKind.make("codex");
426 state.settings = {
427 ...DEFAULT_UNIFIED_SETTINGS,
428 providerInstances: {
429 [codexId]: {
430 driver: codex,
431 enabled: true,
432 config: { enabled: true, setupMode: "managed" },
433 },
434 },
435 };
436 state.providerAuth = {
437 instanceId: codexId,
438 phase: "idle",
439 flowId: null,
440 authorizationUrl: null,
441 expiresAt: null,
442 message: null,
443 methods: [{ id: "chatgpt-profile:work", name: "Work", description: null, type: "agent" }],
444 };
445 await setProviders([
446 {
447 ...provider(),
448 auth: { status: "unauthenticated" },
449 setup: { canAuthenticate: true, canInstall: true },
450 },
451 ]);
452 await using _screen = await render(panel({ targetInstanceId: codexId }));
453 await expect.element(page.getByRole("button", { name: "Reconnect account" })).toBeVisible();
454 await userEvent.click(page.getByRole("button", { name: "Use existing CLI" }).element());
455​
456 expect(state.mutateProviderInstance).toHaveBeenCalledWith(
457 {
458 operation: "upsert",
459 instanceId: codexId,
460 instance: {
461 driver: codex,
462 enabled: true,
463 config: { enabled: true, setupMode: "existing" },
464 },
465 },
466 {},
467 );
468 });
469});
470​
471describe("EnvironmentProviderSettings provider list", () => {
472 it("selects a provider when its row is clicked near the edge", async () => {
473 await setup();
474 await setProviders([provider()]);
475 await using _screen = await render(panel());
476 const select = page.getByRole("button", { name: "Select Claude" });
477 await expect.element(select).toHaveAttribute("aria-pressed", "false");
478 const row = page.elementLocator(select.element().closest('[data-slot="settings-row"]')!);
479​
480 await userEvent.click(row, { position: { x: 3, y: 3 } });
481​
482 await expect.element(select).toHaveAttribute("aria-pressed", "true");
483 });
484});
485​
486describe("EnvironmentProviderSettings instance removal", () => {
487 it("resets a configured default instance only once the user confirms", async () => {
488 await setup();
489 state.settings = {
490 ...DEFAULT_UNIFIED_SETTINGS,
491 providerInstances: {
492 [codexId]: {
493 driver: ProviderDriverKind.make("codex"),
494 enabled: true,
495 config: { binaryPath: "/opt/codex/bin/codex" },
496 },
497 },
498 };
499 await setProviders([provider()]);
500 await using _screen = await render(panel({ targetInstanceId: codexId }));
501 const reset = page.getByRole("button", { name: "Reset Codex provider settings to default" });
502 await userEvent.click(reset);
503​
504 expect(state.mutateProviderInstance).not.toHaveBeenCalled();
505 expect(state.confirm).toHaveBeenCalledOnce();
506​
507 state.confirm.mockResolvedValue(true);
508 await userEvent.click(reset);
509 await vi.waitFor(() =>
510 expect(state.mutateProviderInstance).toHaveBeenCalledWith({
511 operation: "remove",
512 instanceId: codexId,
513 }),
514 );
515 });
516​
517 it("warns that resetting an enabled default-off provider turns it off", async () => {
518 await setup();
519 const antigravityId = ProviderInstanceId.make("antigravity");
520 const antigravity = ProviderDriverKind.make("antigravity");
521 state.settings = {
522 ...DEFAULT_UNIFIED_SETTINGS,
523 providerInstances: {
524 [antigravityId]: {
525 driver: antigravity,
526 enabled: true,
527 config: { binaryPath: "/opt/antigravity/bin/agy" },
528 },
529 },
530 };
531 await setProviders([{ ...provider(), instanceId: antigravityId, driver: antigravity }]);
532 await using _screen = await render(panel({ targetInstanceId: antigravityId }));
533 await userEvent.click(page.getByRole("button", { name: /^Reset .* provider settings/ }));
534​
535 expect(state.confirm).toHaveBeenCalledWith(
536 expect.stringContaining("It will also be turned off."),
537 {
538 variant: "destructive",
539 },
540 );
541 expect(state.mutateProviderInstance).not.toHaveBeenCalled();
542 });
543​
544 it("deletes a custom instance only once the user confirms", async () => {
545 await setup();
546 state.settings = {
547 ...DEFAULT_UNIFIED_SETTINGS,
548 providerInstances: {
549 [customId]: { driver: ProviderDriverKind.make("codex"), enabled: true },
550 },
551 };
552 await using _screen = await render(panel({ targetInstanceId: customId }));
553 const remove = page.getByRole("button", { name: `Delete instance ${customId}` });
554 await userEvent.click(remove);
555​
556 expect(state.mutateProviderInstance).not.toHaveBeenCalled();
557 expect(state.confirm).toHaveBeenCalledOnce();
558​
559 state.confirm.mockResolvedValue(true);
560 await userEvent.click(remove);
561 await vi.waitFor(() =>
562 expect(state.mutateProviderInstance).toHaveBeenCalledWith({
563 operation: "remove",
564 instanceId: customId,
565 }),
566 );
567 });
568});
569​
570describe("EnvironmentProviderSettings health check interval", () => {
571 it("commits interval drafts and allows retrying rejected edits", async () => {
572 await setup();
573 await using _screen = await render(panel());
574 const input = page.getByRole("textbox", { name: "Provider health check interval in seconds" });
575​
576 await userEvent.type(input, "{ArrowUp}{ArrowDown}");
577 expect(
578 state.updateSettings.mock.calls.map(([patch]) =>
579 Duration.toSeconds(patch.backgroundActivity.overrides.providerHealthRefreshInterval),
580 ),
581 ).toEqual([330, 300]);
582 state.updateSettings.mockClear();
583​
584 // Replacing 300 with 120 passes through 30, 3, an empty field, 1 and 12.
585 await userEvent.type(input, "{Backspace}{Backspace}{Backspace}120");
586 await expect.element(input).toHaveValue("120");
587 expect(state.updateSettings).not.toHaveBeenCalled();
588​
589 await userEvent.tab();
590 expect(
591 state.updateSettings.mock.calls.map(([patch]) =>
592 Duration.toSeconds(patch.backgroundActivity.overrides.providerHealthRefreshInterval),
593 ),
594 ).toEqual([120]);
595​
596 await act(() => state.updateSettings.mock.calls.at(-1)?.[1]?.onFailed?.());
597 await expect.element(input).toHaveValue("300");
598 await userEvent.type(input, "{Backspace}{Backspace}{Backspace}120");
599 await userEvent.tab();
600 expect(
601 state.updateSettings.mock.calls.map(([patch]) =>
602 Duration.toSeconds(patch.backgroundActivity.overrides.providerHealthRefreshInterval),
603 ),
604 ).toEqual([120, 120]);
605​
606 const rejectPrevious = state.updateSettings.mock.calls.at(-1)?.[1]?.onFailed;
607 await userEvent.type(input, "{ArrowUp}");
608 await act(() => rejectPrevious?.());
609 await expect.element(input).toHaveValue("150");
610 });
611​
612 it("restores the saved interval when a cleared field loses focus", async () => {
613 await setup();
614 await using _screen = await render(panel());
615 const input = page.getByRole("textbox", { name: "Provider health check interval in seconds" });
616​
617 await userEvent.type(input, "{Backspace}{Backspace}{Backspace}");
618 await userEvent.tab();
619​
620 await expect.element(input).toHaveValue("300");
621 expect(state.updateSettings).not.toHaveBeenCalled();
622 });
623});