Files
bare-operating-system/packages/bare-os-coreutils/test/agent-tools-run-command.test.mjs
T
2026-08-18 18:11:34 -04:00

130 lines
3.6 KiB
JavaScript

import test from 'brittle'
import { readFileSync } from 'node:fs'
import vm from 'node:vm'
const CODE =
readFileSync(new URL('../lib/agent/agent-tools.js', import.meta.url), 'utf8') +
'\n' +
readFileSync(
new URL('../lib/agent/agent-tool-definitions.js', import.meta.url),
'utf8'
) +
'\n' +
readFileSync(
new URL('../lib/agent/agent-tool-dispatch.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<string>} */ (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'))
})