The controller asks no build of anybody (ADR 0501 §5, ADR 0525 clause 6, ADR 0503 §3). Every module of a walk is taken from the builder's done key; a key not done yet waits, named in the plan, and never fails it. - C2b/C2c: internal/builder and cmd/mesh-controller's asks go (askABuild, onlyTheBuildersOwn, buildOneAsked, takeIn from an outcome, buildAgentsOwn, the builder's say and its fallback, the daemon hearing an outcome). The server follows the retired seats' built subjects only to let go of them, because the installer's carried user list names them. - C2d: replay, queue, cancel, clear, kill, pause, resume, delivery-check, check-here and merge-gate are refused by name. - C3: the check queues (CHECKS_READY, CHECKS_OTHER) and their workers, the checks-running and plans-building buckets and their grants, the build queue and its hold (plans_building.go, queue.go, link/holds.go, builds_nats.go, checks_nats.go). - C4: merge_gate*.go, the pull request's repo-check path, the Checked event and its layers; a pull request asks the controller nothing but to tell mesh-config what its head declares. - C5: the builds bucket is written only by key_takein. - C10 (help and refusals): the terminal's help lists only what is served. Integration tests of the plan's tiers, sends, stops and retries now run on a stand-in builder with a done key for every module (walk_keys_scaffold_integration_test.go); the tests of the asks, the queue, the merge gate and the replays are deleted.
128 lines
6.1 KiB
Go
128 lines
6.1 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// novox/hq issue 296: a plan saved a few seconds ago that has stood in its tier for forty minutes says
|
|
// forty minutes, and LATE — the line counted from the last save, which every advance made.
|
|
func TestAPlansLineCountsFromItsTierNotFromItsLastSave(t *testing.T) {
|
|
now := time.Date(2026, 10, 7, 20, 30, 0, 0, time.UTC)
|
|
asked := now.Add(-40 * time.Minute)
|
|
p := inventory.Plan{ID: "plan-t", Repository: "novox/mesh-catalog", Commit: "3ad22356", State: inventory.PlanBuilding,
|
|
Created: now.Add(-40 * time.Minute), Updated: now.Add(-3 * time.Second), TierEntered: now.Add(-40 * time.Minute),
|
|
Tiers: [][]string{{"a"}}, Modules: map[string]*inventory.PlanModule{"a": {State: "asked", AskedAt: &asked}}}
|
|
|
|
line := planLine(p, now)
|
|
if !strings.Contains(line, "building for 40m0s") || !strings.Contains(line, "LATE") {
|
|
t.Fatalf("a plan in its tier for 40m, saved 3s ago, reads %q", line)
|
|
}
|
|
st := planStatuses([]inventory.Plan{p}, now, nil)[0]
|
|
if !st.Late || !st.Since.Equal(p.TierEntered) {
|
|
t.Errorf("status --json says %+v", st)
|
|
}
|
|
if _, late := openPlans([]inventory.Plan{p}, now, nil); late != 1 {
|
|
t.Errorf("status counted %d late", late)
|
|
}
|
|
|
|
// LATE is the stalled condition said on the line: the same start and the same bound, measured or not.
|
|
bounds := boundsOf([]inventory.Duration{{Subject: p.Repository, Took: 20 * time.Minute}})
|
|
if got := bounds.of(p.Repository); got != time.Hour {
|
|
t.Fatalf("a repository whose tiers took 20m is given %s", got)
|
|
}
|
|
if got := bounds.of("novox/other"); got != tierAtLeast {
|
|
t.Fatalf("a repository with nothing measured is given %s", got)
|
|
}
|
|
for _, in := range []time.Duration{40 * time.Minute, 70 * time.Minute} {
|
|
q := p
|
|
q.TierEntered = now.Add(-in)
|
|
bound := bounds.of(q.Repository)
|
|
late := strings.Contains(planLineWith(q, now, bound), "LATE")
|
|
stalled := len(watchPlans(&signalFacts{now: now, plans: []planFacts{{id: q.ID, repository: q.Repository,
|
|
commit: q.Commit, tiers: 1, entered: inTierSince(q), bound: bound}}})) == 1
|
|
if late != stalled || late != (in > bound) {
|
|
t.Errorf("in its tier %s with a bound of %s: LATE %v, stalled %v", in, bound, late, stalled)
|
|
}
|
|
}
|
|
|
|
// A plan read without its tier's stamp counts from its last save, the only time it has.
|
|
old := p
|
|
old.TierEntered = time.Time{}
|
|
if line := planLine(old, now); !strings.Contains(line, "for 3s") || strings.Contains(line, "LATE") {
|
|
t.Errorf("a plan without its tier's stamp reads %q", line)
|
|
}
|
|
|
|
// A walk that waited for its delivery's word counts its wait from when it was opened, and its tier
|
|
// from the word.
|
|
waiting := p
|
|
waiting.Delivery = &inventory.PlanDelivery{Awaits: catalogue.DeliverySeat}
|
|
waiting.Modules = map[string]*inventory.PlanModule{}
|
|
if line := planLine(waiting, now); !strings.Contains(line, "for 40m0s") || strings.Contains(line, "LATE") {
|
|
t.Errorf("a walk waiting 40m reads %q", line)
|
|
}
|
|
if st := planStatuses([]inventory.Plan{waiting}, now, nil)[0]; st.Late {
|
|
t.Errorf("a waiting walk is called late: %+v", st)
|
|
}
|
|
let := now.Add(-5 * time.Minute)
|
|
gone := p
|
|
gone.Delivery = &inventory.PlanDelivery{Awaits: catalogue.DeliverySeat, Go: &let}
|
|
if line := planLine(gone, now); !strings.Contains(line, "building for 5m0s") || strings.Contains(line, "LATE") {
|
|
t.Errorf("a walk let go 5m ago, opened 40m ago, reads %q", line)
|
|
}
|
|
}
|
|
|
|
// novox/hq issue 296: a plan whose tier stands still is not written again by every advance — its revision,
|
|
// its last save and its `plan-moved` stay where its last change left them.
|
|
|
|
// novox/hq issue 920: a building plan whose current tier is all built waits on its note (a send refused at the
|
|
// gate), and its line, `status` and the stalled condition say that, not "building for ... LATE".
|
|
func TestAPlanLineSaysTheRefusalOnceItsTierIsBuilt(t *testing.T) {
|
|
now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
|
|
built := now.Add(-time.Hour)
|
|
refusal := "tier 1: a build waits there for its gate: novox (mesh-delivery-indexer d68d7ef3→a5aa69a4) — tried again"
|
|
mk := func(second string) inventory.Plan {
|
|
return inventory.Plan{ID: "plan-r", Repository: "novox/mesh-sdk", Commit: "b2f5ebb2", State: inventory.PlanBuilding,
|
|
Tier: 1, Created: now.Add(-2 * time.Hour), Updated: now, TierEntered: now.Add(-time.Hour), Note: refusal,
|
|
Tiers: [][]string{{"a"}, {"b", "c"}},
|
|
Modules: map[string]*inventory.PlanModule{"a": {State: "built", BuiltAt: &built},
|
|
"b": {State: "built", BuiltAt: &built}, "c": {State: second}}}
|
|
}
|
|
|
|
// 920/AC1
|
|
p := mk("built")
|
|
line := planLine(p, now)
|
|
if !strings.Contains(line, "a build waits there for its gate: novox") || strings.Contains(line, "building for") {
|
|
t.Errorf("920/AC1: a plan with its tier built and a refusal reads %q", line)
|
|
}
|
|
st := planStatuses([]inventory.Plan{p}, now, nil)
|
|
if len(st) != 1 || !st[0].Late {
|
|
t.Errorf("920/AC1: status: %+v", st)
|
|
}
|
|
|
|
// 920/AC2
|
|
if line := planLine(mk("asked"), now); !strings.Contains(line, "building for 1h0m0s") {
|
|
t.Errorf("920/AC2: a plan with a tier module not built reads %q", line)
|
|
}
|
|
}
|
|
|
|
// 920/AC3: a tier move writes the previous tier's summary into the note; with the new tier all built, the line still
|
|
// says "building for", for that note is no refusal.
|
|
func TestAPlanLineDoesNotHoldOnATiersSummary(t *testing.T) {
|
|
now := time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
|
|
built := now.Add(-time.Hour)
|
|
for _, note := range []string{"failed or refused its send (apply-failing says it): novox", "tier 0 built; sent mesh-sdk to novox first, in one send"} {
|
|
p := inventory.Plan{ID: "plan-s", Repository: "novox/mesh-sdk", Commit: "b2f5ebb2", State: inventory.PlanBuilding,
|
|
Tier: 1, Created: now.Add(-2 * time.Hour), Updated: now, TierEntered: now.Add(-time.Hour), Note: note,
|
|
Tiers: [][]string{{"a"}, {"b"}},
|
|
Modules: map[string]*inventory.PlanModule{"a": {State: "built", BuiltAt: &built}, "b": {State: "built", BuiltAt: &built}}}
|
|
if line := planLine(p, now); !strings.Contains(line, "building for 1h0m0s") || strings.Contains(line, "held") {
|
|
t.Errorf("920/AC3: a plan with the note %q reads %q", note, line)
|
|
}
|
|
}
|
|
}
|