1import type { ReactElement } from "react";
2import {
3 DEFAULT_UNIFIED_SETTINGS,
4 EnvironmentId,
5 ProviderDriverKind,
6 ProviderInstanceId,
7 type ServerProvider,
8 type UnifiedSettings,
9} from "@t3tools/contracts";
10import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
11
12import { visitElements } from "../../test/reactElementTree";
13import { reactHookHarness as hooks } from "../../test/reactHookHarness";
14
15const atoms = vi.hoisted(() => ({
16 providers: null as ReadonlyArray<ServerProvider> | null,
17 providersAtom: Symbol("providers"),
18 refreshProviders: Symbol("refreshProviders"),
19 updateProvider: Symbol("updateProvider"),
20 uninstallAcpRegistryManagedBinary: Symbol("uninstallAcpRegistryManagedBinary"),
21 acceptAcpRegistryUrlAuth: Symbol("acceptAcpRegistryUrlAuth"),
22}));
23
24const commands = vi.hoisted(() => ({
25 refresh: vi.fn(),
26 updateProvider: vi.fn(),
27 uninstall: vi.fn(),
28 acceptUrlAuth: vi.fn(),
29 canManageProviders: true,
30 canWriteSettings: true,
31}));
32
33const settingsState = vi.hoisted(() => ({
34 value: null as UnifiedSettings | null,
35 readEnvironmentIds: [] as EnvironmentId[],
36 updateEnvironmentIds: [] as EnvironmentId[],
37 mutationEnvironmentIds: [] as EnvironmentId[],
38 updateSettings: vi.fn(),
39 mutateProviderInstance: vi.fn(),
40 updateClientSettings: vi.fn(),
41}));
42
43const settingsSearchState = vi.hoisted(() => ({
44 targetId: null as string | null,
45 effects: [] as Array<() => void>,
46}));
47
48vi.mock("react", async (importOriginal) => {
49 const actual = await importOriginal<typeof import("react")>();
50 const { reactHookHarness } = await import("../../test/reactHookHarness");
51 return {
52 ...actual,
53 useCallback: reactHookHarness.useCallback,
54 useEffect: (effect: () => void) => settingsSearchState.effects.push(effect),
55 useMemo: reactHookHarness.useMemo,
56 useRef: reactHookHarness.useRef,
57 useState: reactHookHarness.useState,
58 };
59});
60
61vi.mock("./settingsLayout", async (importOriginal) => {
62 const actual = await importOriginal<typeof import("./settingsLayout")>();
63 return {
64 ...actual,
65 useSettingsSearchTargetId: () => settingsSearchState.targetId,
66 };
67});
68
69vi.mock("./SettingsScopeSentence", () => ({ SettingsScopeSentence: () => null }));
70vi.mock("react/compiler-runtime", async () => {
71 const { reactHookHarness } = await import("../../test/reactHookHarness");
72 return { c: reactHookHarness.useMemoCache };
73});
74
75vi.mock("@effect/atom-react", () => ({
76 useAtomValue: () => atoms.providers,
77}));
78
79vi.mock("../../state/server", () => ({
80 EMPTY_SERVER_PROVIDERS: [],
81 serverEnvironment: {
82 providersValueAtom: () => atoms.providersAtom,
83 refreshProviders: atoms.refreshProviders,
84 updateProvider: atoms.updateProvider,
85 uninstallAcpRegistryManagedBinary: atoms.uninstallAcpRegistryManagedBinary,
86 acceptAcpRegistryUrlAuth: atoms.acceptAcpRegistryUrlAuth,
87 },
88}));
89
90vi.mock("../../state/use-atom-command", () => ({
91 useAtomCommand: (atom: symbol) =>
92 atom === atoms.refreshProviders
93 ? commands.refresh
94 : atom === atoms.uninstallAcpRegistryManagedBinary
95 ? commands.uninstall
96 : atom === atoms.acceptAcpRegistryUrlAuth
97 ? commands.acceptUrlAuth
98 : commands.updateProvider,
99}));
100
101vi.mock("../../hooks/useSettings", () => ({
102 useUpdateClientSettings: () => settingsState.updateClientSettings,
103 useEnvironmentSettings: (environmentId: EnvironmentId) => {
104 settingsState.readEnvironmentIds.push(environmentId);
105 return settingsState.value;
106 },
107 useUpdateEnvironmentSettings: (environmentId: EnvironmentId) => {
108 settingsState.updateEnvironmentIds.push(environmentId);
109 return settingsState.updateSettings;
110 },
111 useUpdateEnvironmentModelPreferences: () => settingsState.updateSettings,
112 useToggleEnvironmentModelFavorite: () => () => undefined,
113 usePersistEnvironmentProviderInstanceMutation: (environmentId: EnvironmentId) => {
114 settingsState.mutationEnvironmentIds.push(environmentId);
115 return settingsState.mutateProviderInstance;
116 },
117}));
118
119vi.mock("../../environments/primary", () => ({
120 usePrimarySessionState: () => ({ data: null, error: null, isPending: false, refresh: vi.fn() }),
121}));
122
123vi.mock("../../state/session", () => ({
124 useEnvironmentSessionState: () => ({ data: null, hasError: false, isPending: true }),
125 useEnvironmentScope: (environmentId: EnvironmentId, scope: string) =>
126 environmentId === "remote-device" &&
127 (scope === "providers:manage"
128 ? commands.canManageProviders
129 : scope === "settings:write"
130 ? commands.canWriteSettings
131 : scope === "orchestration:read"),
132 readEnvironmentScope: (environmentId: EnvironmentId, scope: string) =>
133 environmentId === "remote-device" &&
134 (scope === "providers:manage"
135 ? commands.canManageProviders
136 : scope === "settings:write"
137 ? commands.canWriteSettings
138 : scope === "orchestration:read"),
139}));
140
141vi.mock("../../state/entities", () => ({
142 useProjects: () => [],
143 useServerConfigs: () => new Map(),
144}));
145
146import { EnvironmentProviderSettings } from "./ProviderSettingsPanel";
147import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog";
148
149const environmentId = EnvironmentId.make("remote-device");
150const codexId = ProviderInstanceId.make("codex");
151const customId = ProviderInstanceId.make("codex_work");
152
153function provider(): ServerProvider {
154 return {
155 instanceId: codexId,
156 driver: ProviderDriverKind.make("codex"),
157 enabled: true,
158 installed: true,
159 version: "1.0.0",
160 status: "ready",
161 auth: { status: "authenticated" },
162 checkedAt: "2026-07-24T12:00:00.000Z",
163 models: [],
164 slashCommands: [],
165 skills: [],
166 versionAdvisory: {
167 status: "behind_latest",
168 currentVersion: "1.0.0",
169 latestVersion: "1.1.0",
170 updateCommand: "pnpm add -g @openai/codex@latest",
171 canUpdate: true,
172 checkedAt: "2026-07-24T12:00:00.000Z",
173 message: "Update available.",
174 },
175 };
176}
177
178function renderPanel(options?: {
179 readonly readOnly?: boolean;
180 readonly targetInstanceId?: ProviderInstanceId;
181}): ReactElement<Record<string, unknown>> {
182 hooks.beginRender();
183 return EnvironmentProviderSettings({
184 environmentId,
185 environmentLabel: "Remote device",
186 ...(options?.readOnly === undefined ? {} : { readOnly: options.readOnly }),
187 ...(options?.targetInstanceId === undefined
188 ? {}
189 : { targetInstanceId: options.targetInstanceId }),
190 }) as ReactElement<Record<string, unknown>>;
191}
192
193function isRefreshButton(element: ReactElement<Record<string, unknown>>): boolean {
194 const children = element.props.children;
195 return (
196 Array.isArray(children) &&
197 children.some(
198 (child) =>
199 typeof child === "object" &&
200 child !== null &&
201 (child as ReactElement<Record<string, unknown>>).props?.className === "sr-only" &&
202 (child as ReactElement<Record<string, unknown>>).props?.children ===
203 "Refresh provider status",
204 )
205 );
206}
207
208function isAddProviderButton(element: ReactElement<Record<string, unknown>>): boolean {
209 return element.props["aria-label"] === "Add provider";
210}
211
212async function flushPromises(): Promise<void> {
213 await Promise.resolve();
214 await Promise.resolve();
215}
216
217describe("EnvironmentProviderSettings routing", () => {
218 beforeEach(() => {
219 hooks.reset();
220 atoms.providers = null;
221 settingsState.value = DEFAULT_UNIFIED_SETTINGS;
222 settingsState.readEnvironmentIds = [];
223 settingsState.updateEnvironmentIds = [];
224 settingsState.mutationEnvironmentIds = [];
225 settingsState.updateSettings.mockReset();
226 settingsState.updateClientSettings.mockReset();
227 settingsSearchState.targetId = null;
228 settingsSearchState.effects = [];
229 settingsState.mutateProviderInstance
230 .mockReset()
231 .mockResolvedValue({ _tag: "Success", value: {} });
232 commands.canManageProviders = true;
233 commands.canWriteSettings = true;
234 commands.refresh.mockReset().mockResolvedValue({ _tag: "Success" });
235 commands.updateProvider.mockReset().mockResolvedValue({ _tag: "Success" });
236 commands.uninstall.mockReset().mockResolvedValue({ _tag: "Success", value: {} });
237 commands.acceptUrlAuth
238 .mockReset()
239 .mockResolvedValue({ _tag: "Success", value: { accepted: true } });
240 });
241
242 it("shows Codex and Claude while hiding untouched disabled provider slots", () => {
243 const panel = renderPanel();
244 for (const driver of ["codex", "claudeAgent"] as const) {
245 expect(
246 visitElements(
247 panel,
248 (element) => element.props.instanceId === driver && element.props.mode === "list",
249 ),
250 ).not.toBeNull();
251 }
252 for (const driver of ["cursor", "grok", "pi", "opencode", "antigravity"] as const) {
253 expect(
254 visitElements(
255 panel,
256 (element) => element.props.instanceId === driver && element.props.mode === "list",
257 ),
258 ).toBeNull();
259 }
260 });
261
262 it("keeps explicitly configured providers visible when disabled", () => {
263 const grokId = ProviderInstanceId.make("grok");
264 settingsState.value = {
265 ...DEFAULT_UNIFIED_SETTINGS,
266 providerInstances: {
267 [grokId]: { driver: ProviderDriverKind.make("grok"), enabled: false },
268 },
269 };
270 const panel = renderPanel();
271 expect(
272 visitElements(
273 panel,
274 (element) => element.props.instanceId === grokId && element.props.mode === "list",
275 ),
276 ).not.toBeNull();
277 });
278
279 it("keeps legacy provider configuration visible when disabled", () => {
280 settingsState.value = {
281 ...DEFAULT_UNIFIED_SETTINGS,
282 providerInstances: {
283 [ProviderInstanceId.make("grok")]: {
284 driver: ProviderDriverKind.make("grok"),
285 enabled: false,
286 config: {
287 binaryPath: "/custom/grok",
288 },
289 },
290 },
291 };
292 const panel = renderPanel();
293 expect(
294 visitElements(
295 panel,
296 (element) => element.props.instanceId === "grok" && element.props.mode === "list",
297 ),
298 ).not.toBeNull();
299 });
300
301 it("coalesces a nullable provider snapshot before rendering array-backed UI", () => {
302 expect(() => renderPanel()).not.toThrow();
303 expect(settingsState.readEnvironmentIds).toEqual([environmentId]);
304 expect(settingsState.updateEnvironmentIds).toEqual([environmentId]);
305 expect(settingsState.mutationEnvironmentIds).toEqual([environmentId]);
306 });
307
308 it("routes refresh and provider update commands to the selected environment", async () => {
309 atoms.providers = [provider()];
310 const panel = renderPanel();
311 const refreshButton = visitElements(panel, isRefreshButton);
312 expect(refreshButton).not.toBeNull();
313 (refreshButton?.props.onClick as (() => void) | undefined)?.();
314 await flushPromises();
315
316 expect(commands.refresh).toHaveBeenCalledWith({
317 environmentId,
318 input: { refreshModels: true },
319 });
320
321 const providerCard = visitElements(
322 panel,
323 (element) =>
324 element.props.instanceId === codexId && typeof element.props.onRunUpdate === "function",
325 );
326 expect(providerCard).not.toBeNull();
327 (providerCard?.props.onRunUpdate as (() => void) | undefined)?.();
328 await flushPromises();
329
330 expect(commands.updateProvider).toHaveBeenCalledWith({
331 environmentId,
332 input: { provider: ProviderDriverKind.make("codex"), instanceId: codexId },
333 });
334 });
335
336 it("opens the requested provider instance instead of the first provider", () => {
337 settingsState.value = {
338 ...DEFAULT_UNIFIED_SETTINGS,
339 providerInstances: {
340 [customId]: { driver: ProviderDriverKind.make("codex"), enabled: true },
341 },
342 };
343 atoms.providers = [provider()];
344 const panel = renderPanel({ targetInstanceId: customId });
345 const editor = visitElements(panel, (element) => element.props.mode === "editor");
346 expect(editor?.props.instanceId).toBe(customId);
347 });
348
349 it.each([
350 [
351 "onFavoriteModelsChange",
352 { setModelFavorites: [{ provider: codexId, model: "chosen", favorite: true }] },
353 ],
354 [
355 "onHiddenModelsChange",
356 { setModelsHidden: [{ provider: codexId, model: "chosen", hidden: true }] },
357 ],
358 [
359 "onModelOrderChange",
360 { setProviderModelOrder: { provider: codexId, modelOrder: ["chosen"] } },
361 ],
362 ])("saves %s on the selected environment for every connected client", (action, expected) => {
363 atoms.providers = [provider()];
364 const panel = renderPanel();
365 const editor = visitElements(
366 panel,
367 (element) => element.props.instanceId === codexId && element.props.mode === "editor",
368 );
369 expect(editor).not.toBeNull();
370 if (!editor) throw new Error("Provider editor was not rendered");
371 (editor.props[action] as (models: string[]) => void)(["chosen"]);
372 expect(settingsState.updateSettings).toHaveBeenCalledExactlyOnceWith(expected);
373 expect(settingsState.updateClientSettings).not.toHaveBeenCalled();
374 });
375
376 it("does not substitute another account when the requested instance was removed", () => {
377 atoms.providers = [provider()];
378 const panel = renderPanel({ targetInstanceId: customId });
379 expect(visitElements(panel, (element) => element.props.mode === "editor")).toBeNull();
380 expect(settingsState.updateSettings).not.toHaveBeenCalled();
381 });
382
383 it("keeps provider selection available while write controls are read only", () => {
384 commands.canWriteSettings = false;
385 settingsState.value = {
386 ...DEFAULT_UNIFIED_SETTINGS,
387 providerInstances: {
388 [customId]: {
389 driver: ProviderDriverKind.make("codex"),
390 enabled: true,
391 },
392 },
393 };
394 atoms.providers = [provider()];
395 let panel = renderPanel({ readOnly: true });
396
397 const inertWrapper = visitElements(panel, (element) => element.props.inert === true);
398 expect(inertWrapper).not.toBeNull();
399
400 const customRow = visitElements(
401 panel,
402 (element) => element.props.instanceId === customId && element.props.mode === "list",
403 );
404 expect(customRow?.props.readOnly).toBe(true);
405 expect(customRow?.props.onSelect).toBeTypeOf("function");
406 (customRow?.props.onSelect as (() => void) | undefined)?.();
407
408 panel = renderPanel({ readOnly: true });
409 const customEditor = visitElements(
410 panel,
411 (element) => element.props.instanceId === customId && element.props.mode === "editor",
412 );
413 expect(customEditor).not.toBeNull();
414
415 const notice = visitElements(panel, (element) => element.props.title === "Limited permissions");
416 expect(notice).not.toBeNull();
417
418 expect(visitElements(panel, isRefreshButton)).not.toBeNull();
419 expect(visitElements(panel, isAddProviderButton)).toBeNull();
420 });
421
422 it("keeps the editable layout interactive when not read only", () => {
423 atoms.providers = [provider()];
424 const panel = renderPanel();
425 expect(visitElements(panel, (element) => element.props.inert === true)).toBeNull();
426 expect(
427 visitElements(panel, (element) => element.props.title === "Limited permissions"),
428 ).toBeNull();
429 expect(visitElements(panel, isRefreshButton)).not.toBeNull();
430 expect(visitElements(panel, isAddProviderButton)).not.toBeNull();
431 });
432
433 it("removes an open add-instance dialog when the provider grant is revoked", () => {
434 let panel = renderPanel();
435 const add = visitElements(panel, isAddProviderButton);
436 if (!add) throw new Error("Missing Add provider action.");
437 (add.props.onClick as () => void)();
438 panel = renderPanel();
439 expect(
440 visitElements(panel, (element) => element.type === AddProviderInstanceDialog),
441 ).not.toBeNull();
442
443 commands.canManageProviders = false;
444 panel = renderPanel({ readOnly: true });
445 expect(
446 visitElements(panel, (element) => element.type === AddProviderInstanceDialog),
447 ).toBeNull();
448 expect(settingsState.updateSettings).not.toHaveBeenCalled();
449 });
450
451 it("keeps Advanced visible when search targets the provider health interval", () => {
452 let panel = renderPanel();
453 expect(visitElements(panel, (element) => element.props.title === "Advanced")).not.toBeNull();
454 expect(
455 visitElements(panel, (element) => element.props.id === "provider-health-check-interval"),
456 ).not.toBeNull();
457
458 settingsSearchState.targetId = "provider-health-check-interval";
459 panel = renderPanel();
460 expect(visitElements(panel, (element) => element.props.title === "Advanced")).not.toBeNull();
461 expect(
462 visitElements(panel, (element) => element.props.id === "provider-health-check-interval"),
463 ).not.toBeNull();
464 });
465
466 it("deletes and resets provider configuration without erasing shared preferences", async () => {
467 settingsState.value = {
468 ...DEFAULT_UNIFIED_SETTINGS,
469 providerInstances: {
470 [codexId]: {
471 driver: ProviderDriverKind.make("codex"),
472 enabled: false,
473 },
474 [customId]: {
475 driver: ProviderDriverKind.make("codex"),
476 enabled: true,
477 },
478 },
479 providerModelPreferences: {
480 [customId]: { hiddenModels: ["hidden"], modelOrder: ["model"] },
481 },
482 favorites: [{ provider: customId, model: "favorite" }],
483 };
484 let panel = renderPanel();
485 const customRow = visitElements(
486 panel,
487 (element) => element.props.instanceId === customId && element.props.mode === "list",
488 );
489 (customRow?.props.onSelect as (() => void) | undefined)?.();
490 panel = renderPanel();
491 const customCard = visitElements(
492 panel,
493 (element) => element.props.instanceId === customId && element.props.mode === "editor",
494 );
495 expect(customCard).not.toBeNull();
496 (customCard?.props.onDelete as (() => void) | undefined)?.();
497 await flushPromises();
498
499 expect(settingsState.mutateProviderInstance).toHaveBeenLastCalledWith({
500 operation: "remove",
501 instanceId: customId,
502 });
503
504 settingsState.mutateProviderInstance.mockClear();
505 const defaultRow = visitElements(
506 panel,
507 (element) => element.props.instanceId === codexId && element.props.mode === "list",
508 );
509 (defaultRow?.props.onSelect as (() => void) | undefined)?.();
510 panel = renderPanel();
511 const defaultCard = visitElements(
512 panel,
513 (element) => element.props.instanceId === codexId && element.props.mode === "editor",
514 );
515 const resetAction = defaultCard?.props.headerAction;
516 const resetButton = visitElements(
517 resetAction,
518 (element) => typeof element.props.onClick === "function",
519 );
520 expect(resetButton).not.toBeNull();
521 (resetButton?.props.onClick as (() => void) | undefined)?.();
522 await flushPromises();
523
524 const [resetMutation, resetPatch] = settingsState.mutateProviderInstance.mock.lastCall ?? [];
525 expect(resetMutation).toEqual({ operation: "remove", instanceId: codexId });
526 // Removing the instance is the whole reset; shared preferences stay untouched.
527 expect(resetPatch ?? {}).toEqual({});
528 });
529
530 it("updates one provider instance without sending a stale whole map", async () => {
531 settingsState.value = {
532 ...DEFAULT_UNIFIED_SETTINGS,
533 providerInstances: {
534 [customId]: {
535 driver: ProviderDriverKind.make("codex"),
536 enabled: true,
537 displayName: "Work",
538 },
539 },
540 };
541 const panel = renderPanel();
542 const card = visitElements(panel, (element) => element.props.instanceId === customId);
543 const next = {
544 driver: ProviderDriverKind.make("codex"),
545 enabled: false,
546 displayName: "Work",
547 };
548 (card?.props.onUpdate as ((instance: typeof next) => void) | undefined)?.(next);
549 await flushPromises();
550
551 expect(settingsState.mutateProviderInstance).toHaveBeenCalledWith(
552 { operation: "upsert", instanceId: customId, instance: next },
553 {},
554 );
555 });
556
557 it("lets the server decide managed ACP cleanup after an atomic delete", async () => {
558 const firstId = ProviderInstanceId.make("acpRegistry_kilo_one");
559 const secondId = ProviderInstanceId.make("acpRegistry_kilo_two");
560 const registryInstance = {
561 driver: ProviderDriverKind.make("acpRegistry"),
562 enabled: true,
563 config: { agentId: "kilo" },
564 };
565 settingsState.value = {
566 ...DEFAULT_UNIFIED_SETTINGS,
567 providerInstances: {
568 [firstId]: registryInstance,
569 [secondId]: registryInstance,
570 },
571 };
572 let panel = renderPanel();
573 const row = visitElements(
574 panel,
575 (element) => element.props.instanceId === firstId && element.props.mode === "list",
576 );
577 (row?.props.onSelect as (() => void) | undefined)?.();
578 panel = renderPanel();
579 const card = visitElements(
580 panel,
581 (element) => element.props.instanceId === firstId && element.props.mode === "editor",
582 );
583 (card?.props.onDelete as (() => void) | undefined)?.();
584 await flushPromises();
585
586 expect(settingsState.mutateProviderInstance).toHaveBeenCalledWith({
587 operation: "remove",
588 instanceId: firstId,
589 });
590 expect(commands.uninstall).toHaveBeenCalledWith({
591 environmentId,
592 input: { agentId: "kilo" },
593 });
594 });
595
596 it("keeps the signed-in ACP account visible when login methods are no longer advertised", () => {
597 const instanceId = ProviderInstanceId.make("acpRegistry_devin");
598 settingsState.value = {
599 ...DEFAULT_UNIFIED_SETTINGS,
600 providerInstances: {
601 [instanceId]: {
602 driver: ProviderDriverKind.make("acpRegistry"),
603 enabled: true,
604 config: { agentId: "devin" },
605 },
606 },
607 };
608 atoms.providers = [
609 {
610 ...provider(),
611 instanceId,
612 driver: ProviderDriverKind.make("acpRegistry"),
613 auth: { status: "authenticated", canLogout: false },
614 setup: { canAuthenticate: false, canInstall: false },
615 },
616 ];
617 const panel = renderPanel({ targetInstanceId: instanceId });
618 expect(
619 visitElements(
620 panel,
621 (element) =>
622 typeof element.type === "function" &&
623 element.type.name === "ProviderAuthenticationSection",
624 ),
625 ).not.toBeNull();
626 });
627
628 it("routes explicit ACP browser authentication consent to the selected environment", async () => {
629 const instanceId = ProviderInstanceId.make("acpRegistry_antigravity");
630 const action = {
631 elicitationId: "google-login-1",
632 url: "https://accounts.google.com/login",
633 message: "Continue with Google",
634 };
635 settingsState.value = {
636 ...DEFAULT_UNIFIED_SETTINGS,
637 providerInstances: {
638 [instanceId]: {
639 driver: ProviderDriverKind.make("acpRegistry"),
640 enabled: true,
641 config: { agentId: "antigravity" },
642 },
643 },
644 };
645 atoms.providers = [
646 {
647 ...provider(),
648 instanceId,
649 driver: ProviderDriverKind.make("acpRegistry"),
650 auth: { status: "unauthenticated", action },
651 },
652 ];
653
654 const panel = renderPanel();
655 const card = visitElements(panel, (element) => element.props.instanceId === instanceId);
656 (card?.props.onAcceptUrlAuth as ((candidate: typeof action) => void) | undefined)?.(action);
657 await flushPromises();
658
659 expect(commands.acceptUrlAuth).toHaveBeenCalledWith({
660 environmentId,
661 input: { instanceId, elicitationId: action.elicitationId },
662 });
663 });
664});