251 lines
8.3 KiB
Go
251 lines
8.3 KiB
Go
// Package integration provides end-to-end integration tests for the Kandev backend.
|
|
// This file tests that agent thinking/reasoning events are properly saved to the database.
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/kandev/kandev/internal/agent/runtime/lifecycle"
|
|
"github.com/kandev/kandev/internal/events"
|
|
"github.com/kandev/kandev/internal/events/bus"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
)
|
|
|
|
// TestThinkingEventsPersistToDatabase verifies that agent thinking/reasoning events
|
|
// are properly saved to the database through the full orchestrator pipeline.
|
|
//
|
|
// This tests the flow:
|
|
// 1. Lifecycle manager receives "reasoning" event from agentctl
|
|
// 2. Manager buffers content and publishes "thinking_streaming" event
|
|
// 3. Orchestrator handles event and calls messageCreator.CreateThinkingMessageStreaming
|
|
// 4. Task service creates message with type="thinking" in database
|
|
func TestThinkingEventsPersistToDatabase(t *testing.T) {
|
|
ts := NewOrchestratorTestServer(t)
|
|
defer ts.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create a task with session
|
|
taskID := ts.CreateTestTask(t, "test-agent", 1)
|
|
|
|
// Create a session for the task using the repository directly
|
|
sessionID := uuid.New().String()
|
|
session := &models.TaskSession{
|
|
ID: sessionID,
|
|
TaskID: taskID,
|
|
AgentProfileID: "test-agent",
|
|
State: models.TaskSessionStateRunning,
|
|
StartedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err := ts.TaskRepo.CreateTaskSession(ctx, session)
|
|
require.NoError(t, err, "failed to create session")
|
|
|
|
// Generate IDs for the test
|
|
thinkingID := uuid.New().String()
|
|
thinkingContent := "The user wants to list the files in the current directory.\nI'll use the view tool with type \"directory\" to show the contents of the workspace."
|
|
|
|
// Simulate a thinking_streaming event being published
|
|
// This is what the lifecycle manager would publish after receiving a "reasoning" event
|
|
eventData := &lifecycle.AgentStreamEventPayload{
|
|
Type: "agent/event",
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
TaskID: taskID,
|
|
SessionID: sessionID,
|
|
Data: &lifecycle.AgentStreamEventData{
|
|
Type: "thinking_streaming",
|
|
Text: thinkingContent,
|
|
MessageID: thinkingID,
|
|
IsAppend: false, // First chunk - creates new message
|
|
MessageType: "thinking",
|
|
},
|
|
}
|
|
|
|
// Publish the event to the event bus (simulating what lifecycle manager does)
|
|
subject := events.BuildAgentStreamSubject(taskID)
|
|
event := bus.NewEvent(events.AgentStream, "test", eventData)
|
|
err = ts.EventBus.Publish(ctx, subject, event)
|
|
require.NoError(t, err, "failed to publish thinking event")
|
|
|
|
// Wait for event to be processed
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
// Check that the thinking message was saved to the database
|
|
messages, err := ts.TaskSvc.ListMessages(ctx, sessionID)
|
|
require.NoError(t, err, "failed to get messages")
|
|
|
|
// Find the thinking message
|
|
var foundThinking bool
|
|
for _, msg := range messages {
|
|
t.Logf("Message: ID=%s, Type=%s, AuthorType=%s", msg.ID, msg.Type, msg.AuthorType)
|
|
if msg.Type == "thinking" {
|
|
foundThinking = true
|
|
assert.Equal(t, thinkingID, msg.ID, "thinking message ID should match")
|
|
assert.Equal(t, models.MessageAuthorAgent, msg.AuthorType, "thinking message author should be agent")
|
|
// Check metadata contains thinking content
|
|
thinking, hasThinking := msg.Metadata["thinking"]
|
|
assert.True(t, hasThinking, "thinking message should have 'thinking' in metadata")
|
|
assert.Equal(t, thinkingContent, thinking, "thinking content should match")
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.True(t, foundThinking, "should find a thinking message in the database")
|
|
}
|
|
|
|
// TestThinkingEventAppend verifies that appending to thinking messages works correctly.
|
|
func TestThinkingEventAppend(t *testing.T) {
|
|
ts := NewOrchestratorTestServer(t)
|
|
defer ts.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create a task with session
|
|
taskID := ts.CreateTestTask(t, "test-agent", 1)
|
|
|
|
// Create a session for the task using the repository directly
|
|
sessionID := uuid.New().String()
|
|
session := &models.TaskSession{
|
|
ID: sessionID,
|
|
TaskID: taskID,
|
|
AgentProfileID: "test-agent",
|
|
State: models.TaskSessionStateRunning,
|
|
StartedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err := ts.TaskRepo.CreateTaskSession(ctx, session)
|
|
require.NoError(t, err)
|
|
|
|
thinkingID := uuid.New().String()
|
|
chunk1 := "First chunk of thinking content.\n"
|
|
chunk2 := "Second chunk with more reasoning."
|
|
|
|
// First chunk - creates the message
|
|
event1 := &lifecycle.AgentStreamEventPayload{
|
|
Type: "agent/event",
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
TaskID: taskID,
|
|
SessionID: sessionID,
|
|
Data: &lifecycle.AgentStreamEventData{
|
|
Type: "thinking_streaming",
|
|
Text: chunk1,
|
|
MessageID: thinkingID,
|
|
IsAppend: false,
|
|
MessageType: "thinking",
|
|
},
|
|
}
|
|
|
|
subject := events.BuildAgentStreamSubject(taskID)
|
|
err = ts.EventBus.Publish(ctx, subject, bus.NewEvent(events.AgentStream, "test", event1))
|
|
require.NoError(t, err)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Second chunk - appends to existing message
|
|
event2 := &lifecycle.AgentStreamEventPayload{
|
|
Type: "agent/event",
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
TaskID: taskID,
|
|
SessionID: sessionID,
|
|
Data: &lifecycle.AgentStreamEventData{
|
|
Type: "thinking_streaming",
|
|
Text: chunk2,
|
|
MessageID: thinkingID,
|
|
IsAppend: true,
|
|
MessageType: "thinking",
|
|
},
|
|
}
|
|
|
|
err = ts.EventBus.Publish(ctx, subject, bus.NewEvent(events.AgentStream, "test", event2))
|
|
require.NoError(t, err)
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Verify the message contains both chunks
|
|
messages, err := ts.TaskSvc.ListMessages(ctx, sessionID)
|
|
require.NoError(t, err)
|
|
|
|
var foundThinking bool
|
|
for _, msg := range messages {
|
|
if msg.Type == "thinking" && msg.ID == thinkingID {
|
|
foundThinking = true
|
|
thinking := msg.Metadata["thinking"].(string)
|
|
assert.Contains(t, thinking, chunk1, "should contain first chunk")
|
|
assert.Contains(t, thinking, chunk2, "should contain second chunk")
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.True(t, foundThinking, "should find thinking message with both chunks")
|
|
}
|
|
|
|
// TestCodexReasoningSummaryEvents verifies that Codex-style reasoning events
|
|
// (which use ReasoningSummary instead of ReasoningText) are properly handled.
|
|
// Codex sends item/reasoning/summaryTextDelta which sets ReasoningSummary.
|
|
func TestCodexReasoningSummaryEvents(t *testing.T) {
|
|
ts := NewOrchestratorTestServer(t)
|
|
defer ts.Close()
|
|
|
|
ctx := context.Background()
|
|
|
|
// Create a task with session
|
|
taskID := ts.CreateTestTask(t, "test-agent", 1)
|
|
|
|
sessionID := uuid.New().String()
|
|
session := &models.TaskSession{
|
|
ID: sessionID,
|
|
TaskID: taskID,
|
|
AgentProfileID: "test-agent",
|
|
State: models.TaskSessionStateRunning,
|
|
StartedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
err := ts.TaskRepo.CreateTaskSession(ctx, session)
|
|
require.NoError(t, err)
|
|
|
|
thinkingID := uuid.New().String()
|
|
// Codex sends reasoning summaries like this
|
|
summaryContent := "**Processing simple calculation**\nUser asks for a calculator response."
|
|
|
|
// Simulate a thinking_streaming event with content from ReasoningSummary
|
|
// This is what would happen after lifecycle manager processes Codex's reasoning
|
|
eventData := &lifecycle.AgentStreamEventPayload{
|
|
Type: "agent/event",
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
|
|
TaskID: taskID,
|
|
SessionID: sessionID,
|
|
Data: &lifecycle.AgentStreamEventData{
|
|
Type: "thinking_streaming",
|
|
Text: summaryContent,
|
|
MessageID: thinkingID,
|
|
IsAppend: false,
|
|
MessageType: "thinking",
|
|
},
|
|
}
|
|
|
|
subject := events.BuildAgentStreamSubject(taskID)
|
|
err = ts.EventBus.Publish(ctx, subject, bus.NewEvent(events.AgentStream, "test", eventData))
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
messages, err := ts.TaskSvc.ListMessages(ctx, sessionID)
|
|
require.NoError(t, err)
|
|
|
|
var foundThinking bool
|
|
for _, msg := range messages {
|
|
if msg.Type == "thinking" && msg.ID == thinkingID {
|
|
foundThinking = true
|
|
thinking := msg.Metadata["thinking"].(string)
|
|
assert.Equal(t, summaryContent, thinking, "thinking content should match Codex summary")
|
|
break
|
|
}
|
|
}
|
|
|
|
assert.True(t, foundThinking, "should find thinking message from Codex-style event")
|
|
}
|