spawn-runner-built.e2e.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. import { spawn } from 'node:child_process'
  2. import type { Buffer } from 'node:buffer'
  3. import { existsSync } from 'node:fs'
  4. import { resolve } from 'node:path'
  5. import { describe, expect, it } from 'vitest'
  6. import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
  7. import {
  8. cleanupLinuxLaunchFiles,
  9. createLinuxLaunchFiles,
  10. } from '../src/runner-protocol.ts'
  11. import {
  12. runnerEnvironment,
  13. SUBPROCESS_RUNNER_ENV,
  14. targetEnvironment,
  15. } from '../src/runner-launch.ts'
  16. import type { RunnerInvocation } from '../src/runner-launch.ts'
  17. import { bindManagedProcess } from '../src/spawn.ts'
  18. import { launchWindowsJob } from '../src/windows-job.ts'
  19. const repoRoot = resolve(import.meta.dirname, '../../../..')
  20. const sourceRunner = resolve(repoRoot, 'packages/subprocess/subprocess-local/src/bin.ts')
  21. const builtRunner = resolve(repoRoot, 'packages/subprocess/subprocess-local/lib/runner.js')
  22. function targetEnv(): Record<string, string> {
  23. return {
  24. ...Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined)),
  25. [SUBPROCESS_RUNNER_ENV]: 'target-collision-restored',
  26. }
  27. }
  28. async function executePosix(invocation: RunnerInvocation): Promise<{ status: number | null; stdout: string; stderr: string }> {
  29. const files = createLinuxLaunchFiles({ cwd: repoRoot, env: targetEnv() })
  30. try {
  31. const child = spawn(invocation[0], [
  32. ...invocation.slice(1),
  33. '--',
  34. process.execPath,
  35. '--input-type=module',
  36. '--eval',
  37. `process.stdout.write(process.argv[0]+'|'+process.cwd()+'|'+process.env.${SUBPROCESS_RUNNER_ENV})`,
  38. ], {
  39. env: runnerEnvironment(files.requestPath, invocation),
  40. stdio: ['ignore', 'pipe', 'pipe'],
  41. })
  42. let stdout = ''
  43. let stderr = ''
  44. child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString() })
  45. child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString() })
  46. const status = await new Promise<number | null>((resolveExit, rejectExit) => {
  47. child.once('error', rejectExit)
  48. child.once('exit', resolveExit)
  49. })
  50. return { status, stdout, stderr }
  51. } finally {
  52. cleanupLinuxLaunchFiles(files)
  53. }
  54. }
  55. async function executeWindows(invocation: RunnerInvocation): Promise<{ status: number | null; stdout: string; stderr: string }> {
  56. const request: SubprocessSpawnSpec = {
  57. argv: [
  58. process.execPath,
  59. '--input-type=module',
  60. '--eval',
  61. `process.stdout.write(process.argv[0]+'|'+process.cwd()+'|'+process.env.${SUBPROCESS_RUNNER_ENV})`,
  62. ],
  63. cwd: repoRoot,
  64. env: targetEnv(),
  65. stdio: {
  66. stdin: 'ignore',
  67. stdout: { maxBytes: 64_000 },
  68. stderr: { maxBytes: 64_000 },
  69. },
  70. graceMs: 3_000,
  71. }
  72. const handle = bindManagedProcess(request, launchWindowsJob(
  73. request,
  74. targetEnvironment(request),
  75. { runnerInvocation: invocation },
  76. ))
  77. const outcome = await handle.done
  78. await handle.waitForExit()
  79. const stdout = handle.collected.stdout?.readFrom(0).text ?? ''
  80. const stderr = handle.collected.stderr?.readFrom(0).text ?? ''
  81. return { status: outcome.exitCode, stdout, stderr }
  82. }
  83. async function execute(invocation: RunnerInvocation): Promise<{ status: number | null; stdout: string; stderr: string }> {
  84. return process.platform === 'win32'
  85. ? executeWindows(invocation)
  86. : executePosix(invocation)
  87. }
  88. describe('subprocess-local runner artifacts', () => {
  89. it('executes the source entry through the provider-owned core', async () => {
  90. const result = await execute([process.execPath, '--import', 'tsx/esm', sourceRunner])
  91. expect(result).toEqual({
  92. status: 0,
  93. stdout: `${process.execPath}|${repoRoot}|target-collision-restored`,
  94. stderr: '',
  95. })
  96. }, 30_000)
  97. it.skipIf(!existsSync(builtRunner))('executes the built ./runner subpath through the same core', async () => {
  98. const result = await execute([process.execPath, builtRunner])
  99. expect(result).toEqual({
  100. status: 0,
  101. stdout: `${process.execPath}|${repoRoot}|target-collision-restored`,
  102. stderr: '',
  103. })
  104. }, 30_000)
  105. })