/ src / lib / server / runtime / process-manager.test.ts
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  })