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