mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
* perf(tui): disable duplicate composer cursor blink * perf(tui): stop composer wakeups after idle * refactor(tools): publish immutable registry snapshots * perf(startup): defer optional tool discovery * perf(provider): reuse compatible response sessions * feat(provider): add native patch tool input * fix(provider): recognize saved chat sessions * perf(tui): render only on screen changes * fix(runtime): address performance review findings * fix(usage): expose cache efficiency details * perf(agent): reduce redundant bounded-task turns * fix: harden runtime review edge cases * fix: address runtime review findings * build: restore official Bubble Tea dependency * fix(cli): redact MCP registration errors * fix: harden turn session regressions * fix: keep run tools and patch roots consistent * fix: bind freeform patch writes to scoped roots * test: keep patch fixtures out of worktree * fix(tools): reject mixed-root freeform patches
247 lines
7.9 KiB
Go
247 lines
7.9 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestUpdatePlanToolStoresAndFormatsPlan(t *testing.T) {
|
|
tool := NewUpdatePlanTool()
|
|
if description := strings.ToLower(tool.Description()); !strings.Contains(description, "skip it for bounded changes in one component") {
|
|
t.Fatalf("update_plan description must preserve the bounded-task fast path: %q", description)
|
|
}
|
|
|
|
result := tool.Run(context.Background(), map[string]any{
|
|
"plan": []any{
|
|
map[string]any{"id": "1", "content": "First step", "status": "completed"},
|
|
map[string]any{"id": "2", "content": "Second step", "status": "in_progress", "notes": "halfway"},
|
|
map[string]any{"id": "3", "content": "Third step", "status": "pending"},
|
|
},
|
|
})
|
|
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("expected ok status, got %s: %s", result.Status, result.Output)
|
|
}
|
|
for _, want := range []string{
|
|
"Current Plan:",
|
|
"1. [completed] First step",
|
|
"2. [in_progress] Second step",
|
|
"Notes: halfway",
|
|
"3. [pending] Third step",
|
|
} {
|
|
if !strings.Contains(result.Output, want) {
|
|
t.Fatalf("expected output to contain %q, got %q", want, result.Output)
|
|
}
|
|
}
|
|
|
|
plan := tool.CurrentPlan()
|
|
if len(plan) != 3 {
|
|
t.Fatalf("expected 3 plan items, got %d", len(plan))
|
|
}
|
|
plan[0].Content = "mutated"
|
|
if tool.CurrentPlan()[0].Content != "First step" {
|
|
t.Fatalf("CurrentPlan returned mutable internal state")
|
|
}
|
|
}
|
|
|
|
func TestUpdatePlanToolCoercesStatuses(t *testing.T) {
|
|
// A weaker model may emit non-canonical statuses ("nope", "done", "in-progress").
|
|
// These must be coerced (unknown -> pending, synonyms -> canonical) rather than
|
|
// failing the whole update_plan call — a rejected call leaves the stored plan
|
|
// unchanged, which freezes the plan panel on its previous state.
|
|
tool := NewUpdatePlanTool()
|
|
result := tool.Run(context.Background(), map[string]any{
|
|
"plan": []any{
|
|
map[string]any{"id": "1", "content": "Bad step", "status": "nope"},
|
|
map[string]any{"id": "2", "content": "Done step", "status": "done"},
|
|
map[string]any{"id": "3", "content": "Active step", "status": "in-progress"},
|
|
},
|
|
})
|
|
|
|
if result.Status == StatusError {
|
|
t.Fatalf("unknown/synonym statuses must not error, got: %q", result.Output)
|
|
}
|
|
plan := tool.CurrentPlan()
|
|
if len(plan) != 3 {
|
|
t.Fatalf("expected 3 steps, got %d", len(plan))
|
|
}
|
|
if plan[0].Status != "pending" {
|
|
t.Errorf("unknown status should coerce to pending, got %q", plan[0].Status)
|
|
}
|
|
if plan[1].Status != "completed" {
|
|
t.Errorf("'done' should coerce to completed, got %q", plan[1].Status)
|
|
}
|
|
if plan[2].Status != "in_progress" {
|
|
t.Errorf("'in-progress' should coerce to in_progress, got %q", plan[2].Status)
|
|
}
|
|
}
|
|
|
|
func TestEnforceSingleInProgress(t *testing.T) {
|
|
// More than one in_progress is downgraded so only the LAST stays.
|
|
plan := []PlanItem{
|
|
{Content: "a", Status: "in_progress"},
|
|
{Content: "b", Status: "completed"},
|
|
{Content: "c", Status: "in_progress"},
|
|
{Content: "d", Status: "pending"},
|
|
}
|
|
out := enforceSingleInProgress(plan)
|
|
if out[0].Status != "completed" {
|
|
t.Errorf("earlier in_progress should downgrade to completed, got %q", out[0].Status)
|
|
}
|
|
if out[2].Status != "in_progress" {
|
|
t.Errorf("last in_progress should remain, got %q", out[2].Status)
|
|
}
|
|
count := 0
|
|
for _, it := range out {
|
|
if it.Status == "in_progress" {
|
|
count++
|
|
}
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("want exactly 1 in_progress, got %d", count)
|
|
}
|
|
|
|
single := []PlanItem{{Status: "completed"}, {Status: "in_progress"}, {Status: "pending"}}
|
|
if got := enforceSingleInProgress(single); got[1].Status != "in_progress" {
|
|
t.Fatalf("single in_progress must be preserved, got %q", got[1].Status)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePlanRunEnforcesSingleInProgress(t *testing.T) {
|
|
tool := NewUpdatePlanTool()
|
|
tool.Run(context.Background(), map[string]any{
|
|
"plan": []any{
|
|
map[string]any{"content": "a", "status": "in_progress"},
|
|
map[string]any{"content": "b", "status": "in_progress"},
|
|
},
|
|
})
|
|
plan := tool.CurrentPlan()
|
|
if plan[0].Status != "completed" || plan[1].Status != "in_progress" {
|
|
t.Fatalf("Run should enforce one in_progress, got %q,%q", plan[0].Status, plan[1].Status)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePlanToolClearPlanResetsState(t *testing.T) {
|
|
tool := NewUpdatePlanTool()
|
|
|
|
result := tool.Run(context.Background(), map[string]any{
|
|
"plan": []any{
|
|
map[string]any{"id": "1", "content": "First", "status": "pending"},
|
|
map[string]any{"id": "2", "content": "Second", "status": "in_progress"},
|
|
},
|
|
})
|
|
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("expected ok status, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if got := tool.CurrentPlan(); len(got) == 0 {
|
|
t.Fatalf("expected stored plan before ClearPlan")
|
|
}
|
|
|
|
tool.ClearPlan()
|
|
if got := tool.CurrentPlan(); len(got) != 0 {
|
|
t.Fatalf("expected empty plan after ClearPlan, got %d items", len(got))
|
|
}
|
|
if got := formatPlan(tool.CurrentPlan()); got != "Plan is currently empty." {
|
|
t.Fatalf("expected empty plan formatting after ClearPlan, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePlanToolAcceptsItemsWithoutID(t *testing.T) {
|
|
tool := NewUpdatePlanTool()
|
|
result := tool.Run(context.Background(), map[string]any{
|
|
"plan": []any{
|
|
map[string]any{"content": "First step", "status": "in_progress"},
|
|
map[string]any{"content": "Second step", "status": "pending"},
|
|
},
|
|
})
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("expected ok status when id omitted, got %s: %s", result.Status, result.Output)
|
|
}
|
|
plan := tool.CurrentPlan()
|
|
if len(plan) != 2 {
|
|
t.Fatalf("expected 2 plan items, got %d", len(plan))
|
|
}
|
|
if plan[0].ID != "1" || plan[1].ID != "2" {
|
|
t.Fatalf("expected ids auto-derived from index, got %q,%q", plan[0].ID, plan[1].ID)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePlanToolDefaultsStatusToPending(t *testing.T) {
|
|
tool := NewUpdatePlanTool()
|
|
result := tool.Run(context.Background(), map[string]any{
|
|
"plan": []any{
|
|
map[string]any{"content": "Only content"},
|
|
},
|
|
})
|
|
if result.Status != StatusOK {
|
|
t.Fatalf("expected ok status when status omitted, got %s: %s", result.Status, result.Output)
|
|
}
|
|
if got := tool.CurrentPlan(); got[0].Status != "pending" {
|
|
t.Fatalf("expected status to default to pending, got %q", got[0].Status)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePlanToolRequiresContent(t *testing.T) {
|
|
result := NewUpdatePlanTool().Run(context.Background(), map[string]any{
|
|
"plan": []any{map[string]any{"status": "pending"}},
|
|
})
|
|
if result.Status != StatusError {
|
|
t.Fatalf("expected error when content missing, got %s", result.Status)
|
|
}
|
|
if !strings.Contains(result.Output, "content is required") {
|
|
t.Fatalf("unexpected output: %q", result.Output)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePlanToolConcurrentRunAndRead(t *testing.T) {
|
|
tool := NewUpdatePlanTool()
|
|
args := map[string]any{
|
|
"plan": []any{
|
|
map[string]any{"content": "First step", "status": "in_progress"},
|
|
map[string]any{"content": "Second step", "status": "pending"},
|
|
},
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 50; i++ {
|
|
wg.Add(3)
|
|
go func() { defer wg.Done(); tool.Run(context.Background(), args) }()
|
|
go func() { defer wg.Done(); _ = tool.CurrentPlan() }()
|
|
go func() { defer wg.Done(); tool.ClearPlan() }()
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestUpdatePlanToolAdvertisesItemSchema(t *testing.T) {
|
|
plan := NewUpdatePlanTool().Parameters().Properties["plan"]
|
|
if plan.Type != "array" {
|
|
t.Fatalf("expected plan to be an array, got %q", plan.Type)
|
|
}
|
|
if plan.Items == nil {
|
|
t.Fatal("plan should have a structured Items schema")
|
|
}
|
|
if plan.Items.Type != "object" {
|
|
t.Fatalf("plan items should be objects, got %q", plan.Items.Type)
|
|
}
|
|
contentProp, ok := plan.Items.Properties["content"]
|
|
if !ok {
|
|
t.Fatal("plan items should have a 'content' property")
|
|
}
|
|
if contentProp.Type != "string" {
|
|
t.Fatalf("content property should be string, got %q", contentProp.Type)
|
|
}
|
|
statusProp, ok := plan.Items.Properties["status"]
|
|
if !ok {
|
|
t.Fatal("plan items should have a 'status' property")
|
|
}
|
|
if len(statusProp.Enum) == 0 {
|
|
t.Fatal("status property should have an enum")
|
|
}
|
|
if len(plan.Items.Required) == 0 || plan.Items.Required[0] != "content" {
|
|
t.Fatalf("plan items should require 'content', got %v", plan.Items.Required)
|
|
}
|
|
}
|