orca.ai/pkg/sandbox/process_test.go
大森 6b94476347 Initial commit: Orca Agent Framework
Core features:
- Microkernel architecture with Actor model
- Session management with JSONL persistence
- Tool system (5 built-in tools)
- Skill system with SKILL.md parsing
- Sandbox security execution
- Ollama integration with gemma4:e4b
- Prompt-based tool calling (compatible with native function calling)
- REPL interface

11 packages, all tests passing
2026-05-08 00:55:48 +08:00

213 lines
5.6 KiB
Go

package sandbox
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestNewProcessSandbox(t *testing.T) {
ps := NewProcessSandbox()
if ps == nil {
t.Fatal("NewProcessSandbox() returned nil")
}
if ps.WorkingDir != DefaultWorkingDir {
t.Errorf("expected WorkingDir %q, got %q", DefaultWorkingDir, ps.WorkingDir)
}
if ps.OutputLimit != DefaultOutputLimit {
t.Errorf("expected OutputLimit %d, got %d", DefaultOutputLimit, ps.OutputLimit)
}
}
func TestExecuteEcho(t *testing.T) {
ps := NewProcessSandbox()
ctx := context.Background()
result, err := ps.Execute(ctx, "echo", "hello", "world")
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if result.ExitCode != 0 {
t.Errorf("expected exit code 0, got %d", result.ExitCode)
}
if strings.TrimSpace(result.Stdout) != "hello world" {
t.Errorf("expected stdout 'hello world', got %q", result.Stdout)
}
}
func TestExecuteWithArgs(t *testing.T) {
ps := NewProcessSandbox()
ctx := context.Background()
result, err := ps.Execute(ctx, "sh", "-c", "echo 'arg1 arg2'")
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if result.ExitCode != 0 {
t.Errorf("expected exit code 0, got %d", result.ExitCode)
}
if strings.TrimSpace(result.Stdout) != "arg1 arg2" {
t.Errorf("expected stdout 'arg1 arg2', got %q", result.Stdout)
}
}
func TestExecuteNonZeroExit(t *testing.T) {
ps := NewProcessSandbox()
ctx := context.Background()
result, err := ps.Execute(ctx, "sh", "-c", "exit 42")
if err != nil {
t.Fatalf("Execute should not error on non-zero exit: %v", err)
}
if result.ExitCode != 42 {
t.Errorf("expected exit code 42, got %d", result.ExitCode)
}
}
func TestExecuteCommandNotFound(t *testing.T) {
ps := NewProcessSandbox()
ctx := context.Background()
_, err := ps.Execute(ctx, "nonexistent-command-12345")
if err == nil {
t.Fatal("expected error for nonexistent command")
}
}
func TestExecuteTimeout(t *testing.T) {
ps := NewProcessSandbox()
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
_, err := ps.Execute(ctx, "sleep", "10")
if err == nil {
t.Fatal("expected timeout error")
}
// On macOS the error may be "signal: killed" or "context deadline exceeded".
// Just verify an error occurred — the exact message varies by platform.
t.Logf("timeout produced error: %v", err)
}
func TestExecuteWorkingDirectory(t *testing.T) {
// Use a temp directory for this test
tmpDir, err := os.MkdirTemp("", "sandbox-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
ps := &ProcessSandbox{
WorkingDir: tmpDir,
OutputLimit: DefaultOutputLimit,
EnvWhitelist: AllowedEnvVars,
}
ctx := context.Background()
result, err := ps.Execute(ctx, "pwd")
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if result.ExitCode != 0 {
t.Errorf("expected exit code 0, got %d", result.ExitCode)
}
// pwd should return the temp directory
gotDir := strings.TrimSpace(result.Stdout)
absGot, _ := filepath.EvalSymlinks(gotDir)
absTmp, _ := filepath.EvalSymlinks(tmpDir)
if absGot != absTmp {
t.Errorf("expected working dir %q, got %q", absTmp, absGot)
}
}
func TestEnvironmentWhitelist(t *testing.T) {
ps := NewProcessSandbox()
ps.EnvWhitelist = []string{"HOME"}
ctx := context.Background()
result, err := ps.Execute(ctx, "sh", "-c", "echo $HOME")
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if result.ExitCode != 0 {
t.Errorf("expected exit code 0, got %d", result.ExitCode)
}
home := os.Getenv("HOME")
if home != "" && strings.TrimSpace(result.Stdout) != home {
t.Errorf("expected HOME=%q, got %q", home, strings.TrimSpace(result.Stdout))
}
}
func TestEnvironmentIsolation(t *testing.T) {
ps := NewProcessSandbox()
// Use an empty whitelist to ensure no env vars are passed
ps.EnvWhitelist = []string{}
ctx := context.Background()
result, err := ps.Execute(ctx, "sh", "-c", "echo $HOME")
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
// HOME should be empty in the child process
if strings.TrimSpace(result.Stdout) != "" {
t.Errorf("expected empty HOME in isolated env, got %q", result.Stdout)
}
}
func TestOutputLimit(t *testing.T) {
ps := NewProcessSandbox()
ps.OutputLimit = 10 // Only 10 bytes
ctx := context.Background()
// Generate a long output well beyond the 10-byte limit
result, err := ps.Execute(ctx, "sh", "-c", "echo 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFF'")
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
// The output should be truncated to approximately 10 bytes (plus newline)
if len(result.Stdout) > 15 {
t.Errorf("expected truncated output (<=15 bytes), got %d bytes: %q", len(result.Stdout), result.Stdout)
}
}
func TestExecuteStderr(t *testing.T) {
ps := NewProcessSandbox()
ctx := context.Background()
result, err := ps.Execute(ctx, "sh", "-c", "echo 'error output' >&2; echo 'normal output'")
if err != nil {
t.Fatalf("Execute failed: %v", err)
}
if result.ExitCode != 0 {
t.Errorf("expected exit code 0, got %d", result.ExitCode)
}
if strings.TrimSpace(result.Stderr) != "error output" {
t.Errorf("expected stderr 'error output', got %q", result.Stderr)
}
if strings.TrimSpace(result.Stdout) != "normal output" {
t.Errorf("expected stdout 'normal output', got %q", result.Stdout)
}
}
func TestSandboxInterfaceSatisfied(t *testing.T) {
// Compile-time check
var ps Sandbox = NewProcessSandbox()
if ps == nil {
t.Fatal("ProcessSandbox does not satisfy Sandbox interface")
}
}
func TestWorkingDirPath(t *testing.T) {
ps := NewProcessSandbox()
path := ps.WorkingDirPath()
if !filepath.IsAbs(path) {
t.Errorf("expected absolute path, got %q", path)
}
}