The builder, its tools bundle, the assigner (process and bundle) and the view's tools bundle answered their seat list's again tool to the modules node-tools and mesh-healers. Their lists are now the operator, seat:node-tool-runner and seat:mesh-healers, resolved by a holder function: - meshcatalogclient's Branches.Claimant (moved from the builder's versions.HolderOf, which now asks it) names the claimant of mesh-module-assigner from the trunk's half, and Branches.SeatHolders follows that claimant's seat-holders view through the part's wiring. It drops the view and follows the new claimant's on a catalog change, asks nothing on a timer, and answers seatlist.HoldersIn. - the assigner answers HoldersIn from its own seat-holders picture (its world in the process, the watched copy in the bundle). The builder's and the view's manifests consume seat:mesh-module-assigner.views.seat-holders. The assigner's seat-holders first-view tool answers both, by module as A-131 has it. Builds: design 47 G A-298, A-295, A-296, A-210, A-131, §717, §616, §614 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
185 lines
13 KiB
Go
185 lines
13 KiB
Go
package manifest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-teller/tellerevents"
|
|
"os"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/views"
|
|
|
|
"mesh-delivery-assigner/internal/buckets"
|
|
)
|
|
|
|
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,.
|
|
// DLV-F2r (ADR 0615 §1, ADR 0631 §1): the indexer's manifests, modules and module graph (whose verb edges a want waits
|
|
// on, ADR 0614 §2, §3) are read through its first views and views family, none of the indexer's states by reads.
|
|
// A-181: what was built is heard from the builder's stream of ends, so the builder's builds bucket is no read.
|
|
// §431, §507: the gate's source is read through its verb `source` and its event `source-moved`, and no `merges` is read.
|
|
// A-175, A-178: the teller's answers are followed through its first view and its views family, never its bucket.
|
|
// §389: the watchman's machine-away and machine-back are consumed, never its presence.
|
|
want := []string{"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)
|
|
}
|
|
// the buckets the program names are the manifest's reads, `<module>.<state>` as `<module>_<state>`
|
|
named := []string{buckets.Grants,
|
|
buckets.ComposedRead, "mesh-provisioner_provisions", buckets.Have}
|
|
slices.Sort(named)
|
|
for i, r := range want {
|
|
if got := strings.Replace(named[i], "_", ".", 1); got != r {
|
|
t.Errorf("the buckets package names %q, the manifest reads %q", got, r)
|
|
}
|
|
}
|
|
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,
|
|
"assignments": 64, "placement": 64, "copies": 1, "proposals": 5, "candidates": 5, "pulses": 0, "findings": 5, "policies": 20, "seat-holders": 5, views.MarksState: 1, views.StatusState: 1,
|
|
"engine-reads": 1, "written": 5, "waits": 5, "put-waits": 1, "delivery-waits": 1, "delivered-again": 1, "inputs": 1, "pins": 5}; !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-module-assigner, serving its five verbs (adopt and hand-back went with the
|
|
// controller, ADR 0590); keeps the seat's events on its stream assignments 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, and (design 47 lines 1362-1366)
|
|
// revision, entry and difference from its revisions kept as objects in the old section, form 1, capped at 512 MiB.
|
|
if len(m.Claims) != 2 || m.Claims[0].Name != meshmoduleassigner.Seat || m.Claims[0].At() != catalogue.ScopeMesh ||
|
|
!slices.Equal(m.Claims[0].Serves, []string{"assignments", "proposals", "propose", "withdraw", "again", "placement"}) ||
|
|
m.Claims[1].Name != meshwants.Seat || m.Claims[1].At() != catalogue.ScopeMesh || !slices.Equal(m.Claims[1].Serves, []string{"upgrade", "bus", "revision", "entry", "difference", "input"}) {
|
|
t.Errorf("the assigner claims %+v, want mesh-module-assigner with its five verbs and mesh-wants with its five", m.Claims)
|
|
}
|
|
if len(m.Objects) != 1 || m.Objects[0].Name != "assignment-revisions" || m.Objects[0].MaxMiB != 512 || m.Objects[0].Form != 1 {
|
|
t.Errorf("objects = %+v, want assignment-revisions, 512 MiB, form 1", m.Objects)
|
|
}
|
|
// Whether the seats' definitions let it hold them is the manifest validator's (the judge, at the seat list).
|
|
// ADR 0621 §1: the first-view tools assignments, 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; ADR 0618 §3: and assigner_engine_reads,
|
|
// which lists and asks again what each node's engine reads.
|
|
if !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts", "assigner_waits", "assigner_inputs", "assignments", "seat-holders", "wants",
|
|
"assigner_read_assignments", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", "assigner_read_asks", "assigner_read_differences", "assigner_read_copies",
|
|
"assigner_first_views", "assigner_first_view_again", "assigner_engine_reads", "assigner_seat_list", "assigner_seat_list_again", "assigner_ask_again", "assigner_written", "assigner_pins", "assigner_unpin", "assigner_delivered_again"}) {
|
|
t.Errorf("the assigner serves tools %v, want its three put tools, three first views, six read tools, assigner_engine_reads, the seat list's two (ADR 0636 §7) and assigner_ask_again (ADR 0668 §5)", 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{"assignments": "mesh-grants,node-tools,mesh-delivery-indexer,mesh-delivery-builder,mesh-delivery-checker", "seat-holders": "node-tools,mesh-delivery-builder,mesh-delivery-view", "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 0635 §5: its views family is its ordinary events, `views.<state>` among its emits, on no stream of its own.
|
|
// The seat's four events are said on the seat's own subject (the seat's family), so the module keeps no stream for them
|
|
// and lists none among its emits.
|
|
// Its one stream is its own findings' events (design 47 G A-30, §383), which its finding writer says.
|
|
if len(m.Streams) != 1 || m.Streams[0].Name != "mesh-delivery-assigner-findings" || !slices.Equal(m.Streams[0].Events, []string{"finding-written.*", "finding-cleared.*", "findings-stated.*"}) {
|
|
t.Errorf("streams = %+v, want its findings' stream alone: the seat's events ride the shared events stream", m.Streams)
|
|
}
|
|
for _, e := range meshmoduleassigner.Events() {
|
|
if slices.Contains(m.Emits, e) {
|
|
t.Errorf("emits lists %s, an event the seat says", e)
|
|
}
|
|
}
|
|
// ADR 0618 §4: and it hears each node's engine say its reads changed; ADR 0636 §4 (issue 1966): and the seat list's two events;
|
|
// issue 1966 note 151: and the teller's ask-ended, its subject the one the assigner hears; §376, §389: its own version-delivered and
|
|
// the watchman's machine-away and machine-back, read whole at its start.
|
|
if !slices.Equal(m.Consumes, []string{"mesh-conditions.findings-asked", "mesh-conditions.condition-raised", "mesh-conditions.condition-changed", "mesh-conditions.condition-cleared", "mesh-catalog.catalog-revision", "mesh-catalog.registration-changed", "mesh-delivery.source-moved", "mesh-catalog.version-released", "mesh-wants.version-delivered", "mesh-watchdogs.machine-away", "mesh-watchdogs.machine-back", "mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", builds.BuilderModule + "." + builds.BuildsStreamEvents, buckets.IndexerModule + ".take-in.taken", buckets.TellerModule + "." + tellerevents.AskEnded, buckets.TellerModule + ".views." + buckets.AnswersState, "mesh-pipeline-runner.pipeline-ended", "mesh-pipeline-runner.group-ended", "node-build-go.step-ended", "node-engine.manifest-reads-changed", "node-engine.running", "mesh-delivery.pinned", "mesh-delivery.freed", "mesh-delivery.heads-replaced", "mesh-delivery.pins-stated"}) {
|
|
t.Errorf("consumes = %v, want the catalog half's revisions (design 47 G §195), the teller's ask-ended, the engines' reads changed and what they run", m.Consumes)
|
|
}
|
|
if want := []string{"mesh-delivery-indexer.manifests", "mesh-delivery-indexer.modules",
|
|
buckets.TellerModule + "." + buckets.AnswersReadTool, // A-175, A-178: the teller's answers' first view
|
|
"seat:mesh-catalog.entry", "seat:mesh-catalog.registrations", "seat:mesh-catalog.revision", "seat:mesh-catalog.seat-definitions", // ADR 0636 §4: the seat list
|
|
"seat:mesh-conditions.conditions", // design 47 G §706: the conditions read at each judging of an outside wait
|
|
"seat:mesh-delivery.source", // §507: the branches the gate keeps a source revision of, before the first registrations
|
|
"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",
|
|
"seat:node-build-go.accept.check", // A-231: the composition run's ask on the Go build seat's queue check (accept.check.<asker>)
|
|
"seat:node-build-go.output", // A-254: a composition run's answer kept as the build seat's object
|
|
"seat:node-engine.compose-candidate", // §354, A-226: each machine's composition input
|
|
"seat:node-engine.manifest-reads", // ADR 0618 §3
|
|
"seat:operator-channel.asks"}; !slices.Equal(slices.Sorted(slices.Values(m.Invokes)), want) { // ADR 0668 §4: the ended asks at bind
|
|
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
|
}
|
|
// design 47 G §558 (AS20): the assigner relies on the revision's `none-yet`, so its invokes name it in the long form.
|
|
if f := m.InvokeFields["seat:mesh-catalog.revision"]; !slices.Equal(f, []string{"none-yet"}) {
|
|
t.Errorf("the fields of the revision invoke = %v", f)
|
|
}
|
|
// design 47 G §633, §706, A-112: the bounds of the wait on an outside cause are `config` keys, each filling a file the
|
|
// manifest declares, which the process reads at its start; the environment names only the files.
|
|
var raw struct {
|
|
Config map[string]struct {
|
|
Default any `json:"default"`
|
|
} `json:"config"`
|
|
}
|
|
file, _ := os.ReadFile("module.json")
|
|
if err := json.Unmarshal(file, &raw); err != nil || raw.Config["outside-wait-minutes"].Default != float64(30) || raw.Config["outside-settle-seconds"].Default != float64(90) || len(raw.Config) != 2 {
|
|
t.Errorf("config = %+v (%v), want outside-wait-minutes of 30 and outside-settle-seconds of 90", raw.Config, err)
|
|
}
|
|
for _, f := range []struct{ env, path, key string }{
|
|
{"MESH_ASSIGNER_OUTSIDE_WAIT_FILE", "${dir:state}/outside-wait-minutes", "outside-wait-minutes"},
|
|
{"MESH_ASSIGNER_OUTSIDE_SETTLE_FILE", "${dir:state}/outside-settle-seconds", "outside-settle-seconds"},
|
|
} {
|
|
if !strings.Contains(string(file), `"`+f.env+`": "`+f.path+`"`) || !strings.Contains(string(file), `"path": "`+f.path+`"`) ||
|
|
!strings.Contains(string(file), `"content": "${config:`+f.key+`}"`) {
|
|
t.Errorf("the setting %s is not a file %s the env names as %s", f.key, f.path, f.env)
|
|
}
|
|
}
|
|
if strings.Contains(string(file), `": "${config:outside-`) && strings.Contains(string(file), `"MESH_ASSIGNER_OUTSIDE_WAIT_MINUTES"`) {
|
|
t.Errorf("the process's env still carries a setting")
|
|
}
|
|
if want := []string{"assignments-copied", "put", "views.asks", "views.copies", "views.differences", "views.findings", "views.assignments", "views.seat-holders", "views.wants"}; !slices.Equal(m.Emits, want) {
|
|
t.Errorf("emits = %v, want the copy, put and its six views", m.Emits)
|
|
}
|
|
}
|