windows-stdio.spec.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { spawnSync } from 'node:child_process'
  2. import { randomUUID } from 'node:crypto'
  3. import { once } from 'node:events'
  4. import { connect } from 'node:net'
  5. import { join } from 'node:path'
  6. import { describe, expect, it } from 'vitest'
  7. import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
  8. import { createWindowsStdioBridge } from '../src/windows-stdio.ts'
  9. function pipeBase(): string {
  10. return process.platform === 'win32'
  11. ? `\\\\.\\pipe\\dsh-windows-stdio-test-${randomUUID()}`
  12. : join('/tmp', `dsh-windows-stdio-${randomUUID()}`)
  13. }
  14. function spec(): SubprocessSpawnSpec {
  15. return {
  16. argv: ['target'],
  17. cwd: process.cwd(),
  18. stdio: { stdin: 'pipe', stdout: 'pipe', stderr: { maxBytes: 1024 } },
  19. graceMs: 100,
  20. }
  21. }
  22. function pathAfter(args: readonly string[], key: string): string {
  23. const path = args[args.indexOf(key) + 1]
  24. if (path === undefined) throw new Error(`missing ${key}`)
  25. return path
  26. }
  27. describe('Windows parent-owned stdio bridge', () => {
  28. it('binds before returning so a synchronously launched peer can connect', async () => {
  29. const bridge = createWindowsStdioBridge({
  30. ...spec(),
  31. stdio: { stdin: 'ignore', stdout: 'pipe', stderr: 'inherit' },
  32. }, pipeBase())
  33. const stdoutPath = pathAfter(bridge.runnerArgs, '--stdout-pipe')
  34. const result = spawnSync(process.execPath, ['-e', `
  35. const { connect } = require('node:net')
  36. const socket = connect(${JSON.stringify(stdoutPath)})
  37. socket.once('connect', () => {
  38. socket.write('blocked-parent', () => {
  39. socket.destroy()
  40. process.exit(0)
  41. })
  42. })
  43. socket.once('error', () => { process.exit(1) })
  44. setTimeout(() => { process.exit(2) }, 2000)
  45. `], { timeout: 5_000 })
  46. expect(result.status).toBe(0)
  47. const chunks: Buffer[] = []
  48. bridge.stdout?.on('data', (chunk: Buffer) => { chunks.push(chunk) })
  49. await once(bridge.stdout as NodeJS.ReadableStream, 'end')
  50. expect(Buffer.concat(chunks).toString()).toBe('blocked-parent')
  51. bridge.dispose()
  52. })
  53. it('moves bytes in both directions and ends output with its target-side peer', async () => {
  54. const bridge = createWindowsStdioBridge(spec(), pipeBase())
  55. const stdoutPath = pathAfter(bridge.runnerArgs, '--stdout-pipe')
  56. const stderrPath = pathAfter(bridge.runnerArgs, '--stderr-pipe')
  57. const stdinPath = pathAfter(bridge.runnerArgs, '--stdin-pipe')
  58. expect(bridge.runnerStdio).toEqual(['ignore', 'ignore', 'ignore', 'ipc'])
  59. await new Promise(resolve => setImmediate(resolve))
  60. bridge.stdin?.end('in')
  61. const stdoutPeer = connect(stdoutPath)
  62. const stderrPeer = connect(stderrPath)
  63. const stdinPeer = connect(stdinPath)
  64. await Promise.all([once(stdoutPeer, 'connect'), once(stderrPeer, 'connect'), once(stdinPeer, 'connect')])
  65. const stdoutChunks: Buffer[] = []
  66. const stderrChunks: Buffer[] = []
  67. const stdinChunks: Buffer[] = []
  68. bridge.stdout?.on('data', (chunk: Buffer) => { stdoutChunks.push(chunk) })
  69. bridge.stderr?.on('data', (chunk: Buffer) => { stderrChunks.push(chunk) })
  70. stdinPeer.on('data', (chunk: Buffer) => { stdinChunks.push(chunk) })
  71. const stdoutEnded = once(bridge.stdout as NodeJS.ReadableStream, 'end')
  72. const stderrEnded = once(bridge.stderr as NodeJS.ReadableStream, 'end')
  73. const stdinEnded = once(stdinPeer, 'end')
  74. stdoutPeer.end('out')
  75. stderrPeer.end('err')
  76. await Promise.all([stdoutEnded, stderrEnded, stdinEnded])
  77. expect(Buffer.concat(stdoutChunks).toString()).toBe('out')
  78. expect(Buffer.concat(stderrChunks).toString()).toBe('err')
  79. expect(Buffer.concat(stdinChunks).toString()).toBe('in')
  80. bridge.dispose()
  81. })
  82. it('uses inherited output directly and disposes unconnected endpoints', () => {
  83. const inherited = createWindowsStdioBridge({
  84. ...spec(),
  85. stdio: { stdin: 'ignore', stdout: 'inherit', stderr: 'inherit' },
  86. }, pipeBase())
  87. expect(inherited.stdin).toBeNull()
  88. expect(inherited.stdout).toBeNull()
  89. expect(inherited.stderr).toBeNull()
  90. expect(inherited.runnerArgs).toEqual([])
  91. expect(inherited.runnerStdio).toEqual(['ignore', 'inherit', 'inherit', 'ipc'])
  92. inherited.dispose()
  93. const pending = createWindowsStdioBridge(spec(), pipeBase())
  94. pending.closeInput()
  95. expect(pending.stdin?.destroyed).toBe(true)
  96. pending.dispose()
  97. expect(pending.stdout?.destroyed).toBe(true)
  98. expect(pending.stderr?.destroyed).toBe(true)
  99. })
  100. })