Fix 9.7a: the grants part resolves a read naming a seat to its holder

Builds: design 47 G A-116 (§636, §616)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 01:16:54 +01:00
parent 9c70fa6599
commit b8abf3ed3b
2 changed files with 64 additions and 0 deletions
+33
View File
@@ -4964,6 +4964,39 @@ func TestTheGrantsPartKeepsWhatEachNodeRuns(t *testing.T) {
}
}
// A module reading a seat holder's state, `seat:<seat>.<state>`, is granted the read of the module holding the seat on record
// (design 47 G A-116).
func TestASeatNamedReadIsResolvedToItsHolder(t *testing.T) {
for _, row := range []struct {
name, holder string
reads, want []string
}{
{"resolved to the holder on record", "prov-one", []string{"seat:mesh-provisioner.plans", "mesh-vault.node-logins"},
[]string{"prov-one.plans", "mesh-vault.node-logins"}},
{"a new holder is read with no change of the manifest", "prov-two", []string{"seat:mesh-provisioner.provisions"},
[]string{"prov-two.provisions"}},
{"no holder on record stays as written", "", []string{"seat:mesh-provisioner.plans"}, []string{"seat:mesh-provisioner.plans"}},
} {
t.Run(row.name, func(t *testing.T) {
in := grants.Inputs{Nodes: []string{"a"}, Assigned: map[string][]string{"a": {"mod-t"}},
Manifests: map[string]catalogue.Manifest{"mod-t": {Module: "mod-t", Version: "1", RunsAs: "mod-t", Reads: row.reads,
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/mod-t/broker"}}}}}
if row.holder != "" {
in.Placed = map[string]meshmoduleassigner.Assignments{"a": {Node: "a",
Holds: []meshmoduleassigner.Holding{{Claim: "mesh-provisioner", Scope: "mesh", Module: row.holder}}}}
}
recs, _ := grants.ListRecords(in)
var got []string
for _, d := range recs.Assigned["a"] {
got = d.Reads
}
if !slices.Equal(got, row.want) {
t.Errorf("reads = %v, want %v", got, row.want)
}
})
}
}
// A module naming a seat's view, `seat:<seat>.views.<state>`, is granted the view of the module holding the seat on record, so a
// new holder is granted with no change of its manifest (design 47 G §616).
func TestASeatNamedViewIsResolvedToItsHolder(t *testing.T) {
@@ -88,6 +88,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) {
}
declare := func(m catalogue.Manifest) buscompose.Declared {
m.Consumes = resolveSeatViews(m.Consumes, holders)
m.Reads = resolveSeatReads(m.Reads, holders)
d := declaredFor(m, seats, declarers)
if len(in.Placed) > 0 {
d.Holds = heldWherePlaced(d.Holds, holders, node, m.Module)
@@ -212,6 +213,36 @@ func resolveSeatViews(consumes []string, holders []catalogue.Held) []string {
return out
}
// resolveSeatReads resolves each `seat:<seat>.<state>` of a module's reads to `<holder>.<state>`, the module holding the seat
// on record, as resolveSeatViews does a view: a new holder is read with no change of the manifest (design 47 G A-116). An entry
// whose seat has no holder on record stays as written, and grants nothing. Pure.
func resolveSeatReads(reads []string, holders []catalogue.Held) []string {
var out []string
for i, r := range reads {
rest, isSeat := strings.CutPrefix(r, seatViewConsume)
seatName, state, named := strings.Cut(rest, ".")
if !isSeat || !named || state == "" || strings.Contains(state, ".") {
if out != nil {
out = append(out, r)
}
continue
}
if out == nil {
out = slices.Clone(reads[:i])
}
holder := slices.IndexFunc(holders, func(h catalogue.Held) bool { return sameSeat(h.Claim, seatName) })
if holder < 0 {
out = append(out, r)
continue
}
out = append(out, holders[holder].Module+"."+state)
}
if out == nil {
return reads
}
return out
}
// consumedOf splits what a manifest consumes as the composer grants it: a seat's event on the seat's own subject (its
// watches), every other by its emitter's (novox/hq ADR 0118). A placed module and the node-engine's own manifest are split
// alike.