Files
deepseek-harness/packages/subprocess/subprocess-local/tests/local.spec.ts
Tianyi Cui fc566119a7 refactor(subprocess): rename the process seam to subprocess and address review
Review feedback (tianyicui): 'process' is a poor service name. The family is
now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess,
abstract SubprocessService, Subprocess* vocabulary) and
@deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed
throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs,
and gates. 'subprocess' is the precise term for managed OS children (the
Python-stdlib sense), avoids colliding with Node's global process object,
and reads as one system beside dsh-subagent-subprocess.

ds-review-bot findings addressed:
- kill() on a settled handle is now a no-op (no signal to a possibly-reused
  pgid, no referenced grace timer delaying exit); pinned by a spy test.
- The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get
  drift-checked type-equiv blocks on the new subprocess.md page, restoring
  their manifest registration.
- subprocess.md is registered in the core.md sub-page index (en+zh).
2026-07-26 12:43:59 +08:00

71 lines
2.8 KiB
TypeScript

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(),
stdoutMaxBytes: 64_000,
stderrMaxBytes: 64_000,
maxSpillBytes: 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 result = await ctx.subprocess.spawn(spec('echo managed')).done
expect(result.exitCode).toBe(0)
expect(result.stdout.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/)
})
})