1package loop
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "shelley.exe.dev/llm"
9)
10
11func TestLoopWithClaudeTools(t *testing.T) {
12 var recordedMessages []llm.Message
13
14 recordFunc := func(ctx context.Context, message llm.Message, usage llm.Usage) error {
15 recordedMessages = append(recordedMessages, message)
16 return nil
17 }
18
19 // Use some actual claudetools
20 tools := []*llm.Tool{
21 // TODO: Add actual tools when needed
22 }
23
24 service := NewPredictableService()
25
26 // Create loop with the configured service
27 loop := NewLoop(Config{
28 LLM: service,
29 History: []llm.Message{},
30 Tools: tools,
31 RecordMessage: recordFunc,
32 })
33
34 // Queue a user message that will trigger a specific predictable response
35 userMessage := llm.Message{
36 Role: llm.MessageRoleUser,
37 Content: []llm.Content{{Type: llm.ContentTypeText, Text: "hello"}},
38 }
39 loop.QueueUserMessage(userMessage)
40
41 // Run the loop with a short timeout
42 ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
43 defer cancel()
44
45 err := loop.Go(ctx)
46 if err != context.DeadlineExceeded {
47 t.Errorf("expected context deadline exceeded, got %v", err)
48 }
49
50 // Verify that messages were recorded
51 // Note: User messages are recorded by ConversationManager, not by Loop,
52 // so we only expect assistant messages to be recorded here
53 if len(recordedMessages) < 1 {
54 t.Errorf("expected at least 1 recorded message (assistant), got %d", len(recordedMessages))
55 }
56
57 // Check that usage was accumulated
58 usage := loop.GetUsage()
59 if usage.IsZero() {
60 t.Error("expected non-zero usage")
61 }
62
63 // Verify conversation history includes user and assistant messages
64 history := loop.GetHistory()
65 if len(history) < 2 {
66 t.Errorf("expected at least 2 history messages, got %d", len(history))
67 }
68
69 // Check for expected response
70 found := false
71 for _, msg := range history {
72 if msg.Role == llm.MessageRoleAssistant {
73 for _, content := range msg.Content {
74 if content.Type == llm.ContentTypeText && content.Text == "Well, hi there!" {
75 found = true
76 break
77 }
78 }
79 }
80 }
81 if !found {
82 t.Error("expected to find 'Well, hi there!' response")
83 }
84}
85
86func TestLoopContextCancellation(t *testing.T) {
87 service := NewPredictableService()
88 loop := NewLoop(Config{
89 LLM: service,
90 History: []llm.Message{},
91 Tools: []*llm.Tool{},
92 RecordMessage: func(ctx context.Context, message llm.Message, usage llm.Usage) error {
93 return nil
94 },
95 })
96
97 // Cancel context immediately
98 ctx, cancel := context.WithCancel(context.Background())
99 cancel()
100
101 err := loop.Go(ctx)
102 if err != context.Canceled {
103 t.Errorf("expected context canceled, got %v", err)
104 }
105}
106
107func TestLoopSystemMessages(t *testing.T) {
108 // Set system messages
109 system := []llm.SystemContent{
110 {Text: "You are a helpful assistant.", Type: "text"},
111 }
112
113 loop := NewLoop(Config{
114 LLM: NewPredictableService(),
115 History: []llm.Message{},
116 Tools: []*llm.Tool{},
117 System: system,
118 RecordMessage: func(ctx context.Context, message llm.Message, usage llm.Usage) error {
119 return nil
120 },
121 })
122
123 // The system messages are stored and would be passed to LLM
124 loop.mu.Lock()
125 if len(loop.system) != 1 {
126 t.Errorf("expected 1 system message, got %d", len(loop.system))
127 }
128 if loop.system[0].Text != "You are a helpful assistant." {
129 t.Errorf("unexpected system message text: %s", loop.system[0].Text)
130 }
131 loop.mu.Unlock()
132}