manager.spec.ts 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. import { describe, expect, it } from 'vitest'
  2. import { Context } from 'cordis'
  3. import LocalProcessManager from '@deepseek-ai/dsh-process-local'
  4. import type { ProcessSpawnSpec } from '@deepseek-ai/dsh-process'
  5. function spec(command: string, overrides: Partial<ProcessSpawnSpec> = {}): ProcessSpawnSpec {
  6. return {
  7. argv: ['bash', '-c', command],
  8. cwd: process.cwd(),
  9. stdoutMaxBytes: 64_000,
  10. stderrMaxBytes: 64_000,
  11. maxSpillBytes: 64 * 1024 * 1024,
  12. graceMs: 200,
  13. ...overrides,
  14. }
  15. }
  16. describe('LocalProcessManager', () => {
  17. it('registers as ctx.processes and spawns managed handles', async () => {
  18. const ctx = new Context()
  19. const fiber = await ctx.plugin(LocalProcessManager)
  20. const result = await ctx.processes.spawn(spec('echo managed')).done
  21. expect(result.exitCode).toBe(0)
  22. expect(result.stdout.text).toBe('managed\n')
  23. await fiber.dispose()
  24. })
  25. it('disposal kills still-running processes and awaits their exit', async () => {
  26. const ctx = new Context()
  27. const fiber = await ctx.plugin(LocalProcessManager)
  28. const handle = ctx.processes.spawn(spec('sleep 60'))
  29. await fiber.dispose()
  30. const outcome = await handle.done
  31. expect(outcome.signal).toBe('SIGTERM')
  32. })
  33. it('a settled process leaves the live set (disposal does not re-kill it)', async () => {
  34. const ctx = new Context()
  35. const fiber = await ctx.plugin(LocalProcessManager)
  36. const handle = ctx.processes.spawn(spec('true'))
  37. const outcome = await handle.done
  38. expect(outcome.exitCode).toBe(0)
  39. await fiber.dispose()
  40. })
  41. it('disposal tolerates a handle whose spawn already failed', async () => {
  42. const ctx = new Context()
  43. const fiber = await ctx.plugin(LocalProcessManager)
  44. const handle = ctx.processes.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-manager-test' }))
  45. await expect(handle.done).rejects.toThrow()
  46. await fiber.dispose()
  47. })
  48. it('disposal contains a spawn-failure rejection that races teardown', async () => {
  49. const ctx = new Context()
  50. const fiber = await ctx.plugin(LocalProcessManager)
  51. // Dispose before the rejection continuation removes the handle from the
  52. // live set, so teardown itself must swallow the rejected done.
  53. const handle = ctx.processes.spawn(spec('true', { cwd: '/nonexistent-dir-dsh-manager-test' }))
  54. await fiber.dispose()
  55. await expect(handle.done).rejects.toThrow()
  56. })
  57. it('loading a second implementation throws (one processes service per context — cordis standard)', async () => {
  58. const ctx = new Context()
  59. await ctx.plugin(LocalProcessManager)
  60. class SecondManager extends LocalProcessManager {}
  61. await expect(ctx.plugin(SecondManager)).rejects.toThrow(/service "processes" has been registered/)
  62. })
  63. })