Two controllers could both act (issue 204), a reconcile's report could overtake the apply after it and the digest decided (issue 267), and a grant could make a second writer of a machine's report. - The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`, 15 s age, renewed every 5 s by compare-and-set; the epoch is the revision it was taken at. The gate is the clock (stops 3 s before expiry); a refused renewal is a loss and the process exits; a holder that stops gives it back. serve takes it before asserting the bus. Epochs kept in the store (migration 0068 controller_epoch) as a floor: a bucket raised from nothing is compacted past it. Unleased (no epoch, S12 urgent) only when nobody holds it and the bus will not let it be written. A shell command acts under the holder's epoch, or its own lease when none. - Declarations carry `epoch` inside the signed envelope, only to a machine whose latest account carried a report_sequence (mesh-host #35); would-send is composed with the epoch last sent. Allot and the send both pass the gate. - Reports: contract in internal/link/order.go (epoch, sequence, report_sequence, older_than, refused_older). Accounts kept by epoch, then sequence, then report sequence; older refused, counted; unordered reports keep the digest rule. Plans by compare-and-set on a revision, with epoch. Conditions and calls carry the epoch and are not written off the lease. - S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the bucket said. Writers table compiled in and enforced in PermissionsFor; the controller no longer publishes mesh.control.>. A contract per consumed kind, and the empty-on-error lint over the repository. - mesh-host pinned to its main with the epoch in the validator (D1 validates the envelope as sent). Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
100 lines
4.6 KiB
Go
100 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// novox/hq issue 254, ADR 0218: a newer plan takes over what the older open plans of its repository
|
|
// and branch had not built, and closes them as superseded; another repository's plan, another
|
|
// branch's, and a plan made after it are left alone.
|
|
func TestANewerPlanSupersedesTheOlderOpenPlansOfItsRepository(t *testing.T) {
|
|
at := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
|
|
sent := at.Add(time.Minute)
|
|
plan := func(id, repository, branch string, created time.Time, modules map[string]*inventory.PlanModule) inventory.Plan {
|
|
return inventory.Plan{ID: id, Repository: repository, Branch: branch, Commit: id + "-commit",
|
|
Created: created, State: inventory.PlanRolling, Modules: modules}
|
|
}
|
|
older := plan("plan-1", "novox/mesh-catalog", "main", at, map[string]*inventory.PlanModule{
|
|
"gitea": {State: "built", SentAt: &sent}, // done with: stays done
|
|
"keycloak": {State: "asked"}, // asked, not answered: folded
|
|
"plex": {}, // not yet asked: folded
|
|
"agent": {State: "built"}, // built, rolls out, not sent: folded
|
|
"notes": {State: "built"}, // built, records: nothing to send
|
|
})
|
|
stuck := plan("plan-0", "Novox/Mesh-Catalog", "", at.Add(-time.Hour), map[string]*inventory.PlanModule{
|
|
"runtime": {State: "asked"},
|
|
})
|
|
other := plan("plan-2", "novox/mesh-controller", "main", at, map[string]*inventory.PlanModule{"mesh-controller": {}})
|
|
release := plan("plan-3", "novox/mesh-catalog", "release", at, map[string]*inventory.PlanModule{"lemurs": {}})
|
|
later := plan("plan-5", "novox/mesh-catalog", "main", at.Add(2*time.Hour), map[string]*inventory.PlanModule{"later": {}})
|
|
done := plan("plan-6", "novox/mesh-catalog", "main", at, map[string]*inventory.PlanModule{"finished": {}})
|
|
done.State = inventory.PlanDone
|
|
|
|
newer := plan("plan-4", "novox/mesh-catalog", "main", at.Add(time.Hour), nil)
|
|
newer.Commit = "97b1b2b0c0ffee"
|
|
rollsOut := func(m string) bool { return m != "notes" }
|
|
folded, closed := supersededBy(newer, []inventory.Plan{stuck, older, other, release, later, done, newer}, rollsOut)
|
|
|
|
if want := []string{"agent", "keycloak", "plex", "runtime"}; !reflect.DeepEqual(folded, want) {
|
|
t.Fatalf("folded %v, wanted %v", folded, want)
|
|
}
|
|
var ids []string
|
|
for _, p := range closed {
|
|
ids = append(ids, p.ID)
|
|
if p.State != inventory.PlanSuperseded || p.Open() {
|
|
t.Errorf("%s was left %s", p.ID, p.State)
|
|
}
|
|
if !strings.Contains(p.Note, "plan-4") || !strings.Contains(p.Note, "97b1b2b0") {
|
|
t.Errorf("%s does not name the plan that superseded it: %q", p.ID, p.Note)
|
|
}
|
|
}
|
|
if want := []string{"plan-0", "plan-1"}; !reflect.DeepEqual(ids, want) {
|
|
t.Fatalf("superseded %v, wanted %v — another repository, another branch, a later plan and a "+
|
|
"finished one are left alone", ids, want)
|
|
}
|
|
if other.State != inventory.PlanRolling {
|
|
t.Fatal("the plan handed in was changed in place")
|
|
}
|
|
if line := planLine(closed[1], time.Now()); !strings.Contains(line, "superseded") {
|
|
t.Fatalf("a superseded plan reads %q", line)
|
|
}
|
|
}
|
|
|
|
// novox/hq issue 254: a person closes a plan that will not move again, by its id.
|
|
func TestAPersonClosesAStuckPlan(t *testing.T) {
|
|
open := aMesh(t)
|
|
ctx := t.Context()
|
|
stuck := inventory.Plan{ID: "plan-97b1b2b", Repository: "novox/mesh-catalog", Commit: "97b1b2b",
|
|
Created: time.Now().UTC(), State: inventory.PlanRolling, Tier: 1, Tiers: [][]string{{"a"}, {"b"}},
|
|
Modules: map[string]*inventory.PlanModule{"a": {State: "built"}, "b": {}}}
|
|
if err := open.inventory.SavePlan(ctx, &stuck); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := plansCommand(ctx, []string{"close", stuck.ID}); err == nil || !strings.Contains(err.Error(), "--why") {
|
|
t.Fatalf("a plan was closed by hand without saying why: %v", err)
|
|
}
|
|
if err := plansCommand(ctx, []string{"close", stuck.ID, "--why", "its report will not come"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
closed, err := open.inventory.PlanByID(ctx, stuck.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if closed.State != inventory.PlanFailed || !strings.Contains(closed.Note, "closed by hand") ||
|
|
!strings.Contains(closed.Note, "its report will not come") {
|
|
t.Fatalf("the plan was left %s: %q", closed.State, closed.Note)
|
|
}
|
|
if err := plansCommand(ctx, []string{"close", stuck.ID, "--why", "again"}); err == nil {
|
|
t.Fatal("a plan already closed was closed again")
|
|
}
|
|
if argv, err := argvFor("plans", map[string]any{"close": stuck.ID, "why": "w"}); err != nil ||
|
|
!reflect.DeepEqual(argv, []string{"plans", "close", stuck.ID, "--why", "w"}) {
|
|
t.Fatalf("the seat's verb does not close a plan: %v %v", argv, err)
|
|
}
|
|
}
|