mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
test(runtime): advance injected teardown policy clocks (#26267)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AgentSessionRecord } from '../../shared/agent-session-record'
|
||||
import type { IPtyProvider } from '../providers/types'
|
||||
import { ABANDONED_SWEEP_GRACE_MS } from './forced-sweep-settlement'
|
||||
|
||||
const hostRef: { current: unknown } = { current: null }
|
||||
|
||||
@@ -512,74 +513,107 @@ describe('worktree teardown and structured agent sessions', () => {
|
||||
})
|
||||
|
||||
it('refuses in agent-session wording when the close outlives the sweep budget', async () => {
|
||||
// A structured close that runs out of time used to reject with the PTY timeout sentinel, which
|
||||
// the classifier reads FIRST — so the toast blamed terminals, and the Force Delete meant to
|
||||
// clear the wedge hit the same rejection again (#11960).
|
||||
installHost({ records: [record('s1', WORKTREE)], closeGate: new Promise<void>(() => {}) })
|
||||
const error = await killAllProcessesForWorktree(
|
||||
WORKTREE,
|
||||
destructiveDeps({ timeoutMs: 5 })
|
||||
).catch((thrown: Error) => thrown.message)
|
||||
expect(error).toContain('could not confirm these closed: 1 agent session (claude)')
|
||||
expect(isUnstoppedPtyRemovalError(error as string)).toBe(false)
|
||||
expect(classifyWorktreeForceDeleteReason(error as string, true)).toBe('running-agent-session')
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
try {
|
||||
// A structured close that runs out of time used to reject with the PTY timeout sentinel, which
|
||||
// the classifier reads FIRST — so the toast blamed terminals, and the Force Delete meant to
|
||||
// clear the wedge hit the same rejection again (#11960).
|
||||
installHost({ records: [record('s1', WORKTREE)], closeGate: new Promise<void>(() => {}) })
|
||||
const outcome = killAllProcessesForWorktree(
|
||||
WORKTREE,
|
||||
destructiveDeps({ timeoutMs: 5 })
|
||||
).catch((thrown: Error) => thrown.message)
|
||||
await vi.advanceTimersByTimeAsync(5)
|
||||
const error = await outcome
|
||||
expect(error).toContain('could not confirm these closed: 1 agent session (claude)')
|
||||
expect(isUnstoppedPtyRemovalError(error as string)).toBe(false)
|
||||
expect(classifyWorktreeForceDeleteReason(error as string, true)).toBe('running-agent-session')
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('never wedges Force Delete on a close that will not settle', async () => {
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
installHost({ records: [record('s1', WORKTREE)], closeGate: new Promise<void>(() => {}) })
|
||||
await expect(
|
||||
killAllProcessesForWorktree(
|
||||
try {
|
||||
installHost({ records: [record('s1', WORKTREE)], closeGate: new Promise<void>(() => {}) })
|
||||
const outcome = killAllProcessesForWorktree(
|
||||
WORKTREE,
|
||||
destructiveDeps({ allowUnverifiedStop: true, timeoutMs: 5 })
|
||||
)
|
||||
).resolves.toMatchObject({ runtimeStopped: 0 })
|
||||
const message = structuredSessionWarning(warn)
|
||||
expect(message).toContain('could not confirm these closed: 1 agent session (claude)')
|
||||
// The pin: a close that ran out of time was never watched stay attached. This warn is the only
|
||||
// record a forced removal leaves, and the removal.ts split exists precisely so "we could not
|
||||
// confirm" is never reported as "we saw it running" — including here.
|
||||
expect(message).not.toContain('still attached')
|
||||
warn.mockRestore()
|
||||
void outcome.catch(() => undefined)
|
||||
await vi.advanceTimersByTimeAsync(5 + ABANDONED_SWEEP_GRACE_MS)
|
||||
await expect(outcome).resolves.toMatchObject({ runtimeStopped: 0 })
|
||||
const message = structuredSessionWarning(warn)
|
||||
expect(message).toContain('could not confirm these closed: 1 agent session (claude)')
|
||||
// The pin: a close that ran out of time was never watched stay attached. This warn is the only
|
||||
// record a forced removal leaves, and the removal.ts split exists precisely so "we could not
|
||||
// confirm" is never reported as "we saw it running" — including here.
|
||||
expect(message).not.toContain('still attached')
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('names only the sessions still open when the budget expires mid-close', async () => {
|
||||
// The close loop is serial, so a deadline can land part-way through it. A fallback assembled
|
||||
// at the deadline could only name the whole list — so a removal that had already closed the
|
||||
// first chat still told the user both were still there, which is the exact thing this sweep
|
||||
// exists to stop doing: never report state nobody observed.
|
||||
installHost({
|
||||
records: [record('s1', WORKTREE), record('s2', WORKTREE, { provider: 'codex' })],
|
||||
closeGates: { s2: new Promise<void>(() => {}) }
|
||||
})
|
||||
const error = await killAllProcessesForWorktree(
|
||||
WORKTREE,
|
||||
destructiveDeps({ timeoutMs: 40 })
|
||||
).catch((thrown: Error) => thrown.message)
|
||||
expect(error).toContain('could not confirm these closed: 1 agent session (codex)')
|
||||
expect(error).not.toContain('claude')
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
try {
|
||||
// The close loop is serial, so a deadline can land part-way through it. A fallback assembled
|
||||
// at the deadline could only name the whole list — so a removal that had already closed the
|
||||
// first chat still told the user both were still there, which is the exact thing this sweep
|
||||
// exists to stop doing: never report state nobody observed.
|
||||
installHost({
|
||||
records: [record('s1', WORKTREE), record('s2', WORKTREE, { provider: 'codex' })],
|
||||
closeGates: { s2: new Promise<void>(() => {}) }
|
||||
})
|
||||
const outcome = killAllProcessesForWorktree(
|
||||
WORKTREE,
|
||||
destructiveDeps({ timeoutMs: 40 })
|
||||
).catch((thrown: Error) => thrown.message)
|
||||
await vi.advanceTimersByTimeAsync(40)
|
||||
const error = await outcome
|
||||
expect(error).toContain('could not confirm these closed: 1 agent session (codex)')
|
||||
expect(error).not.toContain('claude')
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('counts the closes that landed before the budget expired', async () => {
|
||||
vi.useFakeTimers({ toFake: ['Date', 'setTimeout', 'clearTimeout'] })
|
||||
// The other half of the same fallback: it reported zero closes, so the removal log said
|
||||
// `structured=0` for a chat it had just ended.
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const slowClose = new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 300)
|
||||
})
|
||||
installHost({
|
||||
records: [record('s1', WORKTREE), record('s2', WORKTREE, { provider: 'codex' })],
|
||||
closeGates: { s2: slowClose }
|
||||
})
|
||||
const result = await killAllProcessesForWorktree(
|
||||
WORKTREE,
|
||||
destructiveDeps({ allowUnverifiedStop: true, timeoutMs: 40 })
|
||||
)
|
||||
expect(result.structuredStopped).toBe(1)
|
||||
expect(structuredSessionWarning(warn)).toContain(
|
||||
'could not confirm these closed: 1 agent session (codex)'
|
||||
)
|
||||
warn.mockRestore()
|
||||
try {
|
||||
installHost({
|
||||
records: [record('s1', WORKTREE), record('s2', WORKTREE, { provider: 'codex' })],
|
||||
closeGates: { s2: slowClose }
|
||||
})
|
||||
const outcome = killAllProcessesForWorktree(
|
||||
WORKTREE,
|
||||
destructiveDeps({ allowUnverifiedStop: true, timeoutMs: 40 })
|
||||
)
|
||||
void outcome.catch(() => undefined)
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
const result = await outcome
|
||||
expect(result.structuredStopped).toBe(1)
|
||||
expect(structuredSessionWarning(warn)).toContain(
|
||||
'could not confirm these closed: 1 agent session (codex)'
|
||||
)
|
||||
} finally {
|
||||
try {
|
||||
await vi.advanceTimersByTimeAsync(300)
|
||||
await slowClose
|
||||
} finally {
|
||||
warn.mockRestore()
|
||||
vi.useRealTimers()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('stops issuing new closes once the budget is spent', async () => {
|
||||
|
||||
Reference in New Issue
Block a user