package claude import ( "context" "fmt" "os" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" agentContext "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/output/message" "github.com/yaoapp/yao/config" infraSandbox "github.com/yaoapp/yao/sandbox" "github.com/yaoapp/yao/test" ) // TestE2ESkipClaudeCLI verifies that Claude CLI is skipped when no prompts/skills/mcp // This is the "hook-only" mode where hooks take full control func TestE2ESkipClaudeCLI(t *testing.T) { if testing.Short() { t.Skip("Skipping E2E test in short mode") } test.Prepare(t, config.Conf) defer test.Clean() manager := createTestManager(t) if manager == nil { return } defer manager.Close() // Create options WITHOUT SystemPrompt, SkillsDir, or MCPConfig // This should trigger the skip logic opts := &Options{ Command: "claude", Image: "alpine:latest", // Use alpine since we're not calling Claude CLI UserID: "test-user", ChatID: fmt.Sprintf("test-e2e-skip-%d", time.Now().UnixNano()), ConnectorHost: "", ConnectorKey: "", Model: "", // No SystemPrompt, SkillsDir, or MCPConfig } exec, err := NewExecutor(manager, opts) require.NoError(t, err) defer exec.Close() // Verify shouldSkipClaudeCLI returns true assert.True(t, exec.shouldSkipClaudeCLI(), "Should skip Claude CLI when no prompts/skills/mcp") // Execute Stream - it should return immediately without calling Claude CLI ctx := agentContext.New(context.Background(), nil, opts.ChatID) messages := []agentContext.Message{ {Role: "user", Content: "Hello"}, } response, err := exec.Stream(ctx, messages, nil) require.NoError(t, err, "Stream should succeed") require.NotNil(t, response, "Response should not be nil") // Verify response indicates skip assert.Contains(t, response.ID, "sandbox-skip", "Response ID should indicate skip") assert.Equal(t, "sandbox", response.Model, "Model should be 'sandbox' for skip mode") assert.Empty(t, response.Content, "Content should be empty for skip mode") t.Log("✓ Claude CLI skip mode verified") } // TestE2EExecuteClaudeCLI verifies that Claude CLI is called when prompts are configured // This requires the real yaoapp/sandbox-claude image and a valid connector func TestE2EExecuteClaudeCLI(t *testing.T) { if testing.Short() { t.Skip("Skipping E2E test in short mode") } // Check for required environment variables apiKey := os.Getenv("DEEPSEEK_API_KEY") apiProxy := os.Getenv("DEEPSEEK_API_PROXY") model := os.Getenv("DEEPSEEK_MODELS_V3") if apiKey == "" || apiProxy == "" || model == "" { t.Skip("Skipping test: DEEPSEEK_API_KEY, DEEPSEEK_API_PROXY, or DEEPSEEK_MODELS_V3 not set") } test.Prepare(t, config.Conf) defer test.Clean() // Get data root from environment dataRoot := os.Getenv("YAO_ROOT") if dataRoot == "" { t.Skip("Skipping test: YAO_ROOT not set") } // 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) } defer manager.Close() // Create options WITH SystemPrompt (triggers Claude CLI execution) opts := &Options{ Command: "claude", Image: "yaoapp/sandbox-claude:latest", UserID: "test-user", ChatID: fmt.Sprintf("test-e2e-exec-%d", time.Now().UnixNano()), ConnectorHost: apiProxy, ConnectorKey: apiKey, Model: model, SystemPrompt: "You are a helpful assistant. Keep responses brief.", Timeout: 5 * time.Minute, } exec, err := NewExecutor(manager, opts) if err != nil { t.Skipf("Skipping test: Failed to create executor: %v", err) } defer exec.Close() // Verify shouldSkipClaudeCLI returns false assert.False(t, exec.shouldSkipClaudeCLI(), "Should NOT skip Claude CLI when prompts are configured") // Execute Stream with a simple prompt ctx := agentContext.New(context.Background(), nil, opts.ChatID) messages := []agentContext.Message{ {Role: "user", Content: "Reply with exactly: TEST_SUCCESS"}, } // Collect streaming output var streamedContent strings.Builder streamHandler := func(chunkType message.StreamChunkType, data []byte) int { if chunkType == message.ChunkText { streamedContent.Write(data) } return 0 // continue streaming } t.Log("Executing Claude CLI with real API call...") startTime := time.Now() response, err := exec.Stream(ctx, messages, streamHandler) duration := time.Since(startTime) t.Logf("Execution took: %v", duration) if err != nil { t.Logf("Stream error (might be expected if Docker/API issue): %v", err) t.Skipf("Skipping assertion: %v", err) } require.NotNil(t, response, "Response should not be nil") // Log response details t.Logf("Response ID: %s", response.ID) t.Logf("Response Model: %s", response.Model) t.Logf("Response Content: %v", response.Content) t.Logf("Streamed Content: %s", streamedContent.String()) // Verify we got some response var fullResponse string if content, ok := response.Content.(string); ok { fullResponse = content } if fullResponse == "" { fullResponse = streamedContent.String() } if fullResponse != "" { t.Logf("✓ Claude CLI executed successfully with response: %s", truncate(fullResponse, 200)) } else { t.Log("⚠ Empty response (Claude CLI might have issues)") } } // TestE2EBuildInputJSONLIntegration tests the full flow of building input JSONL func TestE2EBuildInputJSONLIntegration(t *testing.T) { // Test with conversation history messages := []agentContext.Message{ {Role: "system", Content: "You are a helpful assistant"}, {Role: "user", Content: "What is 2+2?"}, {Role: "assistant", Content: "4"}, {Role: "user", Content: "What about 3+3?"}, } jsonl, err := BuildInputJSONL(messages) require.NoError(t, err) t.Logf("Input JSONL:\n%s", string(jsonl)) // Verify format lines := strings.Split(string(jsonl), "\n") assert.GreaterOrEqual(t, len(lines), 3, "Should have at least 3 lines (user, assistant, user)") // System message should NOT be in JSONL assert.NotContains(t, string(jsonl), "You are a helpful assistant", "System message should not be in JSONL") // User and assistant messages should be present assert.Contains(t, string(jsonl), "What is 2+2", "First user message should be present") assert.Contains(t, string(jsonl), "4", "Assistant response should be present") assert.Contains(t, string(jsonl), "What about 3+3", "Second user message should be present") t.Log("✓ Input JSONL format verified") } // TestE2EBuildCommand tests the full command building func TestE2EBuildCommand(t *testing.T) { messages := []agentContext.Message{ {Role: "system", Content: "You are helpful"}, {Role: "user", Content: "Hello"}, } opts := &Options{ ConnectorHost: "https://api.example.com", ConnectorKey: "test-key", Model: "test-model", Arguments: map[string]interface{}{ "permission_mode": "bypassPermissions", }, MCPConfig: []byte(`{"mcpServers":{}}`), } cmd, env, err := BuildCommand(messages, opts) require.NoError(t, err) t.Logf("Command: %v", cmd) t.Logf("Environment: %v", env) // Verify command structure assert.Equal(t, "bash", cmd[0]) assert.Equal(t, "-c", cmd[1]) bashCmd := cmd[2] // Should use heredoc with INPUTEOF assert.Contains(t, bashCmd, "cat << 'INPUTEOF'", "Should use heredoc") assert.Contains(t, bashCmd, "INPUTEOF", "Should have INPUTEOF delimiter") // Should have streaming flags assert.Contains(t, bashCmd, "--input-format", "Should have input-format flag") assert.Contains(t, bashCmd, "--output-format", "Should have output-format flag") assert.Contains(t, bashCmd, "--verbose", "Should have verbose flag") assert.Contains(t, bashCmd, "stream-json", "Should use stream-json format") // Should have permission flags assert.Contains(t, bashCmd, "--dangerously-skip-permissions", "Should have skip-permissions flag") assert.Contains(t, bashCmd, "--permission-mode", "Should have permission-mode flag") assert.Contains(t, bashCmd, "bypassPermissions", "Should have bypassPermissions value") // Should have MCP config assert.Contains(t, bashCmd, "--mcp-config", "Should have mcp-config flag") // Environment should have proxy settings assert.Equal(t, "http://127.0.0.1:3456", env["ANTHROPIC_BASE_URL"]) assert.Equal(t, "dummy", env["ANTHROPIC_API_KEY"]) // System prompt should be passed via CLI argument, not environment variable // CLAUDE_SYSTEM_PROMPT env var is NOT supported by Claude CLI assert.Contains(t, bashCmd, "--append-system-prompt", "Should have append-system-prompt flag") assert.Contains(t, bashCmd, "You are helpful", "System prompt should be in CLI args") t.Log("✓ Command building verified") } // Helper function to truncate strings func truncate(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] + "..." }