package context_test import ( stdContext "context" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v8 "github.com/yaoapp/gou/runtime/v8" "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/config" infraSandbox "github.com/yaoapp/yao/sandbox" "github.com/yaoapp/yao/test" ) // createTestSandboxManager creates a real sandbox manager for testing func createTestSandboxManager(t *testing.T) *infraSandbox.Manager { // Get data root from environment or use temp directory dataRoot := os.Getenv("YAO_ROOT") if dataRoot == "" { dataRoot = t.TempDir() } // Create config with proper paths 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 } // createTestContainer creates a container and returns a cleanup function func createTestContainer(t *testing.T, manager *infraSandbox.Manager, userID, chatID string) (*infraSandbox.Container, func()) { container, err := manager.GetOrCreate(stdContext.Background(), userID, chatID) require.NoError(t, err) require.NotNil(t, container) // Return cleanup function that removes the container cleanup := func() { err := manager.Remove(stdContext.Background(), container.Name) if err != nil { t.Logf("Warning: failed to cleanup container %s: %v", container.Name, err) } } return container, cleanup } // realSandboxExecutor wraps infraSandbox.Manager to implement context.SandboxExecutor type realSandboxExecutor struct { manager *infraSandbox.Manager containerName string workDir string } func (e *realSandboxExecutor) ReadFile(ctx stdContext.Context, path string) ([]byte, error) { fullPath := e.workDir + "/" + path return e.manager.ReadFile(ctx, e.containerName, fullPath) } func (e *realSandboxExecutor) WriteFile(ctx stdContext.Context, path string, content []byte) error { fullPath := e.workDir + "/" + path return e.manager.WriteFile(ctx, e.containerName, fullPath, content) } func (e *realSandboxExecutor) ListDir(ctx stdContext.Context, path string) ([]infraSandbox.FileInfo, error) { fullPath := e.workDir + "/" + path return e.manager.ListDir(ctx, e.containerName, fullPath) } func (e *realSandboxExecutor) Exec(ctx stdContext.Context, cmd []string) (string, error) { result, err := e.manager.Exec(ctx, e.containerName, cmd, &infraSandbox.ExecOptions{ WorkDir: e.workDir, }) if err != nil { return "", err } return result.Stdout, nil } func (e *realSandboxExecutor) GetWorkDir() string { return e.workDir } func (e *realSandboxExecutor) GetSandboxID() string { // Extract sandbox ID from container name (format: yao-sandbox-{userID}-{chatID}) // For tests, just return a mock ID return "test-user-test-chat" } func (e *realSandboxExecutor) GetVNCUrl() string { // Tests don't use VNC, return empty return "" } // TestJsSandboxNotAvailable tests ctx.sandbox when not configured func TestJsSandboxNotAvailable(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() ctx := context.New(stdContext.Background(), nil, "test-chat-no-sandbox") ctx.AssistantID = "test-assistant" // Test that ctx.sandbox is undefined when not configured res, err := v8.Call(v8.CallOptions{}, ` function test(ctx) { try { if (ctx.sandbox === undefined || ctx.sandbox === null) { return { success: true, hasSandbox: false }; } return { success: true, hasSandbox: true }; } catch (error) { return { success: false, error: error.message }; } }`, ctx) require.NoError(t, err) result, ok := res.(map[string]interface{}) require.True(t, ok, "Expected map result") assert.Equal(t, true, result["success"]) assert.Equal(t, false, result["hasSandbox"], "ctx.sandbox should not be available when not configured") } // TestJsSandboxWriteFile tests ctx.sandbox.WriteFile via JavaScript func TestJsSandboxWriteFile(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() manager := createTestSandboxManager(t) if manager == nil { return } defer manager.Close() // Create container with auto-cleanup container, cleanup := createTestContainer(t, manager, "test-user", "test-js-writefile") defer cleanup() executor := &realSandboxExecutor{ manager: manager, containerName: container.Name, workDir: "/workspace", } // Create context with sandbox ctx := context.New(stdContext.Background(), nil, "test-chat-writefile") ctx.AssistantID = "test-assistant" ctx.SetSandboxExecutor(executor) // Test WriteFile via JavaScript res, err := v8.Call(v8.CallOptions{}, ` function test(ctx) { try { if (!ctx.sandbox) { return { success: false, error: "sandbox not available" }; } // Write a file ctx.sandbox.WriteFile("js-test.txt", "Hello from JavaScript!"); return { success: true }; } catch (error) { return { success: false, error: error.message }; } }`, ctx) require.NoError(t, err) result, ok := res.(map[string]interface{}) require.True(t, ok, "Expected map result") assert.Equal(t, true, result["success"], "WriteFile should succeed: %v", result["error"]) // Verify file was written by reading it back directly content, err := executor.ReadFile(stdContext.Background(), "js-test.txt") require.NoError(t, err) assert.Equal(t, "Hello from JavaScript!", string(content)) } // TestJsSandboxReadFile tests ctx.sandbox.ReadFile via JavaScript func TestJsSandboxReadFile(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() manager := createTestSandboxManager(t) if manager == nil { return } defer manager.Close() // Create container with auto-cleanup container, cleanup := createTestContainer(t, manager, "test-user", "test-js-readfile") defer cleanup() executor := &realSandboxExecutor{ manager: manager, containerName: container.Name, workDir: "/workspace", } // Write a file first err := executor.WriteFile(stdContext.Background(), "read-test.txt", []byte("Content to read")) require.NoError(t, err) // Create context with sandbox ctx := context.New(stdContext.Background(), nil, "test-chat-readfile") ctx.AssistantID = "test-assistant" ctx.SetSandboxExecutor(executor) // Test ReadFile via JavaScript res, err := v8.Call(v8.CallOptions{}, ` function test(ctx) { try { if (!ctx.sandbox) { return { success: false, error: "sandbox not available" }; } // Read the file const content = ctx.sandbox.ReadFile("read-test.txt"); return { success: true, content: content }; } catch (error) { return { success: false, error: error.message }; } }`, ctx) require.NoError(t, err) result, ok := res.(map[string]interface{}) require.True(t, ok, "Expected map result") assert.Equal(t, true, result["success"], "ReadFile should succeed: %v", result["error"]) assert.Equal(t, "Content to read", result["content"]) } // TestJsSandboxListDir tests ctx.sandbox.ListDir via JavaScript func TestJsSandboxListDir(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() manager := createTestSandboxManager(t) if manager == nil { return } defer manager.Close() // Create container with auto-cleanup container, cleanup := createTestContainer(t, manager, "test-user", "test-js-listdir") defer cleanup() executor := &realSandboxExecutor{ manager: manager, containerName: container.Name, workDir: "/workspace", } // Write some files first err := executor.WriteFile(stdContext.Background(), "file1.txt", []byte("content1")) require.NoError(t, err) err = executor.WriteFile(stdContext.Background(), "file2.txt", []byte("content2")) require.NoError(t, err) // Create context with sandbox ctx := context.New(stdContext.Background(), nil, "test-chat-listdir") ctx.AssistantID = "test-assistant" ctx.SetSandboxExecutor(executor) // Test ListDir via JavaScript res, err := v8.Call(v8.CallOptions{}, ` function test(ctx) { try { if (!ctx.sandbox) { return { success: false, error: "sandbox not available" }; } // List directory const files = ctx.sandbox.ListDir("."); // Find our test files const fileNames = files.map(f => f.name); const hasFile1 = fileNames.includes("file1.txt"); const hasFile2 = fileNames.includes("file2.txt"); return { success: true, fileCount: files.length, hasFile1: hasFile1, hasFile2: hasFile2, files: fileNames }; } catch (error) { return { success: false, error: error.message }; } }`, ctx) require.NoError(t, err) result, ok := res.(map[string]interface{}) require.True(t, ok, "Expected map result") assert.Equal(t, true, result["success"], "ListDir should succeed: %v", result["error"]) assert.Equal(t, true, result["hasFile1"], "Should find file1.txt") assert.Equal(t, true, result["hasFile2"], "Should find file2.txt") } // TestJsSandboxExec tests ctx.sandbox.Exec via JavaScript func TestJsSandboxExec(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() manager := createTestSandboxManager(t) if manager == nil { return } defer manager.Close() // Create container with auto-cleanup container, cleanup := createTestContainer(t, manager, "test-user", "test-js-exec") defer cleanup() executor := &realSandboxExecutor{ manager: manager, containerName: container.Name, workDir: "/workspace", } // Create context with sandbox ctx := context.New(stdContext.Background(), nil, "test-chat-exec") ctx.AssistantID = "test-assistant" ctx.SetSandboxExecutor(executor) // Test Exec via JavaScript res, err := v8.Call(v8.CallOptions{}, ` function test(ctx) { try { if (!ctx.sandbox) { return { success: false, error: "sandbox not available" }; } // Execute echo command const output = ctx.sandbox.Exec(["echo", "hello-from-js"]); return { success: true, output: output.trim() }; } catch (error) { return { success: false, error: error.message }; } }`, ctx) require.NoError(t, err) result, ok := res.(map[string]interface{}) require.True(t, ok, "Expected map result") assert.Equal(t, true, result["success"], "Exec should succeed: %v", result["error"]) // Output may contain Docker stream header bytes, so use Contains output, _ := result["output"].(string) assert.Contains(t, output, "hello-from-js", "Exec output should contain expected text") } // TestJsSandboxWorkdir tests ctx.sandbox.workdir property via JavaScript func TestJsSandboxWorkdir(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() manager := createTestSandboxManager(t) if manager == nil { return } defer manager.Close() // Create container with auto-cleanup container, cleanup := createTestContainer(t, manager, "test-user", "test-js-workdir") defer cleanup() executor := &realSandboxExecutor{ manager: manager, containerName: container.Name, workDir: "/workspace", } // Create context with sandbox ctx := context.New(stdContext.Background(), nil, "test-chat-workdir") ctx.AssistantID = "test-assistant" ctx.SetSandboxExecutor(executor) // Test workdir property via JavaScript res, err := v8.Call(v8.CallOptions{}, ` function test(ctx) { try { if (!ctx.sandbox) { return { success: false, error: "sandbox not available" }; } // Get workdir property const workdir = ctx.sandbox.workdir; return { success: true, workdir: workdir }; } catch (error) { return { success: false, error: error.message }; } }`, ctx) require.NoError(t, err) result, ok := res.(map[string]interface{}) require.True(t, ok, "Expected map result") assert.Equal(t, true, result["success"], "workdir access should succeed: %v", result["error"]) assert.Equal(t, "/workspace", result["workdir"]) } // TestJsSandboxCompleteWorkflow tests a complete workflow via JavaScript func TestJsSandboxCompleteWorkflow(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() manager := createTestSandboxManager(t) if manager == nil { return } defer manager.Close() // Create container with auto-cleanup container, cleanup := createTestContainer(t, manager, "test-user", "test-js-workflow") defer cleanup() executor := &realSandboxExecutor{ manager: manager, containerName: container.Name, workDir: "/workspace", } // Create context with sandbox ctx := context.New(stdContext.Background(), nil, "test-chat-workflow") ctx.AssistantID = "test-assistant" ctx.SetSandboxExecutor(executor) // Test complete workflow: write file, exec cat, verify content res, err := v8.Call(v8.CallOptions{}, ` function test(ctx) { try { if (!ctx.sandbox) { return { success: false, error: "sandbox not available" }; } // 1. Check workdir const workdir = ctx.sandbox.workdir; if (workdir !== "/workspace") { return { success: false, error: "unexpected workdir: " + workdir }; } // 2. Write a file const testContent = "Test workflow content: " + Date.now(); ctx.sandbox.WriteFile("workflow-test.txt", testContent); // 3. Read it back const readContent = ctx.sandbox.ReadFile("workflow-test.txt"); if (readContent !== testContent) { return { success: false, error: "content mismatch after read" }; } // 4. List directory and verify file exists const files = ctx.sandbox.ListDir("."); const fileNames = files.map(f => f.name); if (!fileNames.includes("workflow-test.txt")) { return { success: false, error: "file not found in listing" }; } // 5. Execute cat command const catOutput = ctx.sandbox.Exec(["cat", workdir + "/workflow-test.txt"]); if (!catOutput.includes("Test workflow content")) { return { success: false, error: "cat output mismatch" }; } // 6. Execute pwd command const pwdOutput = ctx.sandbox.Exec(["pwd"]); if (!pwdOutput.includes("/workspace")) { return { success: false, error: "pwd output mismatch: " + pwdOutput }; } return { success: true, workdir: workdir, content: readContent, fileCount: files.length }; } catch (error) { return { success: false, error: error.message, stack: error.stack }; } }`, ctx) require.NoError(t, err) result, ok := res.(map[string]interface{}) require.True(t, ok, "Expected map result") assert.Equal(t, true, result["success"], "Complete workflow should succeed: %v", result["error"]) assert.Equal(t, "/workspace", result["workdir"]) }