Make Complete atomic and guard zero deadline

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-05-31 11:49:03 -04:00
parent 18108c3da3
commit a8216095de
2 changed files with 33 additions and 6 deletions
+13 -6
View File
@@ -71,12 +71,15 @@ func (c *Controller) Deadline() time.Time {
func (c *Controller) stateLocked() State { func (c *Controller) stateLocked() State {
st := State{RuntimeState: c.runtimeState} st := State{RuntimeState: c.runtimeState}
if c.commitment != nil { if c.commitment != nil {
st.Commitment = &CommitmentView{ view := &CommitmentView{
NextAction: c.commitment.NextAction, NextAction: c.commitment.NextAction,
SuccessCondition: c.commitment.SuccessCondition, SuccessCondition: c.commitment.SuccessCondition,
TimeboxSecs: c.commitment.TimeboxSecs, TimeboxSecs: c.commitment.TimeboxSecs,
DeadlineUnixSecs: c.deadline.Unix(),
} }
if !c.deadline.IsZero() {
view.DeadlineUnixSecs = c.deadline.Unix()
}
st.Commitment = view
} }
return st return st
} }
@@ -124,7 +127,9 @@ func (c *Controller) StartManualCommitment(nextAction, successCondition string,
return c.persistLocked() return c.persistLocked()
} }
// Complete moves Active -> Review and marks the commitment completed. // Complete moves Active -> Review and marks the commitment completed. Both
// transitions are computed before any field is mutated, so a failure leaves
// state unchanged.
func (c *Controller) Complete() error { func (c *Controller) Complete() error {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@@ -132,12 +137,14 @@ func (c *Controller) Complete() error {
if err != nil { if err != nil {
return err return err
} }
c.runtimeState = next
if c.commitment != nil { if c.commitment != nil {
if s, e := statemachine.TransitionCommitment(c.commitment.State, statemachine.Complete); e == nil { completed, err := statemachine.TransitionCommitment(c.commitment.State, statemachine.Complete)
c.commitment.State = s if err != nil {
return err
} }
c.commitment.State = completed
} }
c.runtimeState = next
return c.persistLocked() return c.persistLocked()
} }
+20
View File
@@ -85,3 +85,23 @@ func TestStateRestoresFromSnapshot(t *testing.T) {
t.Fatalf("restored commitment missing: %+v", st.Commitment) t.Fatalf("restored commitment missing: %+v", st.Commitment)
} }
} }
func TestRestoredCommitmentKeepsDeadline(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.json")
first, err := New(path)
if err != nil {
t.Fatalf("new: %v", err)
}
_ = first.EnterPlanning()
_ = first.StartManualCommitment("Keep deadline", "done", 25*time.Minute)
want := first.State().Commitment.DeadlineUnixSecs
second, err := New(path)
if err != nil {
t.Fatalf("reopen: %v", err)
}
got := second.State().Commitment
if got == nil || got.DeadlineUnixSecs != want {
t.Fatalf("restored deadline: got %+v want %d", got, want)
}
}