test(runtime): advance injected teardown policy clocks (#26267)

This commit is contained in:
Neil
2026-10-07 10:28:30 -07:00
committed by GitHub
parent c2f3229362
commit 68c493fa72
@@ -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 () => {