3317189dd6
F1 - `codeburn serve` lost an in-flight request's response when stdin closed mid-flight: runStdioServe returned before its queue drained, and the explicit process.exit(0) then hit runCaptured's monkeypatched exit, throwing ExitSignal and exiting 1 with the frame never written. The finally now awaits the queue. F2 - the SIGTERM rationale was false. armSignalCleanup unlinks the refresh lock and re-raises; it publishes no partial cache, and a lock left by SIGKILL already self-heals through the stale-pid takeover. SIGTERM-first is kept for the real (smaller) benefit - a clean lock release instead of a takeover - and every comment plus the CHANGELOG now says only that. F3 - the cold gate had no exit. overviewWarmed only flips on success, so an install that can never hydrate sat behind an indexing splash forever with no error and no route to the CLI recovery. The cold claim now expires with the cold window itself. F4 - the real CLI does not heartbeat the way the demo did: a cold parse's inter-provider cache save measured 31.6s of total silence, which a 45s window survives only until the machine is 1.5x slower. Under CODEBURN_PROGRESS a running parse now emits a keepalive every 10s regardless of phase, so silence genuinely means stopped. Consumers that do not know the event ignore it. F5 - the orphan-reap identity check matched any `cli.js` running `serve`. The pidfile now records the exact argv and `ps -ww` must match it exactly. F6 - bump() re-armed the watchdog after settle, leaving a timer finish() never clears when a killed child's buffered output landed. F7 - kill paths dropped the child from activeChildren before SIGTERM and the SIGKILL backstop was unref'd, so a quit inside the 5s grace orphaned a child that ignores SIGTERM. It now stays registered until it actually dies. N8 - the silence test wrote its only byte at t~0, so it passed without the re-arm. The byte now lands mid-window and the kill is asserted from it. N9 - documented why mutations keep a plain total cap.
77 lines
3.0 KiB
TypeScript
77 lines
3.0 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from 'vitest'
|
|
|
|
import { PROGRESS_LINE_PREFIX, startProgressKeepalive, stopProgressKeepalive } from '../src/parser.js'
|
|
|
|
// A cold parse goes genuinely silent between providers (a measured 31.6s on a
|
|
// large corpus, in the inter-provider cache save), and the desktop app reads
|
|
// silence as a dead child. These pin the heartbeat that makes silence mean
|
|
// stopped rather than slow.
|
|
describe('scan-progress keepalive', () => {
|
|
const original = process.env['CODEBURN_PROGRESS']
|
|
|
|
/** Collects the progress lines written to stderr while `fn` drives the clock. */
|
|
function captureKeepalives(fn: () => void): string[] {
|
|
const written: string[] = []
|
|
const spy = vi.spyOn(process.stderr, 'write').mockImplementation(((chunk: unknown) => {
|
|
written.push(String(chunk))
|
|
return true
|
|
}) as typeof process.stderr.write)
|
|
try { fn() } finally { spy.mockRestore() }
|
|
return written.filter(line => line.startsWith(PROGRESS_LINE_PREFIX) && line.includes('"keepalive"'))
|
|
}
|
|
|
|
afterEach(() => {
|
|
stopProgressKeepalive()
|
|
stopProgressKeepalive()
|
|
vi.useRealTimers()
|
|
if (original === undefined) delete process.env['CODEBURN_PROGRESS']
|
|
else process.env['CODEBURN_PROGRESS'] = original
|
|
})
|
|
|
|
it('beats through a silent stretch far longer than the app watchdog window', () => {
|
|
process.env['CODEBURN_PROGRESS'] = '1'
|
|
vi.useFakeTimers()
|
|
// 90s of a parse doing nothing observable — three times the measured save
|
|
// stall, and twice the app's 45s silence window.
|
|
const beats = captureKeepalives(() => {
|
|
startProgressKeepalive()
|
|
vi.advanceTimersByTime(90_000)
|
|
})
|
|
expect(beats.length).toBeGreaterThanOrEqual(9)
|
|
// No silent gap anywhere near the window the app kills on.
|
|
expect(90_000 / beats.length).toBeLessThan(45_000)
|
|
})
|
|
|
|
it('stops when the parse ends, so an idle process never chatters', () => {
|
|
process.env['CODEBURN_PROGRESS'] = '1'
|
|
vi.useFakeTimers()
|
|
const afterStop = captureKeepalives(() => {
|
|
startProgressKeepalive()
|
|
vi.advanceTimersByTime(25_000)
|
|
stopProgressKeepalive()
|
|
})
|
|
expect(afterStop.length).toBeGreaterThan(0)
|
|
expect(captureKeepalives(() => vi.advanceTimersByTime(60_000))).toEqual([])
|
|
})
|
|
|
|
it('keeps beating until the outermost parse finishes', () => {
|
|
process.env['CODEBURN_PROGRESS'] = '1'
|
|
vi.useFakeTimers()
|
|
startProgressKeepalive()
|
|
startProgressKeepalive()
|
|
stopProgressKeepalive() // an inner parse returned; the outer one is still running
|
|
expect(captureKeepalives(() => vi.advanceTimersByTime(30_000)).length).toBeGreaterThan(0)
|
|
stopProgressKeepalive()
|
|
expect(captureKeepalives(() => vi.advanceTimersByTime(30_000))).toEqual([])
|
|
})
|
|
|
|
it('emits nothing for a plain CLI run (no CODEBURN_PROGRESS)', () => {
|
|
delete process.env['CODEBURN_PROGRESS']
|
|
vi.useFakeTimers()
|
|
expect(captureKeepalives(() => {
|
|
startProgressKeepalive()
|
|
vi.advanceTimersByTime(60_000)
|
|
})).toEqual([])
|
|
})
|
|
})
|