返回 DeepSeek-Reasonix
updategoal_test.go
根目录 / internal / tool / builtin / updategoal_test.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "strings"
8 "testing"
9
10 "reasonix/internal/tool"
11 )
12
13 // recordingRecorder is a minimal GoalTurnRecorder for tool-level tests.
14 type recordingRecorder struct {
15 reports []tool.GoalReport
16 err error
17 }
18
19 func (r *recordingRecorder) RecordGoalReport(report tool.GoalReport) (string, error) {
20 if r.err != nil {
21 return "", r.err
22 }
23 r.reports = append(r.reports, report)
24 return "update_goal: " + report.Status + " recorded for this turn.", nil
25 }
26
27 func goalTool(t *testing.T) (updateGoal, *recordingRecorder, context.Context) {
28 t.Helper()
29 rec := &recordingRecorder{}
30 ctx := tool.WithGoalTurnRecorder(context.Background(), rec)
31 return updateGoal{}, rec, ctx
32 }
33
34 func TestUpdateGoalValidatesStatusAndReason(t *testing.T) {
35 toolFn, rec, ctx := goalTool(t)
36
37 cases := []struct {
38 name string
39 args string
40 wantErr string
41 }{
42 {"continue without reason rejected", `{"status":"continue"}`, "reason is required"},
43 {"blocked without reason rejected", `{"status":"blocked"}`, "reason is required"},
44 {"complete without reason accepted", `{"status":"complete"}`, ""},
45 {"unknown status rejected", `{"status":"sideways"}`, "status must be one of"},
46 {"empty status rejected", `{}`, "status must be one of"},
47 {"invalid json rejected", `{not json`, "invalid update_goal args"},
48 }
49 for _, tc := range cases {
50 t.Run(tc.name, func(t *testing.T) {
51 before := len(rec.reports)
52 _, err := toolFn.Execute(ctx, json.RawMessage(tc.args))
53 if tc.wantErr == "" {
54 if err != nil {
55 t.Fatalf("Execute() error = %v, want nil", err)
56 }
57 return
58 }
59 if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
60 t.Fatalf("Execute() error = %v, want it to mention %q", err, tc.wantErr)
61 }
62 if len(rec.reports) != before {
63 t.Fatalf("rejected call still recorded a report: %+v", rec.reports)
64 }
65 })
66 }
67 }
68
69 func TestUpdateGoalFailsClosedOutsideActiveGoalTurn(t *testing.T) {
70 toolFn := updateGoal{}
71 // No recorder in the context — an ordinary chat turn.
72 _, err := toolFn.Execute(context.Background(), json.RawMessage(`{"status":"continue","reason":"working"}`))
73 if err == nil || !strings.Contains(err.Error(), "only available while an active goal turn") {
74 t.Fatalf("Execute() error = %v, want structured out-of-context error", err)
75 }
76 }
77
78 func TestUpdateGoalRecordsReport(t *testing.T) {
79 toolFn, rec, ctx := goalTool(t)
80 _, err := toolFn.Execute(ctx, json.RawMessage(`{"status":"continue","reason":"fixing the parser","next_action":"run tests"}`))
81 if err != nil {
82 t.Fatalf("Execute() error = %v", err)
83 }
84 if len(rec.reports) != 1 {
85 t.Fatalf("reports = %+v, want 1", rec.reports)
86 }
87 got := rec.reports[0]
88 if got.Status != "continue" || got.Reason != "fixing the parser" || got.NextAction != "run tests" {
89 t.Fatalf("report = %+v", got)
90 }
91 }
92
93 func TestUpdateGoalRecorderErrorsPropagate(t *testing.T) {
94 toolFn, rec, ctx := goalTool(t)
95 rec.err = errors.New("conflicting reports this turn")
96 _, err := toolFn.Execute(ctx, json.RawMessage(`{"status":"complete"}`))
97 if err == nil || !strings.Contains(err.Error(), "conflicting reports") {
98 t.Fatalf("Execute() error = %v, want recorder error propagated", err)
99 }
100 }
101
101 lines GO