| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- package main
- import (
- "context"
- "encoding/json"
- "strings"
- "testing"
- "time"
- "github.com/sashabaranov/go-openai"
- )
- // TestAgent_Initialize tests agent initialization
- func TestAgent_Initialize(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{
- {
- Name: "introspect",
- Description: "Discover the GraphQL schema",
- InputSchema: InputSchema{
- Type: "object",
- Properties: map[string]Property{},
- AdditionalProperties: false,
- },
- },
- {
- Name: "query",
- Description: "Execute a GraphQL query",
- InputSchema: InputSchema{
- Type: "object",
- Properties: map[string]Property{
- "query": {Type: "string", Description: "The GraphQL query"},
- },
- Required: []string{"query"},
- AdditionalProperties: false,
- },
- },
- })
- mockLLM := NewMockLLM(nil)
- agent := NewTestAgent(mockLLM, mockMCP)
- err := agent.Initialize()
- if err != nil {
- t.Fatalf("Initialize failed: %v", err)
- }
- if len(agent.tools) != 2 {
- t.Errorf("Expected 2 tools, got %d", len(agent.tools))
- }
- // Verify tools were converted correctly
- toolNames := make([]string, len(agent.tools))
- for i, tool := range agent.tools {
- toolNames[i] = tool.Function.Name
- }
- expectedNames := []string{"introspect", "query"}
- for i, expected := range expectedNames {
- if toolNames[i] != expected {
- t.Errorf("Tool %d: expected name %s, got %s", i, expected, toolNames[i])
- }
- }
- }
- // TestAgent_ProcessEvent tests event processing
- func TestAgent_ProcessEvent(t *testing.T) {
- ctx := context.Background()
- t.Run("TaskCreatedEvent", func(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{
- {Name: "query", Description: "Execute a GraphQL query", InputSchema: InputSchema{Type: "object"}},
- })
- mockMCP.SetToolResult("query", &CallToolResult{
- Content: []ContentBlock{{Type: "text", Text: `{"data": {"tasks": []}}`}},
- })
- // Mock LLM that makes a tool call then responds
- mockLLM := NewMockLLM([]*openai.ChatCompletionMessage{
- {
- Role: openai.ChatMessageRoleAssistant,
- ToolCalls: []openai.ToolCall{
- {
- ID: "call-1",
- Function: openai.FunctionCall{
- Name: "query",
- Arguments: `{"query": "{ tasks { id title } }"}`,
- },
- },
- },
- },
- {
- Role: openai.ChatMessageRoleAssistant,
- Content: "I've processed the task created event.",
- },
- })
- agent := NewTestAgent(mockLLM, mockMCP)
- agent.Initialize()
- eventData := json.RawMessage(`{"taskId": "task-123", "title": "New Task"}`)
- err := agent.ProcessEvent(ctx, "graphql://subscription/taskCreated", eventData)
- if err != nil {
- t.Errorf("ProcessEvent failed: %v", err)
- }
- })
- t.Run("MessageAddedEvent", func(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{
- {Name: "query", Description: "Execute a GraphQL query", InputSchema: InputSchema{Type: "object"}},
- })
- // Mock LLM that responds directly without tool calls
- mockLLM := NewMockLLM([]*openai.ChatCompletionMessage{
- {
- Role: openai.ChatMessageRoleAssistant,
- Content: "I received the message added event.",
- },
- })
- agent := NewTestAgent(mockLLM, mockMCP)
- agent.Initialize()
- eventData := json.RawMessage(`{"messageId": "msg-456", "content": "Hello!"}`)
- err := agent.ProcessEvent(ctx, "graphql://subscription/messageAdded", eventData)
- if err != nil {
- t.Errorf("ProcessEvent failed: %v", err)
- }
- })
- }
- // TestAgent_Run tests the interactive Run method
- func TestAgent_Run(t *testing.T) {
- ctx := context.Background()
- t.Run("SimpleResponse", func(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{})
- mockLLM := NewMockLLM([]*openai.ChatCompletionMessage{
- {
- Role: openai.ChatMessageRoleAssistant,
- Content: "Hello! How can I help you?",
- },
- })
- agent := NewTestAgent(mockLLM, mockMCP)
- agent.Initialize()
- response, err := agent.Run(ctx, "Hello")
- if err != nil {
- t.Fatalf("Run failed: %v", err)
- }
- if response != "Hello! How can I help you?" {
- t.Errorf("Expected 'Hello! How can I help you?', got '%s'", response)
- }
- })
- t.Run("WithToolCall", func(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{
- {Name: "introspect", Description: "Introspect schema", InputSchema: InputSchema{Type: "object"}},
- })
- mockMCP.SetToolResult("introspect", &CallToolResult{
- Content: []ContentBlock{{Type: "text", Text: "Schema: Query, Mutation, Subscription"}},
- })
- mockLLM := NewMockLLM([]*openai.ChatCompletionMessage{
- {
- Role: openai.ChatMessageRoleAssistant,
- ToolCalls: []openai.ToolCall{
- {
- ID: "call-1",
- Function: openai.FunctionCall{
- Name: "introspect",
- Arguments: `{}`,
- },
- },
- },
- },
- {
- Role: openai.ChatMessageRoleAssistant,
- Content: "The schema has Query, Mutation, and Subscription types.",
- },
- })
- agent := NewTestAgent(mockLLM, mockMCP)
- agent.Initialize()
- response, err := agent.Run(ctx, "What types are in the schema?")
- if err != nil {
- t.Fatalf("Run failed: %v", err)
- }
- if response != "The schema has Query, Mutation, and Subscription types." {
- t.Errorf("Unexpected response: %s", response)
- }
- })
- }
- // TestAgent_BuildEventPrompt tests event prompt building
- func TestAgent_BuildEventPrompt(t *testing.T) {
- tests := []struct {
- name string
- uri string
- eventData json.RawMessage
- wantType string
- }{
- {
- name: "TaskCreated",
- uri: "graphql://subscription/taskCreated",
- eventData: json.RawMessage(`{"id": "1"}`),
- wantType: "task created",
- },
- {
- name: "TaskUpdated",
- uri: "graphql://subscription/taskUpdated",
- eventData: json.RawMessage(`{"id": "2"}`),
- wantType: "task updated",
- },
- {
- name: "TaskDeleted",
- uri: "graphql://subscription/taskDeleted",
- eventData: json.RawMessage(`{"id": "3"}`),
- wantType: "task deleted",
- },
- {
- name: "MessageAdded",
- uri: "graphql://subscription/messageAdded",
- eventData: json.RawMessage(`{"id": "4"}`),
- wantType: "message added",
- },
- {
- name: "UnknownEvent",
- uri: "graphql://subscription/unknown",
- eventData: json.RawMessage(`{}`),
- wantType: "unknown",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- prompt := buildTestEventPrompt(tt.uri, tt.eventData)
- if !strings.Contains(prompt, tt.wantType) {
- t.Errorf("Expected prompt to contain '%s', got: %s", tt.wantType, prompt)
- }
- if !strings.Contains(prompt, tt.uri) {
- t.Errorf("Expected prompt to contain URI '%s', got: %s", tt.uri, prompt)
- }
- })
- }
- }
- // TestAgent_ToolNames tests the toolNames helper function
- func TestAgent_ToolNames(t *testing.T) {
- tools := []Tool{
- {Name: "introspect"},
- {Name: "query"},
- {Name: "mutate"},
- }
- names := toolNames(tools)
- expected := []string{"introspect", "query", "mutate"}
- if len(names) != len(expected) {
- t.Errorf("Expected %d names, got %d", len(expected), len(names))
- }
- for i, name := range names {
- if name != expected[i] {
- t.Errorf("Name %d: expected %s, got %s", i, expected[i], name)
- }
- }
- }
- // TestEventQueue tests the EventQueue operations
- func TestEventQueue(t *testing.T) {
- t.Run("TryEnqueueSuccess", func(t *testing.T) {
- queue := NewEventQueue("test", 10)
- event := &QueuedEvent{
- URI: "test://uri",
- Data: json.RawMessage(`{"test": "data"}`),
- Timestamp: time.Now(),
- }
- success := queue.TryEnqueue(event)
- if !success {
- t.Error("Expected TryEnqueue to succeed")
- }
- if queue.Len() != 1 {
- t.Errorf("Expected queue length 1, got %d", queue.Len())
- }
- })
- t.Run("TryEnqueueFullQueue", func(t *testing.T) {
- queue := NewEventQueue("test", 2)
- // Fill the queue
- for i := 0; i < 2; i++ {
- success := queue.TryEnqueue(&QueuedEvent{URI: "test://uri"})
- if !success {
- t.Errorf("Expected TryEnqueue %d to succeed", i)
- }
- }
- // Try to add one more - should fail
- success := queue.TryEnqueue(&QueuedEvent{URI: "test://overflow"})
- if success {
- t.Error("Expected TryEnqueue to fail on full queue")
- }
- if queue.Len() != 2 {
- t.Errorf("Expected queue length 2, got %d", queue.Len())
- }
- })
- t.Run("Dequeue", func(t *testing.T) {
- queue := NewEventQueue("test", 10)
- event1 := &QueuedEvent{URI: "test://uri1"}
- event2 := &QueuedEvent{URI: "test://uri2"}
- queue.TryEnqueue(event1)
- queue.TryEnqueue(event2)
- // Dequeue should return events in FIFO order
- dequeued1 := queue.Dequeue()
- if dequeued1.URI != "test://uri1" {
- t.Errorf("Expected URI 'test://uri1', got '%s'", dequeued1.URI)
- }
- dequeued2 := queue.Dequeue()
- if dequeued2.URI != "test://uri2" {
- t.Errorf("Expected URI 'test://uri2', got '%s'", dequeued2.URI)
- }
- })
- t.Run("Len", func(t *testing.T) {
- queue := NewEventQueue("test", 10)
- if queue.Len() != 0 {
- t.Errorf("Expected empty queue to have length 0, got %d", queue.Len())
- }
- queue.TryEnqueue(&QueuedEvent{URI: "test://uri1"})
- if queue.Len() != 1 {
- t.Errorf("Expected queue length 1, got %d", queue.Len())
- }
- queue.TryEnqueue(&QueuedEvent{URI: "test://uri2"})
- if queue.Len() != 2 {
- t.Errorf("Expected queue length 2, got %d", queue.Len())
- }
- })
- }
- // TestAgent_QueueEvent tests event routing to queues
- func TestAgent_QueueEvent(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{})
- mockLLM := NewMockLLM(nil)
- agent := NewTestAgent(mockLLM, mockMCP)
- agent.SetupQueues(10)
- tests := []struct {
- name string
- uri string
- expectedTask int
- expectedMsg int
- }{
- {
- name: "TaskCreated",
- uri: "graphql://subscription/taskCreated",
- expectedTask: 1,
- expectedMsg: 0,
- },
- {
- name: "TaskUpdated",
- uri: "graphql://subscription/taskUpdated",
- expectedTask: 1,
- expectedMsg: 0,
- },
- {
- name: "TaskDeleted",
- uri: "graphql://subscription/taskDeleted",
- expectedTask: 1,
- expectedMsg: 0,
- },
- {
- name: "MessageAdded",
- uri: "graphql://subscription/messageAdded",
- expectedTask: 0,
- expectedMsg: 1,
- },
- {
- name: "UnknownEvent",
- uri: "graphql://subscription/unknown",
- expectedTask: 1, // Unknown events go to task queue
- expectedMsg: 0,
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- // Reset queues
- agent.SetupQueues(10)
- agent.QueueEvent(tt.uri, json.RawMessage(`{}`))
- stats := agent.GetQueueStats()
- if stats.TaskQueueSize != tt.expectedTask {
- t.Errorf("Expected task queue size %d, got %d", tt.expectedTask, stats.TaskQueueSize)
- }
- if stats.MessageQueueSize != tt.expectedMsg {
- t.Errorf("Expected message queue size %d, got %d", tt.expectedMsg, stats.MessageQueueSize)
- }
- })
- }
- }
- // TestAgent_QueueEventFullQueue tests that events are dropped when queue is full
- func TestAgent_QueueEventFullQueue(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{})
- mockLLM := NewMockLLM(nil)
- agent := NewTestAgent(mockLLM, mockMCP)
- agent.SetupQueues(2) // Small queue for testing
- // Fill the task queue
- agent.QueueEvent("graphql://subscription/taskCreated", json.RawMessage(`{"id": "1"}`))
- agent.QueueEvent("graphql://subscription/taskCreated", json.RawMessage(`{"id": "2"}`))
- // This should be dropped
- agent.QueueEvent("graphql://subscription/taskCreated", json.RawMessage(`{"id": "3"}`))
- stats := agent.GetQueueStats()
- if stats.TaskQueueSize != 2 {
- t.Errorf("Expected task queue size 2 (full), got %d", stats.TaskQueueSize)
- }
- }
- // TestAgent_StartStop tests the queue processor lifecycle
- func TestAgent_StartStop(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{
- {Name: "query", Description: "Execute a GraphQL query", InputSchema: InputSchema{Type: "object"}},
- })
- mockMCP.SetToolResult("query", &CallToolResult{
- Content: []ContentBlock{{Type: "text", Text: `{"data": {}}`}},
- })
- // Mock LLM that responds immediately
- mockLLM := NewMockLLM([]*openai.ChatCompletionMessage{
- {
- Role: openai.ChatMessageRoleAssistant,
- Content: "Processed",
- },
- })
- agent := NewTestAgent(mockLLM, mockMCP)
- agent.Initialize()
- agent.SetupQueues(10)
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- // Start the queue processor
- agent.Start(ctx)
- // Queue an event
- agent.QueueEvent("graphql://subscription/taskCreated", json.RawMessage(`{"id": "1"}`))
- // Give it time to process
- time.Sleep(100 * time.Millisecond)
- // Stop the processor
- agent.Stop()
- // Verify the queue is empty (event was processed)
- stats := agent.GetQueueStats()
- if stats.TaskQueueSize != 0 {
- t.Errorf("Expected task queue to be empty after processing, got %d", stats.TaskQueueSize)
- }
- }
- // TestAgent_MultipleEventsInOrder tests that events are processed in arrival order
- func TestAgent_MultipleEventsInOrder(t *testing.T) {
- mockMCP := NewMockMCPClient([]Tool{
- {Name: "query", Description: "Execute a GraphQL query", InputSchema: InputSchema{Type: "object"}},
- })
- mockMCP.SetToolResult("query", &CallToolResult{
- Content: []ContentBlock{{Type: "text", Text: `{"data": {}}`}},
- })
- // Track the order of processed events
- var processedOrder []string
- // Mock LLM that responds immediately and tracks order
- mockLLM := NewMockLLM([]*openai.ChatCompletionMessage{
- {
- Role: openai.ChatMessageRoleAssistant,
- Content: "Processed",
- },
- {
- Role: openai.ChatMessageRoleAssistant,
- Content: "Processed",
- },
- {
- Role: openai.ChatMessageRoleAssistant,
- Content: "Processed",
- },
- })
- agent := NewTestAgent(mockLLM, mockMCP)
- agent.Initialize()
- agent.SetupQueues(10)
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
- // Start the queue processor
- agent.Start(ctx)
- // Queue multiple events in order: task, message, task
- agent.QueueEvent("graphql://subscription/taskCreated", json.RawMessage(`{"id": "task-1"}`))
- agent.QueueEvent("graphql://subscription/messageAdded", json.RawMessage(`{"id": "msg-1"}`))
- agent.QueueEvent("graphql://subscription/taskUpdated", json.RawMessage(`{"id": "task-2"}`))
- // Give it time to process all events
- time.Sleep(200 * time.Millisecond)
- // Stop the processor
- agent.Stop()
- // Verify all queues are empty
- stats := agent.GetQueueStats()
- if stats.TaskQueueSize != 0 {
- t.Errorf("Expected task queue to be empty, got %d", stats.TaskQueueSize)
- }
- if stats.MessageQueueSize != 0 {
- t.Errorf("Expected message queue to be empty, got %d", stats.MessageQueueSize)
- }
- // Verify order was preserved (we can't easily check this with the mock, but the test validates the mechanism)
- _ = processedOrder
- }
|