562 lines
16 KiB
Go
562 lines
16 KiB
Go
//go:build smoke
|
|
|
|
// Package e2e contains smoke and regression tests for cc-connect.
|
|
// These tests verify core functionality using real components where possible,
|
|
// and mocks where necessary (e.g., platform network calls).
|
|
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/chenhg5/cc-connect/config"
|
|
"github.com/chenhg5/cc-connect/core"
|
|
"github.com/chenhg5/cc-connect/tests/mocks"
|
|
"github.com/chenhg5/cc-connect/tests/mocks/fake"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-100: Config Loading Smoke Test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_ConfigLoading(t *testing.T) {
|
|
// Create a temporary config file
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "config.toml")
|
|
|
|
configContent := `
|
|
data_dir = "~/.cc-connect"
|
|
|
|
[[projects]]
|
|
name = "test-project"
|
|
|
|
[projects.agent]
|
|
type = "claudecode"
|
|
|
|
[[projects.platforms]]
|
|
type = "feishu"
|
|
|
|
[log]
|
|
level = "info"
|
|
`
|
|
err := os.WriteFile(configPath, []byte(configContent), 0644)
|
|
require.NoError(t, err)
|
|
|
|
// Load the config
|
|
config.ConfigPath = configPath
|
|
cfg, err := config.Load(configPath)
|
|
require.NoError(t, err)
|
|
|
|
// Verify basic config fields
|
|
assert.Equal(t, "~/.cc-connect", cfg.DataDir)
|
|
assert.Len(t, cfg.Projects, 1)
|
|
assert.Equal(t, "test-project", cfg.Projects[0].Name)
|
|
assert.Equal(t, "claudecode", cfg.Projects[0].Agent.Type)
|
|
|
|
t.Log("Config loading: PASS")
|
|
}
|
|
|
|
func TestSmoke_ConfigLoadingInvalid(t *testing.T) {
|
|
// Test that invalid config is properly rejected
|
|
tmpDir := t.TempDir()
|
|
configPath := filepath.Join(tmpDir, "invalid.toml")
|
|
|
|
// Write invalid TOML
|
|
err := os.WriteFile(configPath, []byte("invalid toml content ="), 0644)
|
|
require.NoError(t, err)
|
|
|
|
_, err = config.Load(configPath)
|
|
assert.Error(t, err)
|
|
|
|
t.Log("Config loading (invalid): PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-101: All Agents Initialization Smoke Test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_AllAgentsInit(t *testing.T) {
|
|
// Verify all registered agents can be listed
|
|
ctx := context.Background()
|
|
|
|
// Get list of registered agent factories
|
|
// Note: This tests the factory registration, not actual CLI existence
|
|
t.Log("Registered agents via factory:", listRegisteredAgents())
|
|
|
|
// Verify we have agents registered (at least the ones in the codebase)
|
|
assert.NotEmpty(t, listRegisteredAgents(), "should have at least one agent registered")
|
|
|
|
// Try to create each registered agent with minimal opts
|
|
for _, agentName := range listRegisteredAgents() {
|
|
opts := map[string]any{
|
|
"project_dir": t.TempDir(),
|
|
}
|
|
agent, err := core.CreateAgent(agentName, opts)
|
|
if err != nil {
|
|
t.Logf("Agent %s creation: %v (may require CLI)", agentName, err)
|
|
continue
|
|
}
|
|
assert.NotNil(t, agent)
|
|
assert.Equal(t, agentName, agent.Name())
|
|
|
|
// Clean up
|
|
agent.Stop()
|
|
t.Logf("Agent %s: initialized successfully", agentName)
|
|
}
|
|
|
|
// Use ctx to avoid compiler warning
|
|
assert.NotNil(t, ctx)
|
|
|
|
t.Log("All agents init: PASS")
|
|
}
|
|
|
|
func listRegisteredAgents() []string {
|
|
// This requires access to the internal registry
|
|
// We'll test via the factory pattern
|
|
agents := []string{
|
|
"claudecode", "codex", "cursor", "gemini",
|
|
"iflow", "opencode", "pi", "qoder",
|
|
}
|
|
return agents
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-102: All Platforms Initialization Smoke Test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_AllPlatformsInit(t *testing.T) {
|
|
// Verify all registered platforms can be listed
|
|
t.Log("Registered platforms:", listRegisteredPlatforms())
|
|
|
|
// Verify we have platforms registered
|
|
assert.NotEmpty(t, listRegisteredPlatforms(), "should have at least one platform registered")
|
|
|
|
// Try to create each registered platform with minimal opts
|
|
for _, platformName := range listRegisteredPlatforms() {
|
|
opts := map[string]any{
|
|
"enabled": true,
|
|
}
|
|
platform, err := core.CreatePlatform(platformName, opts)
|
|
if err != nil {
|
|
t.Logf("Platform %s creation: %v", platformName, err)
|
|
continue
|
|
}
|
|
assert.NotNil(t, platform)
|
|
assert.Equal(t, platformName, platform.Name())
|
|
|
|
// Clean up - don't actually start, just verify creation
|
|
t.Logf("Platform %s: created successfully", platformName)
|
|
}
|
|
|
|
t.Log("All platforms init: PASS")
|
|
}
|
|
|
|
func listRegisteredPlatforms() []string {
|
|
platforms := []string{
|
|
"feishu", "telegram", "discord", "slack",
|
|
"dingtalk", "wecom", "qq", "qqbot", "line",
|
|
}
|
|
return platforms
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-103: Session Management Smoke Test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_SessionManagement(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a fake agent for session testing
|
|
agent := fake.NewFakeAgent("test-agent")
|
|
|
|
// Test session creation
|
|
session, err := agent.StartSession(ctx, "test-session-001")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, session)
|
|
assert.Equal(t, "test-session-001", session.CurrentSessionID())
|
|
assert.True(t, session.Alive())
|
|
|
|
// Test session list
|
|
sessions, err := agent.ListSessions(ctx)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, sessions)
|
|
|
|
// Test sending a message
|
|
err = session.Send("Hello", nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Test session is still alive
|
|
assert.True(t, session.Alive())
|
|
|
|
// Test session close
|
|
err = session.Close()
|
|
require.NoError(t, err)
|
|
assert.False(t, session.Alive())
|
|
|
|
t.Log("Session management: PASS")
|
|
}
|
|
|
|
func TestSmoke_SessionMessageFlow(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create a fake agent session with predefined responses
|
|
session := fake.NewFakeAgentSession("test-session-002")
|
|
session.AddTextEvent("Thinking...").AddResultEvent("Final answer.")
|
|
|
|
agent := fake.NewFakeAgentWithSession("test-agent", "test-session-002", session)
|
|
|
|
// Start session
|
|
started, err := agent.StartSession(ctx, "test-session-002")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-session-002", started.CurrentSessionID())
|
|
|
|
// Send message
|
|
err = started.Send("What is 2+2?", nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Collect events
|
|
var events []core.Event
|
|
for e := range started.Events() {
|
|
events = append(events, e)
|
|
if e.Done {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Verify events
|
|
assert.NotEmpty(t, events)
|
|
assert.Equal(t, core.EventType("text"), events[0].Type)
|
|
|
|
t.Log("Session message flow: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-104: Command Parsing Smoke Test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_CommandParsing(t *testing.T) {
|
|
registry := core.NewCommandRegistry()
|
|
|
|
// Add some test commands
|
|
registry.Add("test", "Test command", "echo {{1}}", "", "", "test")
|
|
|
|
// Test command resolution
|
|
cmd, ok := registry.Resolve("test")
|
|
require.True(t, ok)
|
|
assert.NotNil(t, cmd)
|
|
assert.Equal(t, "test", cmd.Name)
|
|
assert.Equal(t, "Test command", cmd.Description)
|
|
|
|
// Test built-in commands
|
|
registry.Add("help", "Show help", "help prompt", "", "", "builtin")
|
|
cmd, ok = registry.Resolve("help")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "help", cmd.Name)
|
|
|
|
// Test command not found
|
|
cmd, ok = registry.Resolve("nonexistent")
|
|
assert.False(t, ok)
|
|
assert.Nil(t, cmd)
|
|
|
|
t.Log("Command parsing: PASS")
|
|
}
|
|
|
|
func TestSmoke_CommandRegistryList(t *testing.T) {
|
|
registry := core.NewCommandRegistry()
|
|
|
|
// Add multiple commands
|
|
registry.Add("cmd1", "Command 1", "{{1}}", "", "", "test")
|
|
registry.Add("cmd2", "Command 2", "{{2}}", "", "", "test")
|
|
registry.Add("alias", "Alias for cmd1", "", "", "", "alias")
|
|
|
|
// List all commands
|
|
commands := registry.ListAll()
|
|
assert.Len(t, commands, 3)
|
|
|
|
// Test clear by source
|
|
registry.ClearSource("test")
|
|
commands = registry.ListAll()
|
|
assert.Len(t, commands, 1) // only alias should remain
|
|
|
|
t.Log("Command registry list: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-105: Agent ↔ Platform Message Flow Smoke Test
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_MessageFlow(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create mock platform
|
|
mockPlatform := new(mocks.MockPlatform)
|
|
mockPlatform.On("Name").Return("test-platform")
|
|
mockPlatform.On("Start", mock.Anything).Return(nil)
|
|
mockPlatform.On("Stop").Return(nil)
|
|
|
|
// Create mock agent session with events
|
|
events := []core.Event{
|
|
{Type: core.EventThinking, Content: "Processing..."},
|
|
{Type: core.EventText, Content: "I am thinking..."},
|
|
{Type: core.EventResult, Content: "Final response.", Done: true},
|
|
}
|
|
mockSession := mocks.NewMockAgentSessionWithEvents("session-001", events)
|
|
|
|
// Create mock agent
|
|
mockAgent := new(mocks.MockAgent)
|
|
mockAgent.On("StartSession", ctx, "session-001").Return(mockSession, nil)
|
|
|
|
// Test message handler
|
|
handler := fake.NewTestMessageHandler()
|
|
|
|
// Start platform with handler
|
|
err := mockPlatform.Start(handler.Handle)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate message flow: platform receives message → routed to agent
|
|
// (This is a simplified test - real flow goes through Engine)
|
|
incomingMessage := fake.TestMessageWithContent("Hello agent")
|
|
|
|
// Simulate platform passing message to handler
|
|
handler.Handle(mockPlatform, incomingMessage)
|
|
|
|
// Verify message was received
|
|
messages := handler.GetMessages()
|
|
require.Len(t, messages, 1)
|
|
assert.Equal(t, "Hello agent", messages[0].Content)
|
|
|
|
// Test agent session responds
|
|
session, err := mockAgent.StartSession(ctx, "session-001")
|
|
require.NoError(t, err)
|
|
|
|
// Send message to agent
|
|
err = session.Send("Hello agent", nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify mock calls happened (only agent, not platform since we didn't call Name/Stop)
|
|
mockAgent.AssertExpectations(t)
|
|
|
|
t.Log("Message flow: PASS")
|
|
}
|
|
|
|
func TestSmoke_PlatformReply(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create mock platform that records replies
|
|
mockPlatform := new(mocks.MockPlatform)
|
|
|
|
// Expect Reply call
|
|
mockPlatform.On("Reply", ctx, mock.Anything, "Hello from agent").Return(nil)
|
|
|
|
// Simulate platform reply
|
|
err := mockPlatform.Reply(ctx, nil, "Hello from agent")
|
|
require.NoError(t, err)
|
|
|
|
mockPlatform.AssertExpectations(t)
|
|
|
|
t.Log("Platform reply: PASS")
|
|
}
|
|
|
|
func TestSmoke_EventTypes(t *testing.T) {
|
|
// Test all event types can be created
|
|
events := []core.Event{
|
|
{Type: core.EventText, Content: "text content"},
|
|
{Type: core.EventThinking, Content: "thinking..."},
|
|
{Type: core.EventToolUse, ToolName: "Bash", ToolInput: "ls"},
|
|
{Type: core.EventToolResult, ToolResult: "file1.go"},
|
|
{Type: core.EventResult, Content: "final result", Done: true},
|
|
{Type: core.EventError, Error: context.DeadlineExceeded, Done: true},
|
|
{
|
|
Type: core.EventPermissionRequest,
|
|
ToolName: "Bash",
|
|
ToolInput: "rm -rf /",
|
|
RequestID: "req-001",
|
|
},
|
|
}
|
|
|
|
for _, e := range events {
|
|
assert.NotEmpty(t, string(e.Type))
|
|
}
|
|
|
|
t.Log("Event types: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-107: Multi-Workspace Switch (P1, but quick to add)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_WorkspaceSwitch(t *testing.T) {
|
|
// Create simple workspace state maps to verify isolation concept
|
|
ws1 := map[string]string{
|
|
"id": "workspace-1",
|
|
"session": "session-A",
|
|
"agent": "claudecode",
|
|
}
|
|
ws2 := map[string]string{
|
|
"id": "workspace-2",
|
|
"session": "session-B",
|
|
"agent": "gemini",
|
|
}
|
|
|
|
assert.Equal(t, "workspace-1", ws1["id"])
|
|
assert.Equal(t, "workspace-2", ws2["id"])
|
|
assert.NotEqual(t, ws1["session"], ws2["session"], "workspaces should have independent sessions")
|
|
|
|
t.Log("Workspace switch: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-108: Rate Limiter Basic (P1, but quick to add)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_RateLimiter(t *testing.T) {
|
|
// Create a rate limiter: 5 messages per 60 seconds
|
|
rl := core.NewRateLimiter(5, 60*time.Second)
|
|
defer rl.Stop()
|
|
|
|
// Should allow messages up to limit
|
|
for i := 0; i < 5; i++ {
|
|
allowed := rl.Allow("user1")
|
|
assert.True(t, allowed, "message %d should be allowed", i+1)
|
|
}
|
|
|
|
// Should block after limit
|
|
allowed := rl.Allow("user1")
|
|
assert.False(t, allowed, "6th message should be blocked")
|
|
|
|
// Different user should be allowed
|
|
allowed = rl.Allow("user2")
|
|
assert.True(t, allowed, "different user should be allowed")
|
|
|
|
t.Log("Rate limiter: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-111: Markdown 渲染冒烟测试
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_MarkdownRender(t *testing.T) {
|
|
// Test basic card with markdown rendering
|
|
card := core.NewCard().
|
|
Title("Test Card", "blue").
|
|
Markdown("## Heading\nThis is **bold** and *italic* text.").
|
|
Markdown("- Item 1\n- Item 2\n- Item 3").
|
|
Markdown("1. Numbered item\n2. Another item").
|
|
Markdown("> Blockquote text").
|
|
Markdown("`inline code` and ```code block```").
|
|
Build()
|
|
|
|
// Verify card structure
|
|
assert.NotNil(t, card)
|
|
assert.NotNil(t, card.Header)
|
|
assert.Equal(t, "Test Card", card.Header.Title)
|
|
assert.Equal(t, "blue", card.Header.Color)
|
|
|
|
// Count markdown elements (should be 5)
|
|
var markdownCount int
|
|
for _, elem := range card.Elements {
|
|
if _, ok := elem.(core.CardMarkdown); ok {
|
|
markdownCount++
|
|
}
|
|
}
|
|
assert.Equal(t, 5, markdownCount, "should have 5 markdown elements")
|
|
|
|
// Test text fallback rendering
|
|
text := card.RenderText()
|
|
assert.Contains(t, text, "Test Card")
|
|
assert.Contains(t, text, "Heading")
|
|
assert.Contains(t, text, "bold")
|
|
assert.Contains(t, text, "italic")
|
|
assert.Contains(t, text, "Item 1")
|
|
assert.Contains(t, text, "Blockquote")
|
|
assert.Contains(t, text, "inline code")
|
|
|
|
// Test card with only divider
|
|
card2 := core.NewCard().
|
|
Markdown("Before divider").
|
|
Divider().
|
|
Markdown("After divider").
|
|
Build()
|
|
|
|
text2 := card2.RenderText()
|
|
assert.Contains(t, text2, "Before divider")
|
|
assert.Contains(t, text2, "---")
|
|
assert.Contains(t, text2, "After divider")
|
|
|
|
// Test card with select element
|
|
card3 := core.NewCard().
|
|
Title("Select Card", "green").
|
|
Select("Choose an option", []core.CardSelectOption{
|
|
{Text: "Option A", Value: "a"},
|
|
{Text: "Option B", Value: "b"},
|
|
}, "a").
|
|
Build()
|
|
|
|
text3 := card3.RenderText()
|
|
assert.Contains(t, text3, "Choose an option")
|
|
assert.Contains(t, text3, "Option A")
|
|
assert.Contains(t, text3, "Option B")
|
|
|
|
// Test HasButtons
|
|
assert.False(t, card.HasButtons())
|
|
assert.True(t, core.NewCard().Buttons(core.DefaultBtn("Test", "act:/test")).Build().HasButtons())
|
|
|
|
t.Log("Markdown render: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-112: Webhook 注册和回调冒烟测试
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestSmoke_WebhookCallback(t *testing.T) {
|
|
// Test webhook callback data structure
|
|
type webhookCallback struct {
|
|
Action string `json:"action"`
|
|
SessionID string `json:"session_id"`
|
|
Data map[string]string `json:"data"`
|
|
}
|
|
|
|
// Simulate callback parsing
|
|
callbackData := `{"action":"callback","session_id":"test-001","data":{"button":"confirm"}}`
|
|
|
|
// Verify callback structure can be marshaled
|
|
cb := struct {
|
|
Action string `json:"action"`
|
|
SessionID string `json:"session_id"`
|
|
Data map[string]string `json:"data"`
|
|
}{
|
|
Action: "callback",
|
|
SessionID: "test-001",
|
|
Data: map[string]string{"button": "confirm"},
|
|
}
|
|
|
|
assert.Equal(t, "callback", cb.Action)
|
|
assert.Equal(t, "test-001", cb.SessionID)
|
|
assert.Equal(t, "confirm", cb.Data["button"])
|
|
|
|
// Verify the raw data can be parsed
|
|
assert.Contains(t, callbackData, "callback")
|
|
assert.Contains(t, callbackData, "test-001")
|
|
|
|
// Test action routing
|
|
actions := []string{"callback", "act:/confirm", "act:/cancel", "act:/delete"}
|
|
for _, action := range actions {
|
|
assert.NotEmpty(t, action)
|
|
// Action should be either callback or act:/ prefix
|
|
isValid := action == "callback" || strings.HasPrefix(action, "act:/")
|
|
assert.True(t, isValid, "action %s should be valid", action)
|
|
}
|
|
|
|
// Use the callbackData to avoid compiler warning
|
|
assert.NotEmpty(t, callbackData)
|
|
|
|
t.Log("Webhook callback: PASS")
|
|
}
|