- Modify the test for the Claude executor to check for the IPC socket at the new path "/run/yao.sock" instead of the previous "/tmp/yao.sock". This aligns the test with the updated default configuration for the IPC socket path, ensuring consistency across the codebase.
388 lines
12 KiB
Go
388 lines
12 KiB
Go
package claude
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/yaoapp/yao/config"
|
|
infraSandbox "github.com/yaoapp/yao/sandbox"
|
|
"github.com/yaoapp/yao/test"
|
|
)
|
|
|
|
// createTestManager creates a sandbox manager for testing with proper configuration
|
|
func createTestManager(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
|
|
}
|
|
|
|
func TestNewClaudeExecutor(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
manager := createTestManager(t)
|
|
if manager == nil {
|
|
return
|
|
}
|
|
defer manager.Close()
|
|
|
|
opts := &Options{
|
|
Command: "claude",
|
|
Image: "yaoapp/sandbox-claude:latest",
|
|
UserID: "test-user",
|
|
ChatID: fmt.Sprintf("test-chat-claude-%d", time.Now().UnixNano()),
|
|
ConnectorHost: "https://api.example.com",
|
|
ConnectorKey: "key123",
|
|
Model: "test-model",
|
|
}
|
|
|
|
exec, err := NewExecutor(manager, opts)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, exec)
|
|
|
|
// Verify executor was created
|
|
assert.Equal(t, "/workspace", exec.GetWorkDir())
|
|
assert.NoError(t, exec.Close())
|
|
}
|
|
|
|
func TestClaudeExecutorMissingRequiredFields(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
manager := createTestManager(t)
|
|
if manager == nil {
|
|
return
|
|
}
|
|
defer manager.Close()
|
|
|
|
// Missing UserID
|
|
_, err := NewExecutor(manager, &Options{
|
|
Command: "claude",
|
|
ChatID: "test-chat",
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "UserID is required")
|
|
|
|
// Missing ChatID
|
|
_, err = NewExecutor(manager, &Options{
|
|
Command: "claude",
|
|
UserID: "test-user",
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "ChatID is required")
|
|
}
|
|
|
|
func TestClaudeExecutorFileOperations(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
manager := createTestManager(t)
|
|
if manager == nil {
|
|
return
|
|
}
|
|
defer manager.Close()
|
|
|
|
opts := &Options{
|
|
Command: "claude",
|
|
Image: "alpine:latest", // Use alpine for simpler testing
|
|
UserID: "test-user",
|
|
ChatID: fmt.Sprintf("test-chat-file-ops-%d", time.Now().UnixNano()),
|
|
}
|
|
|
|
exec, err := NewExecutor(manager, opts)
|
|
require.NoError(t, err)
|
|
defer exec.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Test WriteFile
|
|
content := []byte("Hello, World!")
|
|
err = exec.WriteFile(ctx, "test-file.txt", content)
|
|
require.NoError(t, err)
|
|
|
|
// Test ReadFile
|
|
readContent, err := exec.ReadFile(ctx, "test-file.txt")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, content, readContent)
|
|
|
|
// Test ListDir
|
|
files, err := exec.ListDir(ctx, ".")
|
|
require.NoError(t, err)
|
|
assert.True(t, len(files) > 0, "Expected at least one file in directory")
|
|
|
|
// Find our test file
|
|
var found bool
|
|
for _, f := range files {
|
|
if f.Name == "test-file.txt" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found, "Expected to find test-file.txt in directory listing")
|
|
}
|
|
|
|
func TestClaudeExecutorExec(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
manager := createTestManager(t)
|
|
if manager == nil {
|
|
return
|
|
}
|
|
defer manager.Close()
|
|
|
|
opts := &Options{
|
|
Command: "claude",
|
|
Image: "alpine:latest", // Use alpine for simpler testing
|
|
UserID: "test-user",
|
|
ChatID: fmt.Sprintf("test-chat-exec-%d", time.Now().UnixNano()),
|
|
}
|
|
|
|
exec, err := NewExecutor(manager, opts)
|
|
require.NoError(t, err)
|
|
defer exec.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Test simple echo command
|
|
output, err := exec.Exec(ctx, []string{"echo", "hello-world"})
|
|
require.NoError(t, err)
|
|
assert.Contains(t, output, "hello-world")
|
|
}
|
|
|
|
// TestClaudeExecutorMCPConfigWrite tests that MCP config is correctly written to container
|
|
func TestClaudeExecutorMCPConfigWrite(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
manager := createTestManager(t)
|
|
if manager == nil {
|
|
return
|
|
}
|
|
defer manager.Close()
|
|
|
|
// Create MCP config JSON
|
|
mcpConfig := []byte(`{"mcpServers":{"echo":{"command":"yao-mcp-proxy","args":["echo"],"tools":["ping","echo"]}}}`)
|
|
|
|
opts := &Options{
|
|
Command: "claude",
|
|
Image: "alpine:latest",
|
|
UserID: "test-user",
|
|
ChatID: fmt.Sprintf("test-chat-mcp-write-%d", time.Now().UnixNano()),
|
|
MCPConfig: mcpConfig,
|
|
// No connector config - skip proxy start for alpine test image
|
|
}
|
|
|
|
exec, err := NewExecutor(manager, opts)
|
|
require.NoError(t, err)
|
|
defer exec.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Call prepareEnvironment to write configs
|
|
err = exec.prepareEnvironment(ctx)
|
|
require.NoError(t, err, "prepareEnvironment should succeed")
|
|
|
|
// Verify MCP config was written by reading it back
|
|
readContent, err := exec.ReadFile(ctx, ".mcp.json")
|
|
require.NoError(t, err, "Should be able to read .mcp.json")
|
|
require.NotEmpty(t, readContent, "MCP config should not be empty")
|
|
|
|
t.Logf("MCP config in container: %s", string(readContent))
|
|
|
|
// Verify content matches
|
|
assert.JSONEq(t, string(mcpConfig), string(readContent), "MCP config content should match")
|
|
|
|
t.Log("✓ MCP config verified in container")
|
|
}
|
|
|
|
// TestClaudeExecutorSkillsCopy tests that skills directory is correctly copied to container
|
|
// Uses real test application skills directory
|
|
func TestClaudeExecutorSkillsCopy(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
manager := createTestManager(t)
|
|
if manager == nil {
|
|
return
|
|
}
|
|
defer manager.Close()
|
|
|
|
// Use real skills directory from test application
|
|
appRoot := os.Getenv("YAO_ROOT")
|
|
require.NotEmpty(t, appRoot, "YAO_ROOT should be set")
|
|
|
|
skillsDir := appRoot + "/assistants/tests/sandbox/full/skills"
|
|
|
|
// Verify skills directory exists on host
|
|
info, err := os.Stat(skillsDir)
|
|
require.NoError(t, err, "Skills directory should exist: %s", skillsDir)
|
|
require.True(t, info.IsDir(), "Skills path should be a directory")
|
|
t.Logf("Using real skills directory: %s", skillsDir)
|
|
|
|
opts := &Options{
|
|
Command: "claude",
|
|
Image: "alpine:latest",
|
|
UserID: "test-user",
|
|
ChatID: fmt.Sprintf("test-chat-skills-%d", time.Now().UnixNano()),
|
|
SkillsDir: skillsDir,
|
|
// No connector config - skip proxy start for alpine test image
|
|
}
|
|
|
|
exec, err := NewExecutor(manager, opts)
|
|
require.NoError(t, err)
|
|
defer exec.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Call prepareEnvironment to copy skills
|
|
err = exec.prepareEnvironment(ctx)
|
|
require.NoError(t, err, "prepareEnvironment should succeed")
|
|
|
|
// Verify .claude directory was created
|
|
output, err := exec.Exec(ctx, []string{"ls", "-la", ".claude"})
|
|
require.NoError(t, err, ".claude directory should exist")
|
|
t.Logf(".claude directory contents:\n%s", output)
|
|
|
|
// Verify skills directory exists in container
|
|
output, err = exec.Exec(ctx, []string{"ls", "-la", ".claude/skills"})
|
|
require.NoError(t, err, "skills directory should exist in container")
|
|
t.Logf("skills directory contents:\n%s", output)
|
|
assert.Contains(t, output, "echo-test", "echo-test skill should exist")
|
|
|
|
// Verify echo-test skill was copied correctly
|
|
output, err = exec.Exec(ctx, []string{"ls", "-la", ".claude/skills/echo-test"})
|
|
require.NoError(t, err, "echo-test skill directory should exist")
|
|
assert.Contains(t, output, "SKILL.md", "SKILL.md should exist in echo-test")
|
|
assert.Contains(t, output, "scripts", "scripts directory should exist in echo-test")
|
|
t.Logf("echo-test skill contents:\n%s", output)
|
|
|
|
// Read SKILL.md content to verify
|
|
readContent, err := exec.ReadFile(ctx, ".claude/skills/echo-test/SKILL.md")
|
|
require.NoError(t, err, "Should be able to read SKILL.md from container")
|
|
require.NotEmpty(t, readContent, "SKILL.md content should not be empty")
|
|
// Verify content contains expected strings from the real SKILL.md
|
|
assert.Contains(t, string(readContent), "name: echo-test", "SKILL.md should contain skill name")
|
|
assert.Contains(t, string(readContent), "# Echo Test", "SKILL.md should contain the title")
|
|
t.Logf("✓ SKILL.md content verified (%d bytes)", len(readContent))
|
|
|
|
t.Log("✓ Skills directory verified in container with real test data")
|
|
}
|
|
|
|
// TestClaudeExecutorPrepareEnvironmentIntegration tests full environment preparation
|
|
// Uses real test application data
|
|
func TestClaudeExecutorPrepareEnvironmentIntegration(t *testing.T) {
|
|
test.Prepare(t, config.Conf)
|
|
defer test.Clean()
|
|
|
|
manager := createTestManager(t)
|
|
if manager == nil {
|
|
return
|
|
}
|
|
defer manager.Close()
|
|
|
|
// Use real skills directory from test application
|
|
appRoot := os.Getenv("YAO_ROOT")
|
|
require.NotEmpty(t, appRoot, "YAO_ROOT should be set")
|
|
skillsDir := appRoot + "/assistants/tests/sandbox/full/skills"
|
|
|
|
// Verify skills directory exists
|
|
_, err := os.Stat(skillsDir)
|
|
require.NoError(t, err, "Skills directory should exist")
|
|
|
|
// Create MCP config (simulating what buildMCPConfigForSandbox produces)
|
|
mcpConfig := []byte(`{"mcpServers":{"echo":{"command":"yao-mcp-proxy","args":["echo"],"tools":["ping","echo","status"]}}}`)
|
|
|
|
opts := &Options{
|
|
Command: "claude",
|
|
Image: "alpine:latest",
|
|
UserID: "test-user",
|
|
ChatID: fmt.Sprintf("test-chat-full-env-%d", time.Now().UnixNano()),
|
|
MCPConfig: mcpConfig,
|
|
SkillsDir: skillsDir,
|
|
// No connector config - skip proxy start for alpine test image
|
|
}
|
|
|
|
exec, err := NewExecutor(manager, opts)
|
|
require.NoError(t, err)
|
|
defer exec.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Call prepareEnvironment
|
|
err = exec.prepareEnvironment(ctx)
|
|
require.NoError(t, err, "prepareEnvironment should succeed")
|
|
|
|
// 1. Check MCP config
|
|
mcpContent, err := exec.ReadFile(ctx, ".mcp.json")
|
|
require.NoError(t, err, "MCP config should exist in container")
|
|
assert.JSONEq(t, string(mcpConfig), string(mcpContent), "MCP config content should match")
|
|
t.Logf("✓ MCP config verified: %s", string(mcpContent))
|
|
|
|
// 2. Check Skills directory structure
|
|
output, err := exec.Exec(ctx, []string{"ls", "-la", ".claude/skills"})
|
|
require.NoError(t, err, "Skills directory should exist in container")
|
|
assert.Contains(t, output, "echo-test", "echo-test skill should exist")
|
|
t.Logf("✓ Skills directory contents:\n%s", output)
|
|
|
|
// 3. Check skill content
|
|
skillContent, err := exec.ReadFile(ctx, ".claude/skills/echo-test/SKILL.md")
|
|
require.NoError(t, err, "SKILL.md should exist in container")
|
|
require.NotEmpty(t, skillContent, "SKILL.md should not be empty")
|
|
assert.Contains(t, string(skillContent), "name: echo-test", "SKILL.md should contain skill name")
|
|
assert.Contains(t, string(skillContent), "# Echo Test", "SKILL.md should contain the title")
|
|
t.Logf("✓ SKILL.md verified: %d bytes", len(skillContent))
|
|
|
|
t.Log("✓ Full environment preparation verified with real test data")
|
|
}
|
|
|
|
// TestClaudeExecutorIPCSocketMount verifies that IPC socket is bind mounted to container
|
|
func TestClaudeExecutorIPCSocketMount(t *testing.T) {
|
|
manager := createTestManager(t)
|
|
|
|
opts := &Options{
|
|
Command: "claude",
|
|
Image: "alpine:latest",
|
|
UserID: "test-user",
|
|
ChatID: "test-ipc-socket-" + fmt.Sprintf("%d", time.Now().UnixNano()),
|
|
ConnectorHost: "https://api.test.com",
|
|
ConnectorKey: "test-key",
|
|
Model: "test-model",
|
|
}
|
|
|
|
exec, err := NewExecutor(manager, opts)
|
|
require.NoError(t, err)
|
|
defer exec.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Check if IPC socket exists in container
|
|
output, err := exec.Exec(ctx, []string{"ls", "-la", "/run/yao.sock"})
|
|
require.NoError(t, err, "IPC socket should exist in container")
|
|
assert.Contains(t, output, "yao.sock", "Should find yao.sock file")
|
|
t.Logf("✓ IPC socket mounted: %s", strings.TrimSpace(output))
|
|
|
|
// Verify it's a socket file (starts with 's' in ls output)
|
|
assert.Contains(t, output, "srw", "Should be a socket file (starts with 's')")
|
|
t.Log("✓ IPC socket is correctly bind mounted to container")
|
|
}
|