fix(subprocess): clean runtime state after teardown failure
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { rm, stat } from 'node:fs/promises'
|
||||
import { stat } from 'node:fs/promises'
|
||||
import { basename, dirname, relative, resolve } from 'node:path'
|
||||
import { Context } from 'cordis'
|
||||
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
||||
@@ -119,11 +119,15 @@ describe('LocalSubprocessService', () => {
|
||||
expect(terminals.size).toBe(0)
|
||||
})
|
||||
|
||||
it('waits for every terminal cleanup and retains rejections', async () => {
|
||||
it('waits for every terminal cleanup, removes runtime state, and retains rejections', async () => {
|
||||
const ctx = new Context()
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const service = ctx.subprocess
|
||||
const runtimeRoot = service.runtimeRoot
|
||||
const firstFailure = new Error('first retryable cleanup failure')
|
||||
const secondFailure = new Error('second retryable cleanup failure')
|
||||
const disposalErrors: unknown[] = []
|
||||
ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error
|
||||
const failedTerminal: SubprocessTerminalHandle = {
|
||||
pid: 1,
|
||||
output: new PassThrough(),
|
||||
@@ -132,7 +136,12 @@ describe('LocalSubprocessService', () => {
|
||||
inspectForeground: async () => undefined,
|
||||
signalForeground: async () => 1,
|
||||
terminate: vi.fn(),
|
||||
waitForExit: vi.fn(async () => { throw new Error('retryable cleanup failure') }),
|
||||
waitForExit: vi.fn(async () => { throw firstFailure }),
|
||||
}
|
||||
const secondFailedTerminal: SubprocessTerminalHandle = {
|
||||
...failedTerminal,
|
||||
terminate: vi.fn(),
|
||||
waitForExit: vi.fn(async () => { throw secondFailure }),
|
||||
}
|
||||
let finishCleanup!: () => void
|
||||
const cleanup = new Promise<boolean>((resolve) => {
|
||||
@@ -145,6 +154,7 @@ describe('LocalSubprocessService', () => {
|
||||
}
|
||||
const terminals = (service as unknown as { terminals: Set<SubprocessTerminalHandle> }).terminals
|
||||
terminals.add(failedTerminal)
|
||||
terminals.add(secondFailedTerminal)
|
||||
terminals.add(drainingTerminal)
|
||||
|
||||
let disposed = false
|
||||
@@ -153,9 +163,40 @@ describe('LocalSubprocessService', () => {
|
||||
expect(disposed).toBe(false)
|
||||
finishCleanup()
|
||||
await disposing
|
||||
expect(terminals).toEqual(new Set([failedTerminal]))
|
||||
expect((await stat(runtimeRoot)).isDirectory()).toBe(true)
|
||||
await rm(runtimeRoot, { recursive: true, force: true })
|
||||
expect(terminals).toEqual(new Set([failedTerminal, secondFailedTerminal]))
|
||||
await expect(stat(runtimeRoot)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
expect(disposalErrors).toHaveLength(1)
|
||||
expect(disposalErrors[0]).toMatchObject({
|
||||
errors: [firstFailure, secondFailure],
|
||||
message: 'local subprocess teardown failed',
|
||||
})
|
||||
})
|
||||
|
||||
it('reports one cleanup failure without wrapping it after removing runtime state', async () => {
|
||||
const ctx = new Context()
|
||||
const failure = new Error('single cleanup failure')
|
||||
const disposalErrors: unknown[] = []
|
||||
ctx.logger.error = ((error: unknown) => { disposalErrors.push(error) }) as typeof ctx.logger.error
|
||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||
const service = ctx.subprocess
|
||||
const runtimeRoot = service.runtimeRoot
|
||||
const terminal: SubprocessTerminalHandle = {
|
||||
pid: 1,
|
||||
output: new PassThrough(),
|
||||
done: Promise.resolve({ exitCode: 0, signal: null }),
|
||||
write: async () => {},
|
||||
inspectForeground: async () => undefined,
|
||||
signalForeground: async () => 1,
|
||||
terminate: vi.fn(),
|
||||
waitForExit: vi.fn(async () => { throw failure }),
|
||||
}
|
||||
const terminals = (service as unknown as { terminals: Set<SubprocessTerminalHandle> }).terminals
|
||||
terminals.add(terminal)
|
||||
|
||||
await fiber.dispose()
|
||||
|
||||
await expect(stat(runtimeRoot)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
expect(disposalErrors).toEqual([failure])
|
||||
})
|
||||
|
||||
it('releases a terminal after top-level exit reaches quiescence', async () => {
|
||||
|
||||
Reference in New Issue
Block a user