Keep mobile preference tests in the mobile compiler project

This commit is contained in:
Brennan Benson
2026-10-30 14:59:45 -07:00
parent 254f751ef3
commit ae2aa8910e
5 changed files with 96 additions and 17 deletions
@@ -1,9 +1,8 @@
// @vitest-environment happy-dom
import { act, cleanup, render } from '@testing-library/react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, expect, it, vi } from 'vitest'
import { useMobileNativeChatQueueFollowUps } from '../../../../../mobile/src/session/use-mobile-native-chat-queue-follow-ups'
import type { RpcClient } from '../../../../../mobile/src/transport/rpc-client'
import type { ConnectionState, RpcResponse } from '../../../../../mobile/src/transport/types'
import { useMobileNativeChatQueueFollowUps } from './use-mobile-native-chat-queue-follow-ups'
import type { RpcClient } from '../transport/rpc-client'
import type { ConnectionState, RpcResponse } from '../transport/types'
function connection() {
let state: ConnectionState = 'connected'
@@ -60,7 +59,15 @@ const reply = (behavior: string): RpcResponse => ({
_meta: { runtimeId: 'r' }
})
afterEach(cleanup)
const renderers = new Set<ReactTestRenderer>()
afterEach(() => {
act(() => {
for (const renderer of renderers) {
renderer.unmount()
}
})
renderers.clear()
})
it('shares one live scalar per connection, fences late reads/events, and isolates real hook renders', async () => {
const a = connection(),
@@ -72,13 +79,17 @@ it('shares one live scalar per connection, fences late reads/events, and isolate
renders.set(id, (renders.get(id) ?? 0) + 1)
return null
}
const mounted = render(
<>
<Chat id="a1" client={a.client} />
<Chat id="a2" client={a.client} />
<Chat id="b" client={b.client} />
</>
)
let mounted: ReactTestRenderer | undefined
await act(async () => {
mounted = create(
<>
<Chat id="a1" client={a.client} />
<Chat id="a2" client={a.client} />
<Chat id="b" client={b.client} />
</>
)
renderers.add(mounted)
})
await act(async () => {})
expect(a.client.subscribe).toHaveBeenCalledTimes(1)
expect(b.client.subscribe).toHaveBeenCalledTimes(1)
@@ -135,7 +146,7 @@ it('shares one live scalar per connection, fences late reads/events, and isolate
expect(values.get('a1')).toBe(true)
await act(async () => a.replies[1]!(reply('steer')))
expect(values.get('a1')).toBe(true)
mounted.unmount()
await act(async () => mounted?.unmount())
expect(a.states.size).toBe(0)
expect(b.states.size).toBe(0)
expect(a.stops).toHaveBeenCalledTimes(3)
@@ -143,8 +154,7 @@ it('shares one live scalar per connection, fences late reads/events, and isolate
})
it('existing phone workspace event reader ignores additive preference snapshot and events', async () => {
const { startHostWorktreeRefresh } =
await import('../../../../../mobile/src/worktree/host-worktree-refresh')
const { startHostWorktreeRefresh } = await import('../worktree/host-worktree-refresh')
const host = connection()
const fetchWorktrees = vi.fn(async () => {}),
fetchRepoMetadata = vi.fn(async () => {}),
@@ -376,7 +376,14 @@ it('keeps the draft through Continue admission, refusal, uncertainty and recorde
fence: 1,
submissions: [],
queuedMessageIds: [],
queue: { capability: 'unsupported', enabled: false },
queue: {
capability: 'unsupported',
followUpsQueue: false,
isWorking: false,
stopping: false,
session: {},
prompts: []
},
historyLoaded: false,
sendOptions: () => options
})
@@ -111,6 +111,7 @@ export function createStructuredSessionMocks() {
retryLaunch: vi.fn<(worktreeId: string, sessionId: string) => unknown>(),
relaunchWithMessage: vi.fn<(worktreeId: string, sessionId: string, text: string) => void>(),
controllerProps: nullable<{ transportEnabled?: boolean }>(),
controllerRender: vi.fn<(sessionId: string, queueFollowUps: boolean | undefined) => void>(),
send: vi.fn<(text: string, attachments?: unknown[]) => boolean>(() => true),
status: 'ready' as 'idle' | 'loading' | 'ready' | 'error',
readRefusal: absent<AgentSessionRefusalReference>(),
@@ -182,7 +183,9 @@ export function createStructuredSessionMocks() {
sessionId: string
target: { kind: 'local' } | { kind: 'environment'; environmentId: string }
transportEnabled?: boolean
queueFollowUps?: boolean
}) => {
mocks.controllerRender(props.sessionId, props.queueFollowUps)
mocks.controllerProps = props
return {
contextClear: { clearing: false, clears: 0 },
@@ -363,6 +366,7 @@ export function createStructuredSessionMocks() {
mocks.relaunchWithMessage.mockReset()
mocks.lifecycleLookup.mockReset()
mocks.controllerProps = null
mocks.controllerRender.mockClear()
mocks.send.mockClear()
mocks.status = 'ready'
mocks.readRefusal = undefined
@@ -35,6 +35,61 @@ import { structuredAgentSessionPaneKey } from '../../../../shared/structured-age
import { structuredAgentSessionDraftScopeKey } from './native-chat-composer-draft-store'
describe('NativeChatStructuredSession', () => {
it('isolates mounted chats from unrelated settings and renders their live follow-up choice', async () => {
const original = useAppStore.getState().settings
const settings = { ...getDefaultSettings('/tmp'), nativeChatFollowUpBehavior: 'steer' as const }
useAppStore.setState({ settings })
const view = render(
<>
{['local-chat', 'ssh-chat'].map((sessionId) => (
<NativeChatStructuredSession
key={sessionId}
isVisible
isFocusedGroup
tabId={sessionId}
sessionId={sessionId}
target={
sessionId === 'local-chat'
? { kind: 'local' }
: { kind: 'environment', environmentId: 'ssh-host' }
}
agent="codex"
/>
))}
</>
)
try {
await act(async () => {})
mocks.controllerRender.mockClear()
act(() =>
useAppStore.setState({ settings: { ...settings, nativeChatFollowUpBehavior: 'steer' } })
)
expect(mocks.controllerRender).not.toHaveBeenCalled()
act(() =>
useAppStore.setState({
settings: { ...settings, showTasksButton: !settings.showTasksButton }
})
)
expect(mocks.controllerRender).not.toHaveBeenCalled()
act(() =>
useAppStore.setState({ settings: { ...settings, nativeChatFollowUpBehavior: 'queue' } })
)
expect(new Set(mocks.controllerRender.mock.calls.map(([sessionId]) => sessionId))).toEqual(
new Set(['local-chat', 'ssh-chat'])
)
expect(mocks.controllerRender.mock.calls.every(([, queue]) => queue === true)).toBe(true)
mocks.controllerRender.mockClear()
act(() => useAppStore.setState({ settings }))
expect(new Set(mocks.controllerRender.mock.calls.map(([sessionId]) => sessionId))).toEqual(
new Set(['local-chat', 'ssh-chat'])
)
expect(mocks.controllerRender.mock.calls.every(([, queue]) => queue === false)).toBe(true)
} finally {
view.unmount()
useAppStore.setState({ settings: original })
}
})
it('applies persisted appearance at the chat root and updates it live', () => {
const original = useAppStore.getState().settings
useAppStore.setState({
@@ -177,6 +177,9 @@ export function registerRuntimeClientIpcBridge(
if (event.type === 'sidebarInputsChanged') {
return
}
if (event.type !== 'activateWorktree') {
return
}
// Older hosts broadcast local/CLI activation without an address; that is not this viewer's intent.
if (!event.navigation || !navigationTargetsClients(event.navigation)) {
return