diff --git a/internal/selfcompose/kept_test.go b/internal/selfcompose/kept_test.go index c5eea439..4ef6f1d5 100644 --- a/internal/selfcompose/kept_test.go +++ b/internal/selfcompose/kept_test.go @@ -2,9 +2,11 @@ package selfcompose import ( "context" + "slices" "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants" ) @@ -33,3 +35,20 @@ func TestTheKeepingNodeIsTheWantsKeptOn(t *testing.T) { } } } + +// Design 47 G §558, A-275: a manifest's `invokes` in its long form {verb, fields} is read for its seat, and one of a form that +// cannot be read is refused, never read as no seats. +func TestSeatsOfReadsALongFormInvokeAndRefusesAnUnreadableOne(t *testing.T) { + c := New(Env{Node: "n1"}) + c.modules["m"] = meshcatalog.ModulesByModuleValue{Manifest: "d1"} + c.modules["bad"] = meshcatalog.ModulesByModuleValue{Manifest: "d2"} + c.manifests["d1"] = []byte(`{"module":"m","invokes":["seat:a.b",{"verb":"seat:mesh-catalog.revision","fields":["x"]}]}`) + c.manifests["d2"] = []byte(`{"module":"bad","invokes":[3]}`) + seats, err := c.seatsOf(context.Background(), reader{c: c}, "m") + if err != nil || !slices.Equal(seats, []string{"a", "mesh-catalog"}) { + t.Fatalf("seats %v, %v", seats, err) + } + if seats, err := c.seatsOf(context.Background(), reader{c: c}, "bad"); err == nil { + t.Fatalf("an unreadable invokes gave seats %v", seats) + } +} diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index 2b60ca71..a5dcd90c 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -2035,7 +2035,7 @@ func (c *Composer) heldUntilListed(ctx context.Context, r reader, own meshmodule held := map[string]string{} for _, pm := range own.Modules { if pm.Module == "" || pm.Module == agent.NodeModule || slices.Contains(holders, pm.Module) || - c.env.SeatList.MayAct(c.seatsOf(ctx, r, pm.Module)...) { + c.mayAct(ctx, r, pm.Module) { continue } held[pm.Module] = line + composesNothingNewOfIt @@ -2094,7 +2094,7 @@ func (c *Composer) holdUntilListed(ctx context.Context, r reader, compositions m held[module] = WaitsForTheSeatListOwnPart continue } - } else if c.env.SeatList.MayAct(c.seatsOf(ctx, r, module)...) { + } else if c.mayAct(ctx, r, module) { continue } delete(compositions, key) @@ -2207,41 +2207,55 @@ func (c *Composer) claimants(ctx context.Context, r reader, seat string) []strin return out } -// seatsOf is the seats a module claims, invokes or provides, from the manifest its module record names; none when it -// cannot be read, which the reader judges as a module no bootstrapped seat names. -func (c *Composer) seatsOf(ctx context.Context, r reader, module string) []string { +// mayAct is whether the seat list lets module act: of the seats it claims or invokes (seatsOf). A manifest whose `invokes` +// is of a form that cannot be read is refused, never read as no seats (design 47 G A-275). +func (c *Composer) mayAct(ctx context.Context, r reader, module string) bool { + seats, err := c.seatsOf(ctx, r, module) + if err != nil { + return false // refused: held with the module's other waits, never judged as no seats + } + return c.env.SeatList.MayAct(seats...) +} + +// seatsOf is the seats a module claims or invokes, from the manifest its module record names; none when it cannot be +// fetched, which the reader judges as a module no bootstrapped seat names, and an error when its manifest is of a form that +// cannot be read (design 47 G §558, A-275). +func (c *Composer) seatsOf(ctx context.Context, r reader, module string) ([]string, error) { c.mu.Lock() m, ok := c.modules[module] c.mu.Unlock() if !ok || m.Manifest == "" { - return nil + return nil, nil } raw, err := r.Manifest(ctx, m.Manifest) if err != nil { - return nil + return nil, nil } var man struct { Claims []struct { Name string `json:"name"` } `json:"claims"` - Invokes []string `json:"invokes"` - Provides []struct { - Name string `json:"name"` - } `json:"provides"` + Invokes json.RawMessage `json:"invokes"` } - if json.Unmarshal(raw, &man) != nil { - return nil + if err := json.Unmarshal(raw, &man); err != nil { + return nil, fmt.Errorf("the manifest of %s cannot be read: %w", module, err) + } + var invokes []string + if len(man.Invokes) > 0 { + if invokes, err = catalogue.InvokedVerbs(man.Invokes); err != nil { + return nil, fmt.Errorf("the manifest of %s: %w", module, err) + } } var seats []string for _, cl := range man.Claims { seats = append(seats, cl.Name) } - for _, in := range man.Invokes { + for _, in := range invokes { if seat, _, ok := strings.Cut(strings.TrimPrefix(in, "seat:"), "."); ok && strings.HasPrefix(in, "seat:") { seats = append(seats, seat) } } - return seats + return seats, nil } // unmadeOf is each module wanted at a fingerprint (fps) of which the pass made no composition at it, with why, apart from