On 2026-10-02 a runtime assigned and applied on two machines was undone two seconds later by a declaration that had the assignments of a minute earlier. Every path composes from the records at compose time and holds the machines it sends — but the number went on at SEND time, after composing, so a declaration composed before an assignment changed and sent after a newer one carried the higher number, and the host, which rightly refuses a lower number, took the older content as the mesh's newest word. The record of that send was never written either: it is written after the declaration is away, on the sender's context, and the controller sending it was being replaced in that very second — status read "applied, current" over a machine just told otherwise. Now the number is taken before the composition reads anything, in every path, so what was composed earlier is numbered lower however late it goes out and the host's refusal does what it is for; and what was sent is written down on a context that outlives the sender, bounded, so a dying controller still records what it told a machine. The `declare` command — a declaration a person sends by hand — records its send too. Proven: compositions in one order and sends in the other keep the numbers in composition order; a send is recorded after the sender's context is cancelled.
83 lines
3.3 KiB
Go
83 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// One machine that cannot be worked out is not a reason to leave the mesh unconverged.
|
|
//
|
|
// A whole-mesh push refused outright the moment any single node failed to resolve, so a module on
|
|
// the anchor requiring a provision nobody had assigned a provider for stopped every OTHER machine
|
|
// from being sent anything — machines with no relation to the fault, and nothing wrong with them.
|
|
// The failure and the punishment were on different machines.
|
|
//
|
|
// It is the same rule an un-hostable module already follows one level down (a92c11b: one module on
|
|
// the wrong machine no longer refuses the whole node), applied one level up.
|
|
func TestOneUnresolvableNodeStillLetsTheRestBeSent(t *testing.T) {
|
|
sending, refusals := composeEach(
|
|
[]string{"anchor", "home-server", "laptop"}, numbered(),
|
|
func(node string) (sendable, error) {
|
|
if node == "anchor" {
|
|
return sendable{}, errors.New(`nothing provides "acme-ca", wanted by route-proxy`)
|
|
}
|
|
return sendable{Resources: []map[string]any{{"id": node + ".thing"}}}, nil
|
|
})
|
|
|
|
var told []string
|
|
for _, s := range sending {
|
|
told = append(told, s.node)
|
|
}
|
|
if strings.Join(told, ",") != "home-server,laptop" {
|
|
t.Errorf("a machine with nothing wrong with it was not sent: %v", told)
|
|
}
|
|
if len(refusals) != 1 || !strings.Contains(refusals[0], "anchor") ||
|
|
!strings.Contains(refusals[0], "acme-ca") {
|
|
t.Errorf("the machine that could not be worked out was not named with its reason: %v",
|
|
refusals)
|
|
}
|
|
}
|
|
|
|
// A machine whose declaration composes to nothing is SENT the empty declaration, not skipped
|
|
// (novox/hq issue 127): it may have held something before, and only sending the empty
|
|
// declaration tells it to drop what the mesh owned. It is never a refusal.
|
|
func TestAnEmptyDeclarationIsSentSoTheNodeDropsWhatItHeld(t *testing.T) {
|
|
sending, refusals := composeEach([]string{"spare"}, numbered(),
|
|
func(string) (sendable, error) { return sendable{}, nil })
|
|
if len(sending) != 1 || len(refusals) != 0 {
|
|
t.Errorf("an empty declaration must be sent, not skipped or refused: %v / %v", sending, refusals)
|
|
}
|
|
}
|
|
|
|
// A push that skipped a machine still ends badly, and says what was sent.
|
|
//
|
|
// **Skipping is not succeeding.** The mesh is not in the state somebody asked for, so the command
|
|
// exits non-zero — but it says how many machines it did reach, because the old message ("nothing
|
|
// was sent") was the very claim that had become untrue.
|
|
func TestASkippedMachineIsStillAnError(t *testing.T) {
|
|
if err := couldNotBeResolved(nil, 3); err != nil {
|
|
t.Fatalf("a push that resolved every machine reported a problem: %v", err)
|
|
}
|
|
|
|
err := couldNotBeResolved([]string{"anchor:\nnothing provides \"acme-ca\""}, 2)
|
|
if err == nil {
|
|
t.Fatal("a push that could not work out a machine reported success")
|
|
}
|
|
said := err.Error()
|
|
if strings.Contains(said, "nothing was sent") {
|
|
t.Errorf("the push says nothing was sent, and it sent two machines: %q", said)
|
|
}
|
|
for _, want := range []string{"anchor", "acme-ca", "2 other node(s) were"} {
|
|
if !strings.Contains(said, want) {
|
|
t.Errorf("the refusal does not say %q: %q", want, said)
|
|
}
|
|
}
|
|
}
|
|
|
|
// numbered is an allotter for tests: one higher per call, as the inventory's is per machine.
|
|
func numbered() func(string) (int64, error) {
|
|
var n int64
|
|
return func(string) (int64, error) { n++; return n, nil }
|
|
}
|