first commit
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
// Tests for the goal tools (mirrors pi-goal): goal_complete validation (empty and
|
||||
// contradictory summaries are rejected, a valid summary marks complete and
|
||||
// terminates the run), goal_blocked validation, and GoalState counter/lifecycle
|
||||
// behavior. Mirrors todo_tool_test.go's structure.
|
||||
package agenttool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/smallnest/pigo/internal/agentcore"
|
||||
)
|
||||
|
||||
// execGoalComplete runs the goal_complete tool with the given JSON args.
|
||||
func execGoalComplete(t *testing.T, tool *GoalCompleteTool, 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
|
||||
}
|
||||
|
||||
func isErrorResult(res agentcore.AgentToolResult) bool {
|
||||
// An error result carries no Terminate and its text is the error message;
|
||||
// the tools return errorResult(...) which has Terminate=nil.
|
||||
return res.Terminate == nil
|
||||
}
|
||||
|
||||
func TestGoalToolsRegister(t *testing.T) {
|
||||
reg := NewToolRegistry()
|
||||
st := NewGoalState()
|
||||
if err := reg.Register(&GoalCompleteTool{State: st}); err != nil {
|
||||
t.Fatalf("Register goal_complete: %v", err)
|
||||
}
|
||||
if err := reg.Register(&GoalBlockedTool{State: st}); err != nil {
|
||||
t.Fatalf("Register goal_blocked: %v", err)
|
||||
}
|
||||
if _, ok := reg.Get("goal_complete"); !ok {
|
||||
t.Fatal("goal_complete not found after Register")
|
||||
}
|
||||
if _, ok := reg.Get("goal_blocked"); !ok {
|
||||
t.Fatal("goal_blocked not found after Register")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalCompleteValidSummary(t *testing.T) {
|
||||
st := NewGoalState()
|
||||
st.Start("g1", "do the thing", 0)
|
||||
tool := &GoalCompleteTool{State: st}
|
||||
|
||||
res := execGoalComplete(t, tool, `{"summary":"created hello.txt with the requested contents"}`)
|
||||
if res.Terminate == nil || !*res.Terminate {
|
||||
t.Fatalf("expected Terminate=true, got %v", res.Terminate)
|
||||
}
|
||||
if snap := st.Snapshot(); snap.Status != GoalComplete {
|
||||
t.Errorf("status = %q, want complete", snap.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalCompleteRejectsEmpty(t *testing.T) {
|
||||
st := NewGoalState()
|
||||
st.Start("g1", "do the thing", 0)
|
||||
tool := &GoalCompleteTool{State: st}
|
||||
|
||||
res := execGoalComplete(t, tool, `{"summary":" "}`)
|
||||
if !isErrorResult(res) {
|
||||
t.Fatal("expected error result for empty summary")
|
||||
}
|
||||
if snap := st.Snapshot(); snap.Status != GoalActive {
|
||||
t.Errorf("status = %q, want still active after rejected summary", snap.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalCompleteRejectsContradictory(t *testing.T) {
|
||||
st := NewGoalState()
|
||||
st.Start("g1", "do the thing", 0)
|
||||
tool := &GoalCompleteTool{State: st}
|
||||
|
||||
for _, summary := range []string{
|
||||
`{"summary":"the goal is not complete yet"}`,
|
||||
`{"summary":"tests still fail but I stopped"}`,
|
||||
`{"summary":"the task is unfinished"}`,
|
||||
} {
|
||||
res := execGoalComplete(t, tool, summary)
|
||||
if !isErrorResult(res) {
|
||||
t.Fatalf("expected error result for contradictory summary %s", summary)
|
||||
}
|
||||
}
|
||||
if snap := st.Snapshot(); snap.Status != GoalActive {
|
||||
t.Errorf("status = %q, want still active", snap.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalBlockedRecordsReason(t *testing.T) {
|
||||
st := NewGoalState()
|
||||
st.Start("g1", "do the thing", 0)
|
||||
tool := &GoalBlockedTool{State: st}
|
||||
|
||||
res, err := tool.Execute(context.Background(), "c1",
|
||||
json.RawMessage(`{"reason":"missing API key","evidence":"env AUTH_TOKEN is empty"}`), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if res.Terminate == nil || !*res.Terminate {
|
||||
t.Fatalf("expected Terminate=true, got %v", res.Terminate)
|
||||
}
|
||||
snap := st.Snapshot()
|
||||
if snap.Status != GoalBlocked {
|
||||
t.Errorf("status = %q, want blocked", snap.Status)
|
||||
}
|
||||
if snap.BlockReason != "missing API key" {
|
||||
t.Errorf("block reason = %q", snap.BlockReason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalBlockedRequiresEvidence(t *testing.T) {
|
||||
st := NewGoalState()
|
||||
st.Start("g1", "do the thing", 0)
|
||||
tool := &GoalBlockedTool{State: st}
|
||||
|
||||
res, err := tool.Execute(context.Background(), "c1",
|
||||
json.RawMessage(`{"reason":"stuck","evidence":""}`), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if !isErrorResult(res) {
|
||||
t.Fatal("expected error result when evidence is empty")
|
||||
}
|
||||
if snap := st.Snapshot(); snap.Status != GoalActive {
|
||||
t.Errorf("status = %q, want still active", snap.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalStateRecordIteration(t *testing.T) {
|
||||
st := NewGoalState()
|
||||
st.Start("g1", "obj", 1000)
|
||||
|
||||
st.RecordIteration(100, true) // tool activity resets no-progress
|
||||
st.RecordIteration(50, false) // no tool activity
|
||||
st.RecordIteration(50, false)
|
||||
|
||||
snap := st.Snapshot()
|
||||
if snap.Iterations != 3 {
|
||||
t.Errorf("iterations = %d, want 3", snap.Iterations)
|
||||
}
|
||||
if snap.TokensUsed != 200 {
|
||||
t.Errorf("tokensUsed = %d, want 200", snap.TokensUsed)
|
||||
}
|
||||
if snap.NoProgress != 2 {
|
||||
t.Errorf("noProgress = %d, want 2", snap.NoProgress)
|
||||
}
|
||||
if snap.TokenBudget != 1000 {
|
||||
t.Errorf("tokenBudget = %d, want 1000", snap.TokenBudget)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalStateClear(t *testing.T) {
|
||||
st := NewGoalState()
|
||||
st.Start("g1", "obj", 0)
|
||||
st.Clear()
|
||||
if snap := st.Snapshot(); snap.Status != GoalIdle || snap.Objective != "" {
|
||||
t.Errorf("after Clear: status=%q objective=%q, want idle/empty", snap.Status, snap.Objective)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoalStateResume(t *testing.T) {
|
||||
st := NewGoalState()
|
||||
st.Start("g1", "obj", 1000)
|
||||
// Simulate a run that tripped a safety guard.
|
||||
st.RecordIteration(500, false)
|
||||
st.RecordIteration(500, false)
|
||||
st.SetStatus(GoalPaused)
|
||||
|
||||
st.Resume()
|
||||
snap := st.Snapshot()
|
||||
if snap.Status != GoalActive {
|
||||
t.Errorf("status = %q, want active after Resume", snap.Status)
|
||||
}
|
||||
if snap.Iterations != 0 || snap.NoProgress != 0 || snap.TokensUsed != 0 {
|
||||
t.Errorf("Resume should clear transient counters: iterations=%d noProgress=%d tokensUsed=%d",
|
||||
snap.Iterations, snap.NoProgress, snap.TokensUsed)
|
||||
}
|
||||
if snap.Objective != "obj" || snap.TokenBudget != 1000 {
|
||||
t.Errorf("Resume should preserve objective/budget: objective=%q budget=%d", snap.Objective, snap.TokenBudget)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user