- 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.
319 lines
11 KiB
Go
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")
|
|
}
|