One copy of the reader's rule for every reader, kept where shared code lives (mesh-sdk), as the lead ruled. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
123 lines
7.0 KiB
Go
123 lines
7.0 KiB
Go
package manifest
|
|
|
|
import (
|
|
"os"
|
|
"reflect"
|
|
"slices"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/views"
|
|
)
|
|
|
|
func load(t *testing.T) catalogue.Manifest {
|
|
t.Helper()
|
|
raw, err := os.ReadFile("module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m, err := catalogue.Decode(raw)
|
|
if err != nil {
|
|
t.Fatalf("mesh-sdk cannot decode the manifest: %v", err)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// 1297/B2a: the controller's reader (mesh-sdk catalogue, which the controller reads manifests with) takes the
|
|
// manifest; it reads exactly the buckets the assigner opens (the program's list, held equal in its own test), keeps
|
|
// the states it declares (the differences kept read-only, 1906/D5), and holds a credential of its own.
|
|
func TestManifest(t *testing.T) {
|
|
m := load(t)
|
|
if m.Module != "mesh-delivery-assigner" {
|
|
t.Errorf("module = %q", m.Module)
|
|
}
|
|
reads := slices.Clone(m.Reads)
|
|
slices.Sort(reads)
|
|
// ADR 0593 §8: it composes nothing, so it reads no candidate, held or resolved value, compose step or build log; it reads
|
|
// every node's published compositions to compare them, and the gate's source and merges, for the release that carries a build (ADR 0601 §1);
|
|
// and the indexer's module graph, whose verb edges a want waits on (ADR 0614 §2, §3).
|
|
// DLV-F2r (ADR 0615 §1): the indexer's manifests and modules are read through its first views and views family.
|
|
want := []string{"mesh-delivery-builder.builds", "mesh-delivery-gate.merges", "mesh-delivery-gate.source", "mesh-delivery-indexer.graph",
|
|
"mesh-delivery-teller.answers", "mesh-delivery-watchman.presence", "mesh-grants.grants", "mesh-host.composed",
|
|
"mesh-provisioner.provisions", "node-engine.have"}
|
|
if !reflect.DeepEqual(reads, want) {
|
|
t.Errorf("reads = %v, want exactly %v", reads, want)
|
|
}
|
|
states := map[string]int{}
|
|
for _, s := range m.State {
|
|
states[s.Name] = s.History
|
|
}
|
|
if want := map[string]int{"asks": 5, "differences": 64, "puts": 20, "wants": 20,
|
|
"placement": 64, "proposals": 5, "candidates": 5, "pulses": 0, "findings": 5, "policies": 20, "seat-holders": 5, views.MarksState: 1, views.StatusState: 1}; !reflect.DeepEqual(states, want) {
|
|
t.Errorf("state = %v, want %v", states, want)
|
|
}
|
|
if _, ok := m.OwnSecrets["broker"]; !ok || len(m.OwnSecrets) != 1 {
|
|
t.Errorf("own-secrets = %v, want exactly broker (or no credential is issued)", m.OwnSecrets)
|
|
}
|
|
if m.SecretsOwner != "mesh-delivery-assigner" {
|
|
t.Errorf("secrets-owner = %q", m.SecretsOwner)
|
|
}
|
|
// 1906/D3: it says its conditions on the operator channel; it asks the Go build seat nothing since the composition is
|
|
// the node-engine's (ADR 0593 §7).
|
|
if !slices.Equal(m.Uses, []string{"operator-channel"}) {
|
|
t.Errorf("uses = %v, want exactly operator-channel", m.Uses)
|
|
}
|
|
// ADR 0546: it serves its own tools (put, release, puts) from its bundle through the tool runner (the artifact loads the
|
|
// module), says each put's step as the event put, and calls the hand-act seat and the bus node's backup holder.
|
|
// ADR 0567 §1, §6-§10: it holds the seat mesh-placement, serving its five verbs (adopt and hand-back went with the
|
|
// controller, ADR 0590); keeps the seat's events on its stream placement for 30 days; calls the hand-act log; and reads
|
|
// mesh-config's resolved values to compose every node a proposal touches.
|
|
// ADR 0565 §7, §9 (issue 1953 D4): and the seat mesh-wants, serving upgrade and bus.
|
|
if len(m.Claims) != 2 || m.Claims[0].Name != catalogue.PlacementSeat || m.Claims[0].At() != catalogue.ScopeMesh ||
|
|
!slices.Equal(m.Claims[0].Serves, []string{"placement", "proposals", "propose", "withdraw", "again"}) ||
|
|
m.Claims[1].Name != catalogue.WantsSeat || m.Claims[1].At() != catalogue.ScopeMesh || !slices.Equal(m.Claims[1].Serves, []string{"upgrade", "bus"}) {
|
|
t.Errorf("the assigner claims %+v, want mesh-placement with its five verbs and mesh-wants with its two", m.Claims)
|
|
}
|
|
if seat, _ := catalogue.SeatNamed(catalogue.WantsSeat); catalogue.CanHold(m, seat) != nil {
|
|
t.Errorf("the assigner cannot hold mesh-wants: %v", catalogue.CanHold(m, seat))
|
|
}
|
|
if seat, _ := catalogue.SeatNamed(catalogue.PlacementSeat); catalogue.CanHold(m, seat) != nil {
|
|
t.Errorf("the assigner cannot hold the seat: %v", catalogue.CanHold(m, seat))
|
|
}
|
|
// ADR 0621 §1: the first-view tools placement, seat-holders and wants and design 25 § 12.2's six read tools, of the
|
|
// program's bundle, which runs in a runtime of its own on the assigner's account.
|
|
if !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts", "placement", "seat-holders", "wants",
|
|
"assigner_read_placement", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", "assigner_read_asks", "assigner_read_differences",
|
|
"assigner_first_views", "assigner_first_view_again"}) {
|
|
t.Errorf("the assigner serves tools %v, want its three put tools, three first views and six read tools", m.Tools)
|
|
}
|
|
if m.RunsAs != m.Module || m.SecretsOwner != m.RunsAs {
|
|
t.Errorf("runs-as = %q: the bundle runs on the assigner's own account (ADR 0621 §1)", m.RunsAs)
|
|
}
|
|
// ADR 0621 §2: each first-view tool answers exactly its readers, never the assigner.
|
|
callers := map[string]string{"placement": "mesh-grants,node-tools,mesh-delivery-indexer", "seat-holders": "node-tools", "wants": "mesh-grants"}
|
|
env := m.Build.Artifacts[0].Env
|
|
for tool, list := range callers {
|
|
if env[views.CallersEnv(tool)] != list || views.ParseCallers(env[views.CallersEnv(tool)]).Allows("n1."+m.Module) {
|
|
t.Errorf("%s callers = %q, want %q", tool, env[views.CallersEnv(tool)], list)
|
|
}
|
|
}
|
|
// ADR 0619 §2, ADR 0615 §5: its views family beside placement on its old streams section.
|
|
if len(m.Streams) != 2 || m.Streams[1].Name != views.AssignerStream || !slices.Equal(m.Streams[1].Events, []string{views.EventPattern}) ||
|
|
m.Streams[1].MaxAgeDays != views.StreamDays || m.Streams[1].MaxMiB != views.StreamMiB || m.Streams[1].Retention != "" {
|
|
t.Errorf("streams = %+v, want placement and %s", m.Streams, views.AssignerStream)
|
|
}
|
|
if m.Streams[0].Name != delivery.PlacementStream || m.Streams[0].MaxAgeDays != delivery.PlacementStreamDays ||
|
|
!slices.Equal(slices.Sorted(slices.Values(m.Streams[0].Events)), slices.Sorted(slices.Values(delivery.PlacementEvents))) {
|
|
t.Errorf("streams = %+v, want placement carrying the seat's four events for %d days", m.Streams, delivery.PlacementStreamDays)
|
|
}
|
|
if !slices.Equal(m.Consumes, []string{"mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules"}) {
|
|
t.Errorf("consumes = %v, want the indexer's views of manifests and modules", m.Consumes)
|
|
}
|
|
if want := []string{"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules",
|
|
"seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts", "seat:mesh-hand-acts.warranted", // ADR 0565 §9, §11: the seat alone
|
|
"seat:node-backup.backed-up", "seat:node-backup.now"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) {
|
|
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
|
}
|
|
if !slices.Equal(m.Emits, []string{"put"}) {
|
|
t.Errorf("emits = %v, want put", m.Emits)
|
|
}
|
|
}
|