blob: 6f511e8de3d09a149ec5ac8d321985cdbc012a36 [file] [log] [blame]
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -07001package claudetool
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10)
11
12func TestTodoReadEmpty(t *testing.T) {
13 ctx := WithSessionID(context.Background(), "test-session-1")
14
15 // Ensure todo file doesn't exist
16 todoPath := todoFilePathForContext(ctx)
17 os.Remove(todoPath)
18
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070019 toolOut := todoReadRun(ctx, []byte("{}"))
20 if toolOut.Error != nil {
21 t.Fatalf("expected no error, got %v", toolOut.Error)
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070022 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070023 result := toolOut.LLMContent
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070024
25 if len(result) != 1 {
26 t.Fatalf("expected 1 content item, got %d", len(result))
27 }
28
29 expected := "No todo list found. Use todo_write to create one."
30 if result[0].Text != expected {
31 t.Errorf("expected %q, got %q", expected, result[0].Text)
32 }
33}
34
35func TestTodoWriteAndRead(t *testing.T) {
36 ctx := WithSessionID(context.Background(), "test-session-2")
37
38 // Clean up
39 todoPath := todoFilePathForContext(ctx)
40 defer os.Remove(todoPath)
41 os.Remove(todoPath)
42
43 // Write some todos
44 todos := []TodoItem{
45 {ID: "1", Task: "Implement todo tools", Status: "completed"},
46 {ID: "2", Task: "Update system prompt", Status: "in-progress"},
47 {ID: "3", Task: "Write tests", Status: "queued"},
48 }
49
50 writeInput := TodoWriteInput{Tasks: todos}
51 writeInputJSON, _ := json.Marshal(writeInput)
52
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070053 toolOut := todoWriteRun(ctx, writeInputJSON)
54 if toolOut.Error != nil {
55 t.Fatalf("expected no error, got %v", toolOut.Error)
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070056 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070057 result := toolOut.LLMContent
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070058
59 if len(result) != 1 {
60 t.Fatalf("expected 1 content item, got %d", len(result))
61 }
62
63 expected := "Updated todo list with 3 items."
64 if result[0].Text != expected {
65 t.Errorf("expected %q, got %q", expected, result[0].Text)
66 }
67
68 // Read the todos back
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070069 toolOut = todoReadRun(ctx, []byte("{}"))
70 if toolOut.Error != nil {
71 t.Fatalf("expected no error, got %v", toolOut.Error)
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070072 }
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -070073 result = toolOut.LLMContent
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -070074
75 if len(result) != 1 {
76 t.Fatalf("expected 1 content item, got %d", len(result))
77 }
78
79 resultText := result[0].Text
80 if !strings.Contains(resultText, "<todo_list count=\"3\">") {
81 t.Errorf("expected result to contain XML todo list header, got %q", resultText)
82 }
83
84 // Check that all todos are present with proper XML structure
85 if !strings.Contains(resultText, `<task id="1" status="completed">Implement todo tools</task>`) {
86 t.Errorf("expected result to contain first todo in XML format, got %q", resultText)
87 }
88 if !strings.Contains(resultText, `<task id="2" status="in-progress">Update system prompt</task>`) {
89 t.Errorf("expected result to contain second todo in XML format, got %q", resultText)
90 }
91 if !strings.Contains(resultText, `<task id="3" status="queued">Write tests</task>`) {
92 t.Errorf("expected result to contain third todo in XML format, got %q", resultText)
93 }
94
95 // Check XML structure
96 if !strings.Contains(resultText, "</todo_list>") {
97 t.Errorf("expected result to contain closing XML tag, got %q", resultText)
98 }
99}
100
101func TestTodoWriteMultipleInProgress(t *testing.T) {
102 ctx := WithSessionID(context.Background(), "test-session-3")
103
104 // Try to write todos with multiple in-progress items
105 todos := []TodoItem{
106 {ID: "1", Task: "Task 1", Status: "in-progress"},
107 {ID: "2", Task: "Task 2", Status: "in-progress"},
108 }
109
110 writeInput := TodoWriteInput{Tasks: todos}
111 writeInputJSON, _ := json.Marshal(writeInput)
112
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700113 toolOut := todoWriteRun(ctx, writeInputJSON)
114 if toolOut.Error == nil {
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700115 t.Fatal("expected error for multiple in_progress tasks, got none")
116 }
117
118 expected := "only one task can be 'in-progress' at a time, found 2"
Josh Bleecher Snyder43b60b92025-07-21 14:57:10 -0700119 if toolOut.Error.Error() != expected {
120 t.Errorf("expected error %q, got %q", expected, toolOut.Error.Error())
Josh Bleecher Snyder112b9232025-05-23 11:26:33 -0700121 }
122}
123
124func TestTodoSessionIsolation(t *testing.T) {
125 // Test that different sessions have different todo files
126 ctx1 := WithSessionID(context.Background(), "session-1")
127 ctx2 := WithSessionID(context.Background(), "session-2")
128
129 path1 := todoFilePathForContext(ctx1)
130 path2 := todoFilePathForContext(ctx2)
131
132 if path1 == path2 {
133 t.Errorf("expected different paths for different sessions, both got %q", path1)
134 }
135
136 expected1 := filepath.Join("/tmp", "session-1", "todos.json")
137 expected2 := filepath.Join("/tmp", "session-2", "todos.json")
138
139 if path1 != expected1 {
140 t.Errorf("expected path1 %q, got %q", expected1, path1)
141 }
142
143 if path2 != expected2 {
144 t.Errorf("expected path2 %q, got %q", expected2, path2)
145 }
146}
147
148func TestTodoFallbackPath(t *testing.T) {
149 // Test fallback when no session ID in context
150 ctx := context.Background() // No session ID
151
152 path := todoFilePathForContext(ctx)
153 expected := "/tmp/sketch_todos.json"
154
155 if path != expected {
156 t.Errorf("expected fallback path %q, got %q", expected, path)
157 }
158}