130 lines
3.6 KiB
JavaScript
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'))
|
|
})
|