mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
test(daemon): deflake idle-exit alive-session test; pin onSessionCountChange wiring
Seed the session before server.start() arms the countdown so the invariant is proven by the timer never arming, instead of racing a cancel against a 75ms grace on a loaded machine. Add a deterministic TerminalHost onSessionCountChange wiring test covering create, natural-exit reap, and dispose (mutation-verified: deleting the create-side notify fails it). Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -585,7 +585,10 @@ describe('DaemonServer', () => {
|
||||
})
|
||||
|
||||
describe('idle self-exit', () => {
|
||||
async function startServerWithIdleExit(graceMs: number): Promise<{
|
||||
async function startServerWithIdleExit(
|
||||
graceMs: number,
|
||||
opts: { beforeStart?: (srv: DaemonServer) => Promise<void> } = {}
|
||||
): Promise<{
|
||||
idleExited: Promise<void>
|
||||
onIdleExit: ReturnType<typeof vi.fn>
|
||||
lastSubprocess: () => ReturnType<typeof createMockSubprocess> | null
|
||||
@@ -606,6 +609,7 @@ describe('DaemonServer', () => {
|
||||
onIdleExit,
|
||||
idleExitGraceMs: graceMs
|
||||
})
|
||||
await opts.beforeStart?.(server)
|
||||
await server.start()
|
||||
return { idleExited, onIdleExit, lastSubprocess: () => subprocess }
|
||||
}
|
||||
@@ -684,15 +688,20 @@ describe('DaemonServer', () => {
|
||||
})
|
||||
|
||||
it('never fires while a session is alive and exits after the last session ends', async () => {
|
||||
const { idleExited, onIdleExit, lastSubprocess } = await startServerWithIdleExit(75)
|
||||
const daemon = server as unknown as DaemonServerPrivate
|
||||
|
||||
// Create a session with zero connected clients — the exact state the
|
||||
// daemon exists for (holding sessions across app restarts).
|
||||
await daemon.routeRequest('ghost-client', {
|
||||
id: 'req-idle-1',
|
||||
type: 'createOrAttach',
|
||||
payload: { sessionId: 'idle-session', cols: 80, rows: 24 }
|
||||
// Why seed the session before start(): start() is what arms the
|
||||
// countdown, so with a session already present the timer never arms at
|
||||
// all. The invariant is proven deterministically instead of racing a
|
||||
// cancel against a short grace on a loaded machine.
|
||||
const { idleExited, onIdleExit, lastSubprocess } = await startServerWithIdleExit(75, {
|
||||
beforeStart: async (srv) => {
|
||||
// A session with zero connected clients — the exact state the
|
||||
// daemon exists for (holding sessions across app restarts).
|
||||
await (srv as unknown as DaemonServerPrivate).routeRequest('ghost-client', {
|
||||
id: 'req-idle-1',
|
||||
type: 'createOrAttach',
|
||||
payload: { sessionId: 'idle-session', cols: 80, rows: 24 }
|
||||
})
|
||||
}
|
||||
})
|
||||
expect(idleArmed()).toBe(false)
|
||||
|
||||
|
||||
@@ -516,4 +516,45 @@ describe('TerminalHost', () => {
|
||||
expect(liveSub.dispose).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('onSessionCountChange', () => {
|
||||
// Why: the daemon's idle-exit countdown arms/cancels purely off this hook;
|
||||
// a missed notification on any transition silently disables idle exit (or
|
||||
// worse, lets it fire with a session alive). Pin every transition.
|
||||
it('notifies on create, natural-exit reap, and dispose', async () => {
|
||||
const onSessionCountChange = vi.fn()
|
||||
host.dispose()
|
||||
host = new TerminalHost({
|
||||
spawnSubprocess: spawnFn as MockSpawnFn,
|
||||
onSessionCountChange
|
||||
})
|
||||
|
||||
await host.createOrAttach({
|
||||
sessionId: 'session-1',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
streamClient: { onData: vi.fn(), onExit: vi.fn() }
|
||||
})
|
||||
expect(host.sessionCount()).toBe(1)
|
||||
expect(onSessionCountChange).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Natural exit reaps synchronously — this is the transition that lets
|
||||
// an otherwise-empty daemon start its idle countdown.
|
||||
lastSubprocess._onExitCb?.(0)
|
||||
expect(host.sessionCount()).toBe(0)
|
||||
expect(onSessionCountChange).toHaveBeenCalledTimes(2)
|
||||
|
||||
await host.createOrAttach({
|
||||
sessionId: 'session-2',
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
streamClient: { onData: vi.fn(), onExit: vi.fn() }
|
||||
})
|
||||
expect(onSessionCountChange).toHaveBeenCalledTimes(3)
|
||||
|
||||
host.dispose()
|
||||
expect(host.sessionCount()).toBe(0)
|
||||
expect(onSessionCountChange).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user