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