Make Complete atomic and guard zero deadline
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user