detached dd0b211

test(goals): verify write rollback

commit
dd0b211768fb992ec8746a3d533ce238163049ab
author
brice · · 2026-08-14 06:40 UTC
parent
d395d30
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +27 -5
M packages/agent/cli_session_test.go +27 -5
@@ -114,7 +114,9 @@ func TestToolResultPersistenceUsesBoundSession(t *testing.T) {
114114 func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
115115 const privateGoal = "private goal text"
116116 const privateMission = "private-mission-id"
117+ const privateReason = "private active reason"
117118 const privateSessionContent = "private session content"
119+ var writeFailureGoal core.SessionGoal
118120
119121 newSession := func(t *testing.T) *core.Session {
120122 t.Helper()
@@ -130,10 +132,11 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
130132 }
131133
132134 tests := []struct {
133- name string
134- setup func(*testing.T) *core.Session
135- update tools.GoalUpdate
136- message string
135+ name string
136+ setup func(*testing.T) *core.Session
137+ update tools.GoalUpdate
138+ message string
139+ assertUnchanged func(*testing.T, *core.Session)
137140 }{
138141 {
139142 name: "active goal",
@@ -175,9 +178,10 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
175178 name: "write failure",
176179 setup: func(t *testing.T) *core.Session {
177180 sess := newSession(t)
178- if err := sess.UpdateGoal(&core.SessionGoal{Objective: privateGoal, Status: core.GoalActive}); err != nil {
181+ if err := sess.UpdateGoal(&core.SessionGoal{Objective: privateGoal, Status: core.GoalActive, Reason: privateReason}); err != nil {
179182 t.Fatal(err)
180183 }
184+ writeFailureGoal = *sess.Meta.Goal
181185 if err := sess.Close(); err != nil {
182186 t.Fatal(err)
183187 }
@@ -185,6 +189,21 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
185189 },
186190 update: tools.GoalUpdate{Status: core.GoalDone},
187191 message: "goal state could not be saved",
192+ assertUnchanged: func(t *testing.T, sess *core.Session) {
193+ t.Helper()
194+ assertGoal := func(source string, goal *core.SessionGoal) {
195+ t.Helper()
196+ if goal == nil || goal.Status != writeFailureGoal.Status || goal.Objective != writeFailureGoal.Objective || goal.Reason != writeFailureGoal.Reason {
197+ t.Fatalf("%s goal after failed update = %#v, want status=%q objective=%q reason=%q", source, goal, writeFailureGoal.Status, writeFailureGoal.Objective, writeFailureGoal.Reason)
198+ }
199+ }
200+ assertGoal("in-memory", sess.Meta.Goal)
201+ snapshot, err := core.ReadSessionSnapshot(sess.Path)
202+ if err != nil {
203+ t.Fatal(err)
204+ }
205+ assertGoal("persisted", snapshot.Meta.Goal)
206+ },
188207 },
189208 }
190209 for _, test := range tests {
@@ -201,6 +220,9 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
201220 if strings.Contains(safe.Message, privateGoal) || strings.Contains(safe.Message, privateMission) || strings.Contains(safe.Message, privateSessionContent) || (sess != nil && strings.Contains(safe.Message, sess.Path)) {
202221 t.Fatalf("safe message leaked private state: %q", safe.Message)
203222 }
223+ if test.assertUnchanged != nil {
224+ test.assertUnchanged(t, sess)
225+ }
204226 })
205227 }
206228 }
@@ -114,7 +114,9 @@ func TestToolResultPersistenceUsesBoundSession(t *testing.T) {
114 func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {114 func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
115 const privateGoal = "private goal text"115 const privateGoal = "private goal text"
116 const privateMission = "private-mission-id"116 const privateMission = "private-mission-id"
117+ const privateReason = "private active reason"
117 const privateSessionContent = "private session content"118 const privateSessionContent = "private session content"
119+ var writeFailureGoal core.SessionGoal
118 120
119 newSession := func(t *testing.T) *core.Session {121 newSession := func(t *testing.T) *core.Session {
120 t.Helper()122 t.Helper()
@@ -130,10 +132,11 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
130 }132 }
131 133
132 tests := []struct {134 tests := []struct {
133- name string135+ name string
134- setup func(*testing.T) *core.Session136+ setup func(*testing.T) *core.Session
135- update tools.GoalUpdate137+ update tools.GoalUpdate
136- message string138+ message string
139+ assertUnchanged func(*testing.T, *core.Session)
137 }{140 }{
138 {141 {
139 name: "active goal",142 name: "active goal",
@@ -175,9 +178,10 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
175 name: "write failure",178 name: "write failure",
176 setup: func(t *testing.T) *core.Session {179 setup: func(t *testing.T) *core.Session {
177 sess := newSession(t)180 sess := newSession(t)
178- if err := sess.UpdateGoal(&core.SessionGoal{Objective: privateGoal, Status: core.GoalActive}); err != nil {181+ if err := sess.UpdateGoal(&core.SessionGoal{Objective: privateGoal, Status: core.GoalActive, Reason: privateReason}); err != nil {
179 t.Fatal(err)182 t.Fatal(err)
180 }183 }
184+ writeFailureGoal = *sess.Meta.Goal
181 if err := sess.Close(); err != nil {185 if err := sess.Close(); err != nil {
182 t.Fatal(err)186 t.Fatal(err)
183 }187 }
@@ -185,6 +189,21 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
185 },189 },
186 update: tools.GoalUpdate{Status: core.GoalDone},190 update: tools.GoalUpdate{Status: core.GoalDone},
187 message: "goal state could not be saved",191 message: "goal state could not be saved",
192+ assertUnchanged: func(t *testing.T, sess *core.Session) {
193+ t.Helper()
194+ assertGoal := func(source string, goal *core.SessionGoal) {
195+ t.Helper()
196+ if goal == nil || goal.Status != writeFailureGoal.Status || goal.Objective != writeFailureGoal.Objective || goal.Reason != writeFailureGoal.Reason {
197+ t.Fatalf("%s goal after failed update = %#v, want status=%q objective=%q reason=%q", source, goal, writeFailureGoal.Status, writeFailureGoal.Objective, writeFailureGoal.Reason)
198+ }
199+ }
200+ assertGoal("in-memory", sess.Meta.Goal)
201+ snapshot, err := core.ReadSessionSnapshot(sess.Path)
202+ if err != nil {
203+ t.Fatal(err)
204+ }
205+ assertGoal("persisted", snapshot.Meta.Goal)
206+ },
188 },207 },
189 }208 }
190 for _, test := range tests {209 for _, test := range tests {
@@ -201,6 +220,9 @@ func TestPersistToolResultStateUsesSafeGoalPersistenceCategories(t *testing.T) {
201 if strings.Contains(safe.Message, privateGoal) || strings.Contains(safe.Message, privateMission) || strings.Contains(safe.Message, privateSessionContent) || (sess != nil && strings.Contains(safe.Message, sess.Path)) {220 if strings.Contains(safe.Message, privateGoal) || strings.Contains(safe.Message, privateMission) || strings.Contains(safe.Message, privateSessionContent) || (sess != nil && strings.Contains(safe.Message, sess.Path)) {
202 t.Fatalf("safe message leaked private state: %q", safe.Message)221 t.Fatalf("safe message leaked private state: %q", safe.Message)
203 }222 }
223+ if test.assertUnchanged != nil {
224+ test.assertUnchanged(t, sess)
225+ }
204 })226 })
205 }227 }
206 }228 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close