import React, { act } from 'react'; import { createRoot } from 'react-dom/client'; import { createInitialProviderChecks, deriveEffectiveProvisioningPrepareState, getPrimaryProvisioningFailureDetail, getProvisioningFailureHint, getProvisioningProviderBackendSummary, getProvisioningProviderProgressMessage, ProvisioningProviderStatusList, } from '@renderer/components/team/dialogs/ProvisioningProviderStatusList'; import { afterEach, describe, expect, it, vi } from 'vitest'; describe('ProvisioningProviderStatusList', () => { afterEach(() => { document.body.innerHTML = ''; }); it('shows waiting for pending provider checks', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: createInitialProviderChecks(['anthropic', 'codex']), }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Anthropic: waiting'); expect(host.textContent).toContain('Codex: waiting'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('surfaces mixed selected model diagnostics without hiding verified results', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'codex', status: 'failed', backendSummary: 'Codex native', details: [ '5.4 Mini - available for launch', '5.1 Codex Max - unavailable - Not available on this Codex native runtime', ], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain( 'Codex (Codex native): Selected model checks - 1 model unavailable, 1 available' ); expect(host.textContent).toContain('5.4 Mini - available for launch'); expect(host.textContent).toContain( '5.1 Codex Max - unavailable - Not available on this Codex native runtime' ); const detailLines = Array.from(host.querySelectorAll('p')); expect(detailLines[0]?.className).toContain('text-emerald-400'); expect(detailLines[1]?.className).toContain('text-red-300'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('keeps OpenCode runtime connectivity failures out of selected-model summaries', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const onOpenProviderSettings = vi.fn(); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'opencode', status: 'failed', backendSummary: 'OpenCode CLI', details: [ 'OpenCode /experimental/tool/ids unavailable - Unable to connect. Is the computer able to access the url?', ], }, ], onOpenProviderSettings, }) ); await Promise.resolve(); }); expect(host.textContent).toContain('OpenCode (OpenCode CLI): OpenCode app MCP unreachable'); expect(host.textContent).not.toContain('Selected model checks'); expect(host.textContent).not.toContain('model unavailable'); expect(host.querySelector('button')).toBeNull(); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('gives a concrete hint for missing OpenCode runtime binary failures', () => { expect( getProvisioningFailureHint('Runtime 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('gives a concrete hint for stale OpenCode app MCP bridge failures', () => { expect( getProvisioningFailureHint('Runtime environment is not available - launch is blocked', [ { providerId: 'opencode', status: 'failed', backendSummary: null, details: [ 'OpenCode app MCP is unreachable. Retry launch to refresh the app MCP bridge. Details: Unable to connect. Is the computer able to access the url?', ], }, ]) ).toBe( 'Retry launch to refresh the OpenCode app MCP bridge. If it repeats, restart the app and OpenCode runtime.' ); }); it('gives a concrete hint for OpenCode bridge no-output failures', () => { expect( getProvisioningFailureHint('Runtime environment is not available - launch is blocked', [ { providerId: 'opencode', status: 'failed', backendSummary: null, details: [ 'OpenCode readiness bridge failed: contract_violation: Bridge stdout was empty', ], }, ]) ).toBe( 'Restart the app and OpenCode runtime, then retry. If it repeats, copy diagnostics.' ); }); it('renders Copy diagnostics for OpenCode support diagnostics and copies the prepared payload', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const writeText = vi.fn(async () => undefined); Object.defineProperty(globalThis.navigator, 'clipboard', { configurable: true, value: { writeText }, }); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'opencode', status: 'failed', backendSummary: 'OpenCode CLI', details: ['OpenCode runtime check returned no output.'], supportDiagnostics: [ { id: 'diag-empty-stdout', providerId: 'opencode', kind: 'opencode_bridge_no_output', severity: 'error', title: 'OpenCode runtime check returned no output', summary: 'OpenCode readiness bridge exited without returning diagnostic JSON.', copyText: 'Agent Teams OpenCode diagnostics\noutputReadError: ENOENT', createdAt: '2026-04-21T12:00:00.000Z', }, ], }, { providerId: 'codex', status: 'failed', details: ['Codex failed'], supportDiagnostics: [ { id: 'diag-codex', providerId: 'codex', kind: 'codex_debug', severity: 'error', title: 'Codex debug', summary: 'Codex debug summary', copyText: 'should not render', createdAt: '2026-04-21T12:00:00.000Z', }, ], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain( 'OpenCode (OpenCode CLI): OpenCode runtime check returned no output' ); expect(host.textContent).toContain('Copy diagnostics'); const buttons = Array.from(host.querySelectorAll('button')); expect(buttons).toHaveLength(1); await act(async () => { buttons[0]?.click(); await Promise.resolve(); }); expect(writeText).toHaveBeenCalledWith( 'Agent Teams OpenCode diagnostics\noutputReadError: ENOENT' ); expect(host.textContent).toContain('Copied'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('does not show copied when the Clipboard API is unavailable', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); Object.defineProperty(globalThis.navigator, 'clipboard', { configurable: true, value: undefined, }); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'opencode', status: 'failed', details: ['OpenCode runtime check returned no output.'], supportDiagnostics: [ { id: 'diag-empty-stdout', providerId: 'opencode', kind: 'opencode_bridge_no_output', severity: 'error', title: 'OpenCode runtime check returned no output', summary: 'OpenCode readiness bridge exited without returning diagnostic JSON.', copyText: 'Agent Teams OpenCode diagnostics', createdAt: '2026-04-21T12:00:00.000Z', }, ], }, ], }) ); await Promise.resolve(); }); const button = host.querySelector('button'); expect(button?.textContent).toContain('Copy diagnostics'); await act(async () => { button?.click(); await Promise.resolve(); }); expect(host.textContent).toContain('Copy diagnostics'); expect(host.textContent).not.toContain('Copied'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('picks the first real failure detail instead of a verified line', () => { expect( getPrimaryProvisioningFailureDetail([ { providerId: 'codex', status: 'failed', details: [ '5.2 - verified', '5.3 Codex - check failed - Model verification timed out', '5.1 Codex Max - unavailable - Not available on this Codex native runtime', ], }, ]) ).toBe('5.1 Codex Max - unavailable - Not available on this Codex native runtime'); }); it('summarizes timed out model verification separately from hard failures', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'codex', status: 'notes', backendSummary: 'Codex native', details: ['5.3 Codex - check failed - Model verification timed out'], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain( 'Codex (Codex native): Selected model checks - 1 model timed out' ); expect(host.textContent).toContain('5.3 Codex - check failed - Model verification timed out'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('offers provider settings for actionable Codex auth notes', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const onOpenProviderSettings = vi.fn(); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'codex', status: 'notes', backendSummary: 'Codex native - auth required', details: [ 'Codex native requires OPENAI_API_KEY or CODEX_API_KEY, or a connected ChatGPT account. Add one before launching Codex.', 'Default - available for launch', '5.5 - available for launch', ], }, { providerId: 'anthropic', status: 'notes', details: ['Opus 4.6 - available for launch'], }, ], onOpenProviderSettings, }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Open Codex settings'); const buttons = host.querySelectorAll('button'); expect(buttons).toHaveLength(1); const button = buttons[0]; expect(button).not.toBeNull(); await act(async () => { button?.click(); await Promise.resolve(); }); expect(onOpenProviderSettings).toHaveBeenCalledWith('codex'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('summarizes OpenCode advisory ping misses without failure wording', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'opencode', status: 'notes', backendSummary: 'OpenCode CLI', details: ['big-pickle - ping not confirmed'], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain( 'OpenCode (OpenCode CLI): Selected model checks - 1 ping not confirmed' ); expect(host.textContent).not.toContain('model check failed'); expect(host.textContent).not.toContain('Needs attention'); const detailLines = Array.from(host.querySelectorAll('p')); expect(detailLines[0]?.className).toContain('text-[var(--color-text-muted)]'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('hides internal OpenCode MCP proof cache markers from preflight details', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'opencode', status: 'ready', backendSummary: 'OpenCode CLI', details: ['opencode_app_mcp_tool_proof_persisted_cache_hit', 'big-pickle - verified'], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain( 'OpenCode (OpenCode CLI): Selected model checks - 1 verified' ); expect(host.textContent).toContain('big-pickle - verified'); expect(host.textContent).not.toContain('opencode_app_mcp_tool_proof_persisted_cache_hit'); const detailLines = Array.from(host.querySelectorAll('p')); expect(detailLines).toHaveLength(1); expect(detailLines[0]?.textContent).toBe('big-pickle - verified'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('summarizes OpenCode busy model checks as deferred notes', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'opencode', status: 'notes', backendSummary: 'OpenCode CLI', details: [ 'qwen/qwen3-235b-a22b-thinking-2507 - verification deferred - OpenCode session is busy; retry when idle.', ], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain( 'OpenCode (OpenCode CLI): Selected model checks - 1 verification deferred' ); expect(host.textContent).not.toContain('model check failed'); expect(host.textContent).not.toContain('Needs attention'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('does not count generic one-shot diagnostic timeouts as model timeouts', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'anthropic', status: 'notes', details: [ 'One-shot diagnostic timed out after runtime readiness passed. This does not mark selected models unavailable. Details: Model verification timed out', 'Opus 4.6 - available for launch', 'Opus 4.7 - available for launch', ], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Anthropic: Selected model checks - 2 available'); expect(host.textContent).not.toContain('1 model timed out'); expect(host.textContent).toContain( 'One-shot diagnostic timed out after runtime readiness passed' ); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('summarizes compatibility-pending OpenCode model checks separately from verified ones', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'opencode', status: 'checking', backendSummary: 'OpenCode CLI', details: [ 'minimax-m2.5-free - compatible, deep verification pending...', 'nemotron-3-super-free - verified', ], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain( 'OpenCode (OpenCode CLI): Selected model checks - 1 compatible, deep verification pending, 1 verified' ); expect(host.textContent).toContain( 'minimax-m2.5-free - compatible, deep verification pending...' ); expect(host.textContent).toContain('nemotron-3-super-free - verified'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('normalizes generic preflight timeout notes without depending on a hardcoded CLI name', async () => { vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); await act(async () => { root.render( React.createElement(ProvisioningProviderStatusList, { checks: [ { providerId: 'codex', status: 'notes', backendSummary: 'Codex native', details: [ 'Preflight check for `orchestrator-cli -p` did not complete. Proceeding anyway. Details: Timeout running: orchestrator-cli -p Output only the single word PONG. --output-format text --model gpt-5.4-mini --max-turns 1 --no-session-persistence', ], }, ], }) ); await Promise.resolve(); }); expect(host.textContent).toContain('Codex (Codex native): CLI preflight did not complete'); await act(async () => { root.unmount(); await Promise.resolve(); }); }); it('keeps internal native rollout state visible in provisioning backend summaries', () => { expect( getProvisioningProviderBackendSummary({ providerId: 'codex', selectedBackendId: 'codex-native', resolvedBackendId: 'codex-native', backend: { kind: 'codex-native', label: 'Codex native', }, availableBackends: [ { id: 'codex-native', label: 'Codex native', description: 'Use codex exec JSON mode.', selectable: false, recommended: false, available: true, state: 'ready', audience: 'general', statusMessage: 'Ready', }, ], }) ).toBe('Codex native'); }); it('does not show non-blocking Codex degraded backend state in provisioning summaries', () => { expect( getProvisioningProviderBackendSummary({ providerId: 'codex', selectedBackendId: 'codex-native', resolvedBackendId: 'codex-native', backend: { kind: 'codex-native', label: 'Codex native', }, availableBackends: [ { id: 'codex-native', label: 'Codex native', description: 'Use codex exec JSON mode.', selectable: false, recommended: false, available: true, state: 'degraded', audience: 'general', statusMessage: 'Ready with degraded account verification.', }, ], }) ).toBe('Codex native'); }); it('normalizes persisted legacy codex fallback summaries to Codex native', () => { expect( getProvisioningProviderBackendSummary({ providerId: 'codex', selectedBackendId: 'api', resolvedBackendId: 'api', backend: { kind: 'codex-native', label: 'Codex native', }, availableBackends: [ { id: 'codex-native', label: 'Codex native', description: 'Use codex exec JSON mode.', selectable: true, recommended: true, available: true, state: 'ready', audience: 'general', }, ], }) ).toBe('Codex native'); }); it('promotes loading to ready once every provider check is already terminal', () => { expect( deriveEffectiveProvisioningPrepareState({ state: 'loading', message: 'Checking selected providers in parallel...', warnings: [], checks: [ { providerId: 'codex', status: 'ready', details: ['5.4 - verified', 'Default - verified'], }, { providerId: 'opencode', status: 'ready', details: ['minimax-m2.5-free - verified', 'nemotron-3-super-free - verified'], }, ], }) ).toEqual({ state: 'ready', message: 'All selected providers are ready.', }); }); it('promotes loading to failed once a terminal provider failure is already known', () => { expect( deriveEffectiveProvisioningPrepareState({ state: 'loading', message: 'Checking selected providers in parallel...', warnings: [], checks: [ { providerId: 'opencode', status: 'failed', details: ['nemotron-3-super-free - unavailable - selected model is not available'], }, ], }) ).toEqual({ state: 'failed', message: 'nemotron-3-super-free - unavailable - selected model is not available', }); }); it('shows a more honest loading message while OpenCode deep verification is still pending', () => { expect( deriveEffectiveProvisioningPrepareState({ state: 'loading', message: 'Checking selected providers in parallel...', warnings: [], checks: [ { providerId: 'opencode', status: 'checking', details: [ 'minimax-m2.5-free - compatible, deep verification pending...', 'nemotron-3-super-free - compatible, deep verification pending...', ], }, ], }) ).toEqual({ state: 'loading', message: 'Deep verification is still running. OpenCode free models may take around 20 seconds.', }); }); it('labels provider-scoped prepare refreshes without implying every provider restarted', () => { expect(getProvisioningProviderProgressMessage(['opencode'], 3)).toBe( 'Checking OpenCode provider...' ); expect(getProvisioningProviderProgressMessage(['anthropic', 'codex'], 3)).toBe( 'Checking Anthropic, Codex providers...' ); expect(getProvisioningProviderProgressMessage(['anthropic', 'codex', 'opencode'], 3)).toBe( 'Checking selected providers in parallel...' ); }); });