mirror of
https://github.com/anthropic-experimental/sandbox-runtime.git
synced 2026-05-06 21:52:30 +08:00
In positional mode (`srt cmd arg1 arg2`), the argv is reassembled into a
string and run via `bash -c`. Reassembling with `commandArgs.join(" ")`
discards argument boundaries, so any arg containing whitespace or shell
metacharacters is re-tokenised by bash:
srt printf "%s\n" "hello world"
# before: prints "hello\nworld\n" (two args)
# after: prints "hello world\n"
shell-quote is already a dependency (used by the macOS/Linux wrappers).
The -c path is unchanged — it receives a single string and is meant to
be parsed by the shell.
Fixes #157
158 lines
5.5 KiB
TypeScript
158 lines
5.5 KiB
TypeScript
import { describe, test, expect } from 'bun:test'
|
|
import { spawnSync } from 'node:child_process'
|
|
import { join } from 'node:path'
|
|
|
|
/**
|
|
* Get the path to the CLI entry point
|
|
*/
|
|
function getCliPath(): string {
|
|
return join(process.cwd(), 'src', 'cli.ts')
|
|
}
|
|
|
|
/**
|
|
* Run the CLI with given arguments and return the result
|
|
*/
|
|
function runCli(args: string[], options?: { input?: string; debug?: boolean }) {
|
|
const result = spawnSync('bun', ['run', getCliPath(), ...args], {
|
|
encoding: 'utf-8',
|
|
input: options?.input,
|
|
env: {
|
|
...process.env,
|
|
// Use a non-existent config to get default behavior
|
|
HOME: '/tmp/cli-test-nonexistent',
|
|
// Enable SRT_DEBUG if debug option is set
|
|
...(options?.debug ? { SRT_DEBUG: 'true' } : {}),
|
|
},
|
|
})
|
|
return {
|
|
stdout: result.stdout,
|
|
stderr: result.stderr,
|
|
status: result.status,
|
|
}
|
|
}
|
|
|
|
describe('CLI', () => {
|
|
describe('-c flag (command string mode)', () => {
|
|
test('executes simple command with -c flag', () => {
|
|
const result = runCli(['-c', 'echo hello'])
|
|
expect(result.stdout.trim()).toBe('hello')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('passes command string directly without escaping', () => {
|
|
const result = runCli(['-c', 'echo "hello world"'])
|
|
expect(result.stdout.trim()).toBe('hello world')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('handles JSON arguments correctly', () => {
|
|
// This is the main use case - JSON with quotes and special chars
|
|
const result = runCli(['-c', 'echo \'{"key": "value"}\''])
|
|
expect(result.stdout.trim()).toBe('{"key": "value"}')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('handles complex JSON with nested objects', () => {
|
|
const json = '{"servers":{"name":"test","type":"sdk"}}'
|
|
const result = runCli(['-c', `echo '${json}'`])
|
|
expect(result.stdout.trim()).toBe(json)
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('handles shell expansion in -c mode', () => {
|
|
const result = runCli(['-c', 'echo $HOME'])
|
|
// $HOME should be expanded by the shell
|
|
expect(result.stdout.trim()).not.toBe('$HOME')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('handles pipes in -c mode', () => {
|
|
const result = runCli(['-c', 'echo "hello world" | wc -w'])
|
|
expect(result.stdout.trim()).toBe('2')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('handles command substitution in -c mode', () => {
|
|
const result = runCli(['-c', 'echo "count: $(echo 1 2 3 | wc -w)"'])
|
|
expect(result.stdout.trim()).toContain('3')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('default mode (positional arguments)', () => {
|
|
test('executes simple command with positional args', () => {
|
|
const result = runCli(['echo', 'hello'])
|
|
expect(result.stdout.trim()).toBe('hello')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('joins multiple positional arguments with spaces', () => {
|
|
const result = runCli(['echo', 'hello', 'world'])
|
|
expect(result.stdout.trim()).toBe('hello world')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('handles arguments with flags', () => {
|
|
const result = runCli(['echo', '-n', 'no newline'])
|
|
// -n flag to echo suppresses newline
|
|
expect(result.stdout).toBe('no newline')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('preserves argument boundaries when args contain spaces', () => {
|
|
// Regression for #157: positional args are later run via `bash -c`,
|
|
// so they must be shell-quoted. printf '%s\n' emits one line per
|
|
// arg, which exposes whether "hello world" arrived as one token
|
|
// (correct) or was re-split into "hello" and "world".
|
|
const result = runCli(['printf', '%s\n', 'hello world', 'a b'])
|
|
expect(result.stdout).toBe('hello world\na b\n')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('preserves shell metacharacters in positional args', () => {
|
|
// Positional mode is argv-style; metacharacters in an arg are data,
|
|
// not shell syntax (use -c for shell semantics).
|
|
const result = runCli(['printf', '%s', '$HOME;|&'])
|
|
expect(result.stdout).toBe('$HOME;|&')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('error handling', () => {
|
|
test('shows error when no command specified', () => {
|
|
const result = runCli([])
|
|
expect(result.stderr).toContain('No command specified')
|
|
expect(result.status).toBe(1)
|
|
})
|
|
|
|
test('shows error when only options provided without command', () => {
|
|
const result = runCli(['-d'])
|
|
expect(result.stderr).toContain('No command specified')
|
|
expect(result.status).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe('debug output', () => {
|
|
test('SRT_DEBUG enables debug output for positional args', () => {
|
|
const result = runCli(['echo', 'test'], { debug: true })
|
|
// Debug mode should show additional logging to stderr
|
|
expect(result.stderr).toContain('[SandboxDebug]')
|
|
expect(result.stderr).toContain('Original command')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('SRT_DEBUG enables debug output for -c mode', () => {
|
|
const result = runCli(['-c', 'echo test'], { debug: true })
|
|
expect(result.stderr).toContain('[SandboxDebug]')
|
|
expect(result.stderr).toContain('Command string mode')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
|
|
test('no debug output without SRT_DEBUG', () => {
|
|
const result = runCli(['echo', 'test'], { debug: false })
|
|
expect(result.stderr).not.toContain('[SandboxDebug]')
|
|
expect(result.status).toBe(0)
|
|
})
|
|
})
|
|
})
|