A wait for a build is the value of <node>.<module> with state pending and a pending object (id p-<unix nanoseconds>, a string), so every reader that skips a value with a state skips it, and the controller's own lists skip it through Stands. The transitions are compare-and-set on the key's revision (claim, release, settle, repoint, mark); assigning over a wait carries it into the value that stands, applied when it was waiting; a plan pass never overwrites it; one open wait per machine and module, and a key that holds the module refuses a new one. The table is no longer read or written, ForgetEndedPending is gone (no key is removed), and a wait of a machine the store dropped is none. Callers change type only; their conditions are the next issue.
64 lines
2.8 KiB
Go
64 lines
2.8 KiB
Go
package chain
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// **A pending value is not a standing assignment** (ADR 0261, ADR 0408, issue 1094): it names its node and module, holds no
|
|
// time assigned, says `state: pending`, and every reader that skips a value with a state skips it.
|
|
func TestAPendingValueIsNotAStandingAssignment(t *testing.T) {
|
|
since := time.Date(2026, 10, 18, 3, 0, 0, 0, time.UTC)
|
|
a := Assignment{Node: "anchor", Module: "notes", State: StatePending,
|
|
Pending: &Pending{ID: "p-1", State: PendingWaiting, Build: "build-1", Since: since}}
|
|
if a.Stands() {
|
|
t.Fatal("a pending value stands")
|
|
}
|
|
raw, _ := json.Marshal(a)
|
|
for _, want := range []string{`"node":"anchor"`, `"module":"notes"`, `"state":"pending"`, `"pending":{"id":"p-1","state":"waiting"`} {
|
|
if !strings.Contains(string(raw), want) {
|
|
t.Errorf("the value lacks %s: %s", want, raw)
|
|
}
|
|
}
|
|
if strings.Contains(string(raw), `"assigned"`) {
|
|
t.Errorf("a pending value names a time assigned: %s", raw)
|
|
}
|
|
// The wait carried into the value that stands: no state, so it stands, and the record stays.
|
|
made := Assignment{Node: "anchor", Module: "notes", Assigned: since, Pending: &Pending{ID: "p-1", State: PendingApplied}}
|
|
if !made.Stands() || !a.Pending.Open() || made.Pending.Open() {
|
|
t.Fatalf("stands %v, open before %v, open after %v", made.Stands(), a.Pending.Open(), made.Pending.Open())
|
|
}
|
|
var back Assignment
|
|
if err := json.Unmarshal(raw, &back); err != nil || back.Pending == nil || back.Pending.Build != "build-1" || !back.Pending.Since.Equal(since) {
|
|
t.Fatalf("read back %+v: %v", back, err)
|
|
}
|
|
now := since
|
|
raised := Pending{ID: "p-2", State: PendingExpired, Raised: &now, Acknowledged: &now, Cleared: &now}
|
|
body, _ := json.Marshal(raised)
|
|
if !strings.Contains(string(body), `"raised"`) || !strings.Contains(string(body), `"acknowledged"`) || !strings.Contains(string(body), `"cleared"`) {
|
|
t.Errorf("the marks of a wait: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestTheTransitionsOfAPendingAssignment(t *testing.T) {
|
|
ok := [][2]string{{PendingWaiting, PendingExpired}, {PendingWaiting, PendingWithdrawn}, {PendingApplying, PendingApplied},
|
|
{PendingApplying, PendingRefused}}
|
|
for _, p := range ok {
|
|
if !CanSettle(p[0], p[1]) {
|
|
t.Errorf("%s may end as %s", p[0], p[1])
|
|
}
|
|
}
|
|
for _, p := range [][2]string{{PendingWaiting, PendingApplied}, {PendingWaiting, PendingRefused}, {PendingApplying, PendingExpired},
|
|
{PendingApplying, PendingWithdrawn}, {PendingApplied, PendingExpired}, {PendingExpired, PendingWithdrawn},
|
|
{PendingWaiting, PendingWaiting}} {
|
|
if CanSettle(p[0], p[1]) {
|
|
t.Errorf("%s may not end as %s", p[0], p[1])
|
|
}
|
|
}
|
|
if !CanMove(PendingWaiting, PendingApplying) || !CanMove(PendingApplying, PendingWaiting) || CanMove(PendingApplied, PendingWaiting) {
|
|
t.Error("a claim moves a wait to applying and back")
|
|
}
|
|
}
|