diff --git a/internal/session/session.go b/internal/session/session.go index e9ecddc..53207e7 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -71,12 +71,15 @@ func (c *Controller) Deadline() time.Time { func (c *Controller) stateLocked() State { st := State{RuntimeState: c.runtimeState} if c.commitment != nil { - st.Commitment = &CommitmentView{ + view := &CommitmentView{ NextAction: c.commitment.NextAction, SuccessCondition: c.commitment.SuccessCondition, TimeboxSecs: c.commitment.TimeboxSecs, - DeadlineUnixSecs: c.deadline.Unix(), } + if !c.deadline.IsZero() { + view.DeadlineUnixSecs = c.deadline.Unix() + } + st.Commitment = view } return st } @@ -124,7 +127,9 @@ func (c *Controller) StartManualCommitment(nextAction, successCondition string, 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 { c.mu.Lock() defer c.mu.Unlock() @@ -132,12 +137,14 @@ func (c *Controller) Complete() error { if err != nil { return err } - c.runtimeState = next if c.commitment != nil { - if s, e := statemachine.TransitionCommitment(c.commitment.State, statemachine.Complete); e == nil { - c.commitment.State = s + completed, err := statemachine.TransitionCommitment(c.commitment.State, statemachine.Complete) + if err != nil { + return err } + c.commitment.State = completed } + c.runtimeState = next return c.persistLocked() } diff --git a/internal/session/session_test.go b/internal/session/session_test.go index fe09cc0..37b37a9 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -85,3 +85,23 @@ func TestStateRestoresFromSnapshot(t *testing.T) { 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) + } +}