Files
mesh-controller/cmd/mesh-controller/new_module_on_merge_test.go
T
jschoubben 48d9f22fa5 Retire the controller's merges consumer and read merges from the forge bridge's stream, so a held merge can never again queue every merge or look like a deaf controller (issue 774)
The controller-merges consumer (no max-deliver, one at a time) held the de-spiegel merge e25e1beb from
10-29 09:39: keepForgeMerge put an announcement with its Paths into delivery-merges (16 KiB values), got
10054, retried "not now" forever, and 167 merges queued behind it. Every five-minute hold outran the deaf
bound, so core.controller.deaf flapped.

Merges are mesh-delivery's: the gate merges on the Approve and the forge bridge says each merge on its
stream FORGE (pull request, head, merge commit), kept a week. The controller's copy was a leftover, read
only at a moved module's registration (ADR 0420 §7, ADR 0432). That read now finds the merge on FORGE;
the consumer, its follow, its handler and its hold delays are removed, and every assertion removes the
retired consumer from the bus.

Kind fix for the watchdog: the event loop records when a message it held back is due again, and deafness
counts from then, so no hold longer than the bound (on any consumer) reads as deafness.

Quick fix: strip Paths from the kept announcement and add max-deliver with a dead letter. Thorough fix:
remove the duplicate duty and make the deaf watchdog hold-aware. Chose thorough: the controller is being
deprecated and the merges already have an owner; the hold-vs-deaf confusion is a kind, not one consumer.

Research 072 piece: 03-domains (Control shrinks to placement and composition; the delivery parts own
what a merge is) and milestone 3 (health signals stop sharing the controller's loop assumptions).
2026-10-30 18:05:46 +01:00

104 lines
4.6 KiB
Go

//go:build integration
package main
import (
"context"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-controller/internal/inventory"
)
// ADR 0501 §6 (issue 1851, A5): the live case of 2026-10-07 (novox/hq issue 300), on keys. A merge of the catalogue adds
// one module's directory and touches nothing else the mesh holds: nothing asks its first build, it opens no plan, and
// the module is registered from the builder's first done key of it, assigned nowhere, with no build request written.
func TestAMergeAddingAModuleEndsRegisteredFromItsKeyAndAsksNoBuild(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
noBuildAsked(t)
b := aBuilderOnTheBus(t)
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "networkmanager", Version: "1"},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/networkmanager", Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
watching, stop := context.WithCancel(context.Background())
t.Cleanup(stop)
watchNewModules(watching, open)
const commit = "3da80a4b00000000"
// The indexer writes the module's trunk value; the builder builds it, then its key is done.
fp := fpOf('7')
manifest := b.indexed(t, "systemd-resolved", "1", "novox/mesh-catalog", "modules/systemd-resolved", commit, fp)
b.built(t, "systemd-resolved", "1", fp, manifest, sdkdelivery.BuildBuilding)
time.Sleep(300 * time.Millisecond)
if _, known := inventoryHolds(t, inv, "systemd-resolved"); known {
t.Fatal("0501 §1: registered from a key still building")
}
if obs := newModuleObservations(time.Now().Add(time.Hour)); len(obs) != 1 || obs[0].Kind != kindNewModuleWaits {
t.Fatalf("0501 §6: the new module waiting on its key past its bound is named by %+v", obs)
}
b.built(t, "systemd-resolved", "1", fp, manifest, sdkdelivery.BuildDone)
becomes(t, 10*time.Second, "the new module registered", func() bool {
_, known := inventoryHolds(t, inv, "systemd-resolved")
return known
})
// The take-in forgets the wait just after the registration it made is seen.
becomes(t, 5*time.Second, "the registered module no longer named waiting", func() bool {
return len(newModuleObservations(time.Now().Add(time.Hour))) == 0
})
requests, err := inv.RequestsNamed(ctx, inventory.BuildRequest{Repository: "novox/mesh-catalog",
Path: "modules/systemd-resolved"}.Name())
if err != nil || len(requests) != 0 {
t.Fatalf("0501 §6: a build request was written: %+v (%v)", requests, err)
}
}
// ADR 0501 §7: a new module whose manifest uses a word this controller does not read is named waiting at once, never
// failed, and registered from the same key, with no second build, when a controller that reads it looks.
func TestANewModuleWithAnUnreadWordWaitsForTheControllerThatReadsIt(t *testing.T) {
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
noBuildAsked(t)
b := aBuilderOnTheBus(t)
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "networkmanager", Version: "1"},
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/networkmanager", Ref: "main",
BuiltFrom: "c0", Head: "c0"}); err != nil {
t.Fatal(err)
}
fp := fpOf('8')
raw := `{"module":"watchman","version":"1","not-a-word-yet":"5m","build":{"artifacts":[{"name":"a","kind":"archive","from":"files"}]},` +
`"resources":[{"id":"a","type":"archive","path":"/opt/watchman","artifact":"a"}]}`
digest := sdkdelivery.ManifestDigest([]byte(raw))
value, _ := sdkdelivery.EncodeManifest(sdkdelivery.Manifest{Module: "watchman", JSON: raw})
if _, err := b.manifests.Put(ctx, digest, value); err != nil {
t.Fatal(err)
}
trunk, _ := sdkdelivery.EncodeModule(sdkdelivery.Module{FP: "src1:" + fp[5:], FP2: fp, Manifest: digest,
Repository: "novox/mesh-catalog", Commit: "4444444400000000", Path: "modules/watchman"})
if _, err := b.modules.Put(ctx, "watchman", trunk); err != nil {
t.Fatal(err)
}
b.built(t, "watchman", "1", fp, digest, sdkdelivery.BuildDone)
takeNewModules(ctx, open, "watchman")
if _, known := inventoryHolds(t, inv, "watchman"); known {
t.Fatal("0501 §7: a manifest with an unread word was registered")
}
obs := newModuleObservations(time.Now())
if len(obs) != 1 || obs[0].Kind != kindNewModuleWaits || !strings.Contains(obs[0].Summary, `"not-a-word-yet"`) {
t.Fatalf("0501 §7: the module waiting for a reader of its word is named by %+v", obs)
}
builds, _ := inv.Builds(ctx, "watchman", 5)
for _, b := range builds {
if !b.Worked() {
t.Fatalf("0501 §7: a failed build was recorded: %+v", b)
}
}
}