Files
sandbox-runtime/test/cli.test.ts
Dylan Conway b16b240be4 fix(cli): shell-quote positional args instead of join(" ") (#239)
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
2026-05-05 13:56:01 -07:00

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)
})
})
})