 8d9b62daf3
			
		
	
	8d9b62daf3
	
	
	
		
			
			This commit implements Phase 2 of the CHORUS Task Execution Engine development plan, providing a comprehensive execution environment abstraction layer with Docker container sandboxing support. ## New Features ### Core Sandbox Interface - Comprehensive ExecutionSandbox interface with isolated task execution - Support for command execution, file I/O, environment management - Resource usage monitoring and sandbox lifecycle management - Standardized error handling with SandboxError types and categories ### Docker Container Sandbox Implementation - Full Docker API integration with secure container creation - Transparent repository mounting with configurable read/write access - Advanced security policies with capability dropping and privilege controls - Comprehensive resource limits (CPU, memory, disk, processes, file handles) - Support for tmpfs mounts, masked paths, and read-only bind mounts - Container lifecycle management with proper cleanup and health monitoring ### Security & Resource Management - Configurable security policies with SELinux, AppArmor, and Seccomp support - Fine-grained capability management with secure defaults - Network isolation options with configurable DNS and proxy settings - Resource monitoring with real-time CPU, memory, and network usage tracking - Comprehensive ulimits configuration for process and file handle limits ### Repository Integration - Seamless repository mounting from local paths to container workspaces - Git configuration support with user credentials and global settings - File inclusion/exclusion patterns for selective repository access - Configurable permissions and ownership for mounted repositories ### Testing Infrastructure - Comprehensive test suite with 60+ test cases covering all functionality - Docker integration tests with Alpine Linux containers (skipped in short mode) - Mock sandbox implementation for unit testing without Docker dependencies - Security policy validation tests with read-only filesystem enforcement - Resource usage monitoring and cleanup verification tests ## Technical Details ### Dependencies Added - github.com/docker/docker v28.4.0+incompatible - Docker API client - github.com/docker/go-connections v0.6.0 - Docker connection utilities - github.com/docker/go-units v0.5.0 - Docker units and formatting - Associated Docker API dependencies for complete container management ### Architecture - Interface-driven design enabling multiple sandbox implementations - Comprehensive configuration structures for all sandbox aspects - Resource usage tracking with detailed metrics collection - Error handling with retryable error classification - Proper cleanup and resource management throughout sandbox lifecycle ### Compatibility - Maintains backward compatibility with existing CHORUS architecture - Designed for future integration with Phase 3 Core Task Execution Engine - Extensible design supporting additional sandbox implementations (VM, process) This Phase 2 implementation provides the foundation for secure, isolated task execution that will be integrated with the AI model providers from Phase 1 in the upcoming Phase 3 development. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
		
			
				
	
	
		
			639 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			639 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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)
 | |
| } |