1package agent
2
3import (
4 "errors"
5 "os"
6 "slices"
7 "strings"
8 "testing"
9
10 "github.com/bnema/zut/packages/core"
11 "github.com/bnema/zut/packages/provider"
12)
13
14func newPlanPersistSession(t *testing.T) *core.Session {
15 t.Helper()
16 sess, err := core.NewSession(t.TempDir(), t.TempDir(), "provider", "model", "version")
17 if err != nil {
18 t.Fatal(err)
19 }
20 if err := sess.AppendMessage(provider.Message{
21 Role: provider.RoleUser,
22 Content: []provider.Content{provider.TextBlock{Text: "hello"}},
23 }); err != nil {
24 t.Fatal(err)
25 }
26 t.Cleanup(func() { _ = sess.Close() })
27 return sess
28}
29
30// planMutationResult builds the result shape core produces for a validated
31// plan mutation: materialized details plus the pending operation.
32func planMutationResult(plan []core.PlanStep) core.ToolResult {
33 op := core.PlanOperation{Action: "set"}
34 return core.ToolResult{
35 Content: []provider.Content{provider.TextBlock{Text: "Plan updated"}},
36 Details: core.PlanUpdate{Plan: plan},
37 PlanOperation: &op,
38 }
39}
40
41func TestPersistPlanToolResultWritesMetaRow(t *testing.T) {
42 sess := newPlanPersistSession(t)
43 want := []core.PlanStep{
44 {Step: "one", Status: core.PlanPending},
45 {Step: "two", Status: core.PlanInProgress},
46 }
47 if err := persistToolResultState(nil, sess, planMutationResult(want)); err != nil {
48 t.Fatal(err)
49 }
50 if sess.Meta.Plan == nil || !slices.Equal(sess.Meta.Plan.Steps, want) {
51 t.Fatalf("plan = %#v, want %#v", sess.Meta.Plan, want)
52 }
53}
54
55// TestPersistPlanToolResultSkipsShowAndErrors covers the two results that must
56// not append a meta row: show (no materialized state) and any error.
57func TestPersistPlanToolResultSkipsShowAndErrors(t *testing.T) {
58 errorResult := planMutationResult([]core.PlanStep{{Step: "one", Status: core.PlanPending}})
59 errorResult.IsError = true
60 tests := []struct {
61 name string
62 result core.ToolResult
63 }{
64 {name: "show", result: core.ToolResult{Content: []provider.Content{provider.TextBlock{Text: "Plan is empty"}}}},
65 {name: "error", result: errorResult},
66 }
67 for _, test := range tests {
68 t.Run(test.name, func(t *testing.T) {
69 sess := newPlanPersistSession(t)
70 if err := persistToolResultState(nil, sess, test.result); err != nil {
71 t.Fatal(err)
72 }
73 if sess.Meta.Plan != nil {
74 t.Fatalf("plan = %#v, want nil", sess.Meta.Plan)
75 }
76 raw, err := os.ReadFile(sess.Path)
77 if err != nil {
78 t.Fatal(err)
79 }
80 if strings.Contains(string(raw), `"plan"`) {
81 t.Fatalf("session file gained a plan meta row: %s", raw)
82 }
83 })
84 }
85}
86
87func TestPersistPlanToolResultWriteFailureSurfacesSafeError(t *testing.T) {
88 sess := newPlanPersistSession(t)
89 if err := sess.Close(); err != nil {
90 t.Fatal(err)
91 }
92 err := persistToolResultState(nil, sess, planMutationResult([]core.PlanStep{{Step: "one", Status: core.PlanPending}}))
93 var safe *core.ToolResultCommitError
94 if !errors.As(err, &safe) {
95 t.Fatalf("persist error = %T %v, want safe tool result commit error", err, err)
96 }
97 if safe.Message != "plan state could not be saved" {
98 t.Fatalf("safe message = %q", safe.Message)
99 }
100}
101
102func TestPersistPlanToolResultWithoutSessionSurfacesSafeError(t *testing.T) {
103 err := persistToolResultState(nil, nil, planMutationResult([]core.PlanStep{{Step: "one", Status: core.PlanPending}}))
104 var safe *core.ToolResultCommitError
105 if !errors.As(err, &safe) {
106 t.Fatalf("persist error = %T %v, want safe tool result commit error", err, err)
107 }
108 if safe.Message != "plan state unavailable: session persistence is disabled" {
109 t.Fatalf("safe message = %q", safe.Message)
110 }
111}