yao/agent/sandbox/integration_test.go
Max cd5cf32a20 Enhance Sandbox Integration and CI Workflow for AI Tests
- Added steps to pull necessary Docker images for sandbox testing in both CI workflows.
- Updated the AI test execution to utilize sandbox configurations, ensuring proper environment setup.
- Introduced sandbox initialization in the Assistant's Stream method, allowing for execution of coding agents like Claude and Cursor.
- Enhanced context management to support sandbox execution, improving flexibility in handling agent operations.
2026-01-30 17:41:06 +08:00

207 lines
5.5 KiB
Go

package sandbox
import (
"context"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
agentContext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/config"
infraSandbox "github.com/yaoapp/yao/sandbox"
"github.com/yaoapp/yao/test"
)
// createIntegrationTestManager creates a sandbox manager for integration testing
func createIntegrationTestManager(t *testing.T) *infraSandbox.Manager {
dataRoot := os.Getenv("YAO_ROOT")
if dataRoot == "" {
dataRoot = t.TempDir()
}
cfg := infraSandbox.DefaultConfig()
cfg.Init(dataRoot)
manager, err := infraSandbox.NewManager(cfg)
if err != nil {
t.Skipf("Skipping test: Docker not available: %v", err)
return nil
}
return manager
}
// TestExecutorInterfaceCompatibility verifies that the executor implements both interfaces correctly
func TestExecutorInterfaceCompatibility(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
manager := createIntegrationTestManager(t)
if manager == nil {
return
}
defer manager.Close()
// Create executor via factory function
opts := &Options{
Command: "claude",
Image: "alpine:latest",
UserID: "test-user",
ChatID: "test-compat",
}
executor, err := New(manager, opts)
require.NoError(t, err)
require.NotNil(t, executor)
defer executor.Close()
// Verify executor implements agent/sandbox.Executor interface
var _ Executor = executor
// Verify executor can be cast to context.SandboxExecutor
ctxExecutor, ok := executor.(agentContext.SandboxExecutor)
require.True(t, ok, "executor should implement context.SandboxExecutor")
require.NotNil(t, ctxExecutor)
// Test SandboxExecutor methods work
ctx := context.Background()
// WriteFile
err = ctxExecutor.WriteFile(ctx, "compat-test.txt", []byte("compatibility test"))
require.NoError(t, err)
// ReadFile
content, err := ctxExecutor.ReadFile(ctx, "compat-test.txt")
require.NoError(t, err)
assert.Equal(t, "compatibility test", string(content))
// ListDir
files, err := ctxExecutor.ListDir(ctx, ".")
require.NoError(t, err)
assert.True(t, len(files) > 0)
// Exec
output, err := ctxExecutor.Exec(ctx, []string{"echo", "compat"})
require.NoError(t, err)
assert.Contains(t, output, "compat")
// GetWorkDir
workDir := ctxExecutor.GetWorkDir()
assert.NotEmpty(t, workDir)
}
// TestExecutorRoundTrip tests the full round-trip of creating executor and performing operations
func TestExecutorRoundTrip(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
manager := createIntegrationTestManager(t)
if manager == nil {
return
}
defer manager.Close()
opts := &Options{
Command: "claude",
Image: "alpine:latest",
UserID: "test-user",
ChatID: "test-roundtrip",
ConnectorHost: "https://api.example.com",
ConnectorKey: "test-key",
Model: "test-model",
}
// Create executor
executor, err := New(manager, opts)
require.NoError(t, err)
require.NotNil(t, executor)
defer executor.Close()
ctx := context.Background()
// 1. Write a file
testContent := "Hello, integration test!"
err = executor.WriteFile(ctx, "integration.txt", []byte(testContent))
require.NoError(t, err, "WriteFile should succeed")
// 2. Read the file back
readContent, err := executor.ReadFile(ctx, "integration.txt")
require.NoError(t, err, "ReadFile should succeed")
assert.Equal(t, testContent, string(readContent), "Content should match")
// 3. List directory
files, err := executor.ListDir(ctx, ".")
require.NoError(t, err, "ListDir should succeed")
var found bool
for _, f := range files {
if f.Name == "integration.txt" {
found = true
assert.False(t, f.IsDir, "Should not be a directory")
assert.Equal(t, int64(len(testContent)), f.Size, "Size should match")
break
}
}
assert.True(t, found, "Should find integration.txt in listing")
// 4. Execute command
output, err := executor.Exec(ctx, []string{"cat", "/workspace/integration.txt"})
require.NoError(t, err, "Exec should succeed")
assert.Contains(t, output, testContent, "cat output should contain file content")
// 5. Verify workdir
assert.Equal(t, "/workspace", executor.GetWorkDir(), "WorkDir should be /workspace")
}
// TestMultipleExecutorsIsolation verifies that multiple executors have isolated workspaces
func TestMultipleExecutorsIsolation(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
manager := createIntegrationTestManager(t)
if manager == nil {
return
}
defer manager.Close()
// Create two executors with different chat IDs
opts1 := &Options{
Command: "claude",
Image: "alpine:latest",
UserID: "test-user",
ChatID: "test-isolation-1",
}
opts2 := &Options{
Command: "claude",
Image: "alpine:latest",
UserID: "test-user",
ChatID: "test-isolation-2",
}
exec1, err := New(manager, opts1)
require.NoError(t, err)
defer exec1.Close()
exec2, err := New(manager, opts2)
require.NoError(t, err)
defer exec2.Close()
ctx := context.Background()
// Write different content to each executor
err = exec1.WriteFile(ctx, "test.txt", []byte("executor 1"))
require.NoError(t, err)
err = exec2.WriteFile(ctx, "test.txt", []byte("executor 2"))
require.NoError(t, err)
// Read back and verify isolation
content1, err := exec1.ReadFile(ctx, "test.txt")
require.NoError(t, err)
assert.Equal(t, "executor 1", string(content1), "Executor 1 should have its own content")
content2, err := exec2.ReadFile(ctx, "test.txt")
require.NoError(t, err)
assert.Equal(t, "executor 2", string(content2), "Executor 2 should have its own content")
}