yao/agent/assistant/sandbox_test.go
Max c1e92b726d Implement MCP Configuration and Tool Integration for Sandbox
- Added functionality to build and manage MCP configuration for sandbox environments, allowing for dynamic tool execution.
- Enhanced the Assistant's Stream method to skip MCP tool calls in sandbox mode, with internal handling by Claude CLI.
- Introduced unit tests for MCP configuration building and skills directory resolution, ensuring robust integration.
- Updated sandbox manager to create IPC sessions and manage tool exposure dynamically, improving interaction with external agents.
- Enhanced documentation to reflect new features and integration points for MCP and skills within the sandbox.
2026-01-30 19:57:31 +08:00

319 lines
11 KiB
Go

package assistant_test
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/yaoapp/yao/agent"
"github.com/yaoapp/yao/agent/assistant"
agentContext "github.com/yaoapp/yao/agent/context"
"github.com/yaoapp/yao/config"
"github.com/yaoapp/yao/test"
)
// TestLoadSandboxBasicAssistant tests loading the basic sandbox test assistant
func TestLoadSandboxBasicAssistant(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
ast, err := assistant.LoadPath("/assistants/tests/sandbox/basic")
require.NoError(t, err)
require.NotNil(t, ast)
// Verify basic fields
assert.Equal(t, "tests.sandbox.basic", ast.ID)
assert.Equal(t, "Sandbox Basic Test", ast.Name)
assert.Equal(t, "deepseek.v3", ast.Connector)
// Verify sandbox configuration
require.NotNil(t, ast.Sandbox, "Sandbox should be configured")
assert.Equal(t, "claude", ast.Sandbox.Command)
assert.Equal(t, "5m", ast.Sandbox.Timeout)
// Verify HasSandbox returns true
assert.True(t, ast.HasSandbox(), "HasSandbox should return true")
}
// TestLoadSandboxHooksAssistant tests loading the hooks sandbox test assistant
func TestLoadSandboxHooksAssistant(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
ast, err := assistant.LoadPath("/assistants/tests/sandbox/hooks")
require.NoError(t, err)
require.NotNil(t, ast)
// Verify basic fields
assert.Equal(t, "tests.sandbox.hooks", ast.ID)
assert.Equal(t, "Sandbox Hooks Test", ast.Name)
assert.Equal(t, "deepseek.v3", ast.Connector)
// Verify sandbox configuration
require.NotNil(t, ast.Sandbox, "Sandbox should be configured")
assert.Equal(t, "claude", ast.Sandbox.Command)
// Verify hooks are loaded
assert.NotNil(t, ast.HookScript, "HookScript should be loaded")
}
// TestLoadSandboxFullAssistant tests loading the full sandbox test assistant with MCPs and Skills
func TestLoadSandboxFullAssistant(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Load agent to ensure MCPs are available
err := agent.Load(config.Conf)
require.NoError(t, err, "agent.Load should succeed")
ast, err := assistant.LoadPath("/assistants/tests/sandbox/full")
require.NoError(t, err)
require.NotNil(t, ast)
// Verify basic fields
assert.Equal(t, "tests.sandbox.full", ast.ID)
assert.Equal(t, "Sandbox Full Test", ast.Name)
assert.Equal(t, "deepseek.v3", ast.Connector)
// Verify sandbox configuration
require.NotNil(t, ast.Sandbox, "Sandbox should be configured")
assert.Equal(t, "claude", ast.Sandbox.Command)
assert.Equal(t, "5m", ast.Sandbox.Timeout)
// Verify sandbox arguments (command-specific options)
require.NotNil(t, ast.Sandbox.Arguments, "Sandbox arguments should be configured")
assert.Equal(t, float64(10), ast.Sandbox.Arguments["max_turns"])
assert.Equal(t, "acceptEdits", ast.Sandbox.Arguments["permission_mode"])
// Verify MCP configuration
require.NotNil(t, ast.MCP, "MCP should be configured")
require.NotNil(t, ast.MCP.Servers, "MCP.Servers should be configured")
assert.Len(t, ast.MCP.Servers, 1, "Should have 1 MCP server configured")
assert.Equal(t, "echo", ast.MCP.Servers[0].ServerID, "MCP server ID should be 'echo'")
assert.Contains(t, ast.MCP.Servers[0].Tools, "ping", "MCP tools should contain 'ping'")
assert.Contains(t, ast.MCP.Servers[0].Tools, "echo", "MCP tools should contain 'echo'")
// Verify hooks are loaded
assert.NotNil(t, ast.HookScript, "HookScript should be loaded")
}
// TestSandboxConfigValidation tests sandbox configuration validation
func TestSandboxConfigValidation(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
tests := []struct {
name string
path string
hasError bool
}{
{
name: "Basic sandbox config",
path: "/assistants/tests/sandbox/basic",
hasError: false,
},
{
name: "Hooks sandbox config",
path: "/assistants/tests/sandbox/hooks",
hasError: false,
},
{
name: "Full sandbox config with MCPs",
path: "/assistants/tests/sandbox/full",
hasError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ast, err := assistant.LoadPath(tt.path)
if tt.hasError {
assert.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, ast)
require.NotNil(t, ast.Sandbox)
assert.NotEmpty(t, ast.Sandbox.Command)
})
}
}
// TestSkillsDirectoryResolution tests that skills directory exists and has correct structure
// Note: Skills are auto-discovered from skills/ directory, not stored in AssistantModel
func TestSkillsDirectoryResolution(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
ast, err := assistant.LoadPath("/assistants/tests/sandbox/full")
require.NoError(t, err)
require.NotNil(t, ast)
// Get app root from environment
appRoot := os.Getenv("YAO_ROOT")
require.NotEmpty(t, appRoot, "YAO_ROOT should be set")
// Verify assistant path is set
assert.NotEmpty(t, ast.Path, "Assistant path should be set")
// Build expected skills directory path
// ast.Path is like "/assistants/tests/sandbox/full"
expectedSkillsDir := filepath.Join(appRoot, ast.Path, "skills")
// Verify skills directory exists
info, err := os.Stat(expectedSkillsDir)
require.NoError(t, err, "Skills directory should exist: %s", expectedSkillsDir)
assert.True(t, info.IsDir(), "Skills path should be a directory")
// Verify skills directory structure
entries, err := os.ReadDir(expectedSkillsDir)
require.NoError(t, err, "Should be able to read skills directory")
// Find echo-test skill
var foundEchoTest bool
for _, entry := range entries {
if entry.IsDir() && entry.Name() == "echo-test" {
foundEchoTest = true
// Verify SKILL.md exists (required)
skillMdPath := filepath.Join(expectedSkillsDir, "echo-test", "SKILL.md")
_, err := os.Stat(skillMdPath)
assert.NoError(t, err, "SKILL.md should exist")
// Verify scripts directory exists (optional but we created it)
scriptsDir := filepath.Join(expectedSkillsDir, "echo-test", "scripts")
_, err = os.Stat(scriptsDir)
assert.NoError(t, err, "scripts directory should exist")
// Verify echo.sh exists
echoShPath := filepath.Join(scriptsDir, "echo.sh")
_, err = os.Stat(echoShPath)
assert.NoError(t, err, "echo.sh should exist")
break
}
}
assert.True(t, foundEchoTest, "echo-test skill should exist in skills directory")
}
// TestMCPConfiguration tests that MCP is correctly loaded for sandbox assistant
func TestMCPConfiguration(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Load agent to ensure MCPs are available
err := agent.Load(config.Conf)
require.NoError(t, err, "agent.Load should succeed")
ast, err := assistant.LoadPath("/assistants/tests/sandbox/full")
require.NoError(t, err)
require.NotNil(t, ast)
// Verify MCP configuration structure
require.NotNil(t, ast.MCP, "MCP should not be nil")
require.NotNil(t, ast.MCP.Servers, "MCP.Servers should not be nil")
assert.Len(t, ast.MCP.Servers, 1, "Should have 1 MCP server configured")
// Verify echo server configuration
echoServer := ast.MCP.Servers[0]
assert.Equal(t, "echo", echoServer.ServerID, "Server ID should be 'echo'")
assert.Len(t, echoServer.Tools, 3, "Should have 3 tools configured")
assert.Contains(t, echoServer.Tools, "ping")
assert.Contains(t, echoServer.Tools, "echo")
assert.Contains(t, echoServer.Tools, "status")
}
// TestBuildMCPConfigForSandbox tests that MCP configuration is correctly built for sandbox
func TestBuildMCPConfigForSandbox(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Load agent to ensure MCPs are available
err := agent.Load(config.Conf)
require.NoError(t, err, "agent.Load should succeed")
ast, err := assistant.LoadPath("/assistants/tests/sandbox/full")
require.NoError(t, err)
require.NotNil(t, ast)
require.NotNil(t, ast.MCP, "MCP configuration should exist")
// Create a mock context for the test
ctx := agentContext.New(context.Background(), nil, "test-mcp-config-build")
// Call BuildMCPConfigForSandbox and verify the result
mcpConfig, err := ast.BuildMCPConfigForSandbox(ctx)
require.NoError(t, err, "BuildMCPConfigForSandbox should not error")
require.NotEmpty(t, mcpConfig, "MCP config should not be empty")
t.Logf("MCP config JSON: %s", string(mcpConfig))
// Parse and verify the JSON structure
var config map[string]interface{}
err = json.Unmarshal(mcpConfig, &config)
require.NoError(t, err, "MCP config should be valid JSON")
// Verify mcpServers key exists
mcpServers, ok := config["mcpServers"].(map[string]interface{})
require.True(t, ok, "mcpServers should be a map")
require.NotEmpty(t, mcpServers, "mcpServers should not be empty")
// Verify "yao" server exists (single server using yao-bridge for IPC)
yaoServer, ok := mcpServers["yao"].(map[string]interface{})
require.True(t, ok, "yao server should exist in mcpServers")
// Verify server structure - uses yao-bridge to connect to IPC socket
assert.Equal(t, "yao-bridge", yaoServer["command"], "command should be yao-bridge")
args, ok := yaoServer["args"].([]interface{})
require.True(t, ok, "args should be an array")
require.Len(t, args, 1, "args should have 1 element")
assert.Equal(t, "/tmp/yao.sock", args[0], "first arg should be IPC socket path")
t.Logf("✓ MCP config verified: uses yao-bridge with IPC socket /tmp/yao.sock")
}
// TestSandboxMCPAndSkillsOptions tests that sandbox options include MCP and Skills
func TestSandboxMCPAndSkillsOptions(t *testing.T) {
test.Prepare(t, config.Conf)
defer test.Clean()
// Load agent to ensure MCPs are available
err := agent.Load(config.Conf)
require.NoError(t, err, "agent.Load should succeed")
ast, err := assistant.LoadPath("/assistants/tests/sandbox/full")
require.NoError(t, err)
require.NotNil(t, ast)
// Verify sandbox configuration is present
require.NotNil(t, ast.Sandbox, "Sandbox should be configured")
assert.Equal(t, "claude", ast.Sandbox.Command)
// Verify MCP is configured (will be passed to sandbox)
require.NotNil(t, ast.MCP, "MCP should be configured")
assert.Len(t, ast.MCP.Servers, 1, "Should have 1 MCP server")
// Verify skills directory exists
appRoot := os.Getenv("YAO_ROOT")
require.NotEmpty(t, appRoot, "YAO_ROOT should be set")
skillsDir := filepath.Join(appRoot, ast.Path, "skills")
info, err := os.Stat(skillsDir)
require.NoError(t, err, "Skills directory should exist")
assert.True(t, info.IsDir(), "Skills should be a directory")
// Verify echo-test skill exists
echoTestDir := filepath.Join(skillsDir, "echo-test")
info, err = os.Stat(echoTestDir)
require.NoError(t, err, "echo-test skill should exist")
assert.True(t, info.IsDir(), "echo-test should be a directory")
// Verify SKILL.md exists
skillMd := filepath.Join(echoTestDir, "SKILL.md")
_, err = os.Stat(skillMd)
require.NoError(t, err, "SKILL.md should exist")
}