package inventory import ( "errors" "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 the generation // written with the digest, 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{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.RecordSentWith(ctx, record.ID, "d11", nil, 57, 40, first); err != nil { t.Fatal(err) } stale := Send{Sequence: 12, Epoch: 57, Sender: "a one-shot push by jochen at a shell on anchor", Generation: 38, Digest: "d12", Modules: []string{"docker"}} if err := inv.RecordSentWith(ctx, record.ID, "d12", nil, 57, 38, 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 || !last.Recorded || !slices.Equal(last.Modules, []string{"docker"}) || last.SentAt.IsZero() { t.Fatalf("the last send reads %+v", last) } if generation, err := inv.SentGeneration(ctx, record.ID); err != nil || generation != 38 { t.Fatalf("what the machine was last sent of it reads %d (%v)", generation, err) } refused, found, err := inv.RefusedSend(ctx, record.ID, 12, 40, 0) if err != nil || !found || refused.Sender != stale.Sender || refused.RefusedApplied != 40 || refused.CounterRaisedTo != 0 { 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, 0); err != nil || found { t.Errorf("a refusal of a send never recorded was found: %v, %v", found, err) } } // **A refusal of a sequence the mesh has no send for is kept too, its sender unknown**, so S20 raises it; // it is not the machine's last send, and it says what the counter was raised to. func TestARefusalOfASendNeverRecordedIsKeptWithAnUnknownSender(t *testing.T) { inv, node := aNodeWithModules(t) ctx := t.Context() record, err := inv.NodeByName(ctx, node) if err != nil { t.Fatal(err) } kept, err := inv.RecordUnrecordedRefusal(ctx, Send{Node: record.ID, Sequence: 99, Epoch: 57, Generation: 41, Digest: "d99", RefusedApplied: 44, CounterRaisedTo: 45}) if err != nil { t.Fatal(err) } if kept.Recorded || kept.Sender != unknownSender || kept.RefusedAt == nil || kept.CounterRaisedTo != 45 || kept.NodeName != node { t.Fatalf("the refusal reads %+v", kept) } since, err := inv.RefusedSendsSince(ctx, time.Now().Add(-time.Hour)) if err != nil || len(since) != 1 || since[0].Sequence != 99 { t.Fatalf("the refused sends within the hour read %+v (%v)", since, err) } if _, found, err := inv.LastSend(ctx, node); err != nil || found { t.Errorf("a refusal of a send never recorded reads as the machine's last send: %v, %v", found, err) } } // **The machine's record and the send's are written together, and a send record that cannot be written // leaves the machine's record standing and says so**: the digest and generation are the machine's, and a // send that is away must not read as failed, nor the machine as behind. func TestASendRecordThatCannotBeWrittenLeavesTheMachinesRecord(t *testing.T) { inv, node := aNodeWithModules(t) ctx := t.Context() record, err := inv.NodeByName(ctx, node) if err != nil { t.Fatal(err) } // A NUL byte is text PostgreSQL refuses: the send's own row cannot be written. broken := Send{Sequence: 13, Sender: "a test", Generation: 42, Digest: "d\x0013"} err = inv.RecordSentWith(ctx, record.ID, "d13", nil, 0, 42, broken) var unrecorded *SendNotRecordedError if !errors.As(err, &unrecorded) { t.Fatalf("a send record that could not be written read as %v", err) } if generation, err := inv.SentGeneration(ctx, record.ID); err != nil || generation != 42 { t.Fatalf("the machine's generation was not written with its digest: %d (%v)", generation, err) } if sent, err := inv.Outstanding(ctx, node); err != nil || sent != "d13" { t.Fatalf("the machine's digest was not written: %q (%v)", sent, err) } } // **An update that leaves an assignment as it was raises nothing** (the trigger's WHEN). func TestAnUpdateThatChangesNoAssignmentRaisesNothing(t *testing.T) { inv, node := aNodeWithModules(t, "web") ctx := t.Context() if _, err := inv.Assign(ctx, node, "web"); err != nil { t.Fatal(err) } before := generationNow(t, inv) if _, err := inv.store.Pool().Exec(ctx, `update assignment set module = module`); err != nil { t.Fatal(err) } if after := generationNow(t, inv); after != before { t.Errorf("an update that changed nothing moved the generation from %d to %d", before, after) } } // **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) } } }