diff --git a/landing/components/common/RobotSpeechBubble.vue b/landing/components/common/RobotSpeechBubble.vue index f8974ab5..7f04eb18 100644 --- a/landing/components/common/RobotSpeechBubble.vue +++ b/landing/components/common/RobotSpeechBubble.vue @@ -44,8 +44,11 @@ const bubblePath = computed(() => { position: var(--robot-bubble-position, relative); z-index: var(--robot-bubble-z-index, auto); display: inline-grid; + width: max-content; + inline-size: max-content; min-width: var(--robot-bubble-min-width, 86px); max-width: var(--robot-bubble-max-width, 184px); + max-inline-size: var(--robot-bubble-max-width, 184px); min-height: var(--robot-bubble-min-height, 42px); box-sizing: border-box; color: var(--robot-bubble-color, #07111d); @@ -85,12 +88,15 @@ const bubblePath = computed(() => { justify-self: stretch; box-sizing: border-box; min-width: 0; + width: 100%; + max-width: 100%; padding: var(--robot-bubble-padding, 8px 16px 16px); text-align: center; white-space: normal; - overflow-wrap: anywhere; + word-break: normal; + overflow-wrap: break-word; hyphens: auto; - text-wrap: balance; + text-wrap: normal; } .robot-speech-bubble--tail-right .robot-speech-bubble__text { diff --git a/src/features/workspace-trust/main/infrastructure/workspaceTrustPreflightEnv.ts b/src/features/workspace-trust/main/infrastructure/workspaceTrustPreflightEnv.ts index b1c523f6..dfe93220 100644 --- a/src/features/workspace-trust/main/infrastructure/workspaceTrustPreflightEnv.ts +++ b/src/features/workspace-trust/main/infrastructure/workspaceTrustPreflightEnv.ts @@ -14,6 +14,7 @@ const EXACT_STRIP_ENV_KEYS = new Set([ 'CLAUDE_CODE_GEMINI_BACKEND', 'CLAUDE_CODE_CODEX_BACKEND', 'CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH', + 'OPENCODE_BIN_PATH', 'CODEX_HOME', ]); diff --git a/src/main/index.ts b/src/main/index.ts index c7457eef..07354a5a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -342,6 +342,25 @@ function describeMemberWorkSyncReviewPickupEscalationReason(reason: string): str return 'The current review request is still waiting for explicit review pickup.'; } +async function resolveOpenCodeRuntimeBinaryForBridgeEnv(): Promise { + const manifestBinaryPath = await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath(); + if (manifestBinaryPath) { + return manifestBinaryPath; + } + + try { + const status = await openCodeRuntimeInstallerService?.getStatus(); + return status?.installed === true && status.binaryPath ? status.binaryPath : null; + } catch (error) { + logger.warn( + `[OpenCode] Runtime installer status unavailable while resolving bridge binary: ${ + error instanceof Error ? error.message : String(error) + }` + ); + return null; + } +} + async function createOpenCodeRuntimeAdapterRegistry( reportProgress: (phase: string, message: string) => void = () => undefined ): Promise { @@ -416,7 +435,7 @@ async function createOpenCodeRuntimeAdapterRegistry( await ensureOpenCodeBridgeRuntimeBinaryEnv({ targetEnv, bridgeEnv, - resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath, + resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath: resolveOpenCodeRuntimeBinaryForBridgeEnv, onWarning: (message) => logger.warn(message), }); }; diff --git a/src/main/services/runtime/openCodeBridgeRuntimeEnv.ts b/src/main/services/runtime/openCodeBridgeRuntimeEnv.ts index c2c9eb97..b74e9c13 100644 --- a/src/main/services/runtime/openCodeBridgeRuntimeEnv.ts +++ b/src/main/services/runtime/openCodeBridgeRuntimeEnv.ts @@ -1,6 +1,10 @@ import { getErrorMessage } from '@shared/utils/errorHandling'; -import { applyOpenCodeRuntimeBinaryEnv } from './openCodeRuntimeBinaryEnv'; +import { + applyOpenCodeRuntimeBinaryEnv, + OPENCODE_LEGACY_BINARY_PATH_ENV, + OPENCODE_RUNTIME_BINARY_PATH_ENV, +} from './openCodeRuntimeBinaryEnv'; export interface EnsureOpenCodeBridgeRuntimeBinaryEnvOptions { targetEnv: NodeJS.ProcessEnv; @@ -15,7 +19,10 @@ export async function ensureOpenCodeBridgeRuntimeBinaryEnv({ resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath, onWarning, }: EnsureOpenCodeBridgeRuntimeBinaryEnvOptions): Promise { - if (targetEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH?.trim()) { + if ( + targetEnv[OPENCODE_RUNTIME_BINARY_PATH_ENV]?.trim() || + targetEnv[OPENCODE_LEGACY_BINARY_PATH_ENV]?.trim() + ) { applyOpenCodeRuntimeBinaryEnv(targetEnv, null); return; } @@ -25,10 +32,10 @@ export async function ensureOpenCodeBridgeRuntimeBinaryEnv({ applyOpenCodeRuntimeBinaryEnv(targetEnv, appManagedOpenCodeBinary); if ( targetEnv !== bridgeEnv && - targetEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH && - !bridgeEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH + targetEnv[OPENCODE_RUNTIME_BINARY_PATH_ENV] && + !bridgeEnv[OPENCODE_RUNTIME_BINARY_PATH_ENV] ) { - applyOpenCodeRuntimeBinaryEnv(bridgeEnv, targetEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH); + applyOpenCodeRuntimeBinaryEnv(bridgeEnv, targetEnv[OPENCODE_RUNTIME_BINARY_PATH_ENV]); } } catch (error) { onWarning?.( diff --git a/src/main/services/runtime/openCodeRuntimeBinaryEnv.ts b/src/main/services/runtime/openCodeRuntimeBinaryEnv.ts index 0e668903..08558262 100644 --- a/src/main/services/runtime/openCodeRuntimeBinaryEnv.ts +++ b/src/main/services/runtime/openCodeRuntimeBinaryEnv.ts @@ -1,6 +1,7 @@ import path from 'node:path'; export const OPENCODE_RUNTIME_BINARY_PATH_ENV = 'CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH'; +export const OPENCODE_LEGACY_BINARY_PATH_ENV = 'OPENCODE_BIN_PATH'; function normalizePathEntryForCompare(value: string): string { const normalized = path.resolve(value.trim()); @@ -33,7 +34,9 @@ export function applyOpenCodeRuntimeBinaryEnv( discoveredBinaryPath: string | null | undefined ): void { const existingBinaryPath = env[OPENCODE_RUNTIME_BINARY_PATH_ENV]?.trim(); - const nextBinaryPath = existingBinaryPath || discoveredBinaryPath?.trim() || ''; + const existingLegacyBinaryPath = env[OPENCODE_LEGACY_BINARY_PATH_ENV]?.trim(); + const nextBinaryPath = + existingBinaryPath || existingLegacyBinaryPath || discoveredBinaryPath?.trim() || ''; if (!nextBinaryPath) { return; } @@ -41,6 +44,9 @@ export function applyOpenCodeRuntimeBinaryEnv( if (!existingBinaryPath) { env[OPENCODE_RUNTIME_BINARY_PATH_ENV] = nextBinaryPath; } + if (!existingLegacyBinaryPath) { + env[OPENCODE_LEGACY_BINARY_PATH_ENV] = nextBinaryPath; + } if (!path.isAbsolute(nextBinaryPath)) { return; @@ -48,6 +54,8 @@ export function applyOpenCodeRuntimeBinaryEnv( // Facts: // - The app-managed OpenCode status is resolved from the app runtime manifest. + // - Released claude-multimodel builds have used both OPENCODE_BIN_PATH and + // CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH while the managed runtime path evolved. // - Older claude-multimodel readiness inventory still resolves "opencode" through PATH. // - Exposing the selected binary directory keeps both checks on the same runtime. prependPathEntry(env, path.dirname(nextBinaryPath)); diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index 81e6a970..c6d09c91 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -588,15 +588,13 @@ function shouldShowOpenCodeInstallAction( showSkeleton: boolean, openCodeRuntimeStatus: OpenCodeRuntimeStatus | null ): boolean { - return ( - provider.providerId === 'opencode' && - !showSkeleton && - !provider.supported && - !provider.authenticated && - provider.backend == null && - openCodeRuntimeStatus?.source !== 'path' && - !(openCodeRuntimeStatus?.source === 'app-managed' && openCodeRuntimeStatus.state !== 'failed') - ); + const runtimeReady = + openCodeRuntimeStatus?.installed === true && + (openCodeRuntimeStatus.source === 'path' || + (openCodeRuntimeStatus.source === 'app-managed' && openCodeRuntimeStatus.state !== 'failed')); + const runtimeNeedsInstall = !runtimeReady; + + return provider.providerId === 'opencode' && !showSkeleton && runtimeNeedsInstall; } function shouldShowCodexInstallAction( @@ -1054,7 +1052,7 @@ const InstalledBanner = ({ title={ openCodeRuntimeStatus?.error ?? openCodeRuntimeStatus?.progress?.detail ?? - 'Install OpenCode CLI into app data' + 'Install OpenCode runtime into app data' } > {isRuntimeInstalling( diff --git a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx index a0be7dfe..280c6a6e 100644 --- a/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx +++ b/src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx @@ -690,6 +690,13 @@ export function getProvisioningFailureHint( if (combined.includes('provider is not configured for runtime use')) { return 'Configure the selected provider runtime, then reopen this dialog.'; } + if ( + combined.includes('opencode cli not detected on path') || + combined.includes('opencode cli not found') || + combined.includes('opencode runtime binary is not installed') + ) { + return 'Install or retry OpenCode runtime from the provider status card, then reopen this dialog.'; + } if ( combined.includes('spawn ') || combined.includes(' enoent') || diff --git a/src/renderer/components/team/dialogs/TeamModelSelector.tsx b/src/renderer/components/team/dialogs/TeamModelSelector.tsx index 605a364e..39978124 100644 --- a/src/renderer/components/team/dialogs/TeamModelSelector.tsx +++ b/src/renderer/components/team/dialogs/TeamModelSelector.tsx @@ -640,7 +640,7 @@ export const TeamModelSelector: React.FC = ({ return ( providerStatus.detailMessage ?? providerStatus.statusMessage ?? - 'OpenCode CLI is not installed.' + 'OpenCode runtime is not installed.' ); } if (!providerStatus.authenticated) { diff --git a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts index 5940f46a..ff6ffa90 100644 --- a/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts +++ b/src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts @@ -440,11 +440,26 @@ function createRuntimeWarningLines(result: TeamProvisioningPrepareResult): strin return uniquePrepareLines(result.warnings ?? []); } +function normalizeRuntimeFailureDetailLine(detail: string | null | undefined): string | null { + const trimmed = detail?.trim(); + if (!trimmed) { + return null; + } + + if (/opencode cli (?:not detected on path|not found)/i.test(trimmed)) { + return 'OpenCode runtime binary is not installed or not reachable by launch preflight.'; + } + + return trimmed; +} + function createRuntimeFailureDetailLines( runtimeDetailLines: readonly string[], message: string | null | undefined ): string[] { - return uniquePrepareLines([...runtimeDetailLines, message]); + return uniquePrepareLines( + [...runtimeDetailLines, message].map(normalizeRuntimeFailureDetailLine).filter(Boolean) + ); } function extractTimedOutPreflightProbeModelId(detail: string): string | null { diff --git a/test/features/workspace-trust/main/workspaceTrustPreflightEnv.test.ts b/test/features/workspace-trust/main/workspaceTrustPreflightEnv.test.ts index 4d6eacda..09235d8a 100644 --- a/test/features/workspace-trust/main/workspaceTrustPreflightEnv.test.ts +++ b/test/features/workspace-trust/main/workspaceTrustPreflightEnv.test.ts @@ -15,7 +15,8 @@ describe('workspaceTrustPreflightEnv', () => { CLAUDE_TEAM_CONTROL_URL: 'http://127.0.0.1:1234', CLAUDE_TEAM_ANTHROPIC_AUTH_MODE: 'api_key_helper', CLAUDE_TEAM_ANTHROPIC_AUTH_MODE_API_KEY_HELPER: '1', - CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH: '/tmp/helper-settings.json', + CLAUDE_TEAM_ANTHROPIC_API_KEY_HELPER_SETTINGS_PATH: + '/Users/tester/helper-settings.json', CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: '1', CLAUDE_CODE_ENTRY_PROVIDER: 'codex', CLAUDE_CODE_USE_OPENAI: '1', @@ -25,10 +26,11 @@ describe('workspaceTrustPreflightEnv', () => { CLAUDE_CODE_USE_GEMINI: '1', CLAUDE_CODE_CODEX_BACKEND: 'codex-native', CLAUDE_CODE_GEMINI_BACKEND: 'api', - CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH: '/tmp/opencode', - CODEX_HOME: '/tmp/codex-home', - AGENT_TEAMS_RUNTIME_TURN_SETTLED_SPOOL_ROOT: '/tmp/spool', - AGENT_TEAMS_MCP_CLAUDE_DIR: '/tmp/claude-dir', + CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH: '/Users/tester/bin/opencode', + OPENCODE_BIN_PATH: '/Users/tester/bin/opencode', + CODEX_HOME: '/Users/tester/codex-home', + AGENT_TEAMS_RUNTIME_TURN_SETTLED_SPOOL_ROOT: '/Users/tester/spool', + AGENT_TEAMS_MCP_CLAUDE_DIR: '/Users/tester/claude-dir', CLAUDE_TEAM_BOOTSTRAP_TOKEN: 'bootstrap-token', }); @@ -55,6 +57,7 @@ describe('workspaceTrustPreflightEnv', () => { expect(env.CLAUDE_CODE_CODEX_BACKEND).toBeUndefined(); expect(env.CLAUDE_CODE_GEMINI_BACKEND).toBeUndefined(); expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined(); + expect(env.OPENCODE_BIN_PATH).toBeUndefined(); expect(env.CODEX_HOME).toBeUndefined(); expect(env.AGENT_TEAMS_RUNTIME_TURN_SETTLED_SPOOL_ROOT).toBeUndefined(); expect(env.AGENT_TEAMS_MCP_CLAUDE_DIR).toBeUndefined(); diff --git a/test/main/services/runtime/openCodeBridgeRuntimeEnv.test.ts b/test/main/services/runtime/openCodeBridgeRuntimeEnv.test.ts index 7af85235..4a58532b 100644 --- a/test/main/services/runtime/openCodeBridgeRuntimeEnv.test.ts +++ b/test/main/services/runtime/openCodeBridgeRuntimeEnv.test.ts @@ -17,6 +17,7 @@ describe('ensureOpenCodeBridgeRuntimeBinaryEnv', () => { }); expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath); + expect(env.OPENCODE_BIN_PATH).toBe(binaryPath); expect(env.PATH?.split(path.delimiter)).toEqual([ path.dirname(binaryPath), '/usr/bin', @@ -48,11 +49,32 @@ describe('ensureOpenCodeBridgeRuntimeBinaryEnv', () => { }); expect(commandEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath); + expect(commandEnv.OPENCODE_BIN_PATH).toBe(binaryPath); expect(commandEnv.PATH?.split(path.delimiter)[0]).toBe(path.dirname(binaryPath)); expect(bridgeEnv.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath); + expect(bridgeEnv.OPENCODE_BIN_PATH).toBe(binaryPath); expect(bridgeEnv.PATH?.split(path.delimiter)[0]).toBe(path.dirname(binaryPath)); }); + it('honors a legacy OpenCode binary override already present in the command env', async () => { + const binaryPath = path.join(process.cwd(), 'legacy opencode', 'opencode'); + const env: NodeJS.ProcessEnv = { + OPENCODE_BIN_PATH: binaryPath, + PATH: '/usr/bin', + }; + const resolver = vi.fn<() => Promise>(); + + await ensureOpenCodeBridgeRuntimeBinaryEnv({ + targetEnv: env, + resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath: resolver, + }); + + expect(resolver).not.toHaveBeenCalled(); + expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath); + expect(env.OPENCODE_BIN_PATH).toBe(binaryPath); + expect(env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(binaryPath)); + }); + it('keeps bridge startup non-fatal when the managed resolver fails', async () => { const onWarning = vi.fn(); const env: NodeJS.ProcessEnv = { @@ -72,6 +94,7 @@ describe('ensureOpenCodeBridgeRuntimeBinaryEnv', () => { '[OpenCode] Runtime adapter bundled OpenCode binary unresolved: manifest unreadable' ); expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined(); + expect(env.OPENCODE_BIN_PATH).toBeUndefined(); expect(env.PATH).toBe('/usr/bin'); }); }); diff --git a/test/main/services/runtime/openCodeRuntimeBinaryEnv.test.ts b/test/main/services/runtime/openCodeRuntimeBinaryEnv.test.ts index 75e19135..7103b6ba 100644 --- a/test/main/services/runtime/openCodeRuntimeBinaryEnv.test.ts +++ b/test/main/services/runtime/openCodeRuntimeBinaryEnv.test.ts @@ -14,6 +14,7 @@ describe('applyOpenCodeRuntimeBinaryEnv', () => { applyOpenCodeRuntimeBinaryEnv(env, binaryPath); expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(binaryPath); + expect(env.OPENCODE_BIN_PATH).toBe(binaryPath); expect(env.PATH?.split(path.delimiter)).toEqual([ path.dirname(binaryPath), '/usr/bin', @@ -32,6 +33,21 @@ describe('applyOpenCodeRuntimeBinaryEnv', () => { applyOpenCodeRuntimeBinaryEnv(env, discoveredBinaryPath); expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(explicitBinaryPath); + expect(env.OPENCODE_BIN_PATH).toBe(explicitBinaryPath); + expect(env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(explicitBinaryPath)); + }); + + it('mirrors a legacy OpenCode binary override into the managed env var', () => { + const explicitBinaryPath = path.join(process.cwd(), 'legacy opencode', 'opencode'); + const env: NodeJS.ProcessEnv = { + OPENCODE_BIN_PATH: explicitBinaryPath, + PATH: '/usr/bin', + }; + + applyOpenCodeRuntimeBinaryEnv(env, null); + + expect(env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(explicitBinaryPath); + expect(env.OPENCODE_BIN_PATH).toBe(explicitBinaryPath); expect(env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(explicitBinaryPath)); }); diff --git a/test/main/services/runtime/providerAwareCliEnv.test.ts b/test/main/services/runtime/providerAwareCliEnv.test.ts index 66355dda..d9995c78 100644 --- a/test/main/services/runtime/providerAwareCliEnv.test.ts +++ b/test/main/services/runtime/providerAwareCliEnv.test.ts @@ -371,11 +371,13 @@ describe('buildProviderAwareCliEnv', () => { expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith( expect.objectContaining({ CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH: appManagedBinaryPath, + OPENCODE_BIN_PATH: appManagedBinaryPath, }), 'opencode', undefined ); expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(appManagedBinaryPath); + expect(result.env.OPENCODE_BIN_PATH).toBe(appManagedBinaryPath); expect(result.env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(appManagedBinaryPath)); }); @@ -392,6 +394,7 @@ describe('buildProviderAwareCliEnv', () => { }); expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(explicitBinaryPath); + expect(result.env.OPENCODE_BIN_PATH).toBe(explicitBinaryPath); expect(result.env.PATH?.split(path.delimiter)[0]).toBe(path.dirname(explicitBinaryPath)); }); @@ -407,6 +410,7 @@ describe('buildProviderAwareCliEnv', () => { }); expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined(); + expect(result.env.OPENCODE_BIN_PATH).toBeUndefined(); }); it('injects the verified app-managed Codex binary for Codex launches', async () => { diff --git a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts index 030f6dfd..99d0a019 100644 --- a/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts +++ b/test/renderer/components/team/dialogs/ProvisioningProviderStatusList.test.ts @@ -5,6 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { deriveEffectiveProvisioningPrepareState, getPrimaryProvisioningFailureDetail, + getProvisioningFailureHint, getProvisioningProviderBackendSummary, ProvisioningProviderStatusList, createInitialProviderChecks, @@ -116,6 +117,21 @@ describe('ProvisioningProviderStatusList', () => { }); }); + it('gives a concrete hint for missing OpenCode runtime binary failures', () => { + expect( + getProvisioningFailureHint('CLI environment is not available - launch is blocked', [ + { + providerId: 'opencode', + status: 'failed', + backendSummary: null, + details: ['OpenCode runtime binary is not installed or not reachable by launch preflight.'], + }, + ]) + ).toBe( + 'Install or retry OpenCode runtime from the provider status card, then reopen this dialog.' + ); + }); + it('picks the first real failure detail instead of a verified line', () => { expect( getPrimaryProvisioningFailureDetail([ diff --git a/test/renderer/components/team/dialogs/providerPrepareDiagnosticsOpenCodeRuntime.test.ts b/test/renderer/components/team/dialogs/providerPrepareDiagnosticsOpenCodeRuntime.test.ts new file mode 100644 index 00000000..6cc566d2 --- /dev/null +++ b/test/renderer/components/team/dialogs/providerPrepareDiagnosticsOpenCodeRuntime.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { runProviderPrepareDiagnostics } from '@renderer/components/team/dialogs/providerPrepareDiagnostics'; + +import type { TeamProviderId, TeamProvisioningPrepareResult } from '@shared/types'; + +type PrepareProvisioningFn = ( + cwd?: string, + providerId?: TeamProviderId, + providerIds?: TeamProviderId[], + selectedModels?: string[], + limitContext?: boolean, + modelVerificationMode?: 'compatibility' | 'deep' +) => Promise; + +describe('runProviderPrepareDiagnostics OpenCode runtime failures', () => { + it('normalizes missing OpenCode binary diagnostics for packaged launch preflight', async () => { + const prepareProvisioning = vi.fn().mockResolvedValue({ + ready: false, + message: 'OpenCode CLI not detected on PATH', + details: ['OpenCode CLI not found'], + }); + + const result = await runProviderPrepareDiagnostics({ + cwd: '/Users/tester/project', + providerId: 'opencode', + selectedModelIds: ['opencode/big-pickle'], + prepareProvisioning, + }); + + expect(result.status).toBe('failed'); + expect(result.details).toEqual([ + 'OpenCode runtime binary is not installed or not reachable by launch preflight.', + ]); + expect(result.modelResultsById).toEqual({}); + expect(prepareProvisioning).toHaveBeenCalledWith( + '/Users/tester/project', + 'opencode', + ['opencode'], + ['opencode/big-pickle'], + undefined, + 'compatibility' + ); + }); +});