Node-engine: while the seat list is unread, the node's own composition lets through the holder of the seat mesh-catalog's resources and accounts over what runs (ADR 0636 §1; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:22:15 +01:00
parent c1a1efa62a
commit ae26376a90
2 changed files with 184 additions and 0 deletions
+62
View File
@@ -16,6 +16,7 @@ import (
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/declaration"
"github.com/novox/mesh-host/internal/buckets"
"github.com/novox/mesh-host/internal/buckets/bucketstest"
"github.com/novox/mesh-host/internal/compositions"
@@ -1849,3 +1850,64 @@ func TestAnUnreadSeatListHoldsWhatIsNewAndHandsItAtTheRead(t *testing.T) {
t.Errorf("lemurs is still held after the read: %q", still)
}
}
// ADR 0636 §1, the cycle of the node's own composition: while the list is unread the node's own composition is held, but
// a node whose new own composition must start the holder of the seat mesh-catalog composes of it, whatever the list says,
// the holder's resources and its account and nothing else, over what the node's own composition ran before; and the rest
// of the new one is handed once the list was read.
func TestAnUnreadSeatListLetsThroughOfTheOwnCompositionOnlyWhatTheHolderOfTheCatalogNeeds(t *testing.T) {
catalog := `{"module":"catalog","version":"1","claims":[{"name":"` + meshcatalog.Seat + `"}]}`
other := `{"module":"other","version":"1","claims":[{"name":"node-files"}]}`
c := New(newFakeBus().env())
c.env.SeatList = &gate{}
c.modules = map[string]meshcatalog.ModulesByModuleValue{"catalog": {Manifest: "dc"}, "other": {Manifest: "do"}}
c.manifests = map[string][]byte{"dc": []byte(catalog), "do": []byte(other)}
res := func(id string) json.RawMessage {
return json.RawMessage(`{"id":"` + id + `","type":"directory","path":"/x/` + id + `"}`)
}
ran := compositions.Composed{Node: "n1", Resources: []json.RawMessage{res("base.dir")},
Own: compositions.NodeOwn{Identities: []declaration.Identity{{Module: "base", Name: "mesh-base", UID: 1}}}}
next := compositions.Composed{Node: "n1", Resources: []json.RawMessage{res("base.dir"), res("catalog.dir"), res("other.dir")},
Own: compositions.NodeOwn{Identities: []declaration.Identity{{Module: "base", Name: "mesh-base", UID: 1},
{Module: "catalog", Name: "mesh-catalog", UID: 2}, {Module: "other", Name: "mesh-other", UID: 3}}}}
comps := map[string]compositions.Composed{"n1.node": next}
held := c.holdUntilListed(context.Background(), reader{c: c}, comps, nil, []compositions.Composed{ran})
got, ok := comps["n1.node"]
if !ok {
t.Fatalf("the node's own composition is held whole, so the holder of the seat mesh-catalog is never started and the list never read")
}
var ids []string
for _, raw := range got.Resources {
var h struct{ ID string }
_ = json.Unmarshal(raw, &h)
ids = append(ids, h.ID)
}
var names []string
for _, i := range got.Own.Identities {
names = append(names, i.Name)
}
if !slices.Equal(ids, []string{"base.dir", "catalog.dir"}) || !slices.Equal(names, []string{"mesh-base", "mesh-catalog"}) {
t.Errorf("the own composition lets through resources %v and accounts %v, want what ran and the holder's alone", ids, names)
}
if held[agent.NodeModule] != WaitsForTheSeatListOwnPart {
t.Errorf("the rest of the own composition is held with %q, want %q", held[agent.NodeModule], WaitsForTheSeatListOwnPart)
}
// Nothing ran: a fresh node composes the holder's part alone.
comps = map[string]compositions.Composed{"n1.node": next}
c.holdUntilListed(context.Background(), reader{c: c}, comps, nil, nil)
if got := comps["n1.node"]; len(got.Resources) != 1 || len(got.Own.Identities) != 1 || got.Own.Identities[0].Name != "mesh-catalog" {
t.Errorf("a fresh node lets through %d resources and %d accounts, want the holder's one each", len(got.Resources), len(got.Own.Identities))
}
}
// 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{}
c := New(newFakeBus().env())
comps := map[string]compositions.Composed{"n1.node": {Node: "n1"}}
c.env.SeatList = gate
held := c.holdUntilListed(context.Background(), reader{c: c}, comps, nil, nil)
if _, ok := held[agent.NodeModule]; !ok || len(comps) != 0 {
t.Errorf("held %v, comps %v: a reader that did not start must hold the node's own composition", held, comps)
}
}