104 lines
5.6 KiB
Go
104 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// A plan's tier is sent to each machine once, and judged there by one gate (novox/hq issue 281).
|
|
//
|
|
// On 2026-10-06 a merge rebuilt about seventy modules in one tier, and the plan sent the first machine
|
|
// one module at a time — one whole declaration per module, seconds apart. The node-engine set aside one
|
|
// declaration for the next, the node tools never settled, the mesh said the machine's core was behind,
|
|
// and the gate failed the module it happened to be judging for that machine-level condition; it then
|
|
// could not put the module back, having read as "the build it ran before" the very build an earlier send
|
|
// of the same tier had already carried there.
|
|
|
|
// tierMesh is a mesh with every module of one tier running on anchor and laptop at build c1, a newer
|
|
// build c2 of each registered, a plan whose one tier built them all, and every send recorded and
|
|
// answered: the machine applies what it is sent and reports it. A judging reads the store's reports, the
|
|
// node tools as answering everywhere, and the conditions the test opens.
|
|
type tierMesh struct {
|
|
open *stores
|
|
modules []string
|
|
sent [][]string
|
|
conds []conditions.Condition
|
|
keeper *conditions.Keeper
|
|
}
|
|
|
|
func (tm *tierMesh) plan(t *testing.T) inventory.Plan {
|
|
t.Helper()
|
|
p, err := tm.open.inventory.PlanByID(t.Context(), "plan-tier")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
// raise opens a condition about a machine itself, raised now: after the send.
|
|
func (tm *tierMesh) raise(machine, kind, summary string) {
|
|
tm.conds = append(tm.conds, conditions.Condition{Key: "machine." + machine + "." + kind, Kind: kind,
|
|
Subject: conditions.Subject{Scope: conditions.ScopeMachine, ID: machine, Machine: machine},
|
|
Raised: time.Now().Add(time.Second), Summary: summary, Source: "D10"})
|
|
}
|
|
|
|
// The incident's tier, small: a dozen modules whose first machine is the same are sent there in one
|
|
// send, judged by one gate, and the rest of their machines are sent once — two sends, not two dozen.
|
|
|
|
// The core behind about the node tools, while the node tools are themselves among what was sent and
|
|
// settling, is the node tools' — inside their settle window, the gate's bound — and no failure of the
|
|
// modules sent with them: at the bound only the node tools are put back.
|
|
|
|
// A machine-level condition about anything else holds the machine back as a whole: everything the send
|
|
// moved there fails together, once, with that reason — not one module for it.
|
|
|
|
// The machine-level conditions, sorted: one naming a module is that module's; the core behind is the
|
|
// core's when the send moved it and nobody's when it did not; anything else is the machine's as a whole.
|
|
func TestTheConditionsAboutAMachineItself(t *testing.T) {
|
|
withTheGate(t) // the first-machine gate\'s own path (novox/hq ADR 0360 keeps it for earlier walks)
|
|
since := time.Now().Add(-time.Minute)
|
|
about := func(kind, id string) conditions.Condition {
|
|
return conditions.Condition{Key: "machine." + id + "." + kind, Kind: kind, Raised: time.Now(),
|
|
Subject: conditions.Subject{Scope: conditions.ScopeMachine, ID: id, Machine: "anchor"}}
|
|
}
|
|
f := gateFacts{judged: true, open: []conditions.Condition{about(kindCoreBehind, "anchor")}}
|
|
if w := aboutTheMachine("anchor", []string{"app", "blueman"}, since, f); w.whole != "" || len(w.on) != 0 || len(w.facts.open) != 0 {
|
|
t.Errorf("the core behind, with no core component sent, counted against what was sent: %+v", w)
|
|
}
|
|
if w := aboutTheMachine("anchor", []string{"app", broker.RuntimeModule}, since, f); w.whole != "" ||
|
|
w.on[broker.RuntimeModule] == "" || w.on["app"] != "" {
|
|
t.Errorf("the core behind, with the node tools sent, is not theirs alone: %+v", w)
|
|
}
|
|
f.open = []conditions.Condition{about("x", "anchor.app")}
|
|
if w := aboutTheMachine("anchor", []string{"app", "other"}, since, f); w.on["app"] == "" || w.on["other"] != "" || w.whole != "" {
|
|
t.Errorf("a condition naming a module is not that module's: %+v", w)
|
|
}
|
|
f.open = []conditions.Condition{about("unreachable", "anchor")}
|
|
if w := aboutTheMachine("anchor", []string{"app"}, since, f); !strings.Contains(w.whole, "anchor as a whole") {
|
|
t.Errorf("a condition about the machine is not the machine's as a whole: %+v", w)
|
|
}
|
|
f.open[0].Raised = since.Add(-time.Hour)
|
|
if w := aboutTheMachine("anchor", []string{"app"}, since, f); w.whole != "" || len(w.facts.open) != 1 {
|
|
t.Errorf("a condition older than the send counted: %+v", w)
|
|
}
|
|
}
|
|
|
|
// A module its first send changed nothing of — an earlier send already carried this very build there —
|
|
// fails a gate as no verdict on its build: left as it was, never marked failed, nothing said as a
|
|
// rollback that failed; and `plans retry` asks it again rather than refusing a build that was never
|
|
// judged (the incident's "NOT put back: no build kept").
|
|
|
|
// **In a plan's tier path, a broken module is put back at once too** (novox/hq issue 318 review): the node
|
|
// tools' witness reverts them on the first machine. Whether the gate is kept on them or on the module beside
|
|
// them, they are registered back and marked in the judging that finds them broken, the plan goes on judging
|
|
// the other module to its own pass, and only then fails.
|
|
|
|
// **A send carrying the tool runner waits for its witness window** (issue 524 review, novox/hq ADR 0351
|
|
// decision 4): the leaf beside the node tools has its one reading at once, but the send is judged by the tool
|
|
// runner's rule: one reading, not before the node-engine's witness window for it has ended.
|