first commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user