test(structured-chat): make the stale-binding clear and the availability guard observable

The bridge-route assertion used deep equality with an explicit undefined, which
vitest cannot distinguish from an absent key, so reverting the stale-binding
clear left the suite green. canUseStructuredNativeChat had no coverage at all.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Brennan Benson
2026-08-19 15:26:49 -07:00
co-authored by Codex
parent 0c74172fcd
commit a6d4bc0380
3 changed files with 214 additions and 3 deletions
@@ -0,0 +1,109 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mockCreateTab = vi.fn()
const mockSetTabViewMode = vi.fn()
const mockWaitForAgentReady = vi.fn()
const store = {
activeRepoId: 'repo-1',
activeWorktreeId: 'wt-1',
settings: {
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {},
activeRuntimeEnvironmentId: null,
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true
},
projects: [{ id: 'repo-1', localWindowsRuntimePreference: { kind: 'inherit-global' as const } }],
repos: [{ id: 'repo-1', connectionId: null as string | null, path: '/repo' }],
sshConnectionStates: new Map(),
transientClearedAgentStatusConnectionIds: {},
worktreesByRepo: {
'repo-1': [{ id: 'wt-1', repoId: 'repo-1', projectId: 'repo-1', path: '/repo/worktree' }]
},
detectedWorktreesByRepo: {},
allWorktrees: vi.fn(() => store.worktreesByRepo['repo-1']),
tabsByWorktree: { 'wt-1': [{ id: 'tab-1' }] },
openFiles: [] as { id: string; worktreeId: string }[],
browserTabsByWorktree: {} as Record<string, { id: string }[]>,
tabBarOrderByWorktree: {} as Record<string, string[]>,
terminalLayoutsByTabId: {},
ptyIdsByTabId: {},
createTab: mockCreateTab,
closeTab: vi.fn(),
queueTabStartupCommand: vi.fn(),
setActiveTabType: vi.fn(),
setTabViewMode: mockSetTabViewMode,
setTabBarOrder: vi.fn(),
setAgentStatus: vi.fn(),
seedNativeChatLaunchPrompt: vi.fn(),
seedNativeChatLaunchDraft: vi.fn(),
markNativeChatLaunchPromptFailed: vi.fn()
}
vi.mock('@/store', () => ({ useAppStore: { getState: () => store } }))
vi.mock('sonner', () => ({ toast: { message: vi.fn(), error: vi.fn() } }))
vi.mock('@/components/tab-bar/reconcile-order', () => ({ reconcileTabOrder: vi.fn(() => []) }))
vi.mock('@/lib/agent-paste-draft', () => ({ pasteDraftWhenAgentReady: vi.fn() }))
vi.mock('@/lib/agent-ready-wait', () => ({ waitForAgentReady: mockWaitForAgentReady }))
vi.mock('@/lib/telemetry', () => ({
track: vi.fn(),
tuiAgentToAgentKind: (agent: string) => agent
}))
vi.mock('@/runtime/web-runtime-session', () => ({
createWebRuntimeSessionTerminal: vi.fn(),
createWebRuntimeAgentSessionTerminalWithLaunchDraft: vi.fn(),
isWebRuntimeSessionActive: vi.fn(() => false),
isWebTerminalSurfaceTabId: vi.fn(() => false)
}))
/** Structured adoption creates the tab in terminal mode and flips it to chat once
* Codex is ready; the bridge stamps `viewMode: 'chat'` on the tab up front. That
* difference is the only observable signal that the availability guard ran. */
describe('structured chat adoption guard on the launch path', () => {
beforeEach(() => {
vi.clearAllMocks()
store.repos = [{ id: 'repo-1', connectionId: null, path: '/repo' }]
store.projects = [{ id: 'repo-1', localWindowsRuntimePreference: { kind: 'inherit-global' } }]
mockCreateTab.mockReturnValue({ id: 'tab-1' })
mockWaitForAgentReady.mockResolvedValue({ ready: true, reason: 'foreground-match' })
})
it('adopts a local Codex tab into the structured stack', async () => {
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
launchAgent: 'codex'
})
await vi.waitFor(() => expect(mockSetTabViewMode).toHaveBeenCalledWith('tab-1', 'chat'))
})
it('keeps an SSH Codex tab on the bridge', async () => {
store.repos = [{ id: 'repo-1', connectionId: 'ssh-a', path: '/repo' }]
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
launchAgent: 'codex',
viewMode: 'chat'
})
expect(mockWaitForAgentReady).not.toHaveBeenCalled()
})
it('keeps a runtime-paired Codex tab on the bridge', async () => {
store.repos = [{ id: 'repo-1', connectionId: 'runtime-ssh-a', path: '/repo' }]
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
launchAgent: 'codex',
viewMode: 'chat'
})
expect(mockWaitForAgentReady).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,76 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppState } from '@/store/types'
import { canUseStructuredNativeChat } from './structured-native-chat-availability'
const { mockGetRendererAppPlatform } = vi.hoisted(() => ({
mockGetRendererAppPlatform: vi.fn<() => NodeJS.Platform>(() => 'darwin')
}))
vi.mock('@/lib/renderer-app-platform', () => ({
getRendererAppPlatform: mockGetRendererAppPlatform
}))
function stateFor(input: {
connectionId?: string | null
windowsRuntime?: 'windows-host' | 'wsl'
worktreePath?: string
}): AppState {
return {
activeRepoId: 'repo-1',
activeWorktreeId: 'wt-1',
projects: [
{
id: 'repo-1',
localWindowsRuntimePreference:
input.windowsRuntime === 'wsl'
? { kind: 'wsl', distro: 'Ubuntu' }
: { kind: 'windows-host' }
}
],
repos: [{ id: 'repo-1', connectionId: input.connectionId ?? null, path: 'C:\\repo' }],
settings: {},
worktreesByRepo: {
'repo-1': [
{
id: 'wt-1',
repoId: 'repo-1',
projectId: 'repo-1',
path: input.worktreePath ?? 'C:\\repo\\worktree'
}
]
},
detectedWorktreesByRepo: {}
} as unknown as AppState
}
describe('canUseStructuredNativeChat', () => {
beforeEach(() => {
mockGetRendererAppPlatform.mockReturnValue('darwin')
})
it('allows the structured stack on a local worktree', () => {
expect(canUseStructuredNativeChat(stateFor({}), 'wt-1')).toBe(true)
})
it('refuses an SSH worktree so the pane stays on the bridge', () => {
expect(canUseStructuredNativeChat(stateFor({ connectionId: 'ssh-a' }), 'wt-1')).toBe(false)
})
it('refuses a runtime-paired worktree so the pane stays on the bridge', () => {
expect(canUseStructuredNativeChat(stateFor({ connectionId: 'runtime-ssh-a' }), 'wt-1')).toBe(
false
)
})
it('refuses a WSL project on Windows so the pane stays on the bridge', () => {
mockGetRendererAppPlatform.mockReturnValue('win32')
expect(canUseStructuredNativeChat(stateFor({ windowsRuntime: 'wsl' }), 'wt-1')).toBe(false)
})
it('allows a Windows-host project on Windows', () => {
mockGetRendererAppPlatform.mockReturnValue('win32')
expect(canUseStructuredNativeChat(stateFor({ windowsRuntime: 'windows-host' }), 'wt-1')).toBe(
true
)
})
})
@@ -229,10 +229,36 @@ describe('native chat routing', () => {
).resolves.toBe('bridge')
expect(callStructuredAgentSession).not.toHaveBeenCalled()
expect(patch).toHaveBeenCalledWith(tab.id, {
viewMode: 'chat',
structuredSessionId: undefined
expect(patch).toHaveBeenCalledTimes(1)
const [patchedTabId, bridgePatch] = patch.mock.calls[0] as [string, Record<string, unknown>]
expect(patchedTabId).toBe(tab.id)
expect(bridgePatch.viewMode).toBe('chat')
// Deep equality treats an absent key as undefined, so presence is the only proof it was cleared.
expect(Object.keys(bridgePatch)).toContain('structuredSessionId')
expect(bridgePatch.structuredSessionId).toBeUndefined()
})
it('keeps a Codex binding when the same agent falls back to the bridge', async () => {
const { tab, state } = terminalState({
agent: 'codex',
structuredSessionId: 'codex_thread-codex',
connectionId: 'ssh-1'
})
const patch = vi.fn()
await expect(
setTerminalNativeChatMode({
getState: () => state as never,
patch,
tabId: tab.id,
mode: 'chat'
})
).resolves.toBe('bridge')
expect(callStructuredAgentSession).not.toHaveBeenCalled()
expect(patch).toHaveBeenCalledTimes(1)
const [, codexBridgePatch] = patch.mock.calls[0] as [string, Record<string, unknown>]
expect(Object.keys(codexBridgePatch)).not.toContain('structuredSessionId')
})
it('names the terminal direction when returning ownership fails', async () => {