From 387f54734635d4a03fa86a0c70d205f7e8a8a6e2 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:31:22 +0100 Subject: [PATCH] =?UTF-8?q?Tool=20runner:=20the=20console,=20its=20test=20?= =?UTF-8?q?bed=20and=20the=20provider=20read=20placement,=20the=20catalogu?= =?UTF-8?q?e's=20keys=20and=20the=20provisions=20through=20the=20seats'=20?= =?UTF-8?q?clients,=20not=20go/delivery,=20go/provider=20or=20go/provision?= =?UTF-8?q?s=20(T-1,=20ADR=200634=20=C2=A74;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- node-tools/go.mod | 6 ++ node-tools/internal/console/address.go | 62 ++++++++++++------- node-tools/internal/console/mcp.go | 4 +- node-tools/internal/console/runtimes_test.go | 19 +++--- node-tools/internal/firstview/firstview.go | 12 +++- .../internal/firstview/firstview_test.go | 16 +++++ node-tools/internal/meshtest/meshtest.go | 39 +++++++----- node-tools/internal/provide/provide.go | 4 +- node-tools/internal/provide/provide_test.go | 4 +- 9 files changed, 114 insertions(+), 52 deletions(-) diff --git a/node-tools/go.mod b/node-tools/go.mod index 823bb94..ace34fa 100644 --- a/node-tools/go.mod +++ b/node-tools/go.mod @@ -3,6 +3,12 @@ module github.com/novox/mesh-tools/node-tools go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/provisioning/go/seats/meshprovisioner v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-20261105000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-20261105000000-000000000000 git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-20261105000000-000000000000 github.com/nats-io/nats.go v1.54.0 diff --git a/node-tools/internal/console/address.go b/node-tools/internal/console/address.go index 64c21ec..a5d06a4 100644 --- a/node-tools/internal/console/address.go +++ b/node-tools/internal/console/address.go @@ -17,8 +17,10 @@ package console // session — so a tool that arrived a minute ago is found without the client reconnecting. import ( + assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + meshcatalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "context" "encoding/json" @@ -30,6 +32,7 @@ import ( "github.com/novox/mesh-tools/node-tools/internal/announce" "github.com/novox/mesh-tools/node-tools/internal/bus" + "github.com/novox/mesh-tools/node-tools/internal/firstview" ) // IndexKept is how long what the mesh answered is kept before it is asked again: long enough that @@ -149,7 +152,7 @@ type index struct { // called missing while it is here (2026-10-05). Unheard []unheard // Placed is the state seat-holders as the seat mesh-placement's holder wrote it (novox/hq ADR 0571 clause 4), by its - // keys `` and `.`, read with delivery.HoldersOf; PlacedWhy says why it could not be read. + // keys `` and `.`, read with assigner.HoldersOf; PlacedWhy says why it could not be read. Placed map[string][]byte PlacedWhy string // Replaces is what each verb and tool replaces (novox/hq ADR 0245): @@ -337,7 +340,7 @@ func indexOn(conn *bus.Conn, src Source, held announce.Known, before func(announ } l.NotAnswering = append(l.NotAnswering, recordsWhy...) if x.PlacedWhy != "" { - l.NotAnswering = append(l.NotAnswering, delivery.SeatHoldersRead+" (who holds which seat by placement could not be read: "+ + l.NotAnswering = append(l.NotAnswering, assigner.SeatHoldersRead+" (who holds which seat by placement could not be read: "+ x.PlacedWhy+")") } for _, u := range x.Unheard { @@ -357,7 +360,7 @@ func indexOn(conn *bus.Conn, src Source, held announce.Known, before func(announ } // Source is where the console reads the four states of the indexer and the assigner it indexes the mesh by: the assigner's -// placement and seat-holders, the indexer's modules and manifests, by `.` (delivery.PlacementRead and the +// placement and seat-holders, the indexer's modules and manifests, by `.` (assigner.PlacementRead and the // rest). The tool runner's is its first views (firstview.Views), never the owners' state (novox/hq ADR 0615 §1). type Source interface { // State is every current value of a state by key; an error says why it cannot be told yet. @@ -366,16 +369,16 @@ type Source interface { Entry(name, key string) ([]byte, error) } -// seatHoldersOn reads the state seat-holders whole (novox/hq ADR 0571 clause 4), as delivery.SeatHoldersRead. +// seatHoldersOn reads the state seat-holders whole (novox/hq ADR 0571 clause 4), as assigner.SeatHoldersRead. func seatHoldersOn(src Source) (map[string][]byte, error) { - return src.State(delivery.SeatHoldersRead) + return src.State(assigner.SeatHoldersRead) } // The indexer's states the console reads (novox/hq ADR 0571 clauses 4 and 5): every module, and each manifest by its // digest, by the names a reader's manifest declares. var ( - modulesRead = delivery.IndexerModule + ".modules" - manifestsRead = delivery.IndexerModule + ".manifests" + modulesRead = firstview.ModulesRead + manifestsRead = firstview.ManifestsRead ) // manifestFacts is what the console takes of a manifest: the tools it declares and what each replaces. A manifest's @@ -388,20 +391,20 @@ type manifestFacts struct { var manifestsKept sync.Map // digest → manifestFacts // recordsOn is every module the indexer holds, with where the placement seat places it and whether it declares tools, -// and every machine placement names (delivery.ModuleNames, delivery.MachineNames). What cannot be read is said, one line +// and every machine placement names (moduleNames, assigner.MachineNames). What cannot be read is said, one line // per state, for the listing's not answering. func recordsOn(src Source) (recorded []recordedModule, machines []string, why []string) { - placements, err := src.State(delivery.PlacementRead) + placements, err := src.State(assigner.PlacementRead) if err != nil { - why = append(why, delivery.PlacementRead+" (where modules are placed could not be read, so what is missing cannot "+ + why = append(why, assigner.PlacementRead+" (where modules are placed could not be read, so what is missing cannot "+ "be said: "+err.Error()+")") } - machines = delivery.MachineNames(placements) + machines = assigner.MachineNames(placements) on := map[string][]string{} // The placement's keys in either form, a node key and its module keys or the form before (research 072 row 37). - assembled, err := delivery.PlacementsOf(placements) + assembled, err := assigner.PlacementsOf(placements) if err != nil { - why = append(why, delivery.PlacementRead+" (not every key is a placement: "+err.Error()+")") + why = append(why, assigner.PlacementRead+" (not every key is a placement: "+err.Error()+")") } for _, node := range machines { for _, m := range assembled[node].Modules { @@ -412,13 +415,13 @@ func recordsOn(src Source) (recorded []recordedModule, machines []string, why [] if err != nil { return nil, machines, append(why, modulesRead+" (which modules there are could not be read: "+err.Error()+")") } - names, err := delivery.ModuleNames(modules) + names, err := moduleNames(modules) if err != nil { return nil, machines, append(why, modulesRead+" ("+err.Error()+")") } for _, name := range names { r := recordedModule{Module: name, On: on[name]} - if m, err := delivery.DecodeModule(modules[name]); err == nil && m.Manifest != "" { + if m, err := meshcatalogclient.DecodeModule(modules[name]); err == nil && m.Manifest != "" { facts, err := manifestOf(src, m.Manifest) if err != nil { why = append(why, name+" (its manifest could not be read, so whether it answers tools is not known: "+err.Error()+")") @@ -431,6 +434,21 @@ func recordsOn(src Source) (recorded []recordedModule, machines []string, why [] return recorded, machines, why } +// moduleNames is every module the indexer holds, sorted, from its state `modules` as a reader holds its keys (key to +// value; a deleted key absent). A key that is not a module's is refused, naming it. +func moduleNames(keys map[string][]byte) ([]string, error) { + out := make([]string, 0, len(keys)) + for k := range keys { + module, err := meshcatalogclient.ParseModulesKey(k) + if err != nil { + return nil, err + } + out = append(out, module) + } + sort.Strings(out) + return out, nil +} + // manifestOf is what the console takes of the manifest of a digest, read from the indexer's manifests once. func manifestOf(src Source, digest string) (manifestFacts, error) { if kept, ok := manifestsKept.Load(digest); ok { @@ -443,12 +461,12 @@ func manifestOf(src Source, digest string) (manifestFacts, error) { if entry == nil { return manifestFacts{}, fmt.Errorf("the indexer holds no manifest %s", digest) } - m, err := delivery.DecodeManifest(entry) + m, err := meshcatalogclient.DecodeManifest(entry) if err != nil { return manifestFacts{}, err } var facts manifestFacts - if err := json.Unmarshal([]byte(m.JSON), &facts); err != nil { + if err := json.Unmarshal([]byte(m.Manifest), &facts); err != nil { return manifestFacts{}, err } manifestsKept.Store(digest, facts) @@ -456,19 +474,19 @@ func manifestOf(src Source, digest string) (manifestFacts, error) { } // placedHolders is who holds seat (on node, for a seat held per machine) by placement, or none. -func (x *index) placedHolders(seat, node string) []delivery.SeatHolder { +func (x *index) placedHolders(seat, node string) []meshmoduleassigner.SeatHoldersHoldersItem { if len(x.Placed) == 0 { return nil } - hs, err := delivery.HoldersOf(x.Placed, seat, node) + hs, err := assigner.HoldersOf(x.Placed, seat, node) if err != nil || len(hs) > 0 || node != "" { return hs } // A seat held per machine, asked without one: every node's key of it. - var all []delivery.SeatHolder + var all []meshmoduleassigner.SeatHoldersHoldersItem for key := range x.Placed { if s, n, ok := strings.Cut(key, "."); ok && s == seat { - if more, err := delivery.HoldersOf(x.Placed, seat, n); err == nil { + if more, err := assigner.HoldersOf(x.Placed, seat, n); err == nil { all = append(all, more...) } } diff --git a/node-tools/internal/console/mcp.go b/node-tools/internal/console/mcp.go index ee93c89..2abf4a5 100644 --- a/node-tools/internal/console/mcp.go +++ b/node-tools/internal/console/mcp.go @@ -6,8 +6,8 @@ package console import ( "context" "encoding/json" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "os" "strings" "sync" @@ -78,7 +78,7 @@ func (s *Surface) seatsAnswer() map[string]any { for _, st := range catalogue.Seats() { holders := x.placedHolders(st.Name, "") if holders == nil { - holders = []delivery.SeatHolder{} + holders = []meshmoduleassigner.SeatHoldersHoldersItem{} } tools := st.Serves if tools == nil { diff --git a/node-tools/internal/console/runtimes_test.go b/node-tools/internal/console/runtimes_test.go index 94af57d..2c7df29 100644 --- a/node-tools/internal/console/runtimes_test.go +++ b/node-tools/internal/console/runtimes_test.go @@ -8,7 +8,8 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + seatplacement "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" "github.com/nats-io/nats.go/micro" "github.com/novox/mesh-tools/node-tools/internal/announce" @@ -61,13 +62,15 @@ func TestARuntimeThatAnswersLateIsFoundAndOneThatNeverDoesIsNamed(t *testing.T) map[string][]string{"laptop": {"slack", "nextcloud-client"}, "sleeper": {"nap", "nextcloud-client"}, "desktop": {"nextcloud-client"}, "bench": nil}) // Who holds which seat, by placement (ADR 0571 clause 4), as the seat mesh-placement's holder composes it. - placedKV := mesh.Bucket(t, delivery.SeatHoldersBucket) - for key, sh := range delivery.SeatHoldersOf(map[string]delivery.Placement{"bench": {Node: "bench", - Holds: []delivery.Holding{{Claim: "mesh-wants", Scope: "mesh", Module: "mesh-delivery-assigner"}}}}) { - raw, _ := json.Marshal(sh) - if _, err := placedKV.Put(key, raw); err != nil { - t.Fatal(err) - } + placedKV := mesh.Bucket(t, assigner.SeatHoldersBucket) + holdersKey, err := assigner.SeatHoldersKey("mesh-wants", "") + if err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(seatplacement.SeatHolders{V: assigner.Version, Seat: "mesh-wants", Scope: "mesh", + Holders: []seatplacement.SeatHoldersHoldersItem{{Module: "mesh-delivery-assigner", Node: "bench"}}}) + if _, err := placedKV.Put(holdersKey, raw); err != nil { + t.Fatal(err) } consoleMembership := mt.MembershipOf("node-tools", "laptop-console", false, nil) consoleMembership.State = mt.ConsoleReads() diff --git a/node-tools/internal/firstview/firstview.go b/node-tools/internal/firstview/firstview.go index 967fc47..efc776f 100644 --- a/node-tools/internal/firstview/firstview.go +++ b/node-tools/internal/firstview/firstview.go @@ -13,6 +13,7 @@ import ( "fmt" conditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" condseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "sort" "sync" "time" @@ -25,7 +26,14 @@ import ( // The owners and their first-view tools (the tools' addresses are `.`). const ( Indexer = "mesh-delivery-indexer" - Assigner = "mesh-delivery-assigner" + Assigner = assigner.Module +) + +// The names the console reads the indexer's two states by (`.`), as a manifest's `reads` names them; the +// assigner's two are its client's (assigner.PlacementRead, assigner.SeatHoldersRead). +const ( + ModulesRead = Indexer + ".modules" + ManifestsRead = Indexer + ".manifests" ) // Spec is one first view the runner holds. @@ -39,7 +47,7 @@ var Specs = []Spec{ {Indexer, "modules", "modules"}, } -// Name is the name a state is read by, `.`: delivery.PlacementRead, delivery.SeatHoldersRead and the indexer's. +// Name is the name a state is read by, `.`: assigner.PlacementRead, assigner.SeatHoldersRead and the indexer's. func (s Spec) Name() string { return s.Owner + "." + s.State } // Views is the four followers. diff --git a/node-tools/internal/firstview/firstview_test.go b/node-tools/internal/firstview/firstview_test.go index ae2d5a0..ed25d20 100644 --- a/node-tools/internal/firstview/firstview_test.go +++ b/node-tools/internal/firstview/firstview_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" "git.novox.be/novox/mesh-sdk/go/views" "github.com/novox/mesh-tools/node-tools/internal/bus" @@ -166,3 +167,18 @@ func TestTheRunnerHoldsTheFourStatesFromTheOwnersFirstViewsAndTheirEvents(t *tes t.Errorf("the gap was not said in the log: %v", logs) } } + +// The four first views are read by the names the seats' clients give the states, and the console reads the indexer's +// two by the names here. +func TestTheFirstViewsAreNamedAsTheClientsNameTheirStates(t *testing.T) { + want := map[string]bool{assigner.PlacementRead: true, assigner.SeatHoldersRead: true, ModulesRead: true, ManifestsRead: true} + for _, s := range Specs { + if !want[s.Name()] { + t.Errorf("the first view %s is read by no name a client gives", s.Name()) + } + delete(want, s.Name()) + } + if len(want) > 0 { + t.Errorf("no first view reads %v", want) + } +} diff --git a/node-tools/internal/meshtest/meshtest.go b/node-tools/internal/meshtest/meshtest.go index 9ca7cbf..704f5d1 100644 --- a/node-tools/internal/meshtest/meshtest.go +++ b/node-tools/internal/meshtest/meshtest.go @@ -13,8 +13,11 @@ package meshtest import ( "context" "encoding/json" + assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + seatplacement "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + meshcatalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + meshcatalog "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-sdk/go/buscompose" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/membership" "os" "path/filepath" @@ -150,14 +153,22 @@ func sdkMembership(t *testing.T, mem bus.Membership) membership.Membership { return out } +// The indexer's two states the console reads, and their buckets (the test bed's own names: the indexer's client names +// no holder). +const ( + indexer = "mesh-delivery-indexer" + ModulesBucket = indexer + "_modules" + ManifestsBucket = indexer + "_manifests" +) + // ConsoleReads are the states a console's runtime reads to say what the mesh holds (novox/hq ADR 0571 clauses 4, 5): // placement, the indexer's modules and manifests, and the seat holders. func ConsoleReads() []bus.StateIssued { return []bus.StateIssued{ - {Name: delivery.PlacementRead, Bucket: delivery.PlacementBucket}, - {Name: delivery.IndexerModule + ".modules", Bucket: delivery.ModulesBucket}, - {Name: delivery.IndexerModule + ".manifests", Bucket: delivery.ManifestsBucket}, - {Name: delivery.SeatHoldersRead, Bucket: delivery.SeatHoldersBucket}, + {Name: assigner.PlacementRead, Bucket: assigner.PlacementBucket}, + {Name: indexer + ".modules", Bucket: ModulesBucket}, + {Name: indexer + ".manifests", Bucket: ManifestsBucket}, + {Name: assigner.SeatHoldersRead, Bucket: assigner.SeatHoldersBucket}, } } @@ -173,20 +184,20 @@ func (m *Mesh) Placed(t *testing.T, tools map[string][]string, on map[string][]s } return b } - modules, manifests, placement := kv(delivery.ModulesBucket), kv(delivery.ManifestsBucket), kv(delivery.PlacementBucket) - kv(delivery.SeatHoldersBucket) + modules, manifests, placement := kv(ModulesBucket), kv(ManifestsBucket), kv(assigner.PlacementBucket) + kv(assigner.SeatHoldersBucket) for module, declared := range tools { manifest, _ := json.Marshal(map[string]any{"module": module, "tools": declared}) - digest := delivery.ManifestDigest(manifest) - raw, err := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: string(manifest)}) + digest := meshcatalogclient.ManifestDigest(manifest) + raw, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: module, Manifest: string(manifest), V: 1}) if err != nil { t.Fatal(err) } if _, err := manifests.Put(digest, raw); err != nil { t.Fatal(err) } - raw, err = delivery.EncodeModule(delivery.Module{None: "not built here", Manifest: digest, Repository: "novox/" + module, - Commit: strings.Repeat("a", 40)}) + raw, err = meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{None: "not built here", Manifest: digest, Repository: "novox/" + module, + Commit: strings.Repeat("a", 40), V: 1}) if err != nil { t.Fatal(err) } @@ -195,12 +206,12 @@ func (m *Mesh) Placed(t *testing.T, tools map[string][]string, on map[string][]s } } for node, placed := range on { - p := delivery.Placement{V: delivery.Version, Node: node} + p := seatplacement.Placement{V: assigner.Version, Node: node} for _, module := range placed { - p.Modules = append(p.Modules, delivery.PlacedModule{Module: module}) + p.Modules = append(p.Modules, seatplacement.PlacedModule{Module: module}) } // As the seat writes it: its node key and a key per module (research 072 row 37). - values, err := delivery.PlacementValues(p) + values, err := assigner.PlacementValues(p) if err != nil { t.Fatal(err) } diff --git a/node-tools/internal/provide/provide.go b/node-tools/internal/provide/provide.go index c7a400c..dec22be 100644 --- a/node-tools/internal/provide/provide.go +++ b/node-tools/internal/provide/provide.go @@ -15,8 +15,8 @@ import ( "fmt" "time" - "git.novox.be/novox/mesh-sdk/go/provider" - "git.novox.be/novox/mesh-sdk/go/provisions" + provisions "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" + "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider" "github.com/novox/mesh-tools/node-tools/internal/bus" ) diff --git a/node-tools/internal/provide/provide_test.go b/node-tools/internal/provide/provide_test.go index 47957c9..988119d 100644 --- a/node-tools/internal/provide/provide_test.go +++ b/node-tools/internal/provide/provide_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" + provisions "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient" + "git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient/provider" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/provider" - "git.novox.be/novox/mesh-sdk/go/provisions" "github.com/novox/mesh-tools/node-tools/internal/bus" mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"