import test from 'node:test'; import assert from 'node:assert/strict'; import { mkdtemp, mkdir, writeFile, rm, readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { ContainerBackend, command, quote } from './container.mjs'; test('quoting prevents shell evaluation', async () => { const value = "a'\"; $(echo wrong) `echo wrong`\nline"; const result = await command(['bash', '-lc', `printf '%s' ${quote(value)}`]); assert.equal(result.output, value); }); test('separate clones share canonical main; snapshots contain only committed files', async t => { const root = await mkdtemp(join(tmpdir(), 'pi-git-test-')); t.after(() => rm(root, { recursive: true, force: true })); const seed = join(root, 'solution'); await mkdir(seed); await writeFile(join(seed, 'compile.sh'), '#!/bin/sh\necho first\n'); const init = await command(['bash', '-lc', `git -C ${quote(seed)} init -b main && git -C ${quote(seed)} add . && git -C ${quote(seed)} -c user.name=test -c user.email=test@example.invalid commit -m initial`]); assert.equal(init.exitCode, 0, init.output); const executed = []; const backend = new ContainerBackend({ container_name: 'fake', work_root: root }, () => {}, async (argv, options) => { executed.push(argv); assert.deepEqual(argv.slice(0, 5), ['docker', 'exec', '--user', 'agent', 'fake']); // Execute only deterministic backend management scripts in a temporary test directory. return command(['bash', '-lc', argv.at(-1)], options); }); await backend.prepare(); await backend.createAgent('agent-0'); await backend.createAgent('agent-1'); const first = backend.agentPath('agent-0'); await writeFile(join(first, 'compile.sh'), '#!/bin/sh\necho updated\n'); await writeFile(join(first, 'uncommitted.txt'), 'excluded'); const push = await command(['bash', '-lc', `git -C ${quote(first)} add compile.sh && git -C ${quote(first)} commit -m update && git -C ${quote(first)} push origin HEAD:main`]); assert.equal(push.exitCode, 0, push.output); assert.match(await readFile(join(backend.agentPath('agent-1'), 'compile.sh'), 'utf8'), /first/); const saved = await backend.snapshot(join(root, 'snapshot.tar.gz')); const contents = await command(['tar', '-tzf', saved.path]); assert.match(contents.output, /compile.sh/); assert.doesNotMatch(contents.output, /uncommitted/); assert.match(saved.revision, /^[0-9a-f]{40}$/); await assert.rejects(backend.prepare(), /Container command failed/); }); test('bash runs with container user, checkout, timeout and abort process-group cleanup', async () => { const calls = []; const controller = new AbortController(); const backend = new ContainerBackend({ container_name: 'solver', work_root: '/workspace', docker_prefix: ['custom-docker'] }, () => {}, async (argv, options = {}) => { calls.push(argv); if (options.signal) return new Promise(resolve => options.signal.addEventListener('abort', () => resolve({ exitCode: 143, output: 'terminated' }), { once: true })); return { exitCode: 0, output: '' }; }); const running = backend.bash('agent-2', "echo 'a'; sleep 50", { signal: controller.signal, timeout: 7 }); controller.abort(); await running; assert.deepEqual(calls[0].slice(0, 5), ['custom-docker', 'exec', '--user', 'agent', 'solver']); assert.match(calls[0].at(-1), /cd '\/workspace\/\.pi-study\/agents\/agent-2'/); assert.match(calls[0].at(-1), /timeout --signal=TERM --kill-after=5s 7s/); assert.ok(calls.slice(1).some(argv => argv.at(-1).includes('kill -KILL'))); });