Node-engine composer: a module new to the node is held until the seat list was read, unless its seats are bootstrapped; the read hands it (H-7, ADR 0636 §1; issue 1966)

This commit is contained in:
2026-11-05 14:17:28 +01:00
parent 66fe7c8a0d
commit a222db8c69
2 changed files with 190 additions and 0 deletions
+81
View File
@@ -1750,3 +1750,84 @@ func setFor[T any](t *testing.T, p *T, v T) {
*p = v
t.Cleanup(func() { *p = old })
}
type gate struct{ open atomic.Bool }
func (g *gate) MayAct(...string) bool { return g.open.Load() }
// ADR 0636 §1: a node that has not read the seat list composes nothing new of a module whose seats are not bootstrapped,
// says it holds the module (so its have says why) and hands it at the read, which the reader says as an event; what
// already runs with the same composition is not held.
func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) {
b := newFakeBus()
fp := delivery.SourcePrefix2 + strings.Repeat("a", 64)
digest := delivery.ManifestDigest([]byte(lemurs))
man, _ := delivery.EncodeManifest(delivery.Manifest{Module: "lemurs", JSON: lemurs})
key, _ := delivery.ManifestsKey(digest)
b.kv[delivery.ManifestsBucket+"/"+key] = man
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
env := b.env()
g := &gate{}
changed := make(chan struct{}, 1)
env.SeatList, env.SeatListChanged = g, changed
var mu sync.Mutex
heldBy := map[string]string{}
env.Refused = func(_ string, held map[string]string) { mu.Lock(); heldBy = held; mu.Unlock() }
c := New(env)
out, err := c.Watch(ctx, "n1.>")
if err != nil {
t.Fatal(err)
}
place, _ := delivery.EncodePlacement(delivery.Placement{Node: "n1", Decided: delivery.NodeDecided{Hub: true, Address: "10.77.0.1"},
Modules: []delivery.PlacedModule{{Module: "lemurs"}}})
have, _ := delivery.EncodeHaveNode(delivery.HaveNode{Record: &delivery.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
mod, _ := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: digest, Repository: "novox/x", Commit: "c"})
want, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true})
nodeWant, _ := delivery.EncodeNodeWant(delivery.NodeWant{Self: true})
b.put(t, delivery.PlacementBucket, "placement.n1", place)
b.put(t, delivery.HaveBucket, "n1.node", have)
b.put(t, delivery.ModulesBucket, "lemurs", mod)
b.put(t, delivery.AssignerWantsBucket, "want.n1.lemurs", want)
b.put(t, delivery.AssignerWantsBucket, "want.n1.node", nodeWant)
for _, bucket := range []string{delivery.AssignerWantsBucket, delivery.PlacementBucket, delivery.HaveBucket, delivery.ModulesBucket, ResolvedBucket, delivery.BuilderBuildsBucket} {
b.send(bucket, agent.Update{Caught: true})
}
handed := map[string]bool{}
for {
u := next(t, out)
if u.Caught {
break
}
handed[u.Key] = true
}
mu.Lock()
why := heldBy["lemurs"]
mu.Unlock()
if why != WaitsForTheSeatList {
t.Errorf("lemurs is held with %q, want %q", why, WaitsForTheSeatList)
}
for k := range handed {
if strings.Contains(k, "lemurs") {
t.Errorf("the node handed %s before it read the seat list", k)
}
}
if !handed["n1.node"] {
t.Errorf("the node's own composition was held with the module: %v", handed)
}
// The reader reads the list and says so: the module is handed, with no timer.
g.open.Store(true)
changed <- struct{}{}
for {
u := next(t, out)
if strings.Contains(u.Key, "lemurs") && !u.Deleted {
break
}
}
mu.Lock()
still := heldBy["lemurs"]
mu.Unlock()
if still != "" {
t.Errorf("lemurs is still held after the read: %q", still)
}
}