// Tests for the todo tool (US-011, #127): registration/validation, status // transitions across successive writes, and the rendered progress view. package agenttool import ( "context" "encoding/json" "strings" "testing" "github.com/smallnest/pigo/internal/agentcore" ) // execTodo runs the tool with the given JSON args and returns the result. func execTodo(t *testing.T, tool *TodoTool, args string) agentcore.AgentToolResult { t.Helper() res, err := tool.Execute(context.Background(), "call-1", json.RawMessage(args), nil) if err != nil { t.Fatalf("Execute returned Go error: %v", err) } return res } // TestTodoToolRegisters checks the tool registers cleanly (valid schema) and is // retrievable — the "registered in the agenttool registry" acceptance criterion. func TestTodoToolRegisters(t *testing.T) { reg := NewToolRegistry() tool := &TodoTool{Store: NewTodoStore()} if err := reg.Register(tool); err != nil { t.Fatalf("Register: %v", err) } got, ok := reg.Get("todo") if !ok { t.Fatal("todo tool not found after Register") } if got.Name() != "todo" { t.Errorf("Name = %q, want todo", got.Name()) } } // TestTodoToolStoresList checks a write lands in the session store. func TestTodoToolStoresList(t *testing.T) { store := NewTodoStore() tool := &TodoTool{Store: store} execTodo(t, tool, `{"todos":[ {"content":"first","status":"in_progress"}, {"content":"second","status":"pending"} ]}`) items := store.Snapshot() if len(items) != 2 { t.Fatalf("Snapshot len = %d, want 2", len(items)) } if items[0].Content != "first" || items[0].Status != TodoInProgress { t.Errorf("item 0 = %+v", items[0]) } if items[1].Status != TodoPending { t.Errorf("item 1 status = %q, want pending", items[1].Status) } } // TestTodoToolStatusTransition checks a later write replaces the previous list, // reflecting a status flow pending → in_progress → completed. func TestTodoToolStatusTransition(t *testing.T) { store := NewTodoStore() tool := &TodoTool{Store: store} execTodo(t, tool, `{"todos":[{"content":"build feature","status":"pending"}]}`) if s := store.Snapshot()[0].Status; s != TodoPending { t.Fatalf("after write 1 status = %q, want pending", s) } execTodo(t, tool, `{"todos":[{"content":"build feature","status":"in_progress"}]}`) if s := store.Snapshot()[0].Status; s != TodoInProgress { t.Fatalf("after write 2 status = %q, want in_progress", s) } execTodo(t, tool, `{"todos":[{"content":"build feature","status":"completed"}]}`) items := store.Snapshot() if len(items) != 1 { t.Fatalf("after write 3 len = %d, want 1", len(items)) } if items[0].Status != TodoCompleted { t.Errorf("after write 3 status = %q, want completed", items[0].Status) } } // TestTodoToolRejectsInvalidStatus checks an unknown status degrades to an error // result and does not mutate the store. func TestTodoToolRejectsInvalidStatus(t *testing.T) { store := NewTodoStore() tool := &TodoTool{Store: store} res := execTodo(t, tool, `{"todos":[{"content":"x","status":"done"}]}`) if !strings.Contains(agentcore.ContentToText(res.Content), "invalid status") { t.Errorf("expected invalid-status error, got %q", agentcore.ContentToText(res.Content)) } if len(store.Snapshot()) != 0 { t.Error("store mutated despite invalid input") } } // TestTodoToolRejectsEmptyContent checks a blank content is rejected. func TestTodoToolRejectsEmptyContent(t *testing.T) { tool := &TodoTool{Store: NewTodoStore()} res := execTodo(t, tool, `{"todos":[{"content":" ","status":"pending"}]}`) if !strings.Contains(agentcore.ContentToText(res.Content), "empty content") { t.Errorf("expected empty-content error, got %q", agentcore.ContentToText(res.Content)) } } // TestRenderTodoList checks the rendered progress view: marks per status and a // completion count. func TestRenderTodoList(t *testing.T) { out := RenderTodoList([]TodoItem{ {Content: "alpha", Status: TodoCompleted}, {Content: "beta", Status: TodoInProgress}, {Content: "gamma", Status: TodoPending}, }) for _, want := range []string{"[x] alpha", "[~] beta", "[ ] gamma", "(1/3 completed)"} { if !strings.Contains(out, want) { t.Errorf("render missing %q in:\n%s", want, out) } } } // TestRenderTodoListEmpty checks an empty list renders visibly (an intentional // clear should still show). func TestRenderTodoListEmpty(t *testing.T) { if out := RenderTodoList(nil); !strings.Contains(out, "no tasks") { t.Errorf("empty render = %q, want a (no tasks) marker", out) } } // TestTodoToolResultRenders checks Execute returns the rendered list as content // so the REPL has something to display. func TestTodoToolResultRenders(t *testing.T) { tool := &TodoTool{Store: NewTodoStore()} res := execTodo(t, tool, `{"todos":[{"content":"do it","status":"completed"}]}`) text := agentcore.ContentToText(res.Content) if !strings.Contains(text, "[x] do it") || !strings.Contains(text, "(1/1 completed)") { t.Errorf("result content = %q", text) } }