diff --git a/src/renderer/src/lib/agent-launch-routing.test.ts b/src/renderer/src/lib/agent-launch-routing.test.ts index cb3a2b70b00..6bcc5e97287 100644 --- a/src/renderer/src/lib/agent-launch-routing.test.ts +++ b/src/renderer/src/lib/agent-launch-routing.test.ts @@ -76,6 +76,7 @@ describe('resolveAgentLaunchRoute', () => { it('fails closed for missing capability, unsupported providers, and explicit TUI options', () => { expect(route({ hostCapabilities: [] })).toBe('legacy-native-chat') + expect(route({ hostCapabilities: null })).toBe('legacy-native-chat') // openclaude and grok render native chat but have no structured adapter. expect(route({ agent: 'openclaude' })).toBe('legacy-native-chat') expect(route({ agent: 'grok' })).toBe('legacy-native-chat') diff --git a/src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts b/src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts index 179f6203d0b..8e083618925 100644 --- a/src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts +++ b/src/renderer/src/lib/launch-agent-structured-chat-guard.test.ts @@ -14,7 +14,7 @@ const mockRefreshLocalStructuredSessionTabs = vi.fn() const mockToastError = vi.fn() const mockCallStructuredAgentSession = vi.fn() const STRUCTURED_HOST_CAPABILITIES = ['agent-session.structured.v1'] -let hostCapabilities: readonly string[] = STRUCTURED_HOST_CAPABILITIES +let hostCapabilities: readonly string[] | null = STRUCTURED_HOST_CAPABILITIES function structuredLaunchIntent(worktreeId: string, sessionId = 'codex-session-1') { return { @@ -113,7 +113,7 @@ vi.mock('@/runtime/local-structured-session-tabs-sync', () => ({ LOCAL_STRUCTURED_SESSION_OWNER: 'local-structured-session' })) vi.mock('@/runtime/local-runtime-capabilities', () => ({ - readLocalRuntimeCapabilities: () => hostCapabilities + readLocalRuntimeCapabilitiesOrUnknown: () => hostCapabilities })) vi.mock('@/lib/worktree-runtime-owner', () => ({ getExecutionHostIdForWorktree: () => @@ -233,16 +233,19 @@ describe('structured chat adoption guard on the launch path', () => { expect(mockToastError).not.toHaveBeenCalled() }) - it('routes every structured launch through the shared host capability gate', async () => { - hostCapabilities = [] - const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') + it.each([[], null])( + 'preserves terminal-backed launches with capability answer %s', + async (capabilities) => { + hostCapabilities = capabilities + const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab') - launchAgentInNewTab({ agent: 'claude', worktreeId: 'wt-1' }) - launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' }) + launchAgentInNewTab({ agent: 'claude', worktreeId: 'wt-1' }) + launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' }) - expect(mockCreateStructuredCodexSessionLaunchIntent).not.toHaveBeenCalled() - expect(mockCreateTab).toHaveBeenCalledTimes(2) - }) + expect(mockCreateStructuredCodexSessionLaunchIntent).not.toHaveBeenCalled() + expect(mockCreateTab).toHaveBeenCalledTimes(2) + } + ) /** The toggle is hidden under Terminal chat but its persisted value survives, so the launch * path must re-check the default view rather than trust a stale opt-in. */ diff --git a/src/renderer/src/runtime/local-runtime-capabilities.test.ts b/src/renderer/src/runtime/local-runtime-capabilities.test.ts index 264fcdb1403..eedd31a748a 100644 --- a/src/renderer/src/runtime/local-runtime-capabilities.test.ts +++ b/src/renderer/src/runtime/local-runtime-capabilities.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { readLocalRuntimeCapabilities, + readLocalRuntimeCapabilitiesOrUnknown, refreshLocalRuntimeCapabilities, setLocalRuntimeCapabilitiesForTests } from './local-runtime-capabilities' @@ -12,6 +13,22 @@ describe('local runtime capabilities', () => { setLocalRuntimeCapabilitiesForTests([]) }) + it('starts unknown while the array reader stays compatible', async () => { + vi.resetModules() + const fresh = await import('./local-runtime-capabilities') + expect(fresh.readLocalRuntimeCapabilitiesOrUnknown()).toBeNull() + expect(fresh.readLocalRuntimeCapabilities()).toEqual([]) + }) + + it.each([{}, { capabilities: [] }])( + 'treats a successful legacy or empty response as known denial: %j', + async (status) => { + Object.assign(window, { api: { runtime: { getStatus: vi.fn(async () => status) } } }) + await expect(refreshLocalRuntimeCapabilities()).resolves.toEqual([]) + expect(readLocalRuntimeCapabilitiesOrUnknown()).toEqual([]) + } + ) + it('fails closed until the live host advertises support', async () => { const getStatus = vi.fn(async () => ({ capabilities: ['agent-session.structured.v1'] })) Object.assign(window, { api: { runtime: { getStatus } } }) @@ -21,6 +38,7 @@ describe('local runtime capabilities', () => { 'agent-session.structured.v1' ]) expect(readLocalRuntimeCapabilities()).toEqual(['agent-session.structured.v1']) + expect(readLocalRuntimeCapabilitiesOrUnknown()).toEqual(['agent-session.structured.v1']) }) it('coalesces concurrent live status reads', async () => { @@ -38,6 +56,7 @@ describe('local runtime capabilities', () => { ['agent-session.structured.v1'], ['agent-session.structured.v1'] ]) + expect(first).toBe(second) expect(getStatus).toHaveBeenCalledOnce() }) @@ -55,5 +74,12 @@ describe('local runtime capabilities', () => { await expect(refreshLocalRuntimeCapabilities()).resolves.toEqual([]) expect(readLocalRuntimeCapabilities()).toEqual([]) + expect(readLocalRuntimeCapabilitiesOrUnknown()).toBeNull() + + window.api.runtime.getStatus = vi + .fn() + .mockResolvedValue({ capabilities: ['agent-session.structured.v1'] }) + await refreshLocalRuntimeCapabilities() + expect(readLocalRuntimeCapabilitiesOrUnknown()).toEqual(['agent-session.structured.v1']) }) })