process-manager.test.ts
1 import assert from 'node:assert/strict' 2 import { afterEach, test } from 'node:test' 3 import { setTimeout as delay } from 'node:timers/promises' 4 import { 5 buildDockerExecArgs, 6 clearManagedProcess, 7 getManagedProcess, 8 getShellCommand, 9 listManagedProcesses, 10 startManagedProcess, 11 } from '@/lib/server/runtime/process-manager' 12 import { clearDockerDetectCache } from '@/lib/server/sandbox/docker-detect' 13 14 const originalPath = process.env.PATH 15 16 afterEach(async () => { 17 process.env.PATH = originalPath 18 clearDockerDetectCache() 19 for (const rec of listManagedProcesses()) { 20 clearManagedProcess(rec.id) 21 } 22 }) 23 24 test('sandboxed processes fail cleanly when docker is unavailable', async () => { 25 process.env.PATH = '' 26 clearDockerDetectCache() 27 28 const result = await startManagedProcess({ 29 command: 'echo hello', 30 cwd: process.cwd(), 31 env: { PATH: '' }, 32 timeoutMs: 2_000, 33 yieldMs: 2_000, 34 sandbox: { 35 image: 'alpine:3.20', 36 network: 'none', 37 memoryMb: 64, 38 cpus: 1, 39 readonlyRoot: true, 40 workspaceMounts: [], 41 }, 42 }) 43 44 await delay(25) 45 46 assert.equal(result.status, 'completed') 47 assert.equal(result.exitCode, 1) 48 assert.match(result.output || '', /Spawn error: spawn docker ENOENT/) 49 50 const record = getManagedProcess(result.processId) 51 assert.equal(record?.status, 'failed') 52 }) 53 54 test('persistent sandboxes use docker exec with container workdir and env passthrough', () => { 55 const shell = getShellCommand('pwd', 'proc-1', { 56 kind: 'persistent', 57 containerName: 'swarmclaw-sb-session', 58 containerWorkdir: '/workspace/apps/web', 59 env: { 60 HOME: '/workspace', 61 PATH: '/custom/bin:/usr/local/bin', 62 }, 63 }) 64 65 assert.equal(shell.shell, 'docker') 66 assert.deepEqual(shell.args.slice(0, 4), ['exec', '-i', '-w', '/workspace/apps/web']) 67 assert.ok(shell.args.includes('swarmclaw-sb-session')) 68 assert.ok(shell.args.includes('SWARMCLAW_PREPEND_PATH=/custom/bin:/usr/local/bin')) 69 assert.match(shell.args.at(-1) || '', /export PATH="\$\{SWARMCLAW_PREPEND_PATH\}:\$PATH"; unset SWARMCLAW_PREPEND_PATH; pwd/) 70 }) 71 72 test('buildDockerExecArgs preserves container env without interpolating PATH into the command', () => { 73 const args = buildDockerExecArgs({ 74 containerName: 'sandbox-1', 75 command: 'echo hello', 76 workdir: '/workspace', 77 env: { 78 HOME: '/workspace', 79 PATH: '$(touch /tmp/swarmclaw-path-injection)', 80 }, 81 }) 82 83 assert.ok(args.includes('SWARMCLAW_PREPEND_PATH=$(touch /tmp/swarmclaw-path-injection)')) 84 assert.equal((args.at(-1) || '').includes('$(touch /tmp/swarmclaw-path-injection)'), false) 85 assert.match(args.at(-1) || '', /SWARMCLAW_PREPEND_PATH/) 86 })