"Behind" means not running what the mesh would send

It meant "failed or refused". So a machine that applied cleanly and whose
declaration has since changed was not behind — and novox/hq ADR 0010's
question, did my change go out?, was answerable exactly for the machines that
broke. For every machine that worked, the answer was silence whether the change
had gone out or not, which is the thing replacing a pipeline was supposed not
to cost.

The mesh now records a digest of what it last sent each machine. A digest
rather than the declaration: it can compute what a machine should be at any
moment, and keeping a copy would be a second account of it able to disagree
with the first. What cannot be recomputed is what was actually sent.

Recorded after the send, not before — a digest kept for something that failed
to send would make the machine look current for a declaration it never
received.

Never told stays separate from out of date. The remedy is the same push and the
situations are not alike: nobody has ever asked that machine to be anything.
And a machine the mesh could not work out is not reported as waiting, because
saying so would invent a comparison — that is `plan`'s answer to give.

`status` says it and `push --behind` sends it, or the flag would know something
the person reading the status does not.
This commit is contained in:
2026-08-31 05:17:21 +02:00
parent dfca21fa55
commit 5a28434ba8
4 changed files with 290 additions and 5 deletions
+90
View File
@@ -155,3 +155,93 @@ func TestWhatANodeSaidGoesWhenTheNodeDoes(t *testing.T) {
t.Fatalf("%d report(s) outlived the machine", left)
}
}
// "Behind" must mean not running what the mesh would send, not only "failed".
//
// novox/hq ADR 0010 names losing "did my change go out?" as the real risk of replacing a pipeline
// with a comparison. With behind meaning only failed-or-refused, that question was answerable
// exactly for the machines that broke — and for every machine that worked, the answer was silence
// whether the change had gone out or not.
func TestAMachineIsWaitingWhenWhatItWasSentIsNotWhatItShouldBe(t *testing.T) {
inv := ForTest(t)
ctx := t.Context()
anchor, err := inv.AddNode(ctx, "anchor")
if err != nil {
t.Fatal(err)
}
if _, err := inv.AddNode(ctx, "laptop"); err != nil {
t.Fatal(err)
}
// Never sent anything: waiting, and said differently. Nobody has ever asked it to be
// anything, which is not the same as it being out of date.
waiting, err := inv.Waiting(ctx, map[string]string{"anchor": "aaa", "laptop": "bbb"})
if err != nil {
t.Fatal(err)
}
if len(waiting) != 2 {
t.Fatalf("machines that were never sent anything are not waiting: %+v", waiting)
}
for _, m := range waiting {
if !m.Never {
t.Fatalf("%s was never sent anything and does not say so: %+v", m.Node, m)
}
}
// Sent what it should be: not waiting.
if err := inv.RecordSent(ctx, anchor.ID, "aaa"); err != nil {
t.Fatal(err)
}
waiting, err = inv.Waiting(ctx, map[string]string{"anchor": "aaa", "laptop": "bbb"})
if err != nil {
t.Fatal(err)
}
if len(waiting) != 1 || waiting[0].Node != "laptop" {
t.Fatalf("a machine sent exactly what it should be is still waiting: %+v", waiting)
}
// The declaration changes: waiting again, and no longer "never".
waiting, err = inv.Waiting(ctx, map[string]string{"anchor": "ccc", "laptop": "bbb"})
if err != nil {
t.Fatal(err)
}
var found bool
for _, m := range waiting {
if m.Node != "anchor" {
continue
}
found = true
if m.Never {
t.Fatal("a machine that has been sent something is reported as never told")
}
if m.Sent != "aaa" {
t.Fatalf("what it was last sent was lost: %+v", m)
}
}
if !found {
t.Fatal("a machine whose declaration changed since it was sent is not waiting")
}
}
// A machine nobody worked out is not reported as waiting: saying so would invent a comparison.
func TestAMachineWithNothingComputedForItIsNotWaiting(t *testing.T) {
inv := ForTest(t)
ctx := t.Context()
node, err := inv.AddNode(ctx, "unresolvable")
if err != nil {
t.Fatal(err)
}
// It has been sent something before, which is what makes this the case the guard is for: a
// machine with a digest and nothing computed for it would compare against the empty string
// and look out of date, when the truth is that nobody worked out what it should be.
if err := inv.RecordSent(ctx, node.ID, "what-it-got-last-time"); err != nil {
t.Fatal(err)
}
waiting, err := inv.Waiting(ctx, map[string]string{})
if err != nil {
t.Fatal(err)
}
if len(waiting) != 0 {
t.Fatalf("a machine the caller could not work out was reported as waiting: %+v", waiting)
}
}