package execution import ( "context" "errors" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSandboxError(t *testing.T) { tests := []struct { name string err *SandboxError expected string retryable bool }{ { name: "simple error", err: ErrSandboxNotFound, expected: "Sandbox not found", retryable: false, }, { name: "error with details", err: NewSandboxError(ErrResourceLimitExceeded, "Memory limit of 1GB exceeded"), expected: "Resource limit exceeded: Memory limit of 1GB exceeded", retryable: false, }, { name: "retryable error", err: &SandboxError{ Code: "TEMPORARY_FAILURE", Message: "Temporary network failure", Retryable: true, }, expected: "Temporary network failure", retryable: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert.Equal(t, tt.expected, tt.err.Error()) assert.Equal(t, tt.retryable, tt.err.IsRetryable()) }) } } func TestSandboxErrorUnwrap(t *testing.T) { baseErr := errors.New("underlying error") sandboxErr := NewSandboxErrorWithCause(ErrCommandExecutionFailed, "command failed", baseErr) unwrapped := sandboxErr.Unwrap() assert.Equal(t, baseErr, unwrapped) } func TestSandboxConfig(t *testing.T) { config := &SandboxConfig{ Type: "docker", Image: "alpine:latest", Runtime: "docker", Architecture: "amd64", Resources: ResourceLimits{ MemoryLimit: 1024 * 1024 * 1024, // 1GB MemoryRequest: 512 * 1024 * 1024, // 512MB CPULimit: 2.0, CPURequest: 1.0, DiskLimit: 10 * 1024 * 1024 * 1024, // 10GB ProcessLimit: 100, FileLimit: 1024, WallTimeLimit: 30 * time.Minute, CPUTimeLimit: 10 * time.Minute, }, Security: SecurityPolicy{ RunAsUser: "1000", RunAsGroup: "1000", ReadOnlyRoot: true, NoNewPrivileges: true, AddCapabilities: []string{"NET_BIND_SERVICE"}, DropCapabilities: []string{"ALL"}, SELinuxContext: "unconfined_u:unconfined_r:container_t:s0", AppArmorProfile: "docker-default", SeccompProfile: "runtime/default", AllowNetworking: false, AllowedHosts: []string{"api.example.com"}, BlockedHosts: []string{"malicious.com"}, AllowedPorts: []int{80, 443}, ReadOnlyPaths: []string{"/etc", "/usr"}, MaskedPaths: []string{"/proc/kcore", "/proc/keys"}, TmpfsPaths: []string{"/tmp", "/var/tmp"}, PreventEscalation: true, IsolateNetwork: true, IsolateProcess: true, EnableAuditLog: true, LogSecurityEvents: true, }, Repository: RepositoryConfig{ URL: "https://github.com/example/repo.git", Branch: "main", LocalPath: "/home/user/repo", MountPoint: "/workspace", ReadOnly: false, GitConfig: GitConfig{ UserName: "Test User", UserEmail: "test@example.com", ConfigValues: map[string]string{ "core.autocrlf": "input", }, }, IncludeFiles: []string{"*.go", "*.md"}, ExcludeFiles: []string{"*.tmp", "*.log"}, Permissions: "755", Owner: "user", Group: "user", }, Network: NetworkConfig{ Isolated: false, Bridge: "docker0", DNSServers: []string{"8.8.8.8", "1.1.1.1"}, DNSSearch: []string{"example.com"}, HTTPProxy: "http://proxy:8080", HTTPSProxy: "http://proxy:8080", NoProxy: "localhost,127.0.0.1", PortMappings: []PortMapping{ {HostPort: 8080, ContainerPort: 80, Protocol: "tcp"}, }, IngressLimit: 1024 * 1024, // 1MB/s EgressLimit: 2048 * 1024, // 2MB/s }, Environment: map[string]string{ "NODE_ENV": "test", "DEBUG": "true", }, WorkingDir: "/workspace", Tools: []string{"git", "node", "npm"}, MCPServers: []string{"file-server", "web-server"}, Timeout: 5 * time.Minute, CleanupDelay: 30 * time.Second, Labels: map[string]string{ "app": "chorus", "version": "1.0.0", }, Annotations: map[string]string{ "description": "Test sandbox configuration", }, } // Validate required fields assert.NotEmpty(t, config.Type) assert.NotEmpty(t, config.Image) assert.NotEmpty(t, config.Architecture) // Validate resource limits assert.Greater(t, config.Resources.MemoryLimit, int64(0)) assert.Greater(t, config.Resources.CPULimit, 0.0) // Validate security policy assert.NotEmpty(t, config.Security.RunAsUser) assert.True(t, config.Security.NoNewPrivileges) assert.NotEmpty(t, config.Security.DropCapabilities) // Validate repository config assert.NotEmpty(t, config.Repository.MountPoint) assert.NotEmpty(t, config.Repository.GitConfig.UserName) // Validate network config assert.NotEmpty(t, config.Network.DNSServers) assert.Len(t, config.Network.PortMappings, 1) // Validate timeouts assert.Greater(t, config.Timeout, time.Duration(0)) assert.Greater(t, config.CleanupDelay, time.Duration(0)) } func TestCommand(t *testing.T) { cmd := &Command{ Executable: "python3", Args: []string{"-c", "print('hello world')"}, WorkingDir: "/workspace", Environment: map[string]string{"PYTHONPATH": "/custom/path"}, StdinContent: "input data", Timeout: 30 * time.Second, User: "1000", AllowNetwork: true, AllowWrite: true, RestrictPaths: []string{"/etc", "/usr"}, } // Validate command structure assert.Equal(t, "python3", cmd.Executable) assert.Len(t, cmd.Args, 2) assert.Equal(t, "/workspace", cmd.WorkingDir) assert.Equal(t, "/custom/path", cmd.Environment["PYTHONPATH"]) assert.Equal(t, "input data", cmd.StdinContent) assert.Equal(t, 30*time.Second, cmd.Timeout) assert.True(t, cmd.AllowNetwork) assert.True(t, cmd.AllowWrite) assert.Len(t, cmd.RestrictPaths, 2) } func TestCommandResult(t *testing.T) { startTime := time.Now() endTime := startTime.Add(2 * time.Second) result := &CommandResult{ ExitCode: 0, Success: true, Stdout: "Standard output", Stderr: "Standard error", Combined: "Combined output", StartTime: startTime, EndTime: endTime, Duration: endTime.Sub(startTime), ResourceUsage: ResourceUsage{ CPUUsage: 25.5, MemoryUsage: 1024 * 1024, // 1MB }, ProcessID: 12345, Metadata: map[string]interface{}{ "container_id": "abc123", "image": "alpine:latest", }, } // Validate result structure assert.Equal(t, 0, result.ExitCode) assert.True(t, result.Success) assert.Equal(t, "Standard output", result.Stdout) assert.Equal(t, "Standard error", result.Stderr) assert.Equal(t, 2*time.Second, result.Duration) assert.Equal(t, 25.5, result.ResourceUsage.CPUUsage) assert.Equal(t, int64(1024*1024), result.ResourceUsage.MemoryUsage) assert.Equal(t, 12345, result.ProcessID) assert.Equal(t, "abc123", result.Metadata["container_id"]) } func TestFileInfo(t *testing.T) { modTime := time.Now() fileInfo := FileInfo{ Name: "test.txt", Path: "/workspace/test.txt", Size: 1024, Mode: 0644, ModTime: modTime, IsDir: false, Owner: "user", Group: "user", Permissions: "-rw-r--r--", } // Validate file info structure assert.Equal(t, "test.txt", fileInfo.Name) assert.Equal(t, "/workspace/test.txt", fileInfo.Path) assert.Equal(t, int64(1024), fileInfo.Size) assert.Equal(t, uint32(0644), fileInfo.Mode) assert.Equal(t, modTime, fileInfo.ModTime) assert.False(t, fileInfo.IsDir) assert.Equal(t, "user", fileInfo.Owner) assert.Equal(t, "user", fileInfo.Group) assert.Equal(t, "-rw-r--r--", fileInfo.Permissions) } func TestResourceLimits(t *testing.T) { limits := ResourceLimits{ CPULimit: 2.5, CPURequest: 1.0, MemoryLimit: 2 * 1024 * 1024 * 1024, // 2GB MemoryRequest: 1 * 1024 * 1024 * 1024, // 1GB DiskLimit: 50 * 1024 * 1024 * 1024, // 50GB DiskRequest: 10 * 1024 * 1024 * 1024, // 10GB NetworkInLimit: 10 * 1024 * 1024, // 10MB/s NetworkOutLimit: 5 * 1024 * 1024, // 5MB/s ProcessLimit: 200, FileLimit: 2048, WallTimeLimit: 1 * time.Hour, CPUTimeLimit: 30 * time.Minute, } // Validate resource limits assert.Equal(t, 2.5, limits.CPULimit) assert.Equal(t, 1.0, limits.CPURequest) assert.Equal(t, int64(2*1024*1024*1024), limits.MemoryLimit) assert.Equal(t, int64(1*1024*1024*1024), limits.MemoryRequest) assert.Equal(t, int64(50*1024*1024*1024), limits.DiskLimit) assert.Equal(t, 200, limits.ProcessLimit) assert.Equal(t, 2048, limits.FileLimit) assert.Equal(t, 1*time.Hour, limits.WallTimeLimit) assert.Equal(t, 30*time.Minute, limits.CPUTimeLimit) } func TestResourceUsage(t *testing.T) { timestamp := time.Now() usage := ResourceUsage{ Timestamp: timestamp, CPUUsage: 75.5, CPUTime: 15 * time.Minute, MemoryUsage: 512 * 1024 * 1024, // 512MB MemoryPercent: 25.0, MemoryPeak: 768 * 1024 * 1024, // 768MB DiskUsage: 1 * 1024 * 1024 * 1024, // 1GB DiskReads: 1000, DiskWrites: 500, NetworkIn: 10 * 1024 * 1024, // 10MB NetworkOut: 5 * 1024 * 1024, // 5MB ProcessCount: 25, ThreadCount: 100, FileHandles: 50, Uptime: 2 * time.Hour, } // Validate resource usage assert.Equal(t, timestamp, usage.Timestamp) assert.Equal(t, 75.5, usage.CPUUsage) assert.Equal(t, 15*time.Minute, usage.CPUTime) assert.Equal(t, int64(512*1024*1024), usage.MemoryUsage) assert.Equal(t, 25.0, usage.MemoryPercent) assert.Equal(t, int64(768*1024*1024), usage.MemoryPeak) assert.Equal(t, 25, usage.ProcessCount) assert.Equal(t, 100, usage.ThreadCount) assert.Equal(t, 50, usage.FileHandles) assert.Equal(t, 2*time.Hour, usage.Uptime) } func TestSandboxInfo(t *testing.T) { createdAt := time.Now() startedAt := createdAt.Add(5 * time.Second) info := SandboxInfo{ ID: "sandbox-123", Name: "test-sandbox", Type: "docker", Status: StatusRunning, CreatedAt: createdAt, StartedAt: startedAt, Runtime: "docker", Image: "alpine:latest", Platform: "linux/amd64", IPAddress: "172.17.0.2", MACAddress: "02:42:ac:11:00:02", Hostname: "sandbox-123", AllocatedResources: ResourceLimits{ MemoryLimit: 1024 * 1024 * 1024, // 1GB CPULimit: 2.0, }, Labels: map[string]string{ "app": "chorus", }, Annotations: map[string]string{ "creator": "test", }, } // Validate sandbox info assert.Equal(t, "sandbox-123", info.ID) assert.Equal(t, "test-sandbox", info.Name) assert.Equal(t, "docker", info.Type) assert.Equal(t, StatusRunning, info.Status) assert.Equal(t, createdAt, info.CreatedAt) assert.Equal(t, startedAt, info.StartedAt) assert.Equal(t, "docker", info.Runtime) assert.Equal(t, "alpine:latest", info.Image) assert.Equal(t, "172.17.0.2", info.IPAddress) assert.Equal(t, "chorus", info.Labels["app"]) assert.Equal(t, "test", info.Annotations["creator"]) } func TestSandboxStatus(t *testing.T) { statuses := []SandboxStatus{ StatusCreating, StatusStarting, StatusRunning, StatusPaused, StatusStopping, StatusStopped, StatusFailed, StatusDestroyed, } expectedStatuses := []string{ "creating", "starting", "running", "paused", "stopping", "stopped", "failed", "destroyed", } for i, status := range statuses { assert.Equal(t, expectedStatuses[i], string(status)) } } func TestPortMapping(t *testing.T) { mapping := PortMapping{ HostPort: 8080, ContainerPort: 80, Protocol: "tcp", } assert.Equal(t, 8080, mapping.HostPort) assert.Equal(t, 80, mapping.ContainerPort) assert.Equal(t, "tcp", mapping.Protocol) } func TestGitConfig(t *testing.T) { config := GitConfig{ UserName: "Test User", UserEmail: "test@example.com", SigningKey: "ABC123", ConfigValues: map[string]string{ "core.autocrlf": "input", "pull.rebase": "true", "init.defaultBranch": "main", }, } assert.Equal(t, "Test User", config.UserName) assert.Equal(t, "test@example.com", config.UserEmail) assert.Equal(t, "ABC123", config.SigningKey) assert.Equal(t, "input", config.ConfigValues["core.autocrlf"]) assert.Equal(t, "true", config.ConfigValues["pull.rebase"]) assert.Equal(t, "main", config.ConfigValues["init.defaultBranch"]) } // MockSandbox implements ExecutionSandbox for testing type MockSandbox struct { id string status SandboxStatus workingDir string environment map[string]string shouldFail bool commandResult *CommandResult files []FileInfo resourceUsage *ResourceUsage } func NewMockSandbox() *MockSandbox { return &MockSandbox{ id: "mock-sandbox-123", status: StatusStopped, workingDir: "/workspace", environment: make(map[string]string), files: []FileInfo{}, commandResult: &CommandResult{ Success: true, ExitCode: 0, Stdout: "mock output", }, resourceUsage: &ResourceUsage{ CPUUsage: 10.0, MemoryUsage: 100 * 1024 * 1024, // 100MB }, } } func (m *MockSandbox) Initialize(ctx context.Context, config *SandboxConfig) error { if m.shouldFail { return NewSandboxError(ErrSandboxInitFailed, "mock initialization failed") } m.status = StatusRunning return nil } func (m *MockSandbox) ExecuteCommand(ctx context.Context, cmd *Command) (*CommandResult, error) { if m.shouldFail { return nil, NewSandboxError(ErrCommandExecutionFailed, "mock command execution failed") } return m.commandResult, nil } func (m *MockSandbox) CopyFiles(ctx context.Context, source, dest string) error { if m.shouldFail { return NewSandboxError(ErrFileOperationFailed, "mock file copy failed") } return nil } func (m *MockSandbox) WriteFile(ctx context.Context, path string, content []byte, mode uint32) error { if m.shouldFail { return NewSandboxError(ErrFileOperationFailed, "mock file write failed") } return nil } func (m *MockSandbox) ReadFile(ctx context.Context, path string) ([]byte, error) { if m.shouldFail { return nil, NewSandboxError(ErrFileOperationFailed, "mock file read failed") } return []byte("mock file content"), nil } func (m *MockSandbox) ListFiles(ctx context.Context, path string) ([]FileInfo, error) { if m.shouldFail { return nil, NewSandboxError(ErrFileOperationFailed, "mock file list failed") } return m.files, nil } func (m *MockSandbox) GetWorkingDirectory() string { return m.workingDir } func (m *MockSandbox) SetWorkingDirectory(path string) error { if m.shouldFail { return NewSandboxError(ErrFileOperationFailed, "mock set working directory failed") } m.workingDir = path return nil } func (m *MockSandbox) GetEnvironment() map[string]string { env := make(map[string]string) for k, v := range m.environment { env[k] = v } return env } func (m *MockSandbox) SetEnvironment(env map[string]string) error { if m.shouldFail { return NewSandboxError(ErrFileOperationFailed, "mock set environment failed") } for k, v := range env { m.environment[k] = v } return nil } func (m *MockSandbox) GetResourceUsage(ctx context.Context) (*ResourceUsage, error) { if m.shouldFail { return nil, NewSandboxError(ErrSandboxInitFailed, "mock resource usage failed") } return m.resourceUsage, nil } func (m *MockSandbox) Cleanup() error { if m.shouldFail { return NewSandboxError(ErrSandboxInitFailed, "mock cleanup failed") } m.status = StatusDestroyed return nil } func (m *MockSandbox) GetInfo() SandboxInfo { return SandboxInfo{ ID: m.id, Status: m.status, Type: "mock", } } func TestMockSandbox(t *testing.T) { sandbox := NewMockSandbox() ctx := context.Background() // Test initialization err := sandbox.Initialize(ctx, &SandboxConfig{}) require.NoError(t, err) assert.Equal(t, StatusRunning, sandbox.status) // Test command execution result, err := sandbox.ExecuteCommand(ctx, &Command{}) require.NoError(t, err) assert.True(t, result.Success) assert.Equal(t, "mock output", result.Stdout) // Test file operations err = sandbox.WriteFile(ctx, "/test.txt", []byte("test"), 0644) require.NoError(t, err) content, err := sandbox.ReadFile(ctx, "/test.txt") require.NoError(t, err) assert.Equal(t, []byte("mock file content"), content) files, err := sandbox.ListFiles(ctx, "/") require.NoError(t, err) assert.Empty(t, files) // Mock returns empty list by default // Test environment env := sandbox.GetEnvironment() assert.Empty(t, env) err = sandbox.SetEnvironment(map[string]string{"TEST": "value"}) require.NoError(t, err) env = sandbox.GetEnvironment() assert.Equal(t, "value", env["TEST"]) // Test resource usage usage, err := sandbox.GetResourceUsage(ctx) require.NoError(t, err) assert.Equal(t, 10.0, usage.CPUUsage) // Test cleanup err = sandbox.Cleanup() require.NoError(t, err) assert.Equal(t, StatusDestroyed, sandbox.status) } func TestMockSandboxFailure(t *testing.T) { sandbox := NewMockSandbox() sandbox.shouldFail = true ctx := context.Background() // All operations should fail when shouldFail is true err := sandbox.Initialize(ctx, &SandboxConfig{}) assert.Error(t, err) _, err = sandbox.ExecuteCommand(ctx, &Command{}) assert.Error(t, err) err = sandbox.WriteFile(ctx, "/test.txt", []byte("test"), 0644) assert.Error(t, err) _, err = sandbox.ReadFile(ctx, "/test.txt") assert.Error(t, err) _, err = sandbox.ListFiles(ctx, "/") assert.Error(t, err) err = sandbox.SetWorkingDirectory("/tmp") assert.Error(t, err) err = sandbox.SetEnvironment(map[string]string{"TEST": "value"}) assert.Error(t, err) _, err = sandbox.GetResourceUsage(ctx) assert.Error(t, err) err = sandbox.Cleanup() assert.Error(t, err) }