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.
85 lines
4.0 KiB
Go
85 lines
4.0 KiB
Go
package inventory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/chain"
|
|
)
|
|
|
|
// **An assignment's key is `<node>.<module>`, as the bucket has always had it** (ADR 0408 §3): its readers parse nothing
|
|
// but it.
|
|
func TestAnAssignmentsKeyIsTheNodeAndTheModule(t *testing.T) {
|
|
if got := assignmentKey("anchor", "notes"); got != "anchor.notes" {
|
|
t.Fatalf("the key is %q", got)
|
|
}
|
|
if got := assignmentKey("g14.example", "dunst"); got != "g14.example.dunst" {
|
|
t.Fatalf("the key of a machine whose name holds a dot is %q", got)
|
|
}
|
|
}
|
|
|
|
// **A plan that is absent and a null one are one** (the provisioner's plan in an assignment's value): the pass that keeps
|
|
// the plans writes only where they differ.
|
|
func TestAnAbsentPlanAndANullOneAreTheSame(t *testing.T) {
|
|
for name, tc := range map[string]struct {
|
|
a, b json.RawMessage
|
|
same bool
|
|
}{
|
|
"both absent": {nil, nil, true},
|
|
"null and absent": {json.RawMessage(`null`), nil, true},
|
|
"the same plan": {json.RawMessage(`{"files":[]}`), json.RawMessage(` {"files":[]} `), true},
|
|
"another plan": {json.RawMessage(`{"files":[]}`), json.RawMessage(`{"files":[1]}`), false},
|
|
"a plan and none": {json.RawMessage(`{"files":[]}`), nil, false},
|
|
"none and a plan": {nil, json.RawMessage(`{"files":[]}`), false},
|
|
} {
|
|
if got := sameJSON(tc.a, tc.b); got != tc.same {
|
|
t.Errorf("%s: %v", name, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **A pass that keeps the plans never brings back an assignment that ended** (issue 1093): it reads what it wants and what
|
|
// the bucket holds in two reads, and an unassignment between them leaves a key that ended while the pass still wants it
|
|
// assigned.
|
|
func TestAPassThatKeepsThePlansNeverRevivesAnEndedAssignment(t *testing.T) {
|
|
at := time.Date(2026, 10, 18, 3, 0, 0, 0, time.UTC)
|
|
plan := json.RawMessage(`{"files":[]}`)
|
|
assigned := chain.Assignment{Node: "a", Module: "m", Assigned: at, Provisioning: plan}
|
|
ended := chain.Assignment{Node: "a", Module: "m", Assigned: at, Ended: at.Add(time.Minute)}
|
|
pulledIn := chain.Assignment{Node: "a", Module: "p", RequiredBy: "m", Provisioning: plan}
|
|
pulledInEnded := chain.Assignment{Node: "a", Module: "p", Ended: at}
|
|
waiting := chain.Assignment{Node: "a", Module: "p", State: chain.StatePending,
|
|
Pending: &chain.Pending{ID: "p-1", State: chain.PendingWaiting}}
|
|
for name, tc := range map[string]struct {
|
|
has bool
|
|
cur, next chain.Assignment
|
|
want string
|
|
}{
|
|
"the interleaving: wanted assigned, ended since": {true, ended, assigned, keepSkip},
|
|
"wanted assigned, not in the bucket yet": {false, chain.Assignment{}, assigned, keepSkip},
|
|
"a pulled-in module, not in the bucket": {false, chain.Assignment{}, pulledIn, keepCreate},
|
|
"a pulled-in module that ended, pulled again": {true, pulledInEnded, pulledIn, keepFresh},
|
|
"an assignment ended, a plan for it": {true, ended, chain.Assignment{Node: "a", Module: "m", RequiredBy: "x", Provisioning: plan}, keepSkip},
|
|
"a wait for a build, a plan for the module pulled in": {true, waiting, pulledIn, keepSkip},
|
|
"the plan is the same": {true, assigned, assigned, keepSkip},
|
|
"the plan changed": {true, chain.Assignment{Node: "a", Module: "m", Assigned: at}, assigned, keepField},
|
|
} {
|
|
if got := keepStep(tc.has, tc.cur, tc.next); got != tc.want {
|
|
t.Errorf("%s: %s, want %s", name, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **The store's mirror of the assignments is refused to its writer once the assignments are moved** (ADR 0408 §4, issue
|
|
// 1093): asked at the moment of the write, so a tick that decided before the switch lays nothing after it.
|
|
func TestTheStoresMirrorOfTheAssignmentsIsRefusedToItsWriterOnceTheyAreMoved(t *testing.T) {
|
|
inv := &Inventory{moved: &movedKinds{kind: map[string]bool{KindAssignments: true}}}
|
|
inv.UseChain(chain.InMemory(nil))
|
|
if _, err := inv.AssignmentsMirrorToWrite(context.Background()); !errors.Is(err, ErrAssignmentsMoved) {
|
|
t.Fatalf("the writer was given the store's rows: %v", err)
|
|
}
|
|
}
|