| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- import { describe, expect, it } from 'vitest'
- import { Context } from 'cordis'
- import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
- import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
- function spec(command: string, overrides: Partial<SubprocessSpawnSpec> = {}): SubprocessSpawnSpec {
- return {
- argv: ['bash', '-c', command],
- cwd: process.cwd(),
- stdio: {
- stdin: 'ignore',
- stdout: { maxBytes: 64_000, spill: { maxBytes: 64 * 1024 * 1024 } },
- stderr: { maxBytes: 64_000, spill: { maxBytes: 64 * 1024 * 1024 } },
- },
- graceMs: 200,
- ...overrides,
- }
- }
- describe('LocalSubprocessService', () => {
- it('registers as ctx.subprocess and spawns managed handles', async () => {
- const ctx = new Context()
- const fiber = await ctx.plugin(LocalSubprocessService)
- const handle = ctx.subprocess.spawn(spec('echo managed'))
- const result = await handle.done
- expect(result.exitCode).toBe(0)
- expect(handle.collected.stdout!.readFrom(0).text).toBe('managed\n')
- await fiber.dispose()
- })
- it('disposal kills still-running processes and awaits their exit', async () => {
- const ctx = new Context()
- const fiber = await ctx.plugin(LocalSubprocessService)
- const handle = ctx.subprocess.spawn(spec('sleep 60'))
- 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()
- const fiber = await ctx.plugin(LocalSubprocessService)
- const handle = ctx.subprocess.spawn(spec('true'))
- 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()
- const fiber = await ctx.plugin(LocalSubprocessService)
- const handle = ctx.subprocess.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-subprocess-test' }))
- await expect(handle.done).rejects.toThrow()
- await fiber.dispose()
- })
- it('disposal contains a spawn-failure rejection that races teardown', async () => {
- const ctx = new Context()
- const fiber = await ctx.plugin(LocalSubprocessService)
- // Dispose before the rejection continuation removes the handle from the
- // live set, so teardown itself must swallow the rejected done.
- const handle = ctx.subprocess.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-subprocess-test' }))
- 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()
- await ctx.plugin(LocalSubprocessService)
- class SecondManager extends LocalSubprocessService {}
- await expect(ctx.plugin(SecondManager)).rejects.toThrow(/service "subprocess" has been registered/)
- })
- })
|