import test from 'brittle' import { readFileSync } from 'node:fs' import vm from 'node:vm' const CODE = readFileSync( new URL('../lib/agent-tools.js', import.meta.url), 'utf8' ) function load(extra = {}) { const sandbox = { TextDecoder, TextEncoder, Uint8Array, console, ...extra } vm.createContext(sandbox) vm.runInContext(CODE, sandbox, { filename: 'agent-tools.js' }) return sandbox } function makeCtx() { const files = new Map() const b4a = { from(v) { return new TextEncoder().encode(typeof v === 'string' ? v : String(v)) }, toString(v) { return new TextDecoder().decode(v) }, concat(parts) { const chunks = parts.map((p) => (p instanceof Uint8Array ? p : b4a.from(String(p)))) const len = chunks.reduce((n, p) => n + p.length, 0) const out = new Uint8Array(len) let off = 0 for (const p of chunks) { out.set(p, off) off += p.length } return out } } /** @type {string[]} */ const seen = [] const ctx = { b4a, exitCode: 0, vfs: { env: {}, async readFile(p) { return files.get(p) || null }, async writeFile(p, buf) { files.set( p, buf instanceof Uint8Array ? buf : new TextEncoder().encode(String(buf)) ) } }, async execLine(cmd) { seen.push(String(cmd)) const m = />\s*'([^']+)'\s*2>&1/.exec(String(cmd)) const outPath = m ? m[1] : '/tmp/agent.out' await ctx.vfs.writeFile(outPath, b4a.from('ok\n')) const fail = String(cmd).includes('false') ctx.exitCode = fail ? 1 : 0 ctx.vfs.env.BARE_OS_EXIT_STATUS = fail ? '1' : '0' } } return { ctx, seen } } async function callRunCommand(ctx, command) { const s = load() const dispatch = /** @type {(o: object) => Promise} */ (s.bareAgentDispatchTool) const raw = await dispatch({ ctx, paths: { cmdOut: '/tmp/agent.out', dir: '/tmp' }, toolName: 'run_command', argsJson: JSON.stringify({ command, capture_exit: true }), configRef: { current: {} }, appendProgress: () => {}, signal: null, bareWebRunTool: async () => ({ ok: false }), bareWebFmtErr: (e) => String(e) }) return JSON.parse(raw) } test('run_command wraps compound shell with sh -c before capture redirection', async (t) => { const { ctx, seen } = makeCtx() const out = await callRunCommand(ctx, 'for x in a b; do if true; then echo $x; fi; done') t.ok(out.ok) t.ok(seen.length >= 1) t.ok(seen[0].startsWith("sh -c 'for x in a b; do if true; then echo $x; fi; done'")) }) test('run_command wraps combined if + arithmetic expansion probe', async (t) => { const { ctx, seen } = makeCtx() const probe = 'n=0; if true; then n=$((n+1)); fi; echo $n' const out = await callRunCommand(ctx, probe) t.ok(out.ok) t.ok(seen.length >= 1) t.ok(seen[0].startsWith(`sh -c '${probe}'`)) }) test('run_command keeps simple command direct with capture redirection', async (t) => { const { ctx, seen } = makeCtx() const out = await callRunCommand(ctx, 'echo hi') t.ok(out.ok) t.ok(seen.length >= 1) t.ok(seen[0].startsWith('echo hi > ')) }) test('run_command captures nonzero status through sh -c wrapper', async (t) => { const { ctx } = makeCtx() const out = await callRunCommand(ctx, 'false; echo $?') t.ok(out.ok) t.ok(String(out.stdout_stderr || '').includes('EXIT:1')) })