| 1 | import type { ReactElement } from "react"; |
| 2 | import { |
| 3 | DEFAULT_UNIFIED_SETTINGS, |
| 4 | EnvironmentId, |
| 5 | ProviderDriverKind, |
| 6 | ProviderInstanceId, |
| 7 | type ServerProvider, |
| 8 | type UnifiedSettings, |
| 9 | } from "@t3tools/contracts"; |
| 10 | import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 11 | |
| 12 | import { visitElements } from "../../test/reactElementTree"; |
| 13 | import { reactHookHarness as hooks } from "../../test/reactHookHarness"; |
| 14 | |
| 15 | const 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 | |
| 24 | const 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 | |
| 33 | const 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 | |
| 43 | const settingsSearchState = vi.hoisted(() => ({ |
| 44 | targetId: null as string | null, |
| 45 | effects: [] as Array<() => void>, |
| 46 | })); |
| 47 | |
| 48 | vi.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 | |
| 61 | vi.mock("./settingsLayout", async (importOriginal) => { |
| 62 | const actual = await importOriginal<typeof import("./settingsLayout")>(); |
| 63 | return { |
| 64 | ...actual, |
| 65 | useSettingsSearchTargetId: () => settingsSearchState.targetId, |
| 66 | }; |
| 67 | }); |
| 68 | |
| 69 | vi.mock("./SettingsScopeSentence", () => ({ SettingsScopeSentence: () => null })); |
| 70 | vi.mock("react/compiler-runtime", async () => { |
| 71 | const { reactHookHarness } = await import("../../test/reactHookHarness"); |
| 72 | return { c: reactHookHarness.useMemoCache }; |
| 73 | }); |
| 74 | |
| 75 | vi.mock("@effect/atom-react", () => ({ |
| 76 | useAtomValue: () => atoms.providers, |
| 77 | })); |
| 78 | |
| 79 | vi.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 | |
| 90 | vi.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 | |
| 101 | vi.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 | |
| 119 | vi.mock("../../environments/primary", () => ({ |
| 120 | usePrimarySessionState: () => ({ data: null, error: null, isPending: false, refresh: vi.fn() }), |
| 121 | })); |
| 122 | |
| 123 | vi.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 | |
| 141 | vi.mock("../../state/entities", () => ({ |
| 142 | useProjects: () => [], |
| 143 | useServerConfigs: () => new Map(), |
| 144 | })); |
| 145 | |
| 146 | import { EnvironmentProviderSettings } from "./ProviderSettingsPanel"; |
| 147 | import { AddProviderInstanceDialog } from "./AddProviderInstanceDialog"; |
| 148 | |
| 149 | const environmentId = EnvironmentId.make("remote-device"); |
| 150 | const codexId = ProviderInstanceId.make("codex"); |
| 151 | const customId = ProviderInstanceId.make("codex_work"); |
| 152 | |
| 153 | function 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 | |
| 178 | function 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 | |
| 193 | function 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 | |
| 208 | function isAddProviderButton(element: ReactElement<Record<string, unknown>>): boolean { |
| 209 | return element.props["aria-label"] === "Add provider"; |
| 210 | } |
| 211 | |
| 212 | async function flushPromises(): Promise<void> { |
| 213 | await Promise.resolve(); |
| 214 | await Promise.resolve(); |
| 215 | } |
| 216 | |
| 217 | describe("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 | }); |