Files
mesh-controller/internal/inventory/chain_assignments_key_test.go
T
jschoubben 88ea361bf6 Fold a pending assignment into the value of its assignment key (issue 1094)
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.
2026-10-18 05:17:30 +02:00

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)
}
}