mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 08:02:02 +00:00
fix(pty): bound kill verdict state and expose fence gaps
This commit is contained in:
@@ -61,6 +61,7 @@ describe('killPtySessions input bounds', () => {
|
||||
expect(results).toEqual([
|
||||
{
|
||||
id: 'session-1',
|
||||
fenceUnavailable: true,
|
||||
verdict: 'unverifiable',
|
||||
treeUnverified: true,
|
||||
reason: 'descendant tree could not be verified'
|
||||
@@ -91,7 +92,18 @@ describe('killPtySessions input bounds', () => {
|
||||
)
|
||||
|
||||
expect(shutdown).toHaveBeenCalledTimes(2)
|
||||
expect(results.every((result) => result.verdict === 'exited')).toBe(true)
|
||||
expect(results).toEqual([
|
||||
{
|
||||
id: 'legacy',
|
||||
verdict: 'exited',
|
||||
fenceUnavailable: true
|
||||
},
|
||||
{
|
||||
id: 'current',
|
||||
incarnationId: 'incarnation-current',
|
||||
verdict: 'exited'
|
||||
}
|
||||
])
|
||||
expect(provider.supportsIncarnationFence).toHaveBeenCalledWith({ sessionId: 'legacy' })
|
||||
expect(provider.supportsIncarnationFence).toHaveBeenCalledWith({ sessionId: 'current' })
|
||||
})
|
||||
@@ -132,6 +144,7 @@ describe('killPtySessions input bounds', () => {
|
||||
expect(results).toEqual([
|
||||
{
|
||||
id: 'session-1',
|
||||
fenceUnavailable: true,
|
||||
verdict: 'unverifiable',
|
||||
reason: 'descendant tree could not be verified',
|
||||
treeUnverified: true
|
||||
|
||||
@@ -97,7 +97,14 @@ export async function killPtySessions(
|
||||
if (shutdownResult?.fenceUnavailable) {
|
||||
return { ...ref, verdict: 'refused', reason: 'incarnation fence unavailable' }
|
||||
}
|
||||
return { ...ref, verdict: 'unverifiable' as const, reason: 'pending verification' }
|
||||
return {
|
||||
...ref,
|
||||
verdict: 'unverifiable' as const,
|
||||
reason: 'pending verification',
|
||||
// Older daemon/relay peers ignore the additive fence field. Preserve
|
||||
// today's kill behavior but expose that the identity door was absent.
|
||||
...(!fenceCapable ? { fenceUnavailable: true as const } : {})
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
...ref,
|
||||
@@ -150,9 +157,17 @@ export async function killPtySessions(
|
||||
}
|
||||
const shutdownResult = shutdownResults.get(result.id)
|
||||
const treeUnverified = Boolean(shutdownResult?.treeUnverified)
|
||||
const cleanResult =
|
||||
result.reason === 'pending verification'
|
||||
? (() => {
|
||||
const copy = { ...result }
|
||||
delete copy.reason
|
||||
return copy
|
||||
})()
|
||||
: result
|
||||
if (!survivor && treeUnverified) {
|
||||
return {
|
||||
...result,
|
||||
...cleanResult,
|
||||
verdict: 'unverifiable' as const,
|
||||
reason: 'descendant tree could not be verified',
|
||||
treeUnverified: true
|
||||
@@ -160,13 +175,13 @@ export async function killPtySessions(
|
||||
}
|
||||
return survivor
|
||||
? {
|
||||
...result,
|
||||
...cleanResult,
|
||||
verdict: 'live' as const,
|
||||
reason: 'session still running',
|
||||
...(treeUnverified ? { treeUnverified: true } : {})
|
||||
}
|
||||
: {
|
||||
...result,
|
||||
...cleanResult,
|
||||
verdict: 'exited' as const
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,8 +5,14 @@ import type { DaemonSession } from './resource-usage-merge-types'
|
||||
import { notifyDaemonSessionInventoryInvalidated } from './daemon-session-inventory-invalidation'
|
||||
import { useResourceSessionInventory } from './use-resource-session-inventory'
|
||||
|
||||
function session(id: string): DaemonSession {
|
||||
return { id, cwd: '/workspace', title: id, agentOwnership: 'absent' as const }
|
||||
function session(id: string, incarnationId?: string): DaemonSession {
|
||||
return {
|
||||
id,
|
||||
cwd: '/workspace',
|
||||
title: id,
|
||||
agentOwnership: 'absent' as const,
|
||||
...(incarnationId ? { incarnationId } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
|
||||
@@ -272,6 +278,34 @@ describe('useResourceSessionInventory', () => {
|
||||
expect(listSessions).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('drops verdicts from retired incarnations while retaining the live id', async () => {
|
||||
listSessions
|
||||
.mockResolvedValueOnce([session('reused', 'incarnation-old')])
|
||||
.mockResolvedValueOnce([session('reused', 'incarnation-new')])
|
||||
const { result } = renderHook(() => useResourceSessionInventory(true))
|
||||
await waitFor(() => expect(result.current.sessionInventory.count).toBe(1))
|
||||
|
||||
act(() => {
|
||||
result.current.setSessionVerdict(
|
||||
'reused',
|
||||
'unverifiable',
|
||||
'inventory unavailable',
|
||||
'incarnation-old'
|
||||
)
|
||||
})
|
||||
expect(result.current.sessionInventory.sessions[0]).toMatchObject({
|
||||
incarnationId: 'incarnation-old',
|
||||
killVerdict: 'unverifiable'
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await result.current.refreshSessions()
|
||||
})
|
||||
expect(result.current.sessionInventory.sessions[0]).toEqual(
|
||||
session('reused', 'incarnation-new')
|
||||
)
|
||||
})
|
||||
|
||||
it('ignores inventory invalidation before session restore is ready and after unmount', async () => {
|
||||
listSessions.mockResolvedValue([session('one')])
|
||||
const { unmount } = renderHook(({ ready }) => useResourceSessionInventory(ready), {
|
||||
|
||||
@@ -89,9 +89,15 @@ export function useResourceSessionInventory(ready: boolean): ResourceSessionInve
|
||||
}
|
||||
}
|
||||
knownSessionIdsRef.current = new Set(liveSessions.map(({ id }) => id))
|
||||
// Keep at most one verdict generation per listed PTY. Session ids can be
|
||||
// recycled with a new incarnation while the inventory stays present; retaining
|
||||
// every historical key would grow this map for the lifetime of the workspace.
|
||||
const currentVerdictKeys = new Set(
|
||||
liveSessions.map((session) => verdictKey(session.id, session.incarnationId))
|
||||
)
|
||||
for (const key of verdictsRef.current.keys()) {
|
||||
const id = key.split('\0', 1)[0]
|
||||
if (!knownSessionIdsRef.current.has(id)) {
|
||||
if (!knownSessionIdsRef.current.has(id) || !currentVerdictKeys.has(key)) {
|
||||
verdictsRef.current.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user