144 lines
4.9 KiB
Go
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)
|
|
}
|
|
}
|