| 1 | import { selectSettingsValue, settingsOptionValues } from "./settingsSelectTestUtils"; |
| 2 | import catalogData from "../lib/providerCatalog.generated.json"; |
| 3 | // Run: tsx src/__tests__/provider-access-card.test.tsx |
| 4 | |
| 5 | import { JSDOM } from "jsdom"; |
| 6 | import React from "react"; |
| 7 | import { act } from "react"; |
| 8 | import { createRoot } from "react-dom/client"; |
| 9 | import { |
| 10 | AddProviderPanel, |
| 11 | ProviderAccessCard, |
| 12 | providerAccessGroups, |
| 13 | SettingsPanel, |
| 14 | type ProviderAccessGroup, |
| 15 | } from "../components/SettingsPanel"; |
| 16 | import { LocaleProvider } from "../lib/i18n"; |
| 17 | import type { AppBindings } from "../lib/bridge"; |
| 18 | import type { ProviderPresetView, ProviderView, SettingsView } from "../lib/types"; |
| 19 | import { baseSettings } from "../test-support/settingsTestFixtures"; |
| 20 | import { installDesktopHostStub } from "./desktopHostStub"; |
| 21 | |
| 22 | let passed = 0; |
| 23 | let failed = 0; |
| 24 | |
| 25 | function ok(value: boolean, label: string) { |
| 26 | if (value) { |
| 27 | process.stdout.write(` PASS ${label}\n`); |
| 28 | passed += 1; |
| 29 | } else { |
| 30 | process.stdout.write(` FAIL ${label}\n`); |
| 31 | failed += 1; |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | function flushPromises(): Promise<void> { |
| 36 | return new Promise((resolve) => setTimeout(resolve, 0)); |
| 37 | } |
| 38 | |
| 39 | const dom = new JSDOM("<!doctype html><html><body><div id=\"root\"></div></body></html>", { |
| 40 | pretendToBeVisual: true, |
| 41 | url: "http://localhost/", |
| 42 | }); |
| 43 | (globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; |
| 44 | globalThis.window = dom.window as unknown as Window & typeof globalThis; |
| 45 | globalThis.document = dom.window.document; |
| 46 | Object.defineProperty(globalThis, "navigator", { configurable: true, value: dom.window.navigator }); |
| 47 | globalThis.Node = dom.window.Node; |
| 48 | globalThis.HTMLElement = dom.window.HTMLElement; |
| 49 | globalThis.Event = dom.window.Event; |
| 50 | globalThis.CustomEvent = dom.window.CustomEvent; |
| 51 | globalThis.MouseEvent = dom.window.MouseEvent; |
| 52 | globalThis.localStorage = dom.window.localStorage; |
| 53 | globalThis.sessionStorage = dom.window.sessionStorage; |
| 54 | globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window); |
| 55 | globalThis.cancelAnimationFrame = dom.window.cancelAnimationFrame.bind(dom.window); |
| 56 | Object.defineProperty(dom.window.HTMLElement.prototype, "attachEvent", { configurable: true, value: () => undefined }); |
| 57 | Object.defineProperty(dom.window.HTMLElement.prototype, "detachEvent", { configurable: true, value: () => undefined }); |
| 58 | window.scrollTo = () => {}; |
| 59 | window.matchMedia = () => ({ |
| 60 | matches: true, |
| 61 | media: "", |
| 62 | onchange: null, |
| 63 | addListener: () => undefined, |
| 64 | removeListener: () => undefined, |
| 65 | addEventListener: () => undefined, |
| 66 | removeEventListener: () => undefined, |
| 67 | dispatchEvent: () => false, |
| 68 | }); |
| 69 | |
| 70 | const deepSeekAnthropic: ProviderView = { |
| 71 | name: "deepseek-flash", |
| 72 | builtIn: true, |
| 73 | added: true, |
| 74 | kind: "anthropic", |
| 75 | baseUrl: "https://api.deepseek.com/anthropic", |
| 76 | models: ["deepseek-v4-flash"], |
| 77 | visionModels: [], |
| 78 | visionModelsConfigured: true, |
| 79 | visionCapability: "unsupported", |
| 80 | modelsUrl: "", |
| 81 | default: "deepseek-v4-flash", |
| 82 | apiKeyEnv: "DEEPSEEK_API_KEY", |
| 83 | keySet: true, |
| 84 | configured: true, |
| 85 | balanceUrl: "", |
| 86 | contextWindow: 128_000, |
| 87 | reasoningProtocol: "deepseek", |
| 88 | thinking: "", |
| 89 | webSearch: true, |
| 90 | serverWebSearchCapability: true, |
| 91 | supportedEfforts: [], |
| 92 | defaultEffort: "", |
| 93 | }; |
| 94 | |
| 95 | const deepSeekLegacyOpenAI: ProviderView = { |
| 96 | ...deepSeekAnthropic, |
| 97 | name: "deepseek-pro", |
| 98 | kind: "openai", |
| 99 | baseUrl: "https://api.deepseek.com", |
| 100 | models: ["deepseek-v4-pro"], |
| 101 | default: "deepseek-v4-pro", |
| 102 | webSearch: false, |
| 103 | serverWebSearchCapability: false, |
| 104 | }; |
| 105 | |
| 106 | function group(providers: ProviderView[]): ProviderAccessGroup { |
| 107 | return { |
| 108 | id: "builtin:deepseek", |
| 109 | label: "DeepSeek Official", |
| 110 | description: "", |
| 111 | builtIn: true, |
| 112 | providers, |
| 113 | apiKeyEnv: "DEEPSEEK_API_KEY", |
| 114 | keySet: true, |
| 115 | requiresKey: true, |
| 116 | configured: true, |
| 117 | baseUrl: providers[0]?.baseUrl ?? "", |
| 118 | kind: providers[0]?.kind ?? "", |
| 119 | models: providers.flatMap((provider) => provider.models), |
| 120 | recommendedUpgradeAvailable: false, |
| 121 | }; |
| 122 | } |
| 123 | |
| 124 | function customGroup(provider: ProviderView): ProviderAccessGroup { |
| 125 | return { |
| 126 | ...group([provider]), |
| 127 | id: `custom:${provider.name}`, |
| 128 | label: provider.name, |
| 129 | builtIn: false, |
| 130 | }; |
| 131 | } |
| 132 | |
| 133 | function renderCard( |
| 134 | providerGroup: ProviderAccessGroup, |
| 135 | actions: { |
| 136 | onRefresh?: (provider: ProviderView) => void; |
| 137 | onDelete?: (providers: ProviderView[]) => Promise<void>; |
| 138 | } = {}, |
| 139 | ) { |
| 140 | return ( |
| 141 | <LocaleProvider> |
| 142 | <ProviderAccessCard |
| 143 | group={providerGroup} |
| 144 | busy={false} |
| 145 | fetching={false} |
| 146 | editing={null} |
| 147 | kinds={["anthropic", "openai"]} |
| 148 | onEdit={() => undefined} |
| 149 | onCancelEdit={() => undefined} |
| 150 | onSave={() => undefined} |
| 151 | onRefresh={actions.onRefresh ?? (() => undefined)} |
| 152 | onToggleDraftModel={() => undefined} |
| 153 | onToggleDraftVision={() => undefined} |
| 154 | onSelectAllDraftModels={() => undefined} |
| 155 | onClearDraftModels={() => undefined} |
| 156 | onCancelDraftModels={() => undefined} |
| 157 | onSaveDraftModels={() => undefined} |
| 158 | onToggleWebSearch={() => undefined} |
| 159 | onUpgradeRecommended={() => undefined} |
| 160 | onSaveEditorKey={async () => undefined} |
| 161 | onDelete={actions.onDelete} |
| 162 | /> |
| 163 | </LocaleProvider> |
| 164 | ); |
| 165 | } |
| 166 | |
| 167 | console.log("\nprovider access card"); |
| 168 | |
| 169 | const rootEl = document.getElementById("root"); |
| 170 | if (!rootEl) throw new Error("missing root"); |
| 171 | const root = createRoot(rootEl); |
| 172 | |
| 173 | await act(async () => { |
| 174 | root.render(renderCard(group([deepSeekAnthropic, deepSeekLegacyOpenAI]))); |
| 175 | await flushPromises(); |
| 176 | }); |
| 177 | ok( |
| 178 | rootEl.querySelector<HTMLInputElement>('input[role="switch"]') === null, |
| 179 | "mixed supported and unsupported profiles hide the grouped server web-search switch", |
| 180 | ); |
| 181 | ok( |
| 182 | rootEl.textContent?.includes("deepseek-v4-flash") === true |
| 183 | && rootEl.textContent?.includes("deepseek-v4-pro") === true, |
| 184 | "mixed-profile cards keep the complete model summary visible", |
| 185 | ); |
| 186 | |
| 187 | let refreshedProvider = ""; |
| 188 | await act(async () => { |
| 189 | root.render(renderCard(group([deepSeekAnthropic, deepSeekLegacyOpenAI]), { |
| 190 | onRefresh: (provider) => { |
| 191 | refreshedProvider = provider.name; |
| 192 | }, |
| 193 | })); |
| 194 | await flushPromises(); |
| 195 | }); |
| 196 | const refreshButtons = Array.from(rootEl.querySelectorAll("button")) |
| 197 | .filter((button) => button.textContent?.trim() === "Refresh models") as HTMLButtonElement[]; |
| 198 | ok(refreshButtons.length === 2, "multi-profile cards expose one model refresh action per profile"); |
| 199 | await act(async () => { |
| 200 | refreshButtons[1]?.click(); |
| 201 | await flushPromises(); |
| 202 | }); |
| 203 | ok(refreshedProvider === "deepseek-pro", "profile refresh targets the selected provider instead of the first profile"); |
| 204 | |
| 205 | let removedProviders: string[] = []; |
| 206 | await act(async () => { |
| 207 | root.render(renderCard(group([deepSeekAnthropic, deepSeekLegacyOpenAI]), { |
| 208 | onDelete: async (providers) => { |
| 209 | removedProviders = providers.map((provider) => provider.name); |
| 210 | }, |
| 211 | })); |
| 212 | await flushPromises(); |
| 213 | }); |
| 214 | const moreButton = rootEl.querySelector<HTMLButtonElement>('button[aria-haspopup="menu"]'); |
| 215 | await act(async () => { |
| 216 | moreButton?.click(); |
| 217 | await flushPromises(); |
| 218 | }); |
| 219 | let removeButton = Array.from(document.querySelectorAll("button")) |
| 220 | .find((button) => button.textContent?.trim() === "Remove access") as HTMLButtonElement | undefined; |
| 221 | await act(async () => { |
| 222 | removeButton?.click(); |
| 223 | await flushPromises(); |
| 224 | }); |
| 225 | removeButton = Array.from(document.querySelectorAll("button")) |
| 226 | .find((button) => button.textContent?.trim() === "Confirm remove access") as HTMLButtonElement | undefined; |
| 227 | ok(removeButton !== undefined, "grouped provider removal requires inline confirmation"); |
| 228 | await act(async () => { |
| 229 | removeButton?.click(); |
| 230 | await flushPromises(); |
| 231 | }); |
| 232 | ok( |
| 233 | removedProviders.join(",") === "deepseek-flash,deepseek-pro", |
| 234 | "card-level removal submits every grouped provider in one action", |
| 235 | ); |
| 236 | |
| 237 | let removedCustomProviders: string[] = []; |
| 238 | await act(async () => { |
| 239 | root.render(renderCard(customGroup({ ...deepSeekAnthropic, name: "my-proxy", builtIn: false }), { |
| 240 | onDelete: async (providers) => { |
| 241 | removedCustomProviders = providers.map((provider) => provider.name); |
| 242 | }, |
| 243 | })); |
| 244 | await flushPromises(); |
| 245 | }); |
| 246 | const defaultCustomMoreButton = rootEl.querySelector<HTMLButtonElement>('button[aria-haspopup="menu"]'); |
| 247 | ok(defaultCustomMoreButton?.disabled === false, "configured custom providers keep the removal menu available"); |
| 248 | await act(async () => { |
| 249 | defaultCustomMoreButton?.click(); |
| 250 | await flushPromises(); |
| 251 | }); |
| 252 | let deleteDefaultCustomButton = Array.from(document.querySelectorAll("button")) |
| 253 | .find((button) => button.textContent?.trim() === "Remove access") as HTMLButtonElement | undefined; |
| 254 | await act(async () => { |
| 255 | deleteDefaultCustomButton?.click(); |
| 256 | await flushPromises(); |
| 257 | }); |
| 258 | deleteDefaultCustomButton = Array.from(document.querySelectorAll("button")) |
| 259 | .find((button) => button.textContent?.trim() === "Confirm delete provider") as HTMLButtonElement | undefined; |
| 260 | ok(deleteDefaultCustomButton !== undefined, "configured custom provider can be confirmed for deletion"); |
| 261 | await act(async () => { |
| 262 | deleteDefaultCustomButton?.click(); |
| 263 | await flushPromises(); |
| 264 | }); |
| 265 | ok( |
| 266 | removedCustomProviders.join(",") === "my-proxy", |
| 267 | "configured custom provider removal submits the selected provider", |
| 268 | ); |
| 269 | |
| 270 | const defaultCustomSettings: SettingsView = baseSettings("standard"); |
| 271 | const defaultCustomProvider: ProviderView = { |
| 272 | ...deepSeekAnthropic, |
| 273 | name: "my-proxy", |
| 274 | builtIn: false, |
| 275 | added: true, |
| 276 | kind: "openai", |
| 277 | baseUrl: "https://proxy.example/v1", |
| 278 | models: ["my-model"], |
| 279 | default: "my-model", |
| 280 | apiKeyEnv: "", |
| 281 | keySet: true, |
| 282 | configured: true, |
| 283 | webSearch: false, |
| 284 | serverWebSearchCapability: false, |
| 285 | }; |
| 286 | defaultCustomSettings.defaultModel = "my-proxy/my-model"; |
| 287 | defaultCustomSettings.providers = [defaultCustomProvider]; |
| 288 | defaultCustomSettings.providerKinds = ["openai"]; |
| 289 | let removedDefaultCustomProviders: string[] = []; |
| 290 | installDesktopHostStub(({ |
| 291 | main: { |
| 292 | App: { |
| 293 | Settings: async () => defaultCustomSettings, |
| 294 | ApplyModelSettings: async (change) => { |
| 295 | if (change.kind !== "access_remove") throw new Error("unexpected settings operation"); |
| 296 | const names = change.names; |
| 297 | removedDefaultCustomProviders = [...names]; |
| 298 | defaultCustomSettings.providers = defaultCustomSettings.providers.filter((provider) => !names.includes(provider.name)); |
| 299 | return {requestId: change.requestId, persisted: true, revision: "removed", application: "not_required", targets: [], issues: [], appliedCatalogs: []}; |
| 300 | }, |
| 301 | } as Partial<AppBindings> as AppBindings, |
| 302 | }, |
| 303 | }).main.App); |
| 304 | const settingsRootEl = document.createElement("div"); |
| 305 | document.body.appendChild(settingsRootEl); |
| 306 | const settingsRoot = createRoot(settingsRootEl); |
| 307 | await act(async () => { |
| 308 | settingsRoot.render( |
| 309 | <LocaleProvider> |
| 310 | <SettingsPanel |
| 311 | initialTab="models" |
| 312 | initialFocus={{ target: "model-access", requestId: 1 }} |
| 313 | desktopPlatform="linux" |
| 314 | onClose={() => undefined} |
| 315 | onChanged={() => undefined} |
| 316 | onUseSubagent={() => undefined} |
| 317 | /> |
| 318 | </LocaleProvider>, |
| 319 | ); |
| 320 | await flushPromises(); |
| 321 | await flushPromises(); |
| 322 | }); |
| 323 | const defaultCustomSettingsMoreButton = settingsRootEl.querySelector<HTMLButtonElement>('button[aria-haspopup="menu"]'); |
| 324 | ok( |
| 325 | defaultCustomSettingsMoreButton?.disabled === false, |
| 326 | "current default custom provider keeps the removal menu available", |
| 327 | ); |
| 328 | await act(async () => { |
| 329 | defaultCustomSettingsMoreButton?.click(); |
| 330 | await flushPromises(); |
| 331 | }); |
| 332 | let deleteCurrentDefaultButton = Array.from(document.querySelectorAll("button")) |
| 333 | .find((button) => button.textContent?.trim() === "Remove access") as HTMLButtonElement | undefined; |
| 334 | await act(async () => { |
| 335 | deleteCurrentDefaultButton?.click(); |
| 336 | await flushPromises(); |
| 337 | }); |
| 338 | deleteCurrentDefaultButton = Array.from(document.querySelectorAll("button")) |
| 339 | .find((button) => button.textContent?.trim() === "Confirm delete provider") as HTMLButtonElement | undefined; |
| 340 | ok(deleteCurrentDefaultButton !== undefined, "current default custom provider can be confirmed for deletion"); |
| 341 | await act(async () => { |
| 342 | deleteCurrentDefaultButton?.click(); |
| 343 | await flushPromises(); |
| 344 | await flushPromises(); |
| 345 | }); |
| 346 | ok( |
| 347 | removedDefaultCustomProviders.join(",") === "my-proxy", |
| 348 | "current default custom provider removal reaches the settings backend", |
| 349 | ); |
| 350 | await act(async () => { |
| 351 | settingsRoot.unmount(); |
| 352 | }); |
| 353 | settingsRootEl.remove(); |
| 354 | |
| 355 | await act(async () => { |
| 356 | root.render(renderCard(group([ |
| 357 | deepSeekAnthropic, |
| 358 | { ...deepSeekAnthropic, name: "deepseek-pro", models: ["deepseek-v4-pro"], default: "deepseek-v4-pro", webSearch: false }, |
| 359 | ]))); |
| 360 | await flushPromises(); |
| 361 | }); |
| 362 | const mixedStateSwitch = rootEl.querySelector<HTMLInputElement>('input[role="switch"]'); |
| 363 | ok(mixedStateSwitch !== null, "all supported grouped profiles expose server web search"); |
| 364 | ok(mixedStateSwitch?.checked === false, "grouped switch is off until every profile has web search enabled"); |
| 365 | |
| 366 | await act(async () => { |
| 367 | root.render(renderCard(group([ |
| 368 | deepSeekAnthropic, |
| 369 | { ...deepSeekAnthropic, name: "deepseek-pro", models: ["deepseek-v4-pro"], default: "deepseek-v4-pro" }, |
| 370 | ]))); |
| 371 | await flushPromises(); |
| 372 | }); |
| 373 | ok( |
| 374 | rootEl.querySelector<HTMLInputElement>('input[role="switch"]')?.checked === true, |
| 375 | "grouped switch is on when every supported profile has web search enabled", |
| 376 | ); |
| 377 | |
| 378 | await act(async () => { |
| 379 | root.render( |
| 380 | <LocaleProvider> |
| 381 | <AddProviderPanel |
| 382 | key="official-installed" |
| 383 | mode="official" |
| 384 | kinds={["anthropic", "openai"]} |
| 385 | officialProviders={[{ ...deepSeekAnthropic, name: "deepseek" }]} |
| 386 | providerPresets={[]} |
| 387 | busy={false} |
| 388 | onMode={() => undefined} |
| 389 | onCancel={() => undefined} |
| 390 | onAddOfficial={async () => undefined} |
| 391 | onAddPreset={async () => undefined} |
| 392 | onViewPresetConflict={() => undefined} |
| 393 | onResetPreset={async () => undefined} |
| 394 | onAddCustom={() => undefined} |
| 395 | /> |
| 396 | </LocaleProvider>, |
| 397 | ); |
| 398 | await flushPromises(); |
| 399 | }); |
| 400 | ok(rootEl.querySelector<HTMLButtonElement>(".provider-catalog__detail .btn--primary")?.disabled === false, "installed brands allow independent new connections"); |
| 401 | ok(rootEl.querySelector(".provider-catalog__detail")?.textContent?.includes("API Key") === true, "new connection has its own credential field"); |
| 402 | |
| 403 | const openCodePreset = ( |
| 404 | id: string, |
| 405 | label: string, |
| 406 | providerNames: string[], |
| 407 | displayTier: "primary" | "advanced" | "compatibility", |
| 408 | displaySection: "go" | "zen" = "go", |
| 409 | ): ProviderPresetView => ({ |
| 410 | catalog: (catalogData.catalogs as Record<string, NonNullable<ProviderPresetView["catalog"]>>)[id], |
| 411 | id, |
| 412 | label, |
| 413 | description: `${label} description`, |
| 414 | keyEnv: displaySection === "zen" ? "OPENCODE_API_KEY" : "OPENCODE_GO_API_KEY", |
| 415 | recommended: id === "opencode-go-recommended", |
| 416 | displayGroup: "opencode", |
| 417 | displaySection, |
| 418 | displayTier, |
| 419 | routeKind: id === "opencode-go-recommended" ? "bundle" : "anthropic", |
| 420 | providerNames, |
| 421 | models: ["model"], |
| 422 | added: false, |
| 423 | status: "available", |
| 424 | statusProviderNames: [], |
| 425 | keySet: false, |
| 426 | requiresKey: true, |
| 427 | configured: false, |
| 428 | }); |
| 429 | |
| 430 | await act(async () => { |
| 431 | root.render( |
| 432 | <LocaleProvider> |
| 433 | <AddProviderPanel |
| 434 | key="opencode-quick-setup" |
| 435 | mode="official" |
| 436 | kinds={["anthropic", "openai", "responses"]} |
| 437 | officialProviders={[{ ...deepSeekAnthropic, name: "deepseek" }]} |
| 438 | providerPresets={[ |
| 439 | openCodePreset("opencode-go-recommended", "OpenCode Go (Recommended)", ["opencode-go", "opencode-go-anthropic", "opencode-go-responses"], "primary"), |
| 440 | openCodePreset("opencode-go-anthropic", "OpenCode Go Anthropic", ["opencode-go-anthropic"], "advanced"), |
| 441 | openCodePreset("opencode-go-responses", "OpenCode Go Responses", ["opencode-go-responses"], "advanced"), |
| 442 | openCodePreset("opencode-go", "OpenCode Go Chat", ["opencode-go"], "compatibility"), |
| 443 | openCodePreset("opencode-zen-anthropic", "OpenCode Zen", ["opencode-zen-anthropic"], "primary", "zen"), |
| 444 | ]} |
| 445 | busy={false} |
| 446 | onMode={() => undefined} |
| 447 | onCancel={() => undefined} |
| 448 | onAddOfficial={async () => undefined} |
| 449 | onAddPreset={async () => undefined} |
| 450 | onViewPresetConflict={() => undefined} |
| 451 | onResetPreset={async () => undefined} |
| 452 | onAddCustom={() => undefined} |
| 453 | /> |
| 454 | </LocaleProvider>, |
| 455 | ); |
| 456 | await flushPromises(); |
| 457 | }); |
| 458 | const brandButtons = rootEl.querySelectorAll(".provider-catalog__brand"); |
| 459 | ok(brandButtons.length === 2, "DeepSeek and all OpenCode products render as two brands"); |
| 460 | ok(Array.from(brandButtons).some(button => button.textContent?.includes("OpenCode")), "OpenCode has one selectable brand entry"); |
| 461 | ok(Array.from(brandButtons).every(button => !button.textContent?.includes("Anthropic")), "protocols never appear as top-level brands"); |
| 462 | await act(async () => { (Array.from(brandButtons).find(button => button.textContent?.includes("OpenCode")) as HTMLButtonElement).click(); }); |
| 463 | const products = rootEl.querySelector<HTMLButtonElement>('button.settings-select[id$="-product"]'); |
| 464 | ok((products ? await settingsOptionValues(products) : []).join(",") === "go,zen", "Go and Zen remain distinct access plans within OpenCode"); |
| 465 | if (products) await selectSettingsValue(products, "zen"); |
| 466 | ok(rootEl.querySelector<HTMLInputElement>('input[placeholder*="OPENCODE_API_KEY"]') !== null, "selecting Zen changes the credential reference"); |
| 467 | ok(rootEl.querySelector<HTMLButtonElement>('button.settings-select[id$="-format"]')?.value === "anthropic", "Zen selects its supported API format"); |
| 468 | |
| 469 | const openCodeProviders: ProviderView[] = [ |
| 470 | { ...deepSeekAnthropic, builtIn: false, name: "opencode-go", kind: "openai", baseUrl: "https://opencode.ai/zen/go/v1", apiKeyEnv: "OPENCODE_GO_API_KEY", webSearch: false }, |
| 471 | { ...deepSeekAnthropic, builtIn: false, name: "opencode-go-anthropic", baseUrl: "https://opencode.ai/zen/go", apiKeyEnv: "OPENCODE_GO_API_KEY", webSearch: false }, |
| 472 | { ...deepSeekAnthropic, builtIn: false, name: "opencode-go-responses", kind: "responses", baseUrl: "https://opencode.ai/zen/go/v1", apiKeyEnv: "OPENCODE_GO_API_KEY", webSearch: false }, |
| 473 | ]; |
| 474 | const openCodeGroups = providerAccessGroups(openCodeProviders, ((key: string) => key) as never); |
| 475 | ok(openCodeGroups.length === 1, "installed OpenCode Go routes render as one product connection"); |
| 476 | ok(openCodeGroups[0]?.id === "custom:opencode-go", "OpenCode Go connection has a stable product-level identity"); |
| 477 | |
| 478 | await act(async () => { |
| 479 | root.render(renderCard(openCodeGroups[0])); |
| 480 | await flushPromises(); |
| 481 | }); |
| 482 | const routeSettings = rootEl.querySelector<HTMLDetailsElement>("details.provider-route-settings"); |
| 483 | ok(routeSettings !== null && routeSettings.open === false, "installed OpenCode Go keeps protocol routes collapsed by default"); |
| 484 | ok( |
| 485 | routeSettings?.querySelector("summary")?.textContent?.trim() === "Advanced route settings", |
| 486 | "installed OpenCode Go exposes route controls only through an advanced disclosure", |
| 487 | ); |
| 488 | |
| 489 | let removedOpenCodeProviders: string[] = []; |
| 490 | await act(async () => { |
| 491 | root.render(renderCard(openCodeGroups[0], { |
| 492 | onDelete: async (providers) => { |
| 493 | removedOpenCodeProviders = providers.map((provider) => provider.name); |
| 494 | }, |
| 495 | })); |
| 496 | await flushPromises(); |
| 497 | }); |
| 498 | const openCodeMoreButton = rootEl.querySelector<HTMLButtonElement>('button[aria-haspopup="menu"]'); |
| 499 | await act(async () => { |
| 500 | openCodeMoreButton?.click(); |
| 501 | await flushPromises(); |
| 502 | }); |
| 503 | let removeOpenCodeButton = Array.from(document.querySelectorAll("button")) |
| 504 | .find((button) => button.textContent?.trim() === "Remove access") as HTMLButtonElement | undefined; |
| 505 | await act(async () => { |
| 506 | removeOpenCodeButton?.click(); |
| 507 | await flushPromises(); |
| 508 | }); |
| 509 | removeOpenCodeButton = Array.from(document.querySelectorAll("button")) |
| 510 | .find((button) => button.textContent?.trim() === "Confirm delete provider") as HTMLButtonElement | undefined; |
| 511 | await act(async () => { |
| 512 | removeOpenCodeButton?.click(); |
| 513 | await flushPromises(); |
| 514 | }); |
| 515 | ok( |
| 516 | removedOpenCodeProviders.join(",") === "opencode-go,opencode-go-anthropic,opencode-go-responses", |
| 517 | "OpenCode Go group removal submits every installed route atomically", |
| 518 | ); |
| 519 | |
| 520 | await act(async () => { |
| 521 | root.unmount(); |
| 522 | }); |
| 523 | dom.window.close(); |
| 524 | |
| 525 | console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`); |
| 526 | if (failed > 0) process.exit(1); |
| 527 |