SentBuilds answers from the keys of the node's assignments that name a commit, known when the node was sent anything and no key says its commit unknown. A send that names its builds writes the commit, build and rollout on each carried module's key (a module nothing assigns gets a standing key marked pulled-in, never a state) and clears what every other key of the node named; a send by hand writes sent-unknown, a time, on every key and clears their commits. A failed write of these fails the record of the send. node.sent_builds is no longer written. The plan pass ends a pulled-in key only when no send carries it.
88 lines
4.2 KiB
Go
88 lines
4.2 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")
|
|
}
|
|
}
|
|
|
|
// **A module only pulled in is a standing value marked pulled-in, never a state, and a hand send makes a key say its commit
|
|
// unknown with a time** (issue 1109): every reader treats a non-empty state as no assignment, so a pulled-in key must have
|
|
// none; and unknown is never written in `commit`.
|
|
func TestAPulledInValueStandsAndAHandSendIsATimeNotACommit(t *testing.T) {
|
|
at := time.Date(2026, 10, 18, 3, 0, 0, 0, time.UTC)
|
|
pulled := Assignment{Node: "anchor", Module: "mesh-wireguard", PulledIn: true, Commit: "c1", Build: "b1", Since: at}
|
|
if !pulled.Stands() || pulled.State != "" {
|
|
t.Fatalf("a pulled-in value does not stand: %+v", pulled)
|
|
}
|
|
raw, _ := json.Marshal(pulled)
|
|
if !strings.Contains(string(raw), `"pulled-in":true`) || strings.Contains(string(raw), `"state"`) || strings.Contains(string(raw), `"assigned"`) {
|
|
t.Fatalf("the pulled-in value is %s", raw)
|
|
}
|
|
unknown := Assignment{Node: "anchor", Module: "notes", Assigned: at, SentUnknown: at}
|
|
raw, _ = json.Marshal(unknown)
|
|
if !strings.Contains(string(raw), `"sent-unknown":"2026-10-18T03:00:00Z"`) || strings.Contains(string(raw), `"commit"`) || !unknown.Stands() {
|
|
t.Fatalf("the hand-sent value is %s", raw)
|
|
}
|
|
plain, _ := json.Marshal(Assignment{Node: "anchor", Module: "notes", Assigned: at})
|
|
if strings.Contains(string(plain), "pulled-in") || strings.Contains(string(plain), "sent-unknown") {
|
|
t.Fatalf("a value that is neither says so: %s", plain)
|
|
}
|
|
}
|