yao/agent/sandbox/v2/prepare_test.go
Max 3f390c223c feat(sandbox/v2): enhance V2 sandbox integration and testing
- Implemented V2 sandbox initialization in the assistant loading process, allowing for standalone sandbox.yao configuration.
- Added support for V2 sandbox execution paths in the Assistant's Stream method, differentiating between V1 and V2 sandboxes.
- Introduced comprehensive tests for V2 sandbox configurations, ensuring correct loading and execution behavior.
- Updated the context and types to accommodate V2 sandbox features, including system information and workspace management.

Made-with: Cursor
2026-03-10 01:13:17 +08:00

549 lines
15 KiB
Go

package sandboxv2_test
import (
"context"
"fmt"
"strings"
"testing"
"time"
sandboxv2 "github.com/yaoapp/yao/agent/sandbox/v2"
"github.com/yaoapp/yao/agent/sandbox/v2/types"
)
// ---------------------------------------------------------------------------
// Box tests (local + remote)
// ---------------------------------------------------------------------------
func TestRunPrepareSteps_Exec(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "exec", Cmd: "echo hello > /tmp/prep-test"},
{Action: "exec", Cmd: "echo world >> /tmp/prep-test"},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "")
if err != nil {
t.Fatalf("RunPrepareSteps: %v", err)
}
result, err := box.Exec(ctx, []string{"cat", "/tmp/prep-test"})
if err != nil {
t.Fatalf("cat: %v", err)
}
got := strings.TrimSpace(result.Stdout)
if got != "hello\nworld" {
t.Errorf("content = %q, want %q", got, "hello\nworld")
}
})
}
}
func TestRunPrepareSteps_File(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
wsID := fmt.Sprintf("test-file-%d", time.Now().UnixNano())
box.BindWorkplace(wsID)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "file", Path: "config/test.txt", Content: []byte("file-content-v2")},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "")
if err != nil {
t.Fatalf("RunPrepareSteps: %v", err)
}
ws := box.Workplace()
data, err := ws.ReadFile("config/test.txt")
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(data) != "file-content-v2" {
t.Errorf("content = %q, want %q", string(data), "file-content-v2")
}
})
}
}
func TestRunPrepareSteps_Copy(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
wsID := fmt.Sprintf("test-copy-%d", time.Now().UnixNano())
box.BindWorkplace(wsID)
ws := box.Workplace()
ws.WriteFile("src.txt", []byte("copy-src"), 0644)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "copy", Src: "src.txt", Dst: "dst.txt"},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "")
if err != nil {
t.Fatalf("RunPrepareSteps: %v", err)
}
data, err := ws.ReadFile("dst.txt")
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(data) != "copy-src" {
t.Errorf("content = %q, want %q", string(data), "copy-src")
}
})
}
}
func TestRunPrepareSteps_OnceMarker(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
wsID := fmt.Sprintf("test-once-%d", time.Now().UnixNano())
box.BindWorkplace(wsID)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
counter := "/tmp/once-counter"
steps := []types.PrepareStep{
{Action: "exec", Cmd: "echo -n x >> " + counter, Once: true},
}
hash := "abc123"
assistantID := "test-once"
if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, hash); err != nil {
t.Fatalf("first run: %v", err)
}
r1, _ := box.Exec(ctx, []string{"cat", counter})
if r1.Stdout != "x" {
t.Fatalf("first run: got %q, want %q", r1.Stdout, "x")
}
if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, hash); err != nil {
t.Fatalf("second run: %v", err)
}
r2, _ := box.Exec(ctx, []string{"cat", counter})
if r2.Stdout != "x" {
t.Errorf("second run: got %q, want %q (once step should be skipped)", r2.Stdout, "x")
}
if err := sandboxv2.RunPrepareSteps(ctx, steps, box, assistantID, "new-hash"); err != nil {
t.Fatalf("third run: %v", err)
}
r3, _ := box.Exec(ctx, []string{"cat", counter})
if r3.Stdout != "xx" {
t.Errorf("third run: got %q, want %q (hash changed, should re-execute)", r3.Stdout, "xx")
}
})
}
}
func TestRunPrepareSteps_OnceIsolation(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
wsID := fmt.Sprintf("test-iso-%d", time.Now().UnixNano())
box.BindWorkplace(wsID)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
stepsA := []types.PrepareStep{
{Action: "exec", Cmd: "echo -n A >> /tmp/iso-a", Once: true},
}
stepsB := []types.PrepareStep{
{Action: "exec", Cmd: "echo -n B >> /tmp/iso-b", Once: true},
}
hash := "same-hash"
if err := sandboxv2.RunPrepareSteps(ctx, stepsA, box, "assistant-a", hash); err != nil {
t.Fatalf("assistant-a: %v", err)
}
if err := sandboxv2.RunPrepareSteps(ctx, stepsB, box, "assistant-b", hash); err != nil {
t.Fatalf("assistant-b: %v", err)
}
rA, _ := box.Exec(ctx, []string{"cat", "/tmp/iso-a"})
rB, _ := box.Exec(ctx, []string{"cat", "/tmp/iso-b"})
if rA.Stdout != "A" {
t.Errorf("assistant-a: got %q, want %q", rA.Stdout, "A")
}
if rB.Stdout != "B" {
t.Errorf("assistant-b: got %q, want %q", rB.Stdout, "B")
}
if err := sandboxv2.RunPrepareSteps(ctx, stepsA, box, "assistant-a", hash); err != nil {
t.Fatalf("assistant-a re-run: %v", err)
}
if err := sandboxv2.RunPrepareSteps(ctx, stepsB, box, "assistant-b", hash); err != nil {
t.Fatalf("assistant-b re-run: %v", err)
}
rA2, _ := box.Exec(ctx, []string{"cat", "/tmp/iso-a"})
rB2, _ := box.Exec(ctx, []string{"cat", "/tmp/iso-b"})
if rA2.Stdout != "A" {
t.Errorf("assistant-a re-run: got %q, want %q (should be skipped)", rA2.Stdout, "A")
}
if rB2.Stdout != "B" {
t.Errorf("assistant-b re-run: got %q, want %q (should be skipped)", rB2.Stdout, "B")
}
})
}
}
func TestRunPrepareSteps_IgnoreError(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "exec", Cmd: "false", IgnoreError: true},
{Action: "exec", Cmd: "echo survived > /tmp/survived"},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "")
if err != nil {
t.Fatalf("RunPrepareSteps: %v (ignore_error should have prevented failure)", err)
}
result, _ := box.Exec(ctx, []string{"cat", "/tmp/survived"})
if strings.TrimSpace(result.Stdout) != "survived" {
t.Errorf("second step should have executed, got %q", result.Stdout)
}
})
}
}
func TestRunPrepareSteps_FailOnError(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "exec", Cmd: "false"},
{Action: "exec", Cmd: "echo should-not-reach > /tmp/unreachable"},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "")
if err == nil {
t.Fatal("expected error from failing step without ignore_error")
}
result, _ := box.Exec(ctx, []string{"cat", "/tmp/unreachable"})
if result.ExitCode == 0 {
t.Error("second step should not have executed")
}
})
}
}
func TestRunPrepareSteps_UnknownAction(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
_ = createBox(t, m, nc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "unknown_action"},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, nil, "test-assistant", "")
if err == nil {
t.Fatal("expected error for unknown action")
}
if !strings.Contains(err.Error(), "unknown_action") {
t.Errorf("error should mention action name, got: %v", err)
}
})
}
}
func TestRunPrepareSteps_EmptySteps(t *testing.T) {
err := sandboxv2.RunPrepareSteps(context.Background(), nil, nil, "test-assistant", "hash")
if err != nil {
t.Fatalf("empty steps should succeed: %v", err)
}
}
func TestRunPrepareSteps_Background(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "exec", Cmd: "sleep 30", Background: true},
{Action: "exec", Cmd: "echo after-bg > /tmp/after-bg"},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "")
if err != nil {
t.Fatalf("RunPrepareSteps: %v", err)
}
result, _ := box.Exec(ctx, []string{"cat", "/tmp/after-bg"})
if strings.TrimSpace(result.Stdout) != "after-bg" {
t.Errorf("background step blocked execution, got %q", result.Stdout)
}
})
}
}
func TestRunPrepareSteps_MixedActions(t *testing.T) {
skipIfNoDocker(t)
for _, nc := range boxNodes() {
nc := nc
t.Run(nc.Name, func(t *testing.T) {
m := setupManager(t, &nc)
box := createBox(t, m, nc)
wsID := fmt.Sprintf("test-mixed-%d", time.Now().UnixNano())
box.BindWorkplace(wsID)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "file", Path: "mixed.conf", Content: []byte("key=value")},
{Action: "exec", Cmd: "echo exec-ok > /tmp/mixed-exec"},
{Action: "copy", Src: "mixed.conf", Dst: "mixed-copy.conf"},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, box, "test-assistant", "")
if err != nil {
t.Fatalf("RunPrepareSteps: %v", err)
}
ws := box.Workplace()
data, err := ws.ReadFile("mixed-copy.conf")
if err != nil {
t.Fatalf("ReadFile mixed-copy.conf: %v", err)
}
if string(data) != "key=value" {
t.Errorf("copy result: got %q, want %q", string(data), "key=value")
}
result, _ := box.Exec(ctx, []string{"cat", "/tmp/mixed-exec"})
if strings.TrimSpace(result.Stdout) != "exec-ok" {
t.Errorf("exec result: got %q, want %q", result.Stdout, "exec-ok")
}
})
}
}
// ---------------------------------------------------------------------------
// HostExec tests
// ---------------------------------------------------------------------------
func TestRunPrepareSteps_HostExec(t *testing.T) {
skipIfNoHostExec(t)
for _, tgt := range hostTargets() {
tgt := tgt
t.Run(tgt.Name, func(t *testing.T) {
m := setupHostManager(t, &tgt)
host := createHost(t, m, tgt)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
t.Logf("SystemInfo: OS=%q Shell=%q TempDir=%q",
host.ComputerInfo().System.OS,
host.ComputerInfo().System.Shell,
host.ComputerInfo().System.TempDir)
isWin := tgt.Name == "win-native"
var cmd string
if isWin {
cmd = `Write-Output 'host-ok'`
} else {
cmd = "echo host-ok"
}
steps := []types.PrepareStep{
{Action: "exec", Cmd: cmd},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "")
if err != nil {
t.Fatalf("RunPrepareSteps on host: %v", err)
}
})
}
}
func TestRunPrepareSteps_HostExecFile(t *testing.T) {
skipIfNoHostExec(t)
for _, tgt := range hostTargets() {
tgt := tgt
t.Run(tgt.Name, func(t *testing.T) {
m := setupHostManager(t, &tgt)
host := createHost(t, m, tgt)
wsID := fmt.Sprintf("test-hostfile-%d", time.Now().UnixNano())
host.BindWorkplace(wsID)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "file", Path: "host-test.txt", Content: []byte("host-file-data")},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "")
if err != nil {
t.Fatalf("RunPrepareSteps file: %v", err)
}
ws := host.Workplace()
data, err := ws.ReadFile("host-test.txt")
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(data) != "host-file-data" {
t.Errorf("content = %q, want %q", string(data), "host-file-data")
}
})
}
}
func TestRunPrepareSteps_HostExecCopy(t *testing.T) {
skipIfNoHostExec(t)
for _, tgt := range hostTargets() {
tgt := tgt
t.Run(tgt.Name, func(t *testing.T) {
m := setupHostManager(t, &tgt)
host := createHost(t, m, tgt)
wsID := fmt.Sprintf("test-hostcopy-%d", time.Now().UnixNano())
host.BindWorkplace(wsID)
ws := host.Workplace()
ws.WriteFile("copy-src.txt", []byte("copy-data"), 0644)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
steps := []types.PrepareStep{
{Action: "copy", Src: "copy-src.txt", Dst: "copy-dst.txt"},
}
err := sandboxv2.RunPrepareSteps(ctx, steps, host, "test-host", "")
if err != nil {
t.Fatalf("RunPrepareSteps copy: %v", err)
}
data, err := ws.ReadFile("copy-dst.txt")
if err != nil {
t.Fatalf("ReadFile: %v", err)
}
if string(data) != "copy-data" {
t.Errorf("content = %q, want %q", string(data), "copy-data")
}
})
}
}
func TestRunPrepareSteps_HostExecOnce(t *testing.T) {
skipIfNoHostExec(t)
for _, tgt := range hostTargets() {
tgt := tgt
t.Run(tgt.Name, func(t *testing.T) {
m := setupHostManager(t, &tgt)
host := createHost(t, m, tgt)
wsID := fmt.Sprintf("test-hostonce-%d", time.Now().UnixNano())
host.BindWorkplace(wsID)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
isWin := tgt.Name == "win-native"
var cmd string
if isWin {
cmd = `Write-Output 'once-ok'`
} else {
cmd = "echo once-ok"
}
steps := []types.PrepareStep{
{Action: "exec", Cmd: cmd, Once: true},
}
hash := "host-once-hash"
aid := "host-once-aid"
if err := sandboxv2.RunPrepareSteps(ctx, steps, host, aid, hash); err != nil {
t.Fatalf("first run: %v", err)
}
ws := host.Workplace()
markerData, err := ws.ReadFile(".yao/prepare/" + aid + "/done")
if err != nil {
t.Fatalf("marker not written: %v", err)
}
if string(markerData) != hash {
t.Errorf("marker = %q, want %q", string(markerData), hash)
}
})
}
}