mirror of
https://github.com/chenhg5/cc-connect.git
synced 2026-07-03 12:28:10 +08:00
- Add 13 integration tests (T-300~T-313) covering engine-platform message flow, multi-agent coordination, session persistence, rate limiting, dedup, command registry, role manager, platform reply, agent permission flow, cron store, card rendering, env merge, and message attachments - Add 14 performance benchmarks (T-400~T-413) for message latency, concurrent throughput, session switching, rate limiting, deduplication, command lookup, card rendering, cron store, role resolution, and multi-agent coordination - Fix FakeAgentSession so NewFakeAgentWithSession properly preserves pre-configured session for first StartSession call - Add performance-test Makefile target and update test-release - Update CI to run performance benchmarks on release events Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
498 lines
14 KiB
Go
498 lines
14 KiB
Go
//go:build integration
|
|
|
|
// Package integration contains integration tests for cc-connect.
|
|
// These tests verify component interactions and require specific setup.
|
|
// Run with: go test -tags=integration ./tests/integration/...
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"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-300: Engine + Platform Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_EnginePlatformMessageFlow(t *testing.T) {
|
|
// This test verifies the message flow between platform and engine
|
|
// using mock components to simulate real behavior
|
|
|
|
// Create mock platform
|
|
platform := new(mocks.MockPlatform)
|
|
platform.On("Name").Return("test-integration")
|
|
platform.On("Start", mock.Anything).Return(nil)
|
|
platform.On("Stop").Return(nil)
|
|
|
|
// Create message handler to capture messages
|
|
var receivedMessages []*core.Message
|
|
var mu sync.Mutex
|
|
|
|
handler := func(p core.Platform, msg *core.Message) {
|
|
mu.Lock()
|
|
receivedMessages = append(receivedMessages, msg)
|
|
mu.Unlock()
|
|
}
|
|
|
|
// Start platform
|
|
err := platform.Start(handler)
|
|
require.NoError(t, err)
|
|
|
|
// Simulate platform receiving a message
|
|
testMsg := fake.TestMessageWithContent("Integration test message")
|
|
handler(platform, testMsg)
|
|
|
|
// Verify message was captured
|
|
mu.Lock()
|
|
require.Len(t, receivedMessages, 1)
|
|
assert.Equal(t, "Integration test message", receivedMessages[0].Content)
|
|
mu.Unlock()
|
|
|
|
// Stop platform
|
|
platform.Stop()
|
|
|
|
t.Log("Engine-Platform message flow: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-301: Multi-Agent Session Coordination
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_MultiAgentSessionCoordination(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create fake agents
|
|
agent1 := fake.NewFakeAgent("agent-1")
|
|
agent2 := fake.NewFakeAgent("agent-2")
|
|
|
|
// Start sessions on both agents
|
|
sess1, err := agent1.StartSession(ctx, "coord-session-1")
|
|
require.NoError(t, err)
|
|
|
|
sess2, err := agent2.StartSession(ctx, "coord-session-2")
|
|
require.NoError(t, err)
|
|
|
|
// Verify both sessions are independent
|
|
assert.NotEqual(t, sess1.CurrentSessionID(), sess2.CurrentSessionID())
|
|
assert.True(t, sess1.Alive())
|
|
assert.True(t, sess2.Alive())
|
|
|
|
// Send message to agent 1
|
|
err = sess1.Send("Message to agent 1", nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Send message to agent 2
|
|
err = sess2.Send("Message to agent 2", nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify both agents received their messages
|
|
prompts1 := agent1.GetSession().GetPrompts()
|
|
prompts2 := agent2.GetSession().GetPrompts()
|
|
|
|
assert.Len(t, prompts1, 1)
|
|
assert.Contains(t, prompts1[0], "Message to agent 1")
|
|
assert.Len(t, prompts2, 1)
|
|
assert.Contains(t, prompts2[0], "Message to agent 2")
|
|
|
|
t.Log("Multi-agent session coordination: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-302: Session Persistence Simulation
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_SessionPersistenceSimulation(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
agent := fake.NewFakeAgent("persist-agent")
|
|
|
|
// Start session
|
|
sess1, err := agent.StartSession(ctx, "persist-session")
|
|
require.NoError(t, err)
|
|
originalSessionID := sess1.CurrentSessionID()
|
|
|
|
// Simulate session close (as would happen on restart)
|
|
err = sess1.Close()
|
|
require.NoError(t, err)
|
|
|
|
// Simulate new session with same ID (as would happen on restore)
|
|
sess2, err := agent.StartSession(ctx, "persist-session")
|
|
require.NoError(t, err)
|
|
restoredSessionID := sess2.CurrentSessionID()
|
|
|
|
// Verify session was "restored"
|
|
assert.Equal(t, originalSessionID, restoredSessionID)
|
|
assert.True(t, sess2.Alive())
|
|
|
|
t.Log("Session persistence simulation: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-303: Rate Limiter Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_RateLimiterIntegration(t *testing.T) {
|
|
// Create rate limiter: 2 messages per second
|
|
rl := core.NewRateLimiter(2, time.Second)
|
|
defer rl.Stop()
|
|
|
|
// First two should succeed
|
|
assert.True(t, rl.Allow("user-rate"))
|
|
assert.True(t, rl.Allow("user-rate"))
|
|
|
|
// Third should fail
|
|
assert.False(t, rl.Allow("user-rate"))
|
|
|
|
// Different user should succeed
|
|
assert.True(t, rl.Allow("other-user"))
|
|
|
|
// Wait for window to reset
|
|
time.Sleep(1100 * time.Millisecond)
|
|
|
|
// Should be allowed again
|
|
assert.True(t, rl.Allow("user-rate"))
|
|
|
|
t.Log("Rate limiter integration: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-304: Message Dedup Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_MessageDedupIntegration(t *testing.T) {
|
|
dedup := &core.MessageDedup{}
|
|
|
|
// Process a message
|
|
msgID := "dedup-test-msg-001"
|
|
isDup := dedup.IsDuplicate(msgID)
|
|
assert.False(t, isDup, "first occurrence should not be duplicate")
|
|
|
|
// Same message again should be duplicate
|
|
isDup = dedup.IsDuplicate(msgID)
|
|
assert.True(t, isDup, "second occurrence should be duplicate")
|
|
|
|
// Different message should not be duplicate
|
|
isDup = dedup.IsDuplicate("different-msg")
|
|
assert.False(t, isDup)
|
|
|
|
// Empty should never be duplicate
|
|
isDup = dedup.IsDuplicate("")
|
|
assert.False(t, isDup)
|
|
|
|
t.Log("Message dedup integration: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-305: Command Registry Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_CommandRegistryIntegration(t *testing.T) {
|
|
registry := core.NewCommandRegistry()
|
|
|
|
// Add multiple commands
|
|
registry.Add("start", "Start the process", "Starting...", "", "", "builtin")
|
|
registry.Add("stop", "Stop the process", "Stopping...", "", "", "builtin")
|
|
registry.Add("status", "Check status", "Status: running", "", "", "builtin")
|
|
|
|
// Verify all commands are registered
|
|
commands := registry.ListAll()
|
|
assert.Len(t, commands, 3)
|
|
|
|
// Resolve each command
|
|
cmd, ok := registry.Resolve("start")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "start", cmd.Name)
|
|
|
|
cmd, ok = registry.Resolve("stop")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "stop", cmd.Name)
|
|
|
|
cmd, ok = registry.Resolve("status")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "status", cmd.Name)
|
|
|
|
// Hyphen/underscore normalization
|
|
registry.Add("my-cmd", "My command", "Running...", "", "", "builtin")
|
|
cmd, ok = registry.Resolve("my_cmd") // Telegram sanitizes hyphens to underscores
|
|
require.True(t, ok)
|
|
assert.Equal(t, "my-cmd", cmd.Name)
|
|
|
|
t.Log("Command registry integration: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-306: Role Manager Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_RoleManagerIntegration(t *testing.T) {
|
|
manager := core.NewUserRoleManager()
|
|
|
|
manager.Configure("viewer", []core.RoleInput{
|
|
{
|
|
Name: "admin",
|
|
UserIDs: []string{"admin1", "admin2"},
|
|
},
|
|
{
|
|
Name: "developer",
|
|
UserIDs: []string{"dev1", "dev2"},
|
|
},
|
|
{
|
|
Name: "viewer",
|
|
UserIDs: []string{"*"}, // wildcard - everyone else
|
|
},
|
|
})
|
|
|
|
// Admin users
|
|
assert.Equal(t, "admin", manager.ResolveRole("admin1").Name)
|
|
assert.Equal(t, "admin", manager.ResolveRole("admin2").Name)
|
|
|
|
// Developer users
|
|
assert.Equal(t, "developer", manager.ResolveRole("dev1").Name)
|
|
assert.Equal(t, "developer", manager.ResolveRole("dev2").Name)
|
|
|
|
// Unknown user gets viewer (wildcard)
|
|
assert.Equal(t, "viewer", manager.ResolveRole("random-user").Name)
|
|
|
|
t.Log("Role manager integration: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-307: Platform Reply Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_PlatformReplyIntegration(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
platform := new(mocks.MockPlatform)
|
|
|
|
// Expect specific reply
|
|
expectedContent := "This is a reply from the platform"
|
|
platform.On("Reply", ctx, mock.Anything, expectedContent).Return(nil)
|
|
|
|
// Simulate engine sending reply
|
|
err := platform.Reply(ctx, nil, expectedContent)
|
|
require.NoError(t, err)
|
|
|
|
// Verify reply was called correctly
|
|
platform.AssertExpectations(t)
|
|
|
|
t.Log("Platform reply integration: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-308: Agent Permission Flow
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_AgentPermissionFlow(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create mock agent session
|
|
session := fake.NewFakeAgentSession("perm-session")
|
|
session.AddPermissionRequest("req-001", "Bash", "rm -rf /")
|
|
session.AddResultEvent("Request handled")
|
|
|
|
agent := fake.NewFakeAgentWithSession("perm-agent", "perm-session", session)
|
|
|
|
// Start session
|
|
sess, err := agent.StartSession(ctx, "perm-session")
|
|
require.NoError(t, err)
|
|
|
|
// Send message
|
|
err = sess.Send("Run dangerous command", nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Collect events
|
|
var events []core.Event
|
|
for e := range sess.Events() {
|
|
events = append(events, e)
|
|
if e.Done {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Should have permission request and result
|
|
assert.GreaterOrEqual(t, len(events), 2)
|
|
|
|
// Find permission request event
|
|
var permRequest core.Event
|
|
for _, e := range events {
|
|
if e.Type == core.EventPermissionRequest {
|
|
permRequest = e
|
|
break
|
|
}
|
|
}
|
|
assert.Equal(t, "Bash", permRequest.ToolName)
|
|
assert.Equal(t, "req-001", permRequest.RequestID)
|
|
|
|
t.Log("Agent permission flow: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-310: Cron Store Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_CronStoreIntegration(t *testing.T) {
|
|
store, err := core.NewCronStore(t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
// Add cron job
|
|
job := &core.CronJob{
|
|
ID: "integration-cron",
|
|
Description: "Integration test cron",
|
|
Prompt: "Run test",
|
|
CronExpr: "0 9 * * *",
|
|
Enabled: true,
|
|
}
|
|
err = store.Add(job)
|
|
require.NoError(t, err)
|
|
|
|
// List jobs
|
|
jobs := store.List()
|
|
assert.Len(t, jobs, 1)
|
|
|
|
// Disable job
|
|
ok := store.SetEnabled("integration-cron", false)
|
|
assert.True(t, ok)
|
|
|
|
// Verify disabled
|
|
job = store.Get("integration-cron")
|
|
assert.False(t, job.Enabled)
|
|
|
|
// Toggle mute
|
|
store.SetMute("integration-cron", true)
|
|
muted, ok := store.ToggleMute("integration-cron")
|
|
assert.True(t, ok)
|
|
assert.False(t, muted) // toggled from true to false
|
|
|
|
// Remove job
|
|
ok = store.Remove("integration-cron")
|
|
assert.True(t, ok)
|
|
|
|
jobs = store.List()
|
|
assert.Empty(t, jobs)
|
|
|
|
t.Log("Cron store integration: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-311: Card Rendering Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_CardRenderingIntegration(t *testing.T) {
|
|
// Create a complex card
|
|
card := core.NewCard().
|
|
Title("Integration Test Card", "green").
|
|
Markdown("## Welcome\nThis is a **test** card.").
|
|
Markdown("- Item 1\n- Item 2\n- Item 3").
|
|
Divider().
|
|
Buttons(
|
|
core.PrimaryBtn("Confirm", "act:/confirm"),
|
|
core.DefaultBtn("Cancel", "act:/cancel"),
|
|
core.DangerBtn("Delete", "act:/delete"),
|
|
).
|
|
Note("This is a footnote").
|
|
Build()
|
|
|
|
// Verify card structure
|
|
assert.NotNil(t, card)
|
|
assert.NotNil(t, card.Header)
|
|
assert.Equal(t, "Integration Test Card", card.Header.Title)
|
|
assert.Equal(t, "green", card.Header.Color)
|
|
assert.GreaterOrEqual(t, len(card.Elements), 5) // markdown + markdown + divider + buttons + note
|
|
|
|
// Verify text fallback
|
|
text := card.RenderText()
|
|
assert.Contains(t, text, "Integration Test Card")
|
|
assert.Contains(t, text, "Welcome")
|
|
assert.Contains(t, text, "Confirm")
|
|
assert.Contains(t, text, "Delete")
|
|
|
|
// Verify button collection
|
|
buttons := card.CollectButtons()
|
|
assert.Len(t, buttons, 1)
|
|
assert.Len(t, buttons[0], 3)
|
|
|
|
// Verify HasButtons
|
|
assert.True(t, card.HasButtons())
|
|
|
|
t.Log("Card rendering integration: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-312: Env Merge Integration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_EnvMergeIntegration(t *testing.T) {
|
|
base := []string{"PATH=/usr/bin", "HOME=/home/user", "VAR=value"}
|
|
|
|
// Merge with overlapping keys
|
|
extra := []string{"VAR=newvalue", "ADD=new", "HOME=/different"}
|
|
merged := core.MergeEnv(base, extra)
|
|
|
|
// Should have: PATH (from base), HOME (overridden), VAR (overridden), ADD (from extra)
|
|
assert.Len(t, merged, 4)
|
|
|
|
// Find VAR - should be new value
|
|
for _, env := range merged {
|
|
if len(env) > 3 && env[:3] == "VAR" {
|
|
assert.Equal(t, "VAR=newvalue", env)
|
|
break
|
|
}
|
|
}
|
|
|
|
// Find ADD - should be new
|
|
found := false
|
|
for _, env := range merged {
|
|
if len(env) > 3 && env[:3] == "ADD" {
|
|
assert.Equal(t, "ADD=new", env)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, found)
|
|
|
|
t.Log("Env merge integration: PASS")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// T-313: Message Attachment Handling
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func TestIntegration_MessageAttachmentHandling(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Create session
|
|
session := fake.NewFakeAgentSession("attach-session")
|
|
agent := fake.NewFakeAgentWithSession("attach-agent", "attach-session", session)
|
|
|
|
sess, err := agent.StartSession(ctx, "attach-session")
|
|
require.NoError(t, err)
|
|
|
|
// Create message with attachments
|
|
images := []core.ImageAttachment{
|
|
{MimeType: "image/png", Data: []byte("fake png data"), FileName: "screenshot.png"},
|
|
}
|
|
files := []core.FileAttachment{
|
|
{MimeType: "application/pdf", Data: []byte("fake pdf data"), FileName: "doc.pdf"},
|
|
}
|
|
|
|
// Send with attachments
|
|
err = sess.Send("Analyze this", images, files)
|
|
require.NoError(t, err)
|
|
|
|
// Verify prompts captured
|
|
prompts := session.GetPrompts()
|
|
assert.Len(t, prompts, 1)
|
|
|
|
t.Log("Message attachment handling: PASS")
|
|
}
|