mirror of
https://github.com/stablyai/orca.git
synced 2026-10-05 08:02:33 +00:00
fix: gate startup agent recovery on host inventory
This commit is contained in:
@@ -3,15 +3,16 @@ import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useTerminalWatcherEffects } from '../use-terminal-watcher-effects'
|
||||
import type { TerminalColdActivationController } from '../terminal-cold-activation'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
gate: vi.fn(),
|
||||
resume: vi.fn(),
|
||||
authority: 'none',
|
||||
launchStatus: vi.fn((_worktreeId: string, _provider: string): string => 'idle'),
|
||||
createTab: vi.fn()
|
||||
}))
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(() => 'none', {
|
||||
useAppStore: Object.assign(() => mocks.authority, {
|
||||
getState: () => ({ activeWorktreeId: 'wt-1' })
|
||||
})
|
||||
}))
|
||||
@@ -22,7 +23,7 @@ vi.mock('@/lib/structured-agent-session-launch', () => ({
|
||||
getStructuredAgentLaunchStatus: mocks.launchStatus
|
||||
}))
|
||||
vi.mock('@/lib/resume-sleeping-agent-session', () => ({
|
||||
resumeSleepingAgentSessionsForWorktree: vi.fn()
|
||||
resumeSleepingAgentSessionsForWorktree: mocks.resume
|
||||
}))
|
||||
vi.mock('@/lib/workspace-terminal-host-authority', () => ({
|
||||
createWorkspaceTerminalHostAuthoritySelector: () => () => 'none'
|
||||
@@ -34,23 +35,48 @@ vi.mock('../terminal-pane/terminal-parked-tab-watchers', () => ({
|
||||
disposeAllParkedTerminalWatchers: vi.fn()
|
||||
}))
|
||||
|
||||
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
|
||||
let root: Root | undefined
|
||||
afterEach(async () => {
|
||||
await act(async () => root?.unmount())
|
||||
vi.clearAllMocks()
|
||||
mocks.authority = 'none'
|
||||
})
|
||||
|
||||
function Watcher(): null {
|
||||
function Watcher({ restored = true, hydrated = false, worktreeId = 'wt-1' } = {}): null {
|
||||
useTerminalWatcherEffects({
|
||||
activeWorktreeId: 'wt-1',
|
||||
activeWorktreeId: worktreeId,
|
||||
workspaceSessionReady: true,
|
||||
terminalStartupRestorationReady: true,
|
||||
terminalStartupRestorationReady: restored,
|
||||
hydrationSucceeded: hydrated,
|
||||
workspaceSurfaceIds: [],
|
||||
tabsByWorktree: {},
|
||||
createTab: mocks.createTab,
|
||||
reconcileWorktreeTabModel: () => ({ renderableTabCount: 0 })
|
||||
} as unknown as TerminalColdActivationController)
|
||||
reconcileWorktreeTabModel: () => ({
|
||||
renderableTabCount: 0,
|
||||
activeRenderableTabId: null
|
||||
}),
|
||||
activationDeferredMountTabIdsByWorktreeRef: { current: new Map() },
|
||||
activeTabId: null,
|
||||
activeTabIdByWorktree: {},
|
||||
activeView: 'terminal',
|
||||
activityTerminalPortals: [],
|
||||
anyMountedWorktreeHasLayout: false,
|
||||
backgroundMountRevision: 0,
|
||||
effectiveParkedTerminalWorktreeIds: new Set(),
|
||||
evictionExemptTerminalTabIds: new Set(),
|
||||
getEffectiveLayoutForWorktree: () => undefined,
|
||||
groupsByWorktree: {},
|
||||
measurableBackgroundWorktreeIdsRef: { current: new Set() },
|
||||
mountedWorktreeIdsRef: { current: new Set() },
|
||||
pairedRuntimeParkingEnvironmentIds: new Set(),
|
||||
pendingStartupByTabId: {},
|
||||
renderedActiveWorktreeId: worktreeId,
|
||||
terminalParkingEnabled: false,
|
||||
terminalProviderSnapshotCapabilityRevision: 0,
|
||||
terminalSshParkingEnabled: false,
|
||||
terminalTitleSnapshotAuthorityEnabled: false
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -81,3 +107,48 @@ describe('passive terminal seeding during native chat creation', () => {
|
||||
expect(mocks.createTab).toHaveBeenCalledTimes(expectedTabs)
|
||||
})
|
||||
})
|
||||
|
||||
describe('startup agent recovery host inventory', () => {
|
||||
it('keeps recovery available until the execution host answers', async () => {
|
||||
mocks.authority = 'unverifiable'
|
||||
mocks.gate.mockResolvedValue('adopted')
|
||||
root = createRoot(document.createElement('div'))
|
||||
await act(async () => root?.render(<Watcher hydrated />))
|
||||
expect(mocks.gate).not.toHaveBeenCalled()
|
||||
expect(mocks.resume).not.toHaveBeenCalled()
|
||||
mocks.authority = 'live'
|
||||
await act(async () => root?.render(<Watcher hydrated />))
|
||||
expect(mocks.gate).toHaveBeenCalledTimes(1)
|
||||
await act(async () => root?.render(<Watcher hydrated />))
|
||||
expect(mocks.gate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('waits for terminal restoration, then uses the activation gate', async () => {
|
||||
mocks.authority = 'live'
|
||||
mocks.gate.mockResolvedValue('adopted')
|
||||
root = createRoot(document.createElement('div'))
|
||||
await act(async () => root?.render(<Watcher hydrated restored={false} />))
|
||||
expect(mocks.resume).not.toHaveBeenCalled()
|
||||
expect(mocks.gate).not.toHaveBeenCalled()
|
||||
await act(async () => root?.render(<Watcher hydrated />))
|
||||
expect(mocks.gate).toHaveBeenCalledWith('wt-1')
|
||||
expect(mocks.resume).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['blocked', 'rejected'])('retries a %s startup after leaving and returning', async (outcome) => {
|
||||
mocks.authority = 'live'
|
||||
if (outcome === 'rejected') {
|
||||
mocks.gate.mockRejectedValue(new Error('host unavailable'))
|
||||
} else {
|
||||
mocks.gate.mockResolvedValue('blocked')
|
||||
}
|
||||
root = createRoot(document.createElement('div'))
|
||||
await act(async () => root?.render(<Watcher hydrated />))
|
||||
expect(mocks.gate).toHaveBeenCalledTimes(1)
|
||||
await act(async () => root?.render(<Watcher hydrated worktreeId="wt-2" />))
|
||||
mocks.gate.mockResolvedValue('adopted')
|
||||
await act(async () => root?.render(<Watcher hydrated />))
|
||||
expect(mocks.gate.mock.calls.map(([id]) => id)).toEqual(['wt-1', 'wt-2', 'wt-1'])
|
||||
expect(mocks.resume).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from './terminal-pane/terminal-parked-tab-watchers'
|
||||
import { useAppStore } from '@/store'
|
||||
import { gateWorktreeAgentActivation } from '@/lib/worktree-agent-activation-gate'
|
||||
import { resumeSleepingAgentSessionsForWorktree } from '@/lib/resume-sleeping-agent-session'
|
||||
import { createWorkspaceTerminalHostAuthoritySelector } from '@/lib/workspace-terminal-host-authority'
|
||||
import { getStructuredAgentLaunchStatus } from '@/lib/structured-agent-session-launch'
|
||||
import { AGENT_SESSION_PROVIDER_HANDLE_PROVIDERS } from '../../../shared/agent-session-provider-handle'
|
||||
@@ -251,7 +250,12 @@ export function useTerminalWatcherEffects(controller: TerminalWatcherController)
|
||||
|
||||
const startupResumeWorktreeIdsRef = useRef(new Set<string>())
|
||||
useEffect(() => {
|
||||
if (!workspaceSessionReady || !hydrationSucceeded || !activeWorktreeId) {
|
||||
if (
|
||||
!workspaceSessionReady ||
|
||||
!terminalStartupRestorationReady ||
|
||||
!hydrationSucceeded ||
|
||||
!activeWorktreeId
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (startupResumeWorktreeIdsRef.current.has(activeWorktreeId)) {
|
||||
@@ -263,7 +267,20 @@ export function useTerminalWatcherEffects(controller: TerminalWatcherController)
|
||||
return
|
||||
}
|
||||
startupResumeWorktreeIdsRef.current.add(activeWorktreeId)
|
||||
// Why: startup hydration restores the worktree without activateAndRevealWorktree, so orphaned live/quit records need a terminal-surface pass after cold restore.
|
||||
resumeSleepingAgentSessionsForWorktree(activeWorktreeId)
|
||||
}, [activeWorktreeId, activeWorktreeHostAuthority, hydrationSucceeded, workspaceSessionReady])
|
||||
// Startup recovery needs the same host census and in-flight gate as explicit activation.
|
||||
void gateWorktreeAgentActivation(activeWorktreeId).then(
|
||||
(outcome) => {
|
||||
if (outcome === 'blocked') {
|
||||
startupResumeWorktreeIdsRef.current.delete(activeWorktreeId)
|
||||
}
|
||||
},
|
||||
() => startupResumeWorktreeIdsRef.current.delete(activeWorktreeId)
|
||||
)
|
||||
}, [
|
||||
activeWorktreeId,
|
||||
activeWorktreeHostAuthority,
|
||||
hydrationSucceeded,
|
||||
terminalStartupRestorationReady,
|
||||
workspaceSessionReady
|
||||
])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user