Node-engine test: a node whose own composition folds the holder of the seat mesh-catalog starts it before the list is read and composes the rest after (ADR 0636 §1; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:30:45 +01:00
parent e282e08917
commit 2bcc34a59b
+84
View File
@@ -1961,6 +1961,90 @@ func TestAnUnreadSeatListLetsThroughOfTheOwnCompositionOnlyWhatTheHolderOfTheCat
}
}
// ADR 0636 §1, the whole case: a node that runs two modules inside its own composition (a build of the old form each),
// one of them the holder of the seat mesh-catalog, and reads no seat list. Its own composition, handed before the read,
// carries the holder; the read, said as an event, hands the rest, the other module's resources, with no timer.
func TestTheHolderOfTheCatalogIsStartedFromTheOwnCompositionBeforeTheListIsReadAndTheRestAfter(t *testing.T) {
catalog := `{"module":"catalog","version":"1","claims":[{"name":"` + meshcatalog.Seat + `"}],"capabilities":["systemd"],` +
`"resources":[{"id":"service","type":"service","unit":"catalog.service","state":"running","boot":"enabled"}]}`
other := `{"module":"other","version":"1","capabilities":["systemd"],` +
`"resources":[{"id":"service","type":"service","unit":"other.service","state":"running","boot":"enabled"},` +
`{"id":"extra","type":"service","unit":"extra.service","state":"running","boot":"enabled"}]}`
b := newFakeBus()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
env := b.env()
g := &gate{}
changed := make(chan struct{}, 1)
env.SeatList, env.SeatListChanged = g, changed
env.Running = func() []compositions.Composed {
return []compositions.Composed{{Node: "n1", Resources: []json.RawMessage{json.RawMessage(`{"id":"other.service","type":"service","unit":"other.service","state":"running","boot":"enabled"}`)}}}
}
c := New(env)
out, err := c.Watch(ctx, "n1.>")
if err != nil {
t.Fatal(err)
}
mods := map[string]string{"catalog": catalog, "other": other}
var placed []meshmoduleassigner.PlacedModule
for _, m := range []string{"catalog", "other"} {
digest := meshcatalogclient.ManifestDigest([]byte(mods[m]))
man, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: m, Manifest: mods[m]})
mk, _ := meshcatalogclient.ManifestsKey(digest)
b.kv[buckets.Manifests+"/"+mk] = man
rec, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: meshcatalogclient.SourcePrefix2 + strings.Repeat("a", 64), Manifest: digest, Repository: "novox/x", Commit: "c"})
b.put(t, buckets.Modules, m, rec)
hk, _ := nodeengineclient.HaveKey("n1", m)
hv, _ := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Build: "build-1", Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
b.put(t, buckets.Have, hk, hv)
placed = append(placed, meshmoduleassigner.PlacedModule{Module: m})
}
place, _ := meshmoduleassignerclient.EncodePlacement(meshmoduleassigner.Placement{Node: "n1", Decided: meshmoduleassigner.NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: placed})
have, _ := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: "n1", TunnelKey: "k", SealingKey: "sk", Capabilities: []string{"systemd"}}})
nodeWant, _ := bucketstest.NodeWant(meshwants.WantsByWantNodeNodeValue{Self: true})
b.put(t, buckets.Placement, "placement.n1", place)
b.put(t, buckets.Have, "n1.node", have)
b.put(t, buckets.Wants, "want.n1.node", nodeWant)
for _, bucket := range []string{buckets.Wants, buckets.Placement, buckets.Have, buckets.Modules, ResolvedBucket, builds.BuilderBuildsBucket} {
b.send(bucket, agent.Update{Caught: true})
}
servicesIn := func(u agent.Update) []string {
var comp compositions.Composed
if err := json.Unmarshal(u.Value, &comp); err != nil {
t.Fatalf("the own composition does not decode: %v", err)
}
var ids []string
for _, raw := range comp.Resources {
var h struct{ ID string }
_ = json.Unmarshal(raw, &h)
if strings.HasPrefix(h.ID, "catalog.") || strings.HasPrefix(h.ID, "other.") {
ids = append(ids, h.ID)
}
}
return ids
}
var first agent.Update
for first.Key == "" {
if u := next(t, out); u.Key == "n1.node" && !u.Deleted {
first = u
}
}
if got := servicesIn(first); !slices.Equal(got, []string{"other.service", "catalog.service"}) {
t.Fatalf("before the read the own composition carries services %v, want what ran and the holder's alone", got)
}
g.open.Store(true)
changed <- struct{}{}
for {
u := next(t, out)
if u.Key == "n1.node" && !u.Deleted {
if got := servicesIn(u); !slices.Equal(got, []string{"catalog.service", "other.service", "other.extra"}) {
t.Fatalf("after the read the own composition carries services %v, want what ran, the rest of the module and the holder", got)
}
return
}
}
}
// ADR 0636 §1: a reader the node could not start is no reader that gates nothing: the node holds like one that waits.
func TestACompositionHoldsWhenTheSeatListReaderCouldNotStart(t *testing.T) {
gate := Unread{}