spawn-runner-built.e2e.ts 4.2 KB

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