Main's rewritten test_tui_npm_install.py tests call _make_tui_argv expecting the Ink/npm flow unconditionally; with the dual-engine dispatch merged in, _resolve_tui_engine() auto-selects opentui whenever ui-opentui/dist is built in the repo, routing the call away from the path under test (first subprocess became 'node --version' instead of 'npm run build'). Pin the engine to ink via an autouse fixture, mirroring the existing pinning precedent in test_tui_resume_flow.py.
216 lines
6.9 KiB
TypeScript
216 lines
6.9 KiB
TypeScript
import { act, cleanup, render } from '@testing-library/react'
|
|
import type { MutableRefObject } from 'react'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
$currentFastMode,
|
|
$currentModel,
|
|
$currentProvider,
|
|
$currentReasoningEffort,
|
|
$currentServiceTier,
|
|
$turnStartedAt,
|
|
setCurrentFastMode,
|
|
setCurrentModel,
|
|
setCurrentProvider,
|
|
setCurrentReasoningEffort,
|
|
setCurrentServiceTier,
|
|
setTurnStartedAt
|
|
} from '@/store/session'
|
|
|
|
import { useSessionStateCache } from './use-session-state-cache'
|
|
|
|
type Cache = ReturnType<typeof useSessionStateCache>
|
|
|
|
interface HarnessProps {
|
|
activeSessionId: string | null
|
|
onReady: (cache: Cache) => void
|
|
selectedStoredSessionId: string | null
|
|
}
|
|
|
|
function Harness({ activeSessionId, onReady, selectedStoredSessionId }: HarnessProps) {
|
|
const busyRef: MutableRefObject<boolean> = { current: false }
|
|
const cache = useSessionStateCache({
|
|
activeSessionId,
|
|
busyRef,
|
|
selectedStoredSessionId,
|
|
setAwaitingResponse: () => undefined,
|
|
setBusy: () => undefined,
|
|
setMessages: () => undefined
|
|
})
|
|
|
|
onReady(cache)
|
|
|
|
return null
|
|
}
|
|
|
|
describe('useSessionStateCache — per-session turn timer', () => {
|
|
beforeEach(() => {
|
|
// The view-sync flush runs on a real rAF in the browser path; in jsdom we
|
|
// want it synchronous so the global mirror is observable immediately. The
|
|
// hook closes over `window.requestAnimationFrame`, so stub that exact ref.
|
|
// Return null (not a handle) so the hook's `viewSyncRafRef.current = rAF(...)`
|
|
// assignment doesn't overwrite the null the synchronous callback just set —
|
|
// otherwise the ref reads truthy and the NEXT sync is suppressed (a real
|
|
// browser returns a handle but runs the callback async, so this race is a
|
|
// test-only artifact of firing synchronously).
|
|
vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: FrameRequestCallback) => {
|
|
cb(0)
|
|
|
|
return null as unknown as number
|
|
})
|
|
setTurnStartedAt(null)
|
|
setCurrentModel('')
|
|
setCurrentProvider('')
|
|
setCurrentReasoningEffort('')
|
|
setCurrentServiceTier('')
|
|
setCurrentFastMode(false)
|
|
})
|
|
|
|
afterEach(() => {
|
|
cleanup()
|
|
vi.restoreAllMocks()
|
|
setTurnStartedAt(null)
|
|
setCurrentModel('')
|
|
setCurrentProvider('')
|
|
setCurrentReasoningEffort('')
|
|
setCurrentServiceTier('')
|
|
setCurrentFastMode(false)
|
|
})
|
|
|
|
it("keeps a background session's running turn clock and never mirrors it to the view", () => {
|
|
let cache!: Cache
|
|
// Active session is "fg-runtime"; the turn starts on the BACKGROUND session.
|
|
render(
|
|
<Harness activeSessionId="fg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="fg-stored" />
|
|
)
|
|
|
|
const startedAt = 1_700_000_000_000
|
|
|
|
act(() => {
|
|
cache.updateSessionState(
|
|
'bg-runtime',
|
|
state => ({ ...state, busy: true, turnStartedAt: startedAt }),
|
|
'bg-stored'
|
|
)
|
|
})
|
|
|
|
// The background session's own cache entry holds the clock...
|
|
expect(cache.sessionStateByRuntimeIdRef.current.get('bg-runtime')?.turnStartedAt).toBe(startedAt)
|
|
// ...but the global atom (statusbar timer) is untouched — a background turn
|
|
// must not drive the foreground timer.
|
|
expect($turnStartedAt.get()).toBeNull()
|
|
})
|
|
|
|
it("mirrors the focused session's turn clock into the global atom on view-sync", () => {
|
|
let cache!: Cache
|
|
render(<Harness activeSessionId="fg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="fg-stored" />)
|
|
|
|
const startedAt = 1_700_000_111_000
|
|
|
|
// A turn on the ACTIVE session stages into the view; the flush mirrors its
|
|
// turnStartedAt into the global atom the statusbar reads.
|
|
act(() => {
|
|
cache.updateSessionState(
|
|
'fg-runtime',
|
|
state => ({ ...state, busy: true, turnStartedAt: startedAt }),
|
|
'fg-stored'
|
|
)
|
|
})
|
|
|
|
expect($turnStartedAt.get()).toBe(startedAt)
|
|
})
|
|
|
|
it('clears the global clock when the focused turn ends', () => {
|
|
let cache!: Cache
|
|
render(<Harness activeSessionId="fg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="fg-stored" />)
|
|
|
|
act(() => {
|
|
cache.updateSessionState(
|
|
'fg-runtime',
|
|
state => ({ ...state, busy: true, turnStartedAt: 1_700_000_222_000 }),
|
|
'fg-stored'
|
|
)
|
|
})
|
|
expect($turnStartedAt.get()).toBe(1_700_000_222_000)
|
|
|
|
act(() => {
|
|
cache.updateSessionState('fg-runtime', state => ({ ...state, busy: false, turnStartedAt: null }))
|
|
})
|
|
expect($turnStartedAt.get()).toBeNull()
|
|
})
|
|
|
|
it('mirrors the focused session model metadata when switching from a cached session', () => {
|
|
let cache!: Cache
|
|
const { rerender } = render(
|
|
<Harness activeSessionId="fg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="fg-stored" />
|
|
)
|
|
|
|
act(() => {
|
|
cache.updateSessionState(
|
|
'bg-runtime',
|
|
state => ({
|
|
...state,
|
|
fast: true,
|
|
model: 'anthropic/claude-opus-4.8',
|
|
provider: 'anthropic',
|
|
reasoningEffort: 'high',
|
|
serviceTier: 'priority'
|
|
}),
|
|
'bg-stored'
|
|
)
|
|
})
|
|
|
|
// Background metadata is cached but must not bleed into the visible statusbar.
|
|
expect($currentModel.get()).toBe('')
|
|
expect($currentReasoningEffort.get()).toBe('')
|
|
expect($currentFastMode.get()).toBe(false)
|
|
|
|
rerender(<Harness activeSessionId="bg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="bg-stored" />)
|
|
|
|
const bgState = cache.sessionStateByRuntimeIdRef.current.get('bg-runtime')
|
|
expect(bgState).toBeTruthy()
|
|
|
|
act(() => {
|
|
cache.syncSessionStateToView('bg-runtime', bgState!)
|
|
})
|
|
|
|
expect($currentModel.get()).toBe('anthropic/claude-opus-4.8')
|
|
expect($currentProvider.get()).toBe('anthropic')
|
|
expect($currentReasoningEffort.get()).toBe('high')
|
|
expect($currentServiceTier.get()).toBe('priority')
|
|
expect($currentFastMode.get()).toBe(true)
|
|
})
|
|
|
|
it('clears stale model metadata when the newly focused session has no cached value', () => {
|
|
setCurrentModel('previous-model')
|
|
setCurrentProvider('previous-provider')
|
|
setCurrentReasoningEffort('high')
|
|
setCurrentServiceTier('priority')
|
|
setCurrentFastMode(true)
|
|
|
|
let cache!: Cache
|
|
const { rerender } = render(
|
|
<Harness activeSessionId="fg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="fg-stored" />
|
|
)
|
|
|
|
act(() => {
|
|
cache.updateSessionState('bg-runtime', state => ({ ...state }), 'bg-stored')
|
|
})
|
|
|
|
rerender(<Harness activeSessionId="bg-runtime" onReady={c => (cache = c)} selectedStoredSessionId="bg-stored" />)
|
|
|
|
const bgState = cache.sessionStateByRuntimeIdRef.current.get('bg-runtime')
|
|
expect(bgState).toBeTruthy()
|
|
|
|
act(() => {
|
|
cache.syncSessionStateToView('bg-runtime', bgState!)
|
|
})
|
|
|
|
expect($currentModel.get()).toBe('')
|
|
expect($currentProvider.get()).toBe('')
|
|
expect($currentReasoningEffort.get()).toBe('')
|
|
expect($currentServiceTier.get()).toBe('')
|
|
expect($currentFastMode.get()).toBe(false)
|
|
})
|
|
})
|