diff --git a/apps/cli/src/process-shutdown.ts b/apps/cli/src/process-shutdown.ts index 1f1d785800..afc7d6fa3d 100644 --- a/apps/cli/src/process-shutdown.ts +++ b/apps/cli/src/process-shutdown.ts @@ -30,6 +30,7 @@ export function createProcessShutdown( const exitOnce = (code: number): void => { if (exited) return exited = true + /* v8 ignore else -- shutdown() arms the timer before any asynchronous exit path can run. */ if (timeout !== undefined) clearTimeout(timeout) exit(code) } diff --git a/apps/cli/tests/process-shutdown.spec.ts b/apps/cli/tests/process-shutdown.spec.ts index 5281d59b07..feceba708f 100644 --- a/apps/cli/tests/process-shutdown.spec.ts +++ b/apps/cli/tests/process-shutdown.spec.ts @@ -14,7 +14,10 @@ function deferred(): { promise: Promise; resolve: () => void; reject: (err return { promise, resolve, reject } } -afterEach(() => { vi.useRealTimers() }) +afterEach(() => { + vi.useRealTimers() + vi.restoreAllMocks() +}) describe('process shutdown', () => { it('exits once after graceful disposal resolves or rejects', async () => { @@ -31,6 +34,16 @@ describe('process shutdown', () => { expect(rejectedExit).toHaveBeenCalledWith(1) }) + it('uses process.exit as the default process boundary', async () => { + const exit = vi.spyOn(process, 'exit').mockImplementation(_code => undefined as never) + const shutdown = createProcessShutdown(() => Promise.resolve()) + + await shutdown.shutdown(7) + + expect(exit).toHaveBeenCalledOnce() + expect(exit).toHaveBeenCalledWith(7) + }) + it('forces exit when graceful disposal reaches its bound', async () => { vi.useFakeTimers() const disposal = deferred() @@ -49,6 +62,22 @@ describe('process shutdown', () => { expect(exit).toHaveBeenCalledOnce() }) + it('honors a caller-supplied grace period', async () => { + vi.useFakeTimers() + const disposal = deferred() + const exit = vi.fn() + const shutdown = createProcessShutdown(() => disposal.promise, exit, 25) + const pending = shutdown.shutdown(0) + + await vi.advanceTimersByTimeAsync(24) + expect(exit).not.toHaveBeenCalled() + await vi.advanceTimersByTimeAsync(1) + expect(exit).toHaveBeenCalledOnce() + + disposal.resolve() + await pending + }) + it('lets Ctrl+C force a normal shutdown already stuck in disposal', async () => { const disposal = deferred() const exit = vi.fn() diff --git a/packages/telemetry/session-telemetry-otel/src/index.ts b/packages/telemetry/session-telemetry-otel/src/index.ts index 062bac1e27..6caeded949 100644 --- a/packages/telemetry/session-telemetry-otel/src/index.ts +++ b/packages/telemetry/session-telemetry-otel/src/index.ts @@ -204,6 +204,7 @@ export class TelemetryOtel extends Telemetry { try { await Promise.race([providerShutdown, deadline]) } finally { + /* v8 ignore else -- the Promise executor assigns timer synchronously before this race starts. */ if (timer !== undefined) clearTimeout(timer) } }