mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheResolverAndWhatAsksItComposeOnOneMachine (0.03s)
mesh/delivery rejected: the gate failed or could not run, or the repository's own check failed
A refusal of a send the mesh never recorded went only to stderr; it is now kept with its sender said to be unknown, and raised. A counter behind the machine (a store put back) is raised and the condition names push, since the receive loop must not push. The node's digest and generation are one transaction, and a send record that fails is said loudly without failing the send. The trigger ignores an update that changes nothing, and the put-back mark is dropped: a put-back is composed afresh with the current generation.
208 lines
8.4 KiB
Go
208 lines
8.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|