A declaration newer in sequence than every other named four fewer modules than the assignments held, a machine applied it, and nothing the mesh kept said who sent it or from what view. The store now raises an assignment generation in the same transaction as every assignment change (a trigger, so a cascade counts too); a send reads it before composing, carries it to engines that said they read it, marks a gate's put-back, and is recorded with its sequence, sender, generation and modules. The would-send is stamped with what was last sent, so nothing reads behind for it; a refusal is kept on the send it refused and raised as S20 naming the sender; plan says what the machine was last told.
139 lines
5.5 KiB
Go
139 lines
5.5 KiB
Go
package inventory
|
|
|
|
import (
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The assignment generation and the record of every send (novox/hq issue 234). On 2026-10-04 a declaration
|
|
// newer in sequence than every other named four fewer modules than the assignments held, a machine applied
|
|
// it, and nothing the mesh kept could say who sent it or from what view.
|
|
|
|
func generationNow(t *testing.T, inv *Inventory) int64 {
|
|
t.Helper()
|
|
g, err := inv.AssignmentGeneration(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return g
|
|
}
|
|
|
|
// **Every change to what is assigned where raises the generation, in its own transaction** — an
|
|
// assignment, an unassignment, and one taken by the machine's removal, which no writer in Go makes — and
|
|
// a repeated assignment, which changes nothing, does not.
|
|
func TestEveryAssignmentChangeRaisesTheGeneration(t *testing.T) {
|
|
inv, node := aNodeWithModules(t, "postgres", "web")
|
|
ctx := t.Context()
|
|
start := generationNow(t, inv)
|
|
if start < 1 {
|
|
t.Fatalf("the generation starts at %d; zero is \"none claimed\" on the wire", start)
|
|
}
|
|
if _, err := inv.Assign(ctx, node, "postgres"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assigned := generationNow(t, inv)
|
|
if assigned <= start {
|
|
t.Fatalf("an assignment left the generation at %d", assigned)
|
|
}
|
|
if _, err := inv.Assign(ctx, node, "postgres"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if again := generationNow(t, inv); again != assigned {
|
|
t.Errorf("an assignment repeated, which changed nothing, moved the generation from %d to %d", assigned, again)
|
|
}
|
|
if err := inv.Unassign(ctx, node, "postgres"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unassigned := generationNow(t, inv)
|
|
if unassigned <= assigned {
|
|
t.Fatalf("an unassignment left the generation at %d", unassigned)
|
|
}
|
|
if _, err := inv.Assign(ctx, node, "web"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before := generationNow(t, inv)
|
|
if _, err := inv.RemoveNodeForTest(ctx, node); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if removed := generationNow(t, inv); removed <= before {
|
|
t.Errorf("a machine's removal took its assignments and left the generation at %d", removed)
|
|
}
|
|
}
|
|
|
|
// **The generation never goes down, and is raised past what a machine applied when the mesh's own counter
|
|
// is behind it** — a store put back from a backup — so the next send is not refused for ever.
|
|
func TestTheGenerationIsRaisedPastWhatAMachineApplied(t *testing.T) {
|
|
inv, _ := aNodeWithModules(t)
|
|
now := generationNow(t, inv)
|
|
if raised, err := inv.RaiseAssignmentGeneration(t.Context(), now+40); err != nil || raised != now+41 {
|
|
t.Fatalf("raised past %d to %d (%v)", now+40, raised, err)
|
|
}
|
|
if raised, err := inv.RaiseAssignmentGeneration(t.Context(), 2); err != nil || raised != now+41 {
|
|
t.Fatalf("a raise below the counter moved it to %d (%v)", raised, err)
|
|
}
|
|
}
|
|
|
|
// **Every send is recorded: its sequence, its sender, the generation it came from and the modules it
|
|
// named** — and the machine's last send is what `plan` reads, the would-send is stamped with its
|
|
// generation and put-back mark, and a refusal is kept on the send it refused, naming its sender.
|
|
func TestASendIsRecordedWithItsSenderGenerationAndModules(t *testing.T) {
|
|
inv, node := aNodeWithModules(t)
|
|
ctx := t.Context()
|
|
record, err := inv.NodeByName(ctx, node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first := Send{Node: record.ID, Sequence: 11, Epoch: 57, Sender: "the controller's daemon (pid 7 on anchor)",
|
|
Generation: 40, Digest: "d11", Modules: []string{"docker", "pacman", "sudo"}}
|
|
if err := inv.RecordSend(ctx, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stale := Send{Node: record.ID, Sequence: 12, Epoch: 57, Sender: "a one-shot push by jochen at a shell on anchor",
|
|
Generation: 38, ToldGeneration: 38, Digest: "d12", Modules: []string{"docker"}}
|
|
if err := inv.RecordSend(ctx, stale); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
last, found, err := inv.LastSend(ctx, node)
|
|
if err != nil || !found {
|
|
t.Fatalf("the last send is not kept: %v", err)
|
|
}
|
|
if last.Sequence != 12 || last.Sender != stale.Sender || last.Generation != 38 ||
|
|
!slices.Equal(last.Modules, []string{"docker"}) || last.SentAt.IsZero() {
|
|
t.Fatalf("the last send reads %+v", last)
|
|
}
|
|
generation, putBack, err := inv.SentGeneration(ctx, record.ID)
|
|
if err != nil || generation != 38 || putBack {
|
|
t.Fatalf("what the machine was last sent of it reads %d, %v (%v)", generation, putBack, err)
|
|
}
|
|
|
|
refused, found, err := inv.RefusedSend(ctx, record.ID, 12, 40)
|
|
if err != nil || !found || refused.Sender != stale.Sender || refused.RefusedApplied != 40 {
|
|
t.Fatalf("the refusal is not kept on the send it refused: %+v, %v, %v", refused, found, err)
|
|
}
|
|
since, err := inv.RefusedSendsSince(ctx, time.Now().Add(-time.Hour))
|
|
if err != nil || len(since) != 1 || since[0].NodeName != node || since[0].Sequence != 12 {
|
|
t.Fatalf("the refused sends within the hour read %+v (%v)", since, err)
|
|
}
|
|
if _, found, err := inv.RefusedSend(ctx, record.ID, 99, 40); err != nil || found {
|
|
t.Errorf("a refusal of a send never recorded was found: %v, %v", found, err)
|
|
}
|
|
}
|
|
|
|
// **A machine that reads a generation is recorded from its reports**, and one rolled back says so no longer.
|
|
func TestWhetherAMachineReadsAGenerationIsItsLatestWord(t *testing.T) {
|
|
inv, node := aNodeWithModules(t)
|
|
record, err := inv.NodeByName(t.Context(), node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, reads := range []bool{false, true, false} {
|
|
if err := inv.RecordReadsGeneration(t.Context(), record.ID, reads); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, err := inv.ReadsGeneration(t.Context(), record.ID); err != nil || got != reads {
|
|
t.Fatalf("recorded %v, read %v (%v)", reads, got, err)
|
|
}
|
|
}
|
|
}
|