Files
2026-08-14 23:41:57 +08:00

144 lines
4.9 KiB
Go

// 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)
}
}