2026-07-26 06:59:01 +08:00
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
|
import { Context } from 'cordis'
|
2026-07-26 12:43:14 +08:00
|
|
|
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
|
|
|
|
|
import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
2026-07-26 06:59:01 +08:00
|
|
|
|
2026-07-26 12:43:14 +08:00
|
|
|
function spec(command: string, overrides: Partial<SubprocessSpawnSpec> = {}): SubprocessSpawnSpec {
|
2026-07-26 06:59:01 +08:00
|
|
|
return {
|
|
|
|
|
argv: ['bash', '-c', command],
|
|
|
|
|
cwd: process.cwd(),
|
2026-07-26 14:07:42 +08:00
|
|
|
stdio: {
|
|
|
|
|
stdin: 'ignore',
|
|
|
|
|
stdout: { maxBytes: 64_000, spill: { maxBytes: 64 * 1024 * 1024 } },
|
|
|
|
|
stderr: { maxBytes: 64_000, spill: { maxBytes: 64 * 1024 * 1024 } },
|
|
|
|
|
},
|
2026-07-26 06:59:01 +08:00
|
|
|
graceMs: 200,
|
|
|
|
|
...overrides,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 12:43:14 +08:00
|
|
|
describe('LocalSubprocessService', () => {
|
|
|
|
|
it('registers as ctx.subprocess and spawns managed handles', async () => {
|
2026-07-26 06:59:01 +08:00
|
|
|
const ctx = new Context()
|
2026-07-26 12:43:14 +08:00
|
|
|
const fiber = await ctx.plugin(LocalSubprocessService)
|
2026-07-26 14:07:42 +08:00
|
|
|
const handle = ctx.subprocess.spawn(spec('echo managed'))
|
|
|
|
|
const result = await handle.done
|
2026-07-26 06:59:01 +08:00
|
|
|
expect(result.exitCode).toBe(0)
|
2026-07-26 14:07:42 +08:00
|
|
|
expect(handle.collected.stdout!.readFrom(0).text).toBe('managed\n')
|
2026-07-26 06:59:01 +08:00
|
|
|
await fiber.dispose()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('disposal kills still-running processes and awaits their exit', async () => {
|
|
|
|
|
const ctx = new Context()
|
2026-07-26 12:43:14 +08:00
|
|
|
const fiber = await ctx.plugin(LocalSubprocessService)
|
|
|
|
|
const handle = ctx.subprocess.spawn(spec('sleep 60'))
|
2026-07-26 06:59:01 +08:00
|
|
|
await fiber.dispose()
|
|
|
|
|
const outcome = await handle.done
|
|
|
|
|
expect(outcome.signal).toBe('SIGTERM')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('a settled process leaves the live set (disposal does not re-kill it)', async () => {
|
|
|
|
|
const ctx = new Context()
|
2026-07-26 12:43:14 +08:00
|
|
|
const fiber = await ctx.plugin(LocalSubprocessService)
|
|
|
|
|
const handle = ctx.subprocess.spawn(spec('true'))
|
2026-07-26 06:59:01 +08:00
|
|
|
const outcome = await handle.done
|
|
|
|
|
expect(outcome.exitCode).toBe(0)
|
|
|
|
|
await fiber.dispose()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('disposal tolerates a handle whose spawn already failed', async () => {
|
|
|
|
|
const ctx = new Context()
|
2026-07-26 12:43:14 +08:00
|
|
|
const fiber = await ctx.plugin(LocalSubprocessService)
|
|
|
|
|
const handle = ctx.subprocess.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-subprocess-test' }))
|
2026-07-26 06:59:01 +08:00
|
|
|
await expect(handle.done).rejects.toThrow()
|
|
|
|
|
await fiber.dispose()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('disposal contains a spawn-failure rejection that races teardown', async () => {
|
|
|
|
|
const ctx = new Context()
|
2026-07-26 12:43:14 +08:00
|
|
|
const fiber = await ctx.plugin(LocalSubprocessService)
|
2026-07-26 06:59:01 +08:00
|
|
|
// Dispose before the rejection continuation removes the handle from the
|
|
|
|
|
// live set, so teardown itself must swallow the rejected done.
|
2026-07-26 12:43:14 +08:00
|
|
|
const handle = ctx.subprocess.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-subprocess-test' }))
|
2026-07-26 06:59:01 +08:00
|
|
|
await fiber.dispose()
|
|
|
|
|
await expect(handle.done).rejects.toThrow()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it('loading a second implementation throws (one processes service per context — cordis standard)', async () => {
|
|
|
|
|
const ctx = new Context()
|
2026-07-26 12:43:14 +08:00
|
|
|
await ctx.plugin(LocalSubprocessService)
|
|
|
|
|
class SecondManager extends LocalSubprocessService {}
|
|
|
|
|
await expect(ctx.plugin(SecondManager)).rejects.toThrow(/service "subprocess" has been registered/)
|
2026-07-26 06:59:01 +08:00
|
|
|
})
|
|
|
|
|
})
|