Author SHA1 Message Date
jschoubben 6c05e42626 gofmt the files the group try listed
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 01:38:43 +01:00
jschoubben 91a4f1b0ad Take tidy's go.mod whole: mesh requirements at the zero version only, unused ones dropped
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 00:52:18 +01:00
jschoubben b4affb3efd Complete go.mod and go.sum for the externals each module builds with
A check's tests run without network, so each module must carry every external module it builds with.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-06 00:51:22 +01:00
jschoubben fb31cc4e99 The console reads the assigner's state assignments and imports the contracts' new paths (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 22:02:06 +01:00
jschoubben bd555e6014 No client names the holder of the seat mesh-provisioner: each reader keeps its own constant, held to its manifest (ADR 0625 section 4, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 19:36:21 +01:00
jschoubben c3754e48c8 No client names a holder: each reader keeps its own constant, held to its manifest (ADR 0625 section 4, issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 18:37:28 +01:00
jschoubben a00c780352 Require every mesh package of the group at the floor version (ADR 0573 sections 2 and 4, issue 1966)
A member's go.mod names the oldest version a module accepts; the check builds the group's own heads whatever it names. go.sum lines of mesh packages are left out.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 16:56:39 +00:00
jschoubben 5bbe669cfb Merge b6/registry into the registry milestone's join: the console reads through the assigner's client, with the seat name mesh-module-assigner (issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:39:17 +01:00
jschoubben c55b1bcf38 First views: the integration test reads the views families of the manifest's consumes, not the seat list's events (T-3; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:37:17 +01:00
jschoubben 6965119bce Tool runner: the grants part's membership bucket and key are membership's, which the SDK keeps (ADR 0635 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:36:18 +01:00
jschoubben 387f547346 Tool runner: the console, its test bed and the provider read placement, the catalogue's keys and the provisions through the seats' clients, not go/delivery, go/provider or go/provisions (T-1, ADR 0634 §4; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 15:31:22 +01:00
jschoubben fce9189579 First views: a finding's times are times, as the conditions client types them (issue 1966) 2026-11-05 15:26:49 +01:00
jschoubben a2402c4873 Merge branch 'b6/registry' into feat/registry-milestone 2026-11-05 14:54:25 +01:00
jschoubben f917fc60c1 Tool runner: waits for the first read of the seat list on the reader's Ready, to a bound, and serves with the bootstrapped seats past it (B9c, ADR 0636 §1; issue 1966)
Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-05 14:49:52 +01:00
jschoubben 0a6fd0cb5c Merge b6/registry into the registry milestone's join (issue 1966) 2026-11-05 14:39:37 +01:00
jschoubben a918352ff1 Tool runner: describes the seats from the seat list it reads through the seat mesh-catalog; the first views' finding in the conditions seat's layout (T-1, T-3, ADR 0635 §1, ADR 0636 §4; issue 1966) 2026-11-05 14:29:02 +01:00
jschoubben d5e4e4e8ab Console: the address text and the tests name the seat mesh-module-assigner (ADR 0634 §9; T-2; issue 1966) 2026-11-05 14:17:14 +01:00
32 changed files with 453 additions and 188 deletions
+1 -1
View File
@@ -72,7 +72,7 @@ of the words agents use for the mesh's (`logs` finds the journal). Best first: a
words in a name or a replaced command, then words in a description; seats before modules on a tie.
Discovery asks the bus (ADR 0197): every runtime answers the NATS services protocol's `$SRV.PING` and
`$SRV.INFO` with what it serves at that moment, and the console reads where the placement seat's records
`$SRV.INFO` with what it serves at that moment, and the console reads where the assigner seat's records
place each module. The console waits at least 750 ms, and up to 5 s for every runtime that answered
PING to send what it serves — a large runtime's answer crossing the bus to a broker on another machine
can arrive well after the rest. A runtime that said it is there and did not say what it serves, or
+2
View File
@@ -50,6 +50,8 @@ func main() {
until := time.Now().Add(startWait)
cred := credential(until)
conn := connectPatiently(cred, until)
// And the seat list it describes the seats by, read through the seat mesh-catalog (novox/hq ADR 0636 §4).
_ = startSeatList(conn)
served, err := runtime.ServedModulesFrom(os.Getenv(runtime.ToolModules), cred.Module)
if err != nil {
+73
View File
@@ -0,0 +1,73 @@
package main
import (
"encoding/json"
"fmt"
"log"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"github.com/novox/mesh-tools/node-tools/internal/bus"
)
// The tool runner reads the seat list as every part does (novox/hq ADR 0635 §1, ADR 0636): through the seat mesh-catalog,
// from the bootstrapped definitions until it has been read, and following the seat's events. What it holds is given to
// catalogue at every change; the console describes the seats from it, so the descriptions are the mesh's own and not a
// list compiled into the runner.
// busOfTheRunner is the reader's bus over the runner's connection: a verb of the seat asked on its subject, an event heard
// on the seat's event subject.
type busOfTheRunner struct{ conn *bus.Conn }
func (b busOfTheRunner) Ask(key string, body any) (json.RawMessage, error) {
seat, verb, _ := strings.Cut(strings.TrimPrefix(key, "seat:"), ".")
answered, err := b.conn.Ask(key, body, "mesh.seat."+seat+".tool."+verb)
if err != nil {
return nil, err
}
return answered.Result, nil
}
func (b busOfTheRunner) Subscribe(pattern string, handle func(string, json.RawMessage) error) error {
seat, event, _ := strings.Cut(pattern, ".")
_, err := b.conn.Raw(seatdef.EventSubject(seat, event), func(_ string, data []byte) []byte {
_ = handle(pattern, data)
return nil // an event is answered by nobody
})
return err
}
// seatListWithin is how long the runner waits for the first read of the seat list before it serves; the read goes on
// after it, and the descriptions are the bootstrapped ones until it ends (ADR 0636 §1).
const seatListWithin = 5 * time.Second
// startSeatList starts the runner's reader and gives catalogue its bootstrapped seats at once, then waits for the first
// read to end (the reader's Ready) for the bound, so the runner's first descriptions are of the list in the common case.
func startSeatList(conn *bus.Conn) *seatlist.Reader {
timer := time.NewTimer(seatListWithin)
defer timer.Stop()
return startSeatListOn(busOfTheRunner{conn}, timer.C, log.Printf)
}
// startSeatListOn is startSeatList over any bus, with the bound as a channel the caller gives.
func startSeatListOn(b seatlist.Bus, bound <-chan time.Time, logf func(string, ...any)) *seatlist.Reader {
reader, err := seatlist.Start(seatlist.Config{Bus: b, Bootstrap: seatlist.Bootstrapped(),
OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats) }})
if err != nil {
logf("node-tools: %v", fmt.Errorf("the tool runner cannot follow the seat list: %w", err))
}
if reader == nil {
return nil
}
catalogue.UseSeats(reader.Seats())
select {
case <-reader.Ready():
catalogue.UseSeats(reader.Seats())
case <-bound:
logf("node-tools: the seat list is not read yet; the runner serves with the bootstrapped seats and reads on")
}
return reader
}
@@ -0,0 +1,69 @@
package main
import (
"encoding/json"
"errors"
"strings"
"sync/atomic"
"testing"
"time"
)
// busOfAStandIn is a seat mesh-catalog that answers after release, or never (a refusal).
type busOfAStandIn struct {
release chan struct{}
asked atomic.Int32
}
func (b *busOfAStandIn) Ask(string, any) (json.RawMessage, error) {
b.asked.Add(1)
<-b.release
return nil, errors.New("the seat does not answer")
}
func (b *busOfAStandIn) Subscribe(string, func(string, json.RawMessage) error) error { return nil }
// ADR 0636 §1: the runner waits for the first read of the seat list on the reader's Ready, but only to a bound that the
// test gives as a channel; past it, it serves with the bootstrapped seats and says so, and nothing sleeps.
func TestTheRunnerWaitsForTheFirstReadOfTheSeatListOnlyToItsBound(t *testing.T) {
for _, row := range []struct {
name string
release bool // the seat ends its first read (here by refusing) before the bound
wantSaid string
wantReady bool
}{
{"the first read ends before the bound", true, "", true},
{"the bound passes with the seat silent", false, "the seat list is not read yet", false},
} {
t.Run(row.name, func(t *testing.T) {
b := &busOfAStandIn{release: make(chan struct{})}
bound := make(chan time.Time, 1)
var said []string
if row.release {
close(b.release)
} else {
bound <- time.Time{}
}
r := startSeatListOn(b, bound, func(f string, a ...any) { said = append(said, f) })
if r == nil {
t.Fatal("no reader")
}
ended := false
select {
case <-r.Ready():
ended = true
default:
}
if ended != row.wantReady {
t.Errorf("first read ended = %v, want %v", ended, row.wantReady)
}
if got := strings.Join(said, ";"); !strings.Contains(got, row.wantSaid) || (row.wantSaid == "" && got != "") {
t.Errorf("said %q, want %q", got, row.wantSaid)
}
if !row.release {
close(b.release)
<-r.Ready()
}
})
}
}
+8 -3
View File
@@ -3,14 +3,19 @@ module github.com/novox/mesh-tools/node-tools
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
github.com/nats-io/nats.go v1.54.0
golang.org/x/sys v0.48.0
)
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.1 // indirect
-4
View File
@@ -1,7 +1,3 @@
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 h1:Zt7CfWt2S2W/fVgWNyW2u2+xwz051LXvGxVTNIyfhdM=
git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639/go.mod h1:0dD1XEJBJ0G5UYDQxSZI84nuujb6m3tOpJl4C8rQVvg=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f h1:jDsfDMd48MXY9Zershc3eRxnhmx2sSpUfQkLq5jAIds=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f/go.mod h1:08OomDXdwVN8/87nyDtv5R3rH/6LoP8SFFXz30bTYCM=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E=
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
+4 -5
View File
@@ -18,7 +18,6 @@ import (
"encoding/json"
"errors"
"fmt"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/membership"
"log"
"regexp"
@@ -355,7 +354,7 @@ func (c *Conn) Credential() Credential { return c.cred }
// Follow reads a module's membership on this machine once and follows it live, so its tools are
// served where the mesh issued them (ADR 0175). It is read from the grants part's state memberships, its key
// `<node>.<module>` (novox/hq ADR 0571 clause 7 and 10, mesh-sdk buscompose.GrantsMembershipsBucket), and nowhere else:
// `<node>.<module>` (novox/hq ADR 0571 clause 7 and 10, mesh-sdk membership.GrantsBucket), and nowhere else:
// the old assignments stream is not read and its subject is not heard.
func (c *Conn) Follow(module string) {
c.mu.Lock()
@@ -434,10 +433,10 @@ func (c *Conn) watchGrants(module string, say bool) bool {
c.Logf(format, a...)
}
}
key := buscompose.MembershipKey(c.node, module)
key := membership.GrantsKey(c.node, module)
// First the key's direct get, which the account is granted alone: a bus that refuses it, or a state not there yet,
// answers at once, where binding the bucket would wait out its bound on a refusal.
bucket := buscompose.GrantsMembershipsBucket
bucket := membership.GrantsBucket
probe, err := c.nc.Request("$JS.API.DIRECT.GET.KV_"+bucket+".$KV."+bucket+"."+key, nil, GrantsPatience)
if err != nil || probe.Header.Get("Status") == "503" {
why := "it did not answer"
@@ -457,7 +456,7 @@ func (c *Conn) watchGrants(module string, say bool) bool {
kv, err := js.KeyValue(ctx, bucket)
if err != nil {
logf("[mesh-tools] the grants part's memberships (%s) cannot be read (%v); %s has no membership "+
"until it can be", buscompose.GrantsMembershipsBucket, err, module)
"until it can be", membership.GrantsBucket, err, module)
return false
}
w, err := kv.Watch(context.Background(), key)
+1 -1
View File
@@ -16,7 +16,7 @@ import (
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
// A bus whose users are granted as the controller grants them (mesh-placement internal/broker): each named
// A bus whose users are granted as the controller grants them (mesh-module-assigner internal/broker): each named
// user publishes the subjects listed for it.
func grantedBus(t *testing.T, publish map[string][]string) string {
t.Helper()
+2 -2
View File
@@ -98,7 +98,7 @@ func TestAModuleSubmitsAndSaysOnlyUnderItsOwnNameAndKind(t *testing.T) {
t.Fatalf("an asker could not ask under its own name: %v", err)
}
for module, subject := range map[string]string{
"mesh-delivery": "mesh.seat.operator-channel.accept.ask.mesh-placement",
"mesh-delivery": "mesh.seat.operator-channel.accept.ask.mesh-module-assigner",
"telegram": "mesh.seat.intake.event.choice.desktop",
"messenger": "mesh.seat.intake.event.choice.telegram",
} {
@@ -206,7 +206,7 @@ func TestAnAskerReadsItsOwnRecordAndNoOther(t *testing.T) {
if _, err := kv.Put("mesh-delivery.a1", []byte(`{"state":"open"}`)); err != nil {
t.Fatal(err)
}
if _, err := kv.Put("mesh-placement.a1", []byte(`{"state":"open"}`)); err != nil {
if _, err := kv.Put("mesh-module-assigner.a1", []byte(`{"state":"open"}`)); err != nil {
t.Fatal(err)
}
got, err := conn.SeatRecord("mesh-delivery", "messenger_asks", "a1")
+1 -1
View File
@@ -545,7 +545,7 @@ var credentialEndings = []string{"password", "passwd", "secret", "token", "crede
// credentialField is the first field anywhere in a JSON value whose name says it is a credential, or "". The one shape a
// credential-shaped name may hold is a provider reference, an object of exactly `node` and `module`, both strings: a
// placement's binding of the provision "secret" to the vault (issue 1126) names who provides it, never its value. Any
// the assignment's binding of the provision "secret" to the vault (issue 1126) names who provides it, never its value. Any
// other shape under such a name is refused.
func credentialField(value json.RawMessage) string {
var v any
+1 -1
View File
@@ -15,7 +15,7 @@ func TestACredentialNamedFieldIsFoundAnywhereInAValue(t *testing.T) {
`{"clientSecret":"s"}`: "clientSecret",
`{"maxTokens":4096,"name":"a","generation":3}`: "",
`"just a string"`: "",
// Issue 1126: a placement binds the provision "secret" to the vault; only an exact provider reference passes.
// Issue 1126: an assignment binds the provision "secret" to the vault; only an exact provider reference passes.
`{"bindings":{"secret":{"node":"anchor","module":"mesh-vault"}}}`: "",
`{"secret":{"value":"s"}}`: "secret",
`{"secret":{"node":"anchor","module":"mesh-vault","value":"s"}}`: "secret",
+1 -1
View File
@@ -33,7 +33,7 @@ func TestAWatchHoldsWhileItsCurrentValuesKeepArriving(t *testing.T) {
for i := 0; i < older; i++ {
keys = append(keys, fmt.Sprintf("novox/mesh-catalog.%d.%040x.status=mesh/check", 400+i, i))
}
for _, member := range []string{"novox/mesh-sdk.76.34f210e5", "novox/mesh-placement.453.c06f75d0", "novox/mesh-delivery.33.28ee22ef"} {
for _, member := range []string{"novox/mesh-sdk.76.34f210e5", "novox/mesh-module-assigner.453.c06f75d0", "novox/mesh-delivery.33.28ee22ef"} {
keys = append(keys, member+strings.Repeat("0", 32)+".status=mesh/check")
}
for _, k := range keys {
+52 -34
View File
@@ -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/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/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
@@ -52,7 +55,7 @@ const (
// searchCap is how many matches a search answers before it says how many more there were.
const searchCap = 25
const grammar = "Addresses: `<seat>.<verb>` for a seat held once for the mesh (e.g. `mesh-placement.placement`); " +
const grammar = "Addresses: `<seat>.<verb>` for a seat held once for the mesh (e.g. `mesh-module-assigner.assignments`); " +
"`<node>/<seat>.<verb>` for a seat every machine holds (e.g. `ace/node-packet-filter.rules`); " +
"`<node>/<module>.<tool>` for a module on one machine (e.g. `novox/postgres.postgres_list_databases`); " +
"and `<module>.<tool>` also for a module whose instances are interchangeable."
@@ -93,7 +96,7 @@ func discovery() []map[string]any {
"for when an address is said to be missing or on another machine."},
{"name": verbFirstViews, "inputSchema": obj(map[string]any{}),
"description": "The tool runner's first views of the four states it reads of the indexer and the assigner (the " +
"assigner's placement and seat-holders, the indexer's manifests and modules): for each, the owner, the tool " +
"assigner's assignments and seat-holders, the indexer's manifests and modules): for each, the owner, the tool " +
"asked, the revision held and when it was made, how many keys, when it was asked and answered, how often " +
"it was asked and how often by hand, whether its condition is raised, and why it was last asked. Read-only."},
{"name": verbFirstAgain, "inputSchema": obj(map[string]any{
@@ -142,14 +145,14 @@ type index struct {
NotAnswering []string
Listing *Listing // the flat catalogue the call path resolves subjects with
Discovery announce.Discovery
// Recorded is where placement places each module that declares tools.
// Recorded is where the assigner places each module that declares tools.
Recorded map[string][]string
// Unheard is every runtime whose answer this index lacks: it said it was there and did not say
// what it serves in time, or it answered before and not now. An address it might answer is never
// 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 `<seat>` and `<seat>.<node>`, read with delivery.HoldersOf; PlacedWhy says why it could not be read.
// Placed is the state seat-holders as the seat mesh-module-assigner's holder wrote it (novox/hq ADR 0571 clause 4), by its
// keys `<seat>` and `<seat>.<node>`, 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):
@@ -242,7 +245,7 @@ func findTool(tools []Tool, name string) *Tool {
return nil
}
// recordedModule is a module as the bus holds it: where the placement seat places it, and whether its manifest declares
// recordedModule is a module as the bus holds it: where the assigner seat places it, and whether its manifest declares
// tools — what should announce itself, and where (novox/hq ADR 0571).
type recordedModule struct {
Module string `json:"module"`
@@ -268,9 +271,9 @@ func indexOn(conn *bus.Conn, src Source, held announce.Known, before func(announ
var recordsWhy []string
wg.Add(2)
// Which modules there are and where they are placed, and which machines the mesh has, from the bus (ADR 0571
// clauses 4 and 5): the indexer's modules and manifests and the placement seat's placements, never any other record.
// clauses 4 and 5): the indexer's modules and manifests and the assigner seat's assignments, never any other record.
go func() { defer wg.Done(); recorded, known, recordsWhy = recordsOn(src) }()
// Who holds which seat on which node, from placement (ADR 0571 clause 4): no other call.
// Who holds which seat on which node, from the assigner (ADR 0571 clause 4): no other call.
go func() { defer wg.Done(); placed, placedErr = seatHoldersOn(src) }()
d, err := announce.Gather(conn, held)
wg.Wait()
@@ -321,7 +324,7 @@ func indexOn(conn *bus.Conn, src Source, held announce.Known, before func(announ
// What should have answered: every assignment of a module that declares tools. Silence is named;
// a module with no tools is never a name here.
x.Replaces = replacesOf(catalogue.DefaultSeats(), recorded)
x.Replaces = replacesOf(catalogue.Seats(), recorded)
for _, m := range recorded {
if !m.Tools {
continue
@@ -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, firstview.SeatHoldersRead+" (who holds which seat by assignment 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 `<owner>.<state>` (delivery.PlacementRead and the
// assignments and seat-holders, the indexer's modules and manifests, by `<owner>.<state>` (firstview.AssignmentsRead 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 firstview.SeatHoldersRead.
func seatHoldersOn(src Source) (map[string][]byte, error) {
return src.State(delivery.SeatHoldersRead)
return src.State(firstview.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
@@ -387,21 +390,21 @@ 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
// recordsOn is every module the indexer holds, with where the assigner seat places it and whether it declares tools,
// and every machine the assignments name (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)
held, err := src.State(firstview.AssignmentsRead)
if err != nil {
why = append(why, delivery.PlacementRead+" (where modules are placed could not be read, so what is missing cannot "+
why = append(why, firstview.AssignmentsRead+" (where modules are placed could not be read, so what is missing cannot "+
"be said: "+err.Error()+")")
}
machines = delivery.MachineNames(placements)
machines = assigner.MachineNames(held)
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)
// The assignments' keys in either form, a node key and its module keys or the form before (research 072 row 37).
assembled, err := assigner.AssignmentsOf(held)
if err != nil {
why = append(why, delivery.PlacementRead+" (not every key is a placement: "+err.Error()+")")
why = append(why, firstview.AssignmentsRead+" (not every key is an assignment: "+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,32 +461,32 @@ 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)
return facts, nil
}
// 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 {
// placedHolders is who holds seat (on node, for a seat held per machine) by assignment, or none.
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...)
}
}
@@ -707,7 +725,7 @@ func resolve(x *index, address string) (target, error) {
for _, h := range hs {
held = append(held, h.Module+" on "+h.Node)
}
return target{}, fmt.Errorf("the seat %s is held by %s by placement, but no holder of it announced itself on the bus. "+
return target{}, fmt.Errorf("the seat %s is held by %s by assignment, but no holder of it announced itself on the bus. "+
"Ask again in a moment; mesh_runtimes shows which runtimes answered", prefix, strings.Join(held, ", "))
}
if len(x.Unheard) > 0 {
+17 -17
View File
@@ -71,19 +71,19 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) {
}
// The controller: its own seat's verbs announced on the bus like every runtime's; its records are asked by no one.
controller := connect(t, "mesh-placement", "bench")
controller := connect(t, "mesh-module-assigner", "bench")
// Its verb nodes, called through the console as any seat's verb is.
stopNodes, err := controller.HandleSubject("mesh.seat.mesh-placement.tool.nodes", func(json.RawMessage) (any, error) {
stopNodes, err := controller.HandleSubject("mesh.seat.mesh-module-assigner.tool.nodes", func(json.RawMessage) (any, error) {
return map[string]any{"ok": true, "output": `[{"name":"bench","mode":"converged"},{"name":"desk","mode":"converged"}]`}, nil
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(stopNodes)
// What the mesh holds, as the indexer and placement write it (ADR 0571 clauses 4, 5): never the controller's records.
// What the mesh holds, as the indexer and the assigner write it (ADR 0571 clauses 4, 5): never the controller's records.
tools := map[string][]string{"alpha": {"one", "two"}, "beta": {"three"}, "gamma": {"given"}, "delta": nil,
"epsilon": {"seven"}, "mesh-placement": {"nodes"}}
mesh.Placed(t, tools, map[string][]string{"desk": {"alpha", "beta", "delta"}, "bench": {"epsilon", "mesh-placement"}})
"epsilon": {"seven"}, "mesh-module-assigner": {"nodes"}}
mesh.Placed(t, tools, map[string][]string{"desk": {"alpha", "beta", "delta"}, "bench": {"epsilon", "mesh-module-assigner"}})
console := mt.MembershipOf("node-tools", "desk", false, nil)
console.State = mt.ConsoleReads()
mesh.Issue(t, console)
@@ -93,11 +93,11 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) {
}
return nil
})
stopAnn, err := announce.Serve(controller, announce.Service{Name: "mesh-placement", ID: "bench"}, func() []announce.Endpoint {
return []announce.Endpoint{{Kind: announce.KindSeat, Module: "mesh-placement", Seat: "mesh-placement", Scope: "mesh",
stopAnn, err := announce.Serve(controller, announce.Service{Name: "mesh-module-assigner", ID: "bench"}, func() []announce.Endpoint {
return []announce.Endpoint{{Kind: announce.KindSeat, Module: "mesh-module-assigner", Seat: "mesh-module-assigner", Scope: "mesh",
// As the live controller announces: a mesh seat's holder names no machine.
Tool: "nodes", Node: "", Description: "Every machine the mesh knows.", Schema: json.RawMessage(`{}`),
Subject: "mesh.seat.mesh-placement.tool.nodes"}}
Subject: "mesh.seat.mesh-module-assigner.tool.nodes"}}
})
if err != nil {
t.Fatal(err)
@@ -131,13 +131,13 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) {
// The overview names the mesh's seats, the machines' seats and the machines.
overview, isErr := call(t, endpoint, "mesh_overview", nil)
if isErr || !strings.Contains(overview, "mesh-placement.nodes") || !strings.Contains(overview, "<node>/node-shelf.list") ||
if isErr || !strings.Contains(overview, "mesh-module-assigner.nodes") || !strings.Contains(overview, "<node>/node-shelf.list") ||
!strings.Contains(overview, `"bench"`) || !strings.Contains(overview, `"desk"`) {
t.Errorf("overview: %s", overview)
}
// Silence is named only where tools should have answered: epsilon declares tools on bench and
// nothing there announced it; delta declares none and is never a name.
if strings.Contains(overview, "mesh-placement on bench") {
if strings.Contains(overview, "mesh-module-assigner on bench") {
t.Errorf("a mesh seat's holder that announced no machine is called silent on its machine:\n%s", overview)
}
if !strings.Contains(overview, "epsilon on bench") || strings.Contains(overview, "delta") {
@@ -150,7 +150,7 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) {
}
// A mesh seat, a node seat, an assignment and an interchangeable module, each by address.
if got, isErr := call(t, endpoint, "mesh_call", map[string]any{"address": "mesh-placement.nodes"}); isErr || !strings.Contains(got, "converged") {
if got, isErr := call(t, endpoint, "mesh_call", map[string]any{"address": "mesh-module-assigner.nodes"}); isErr || !strings.Contains(got, "converged") {
t.Errorf("mesh seat: %s", got)
}
if got, isErr := call(t, endpoint, "mesh_call", map[string]any{"address": "desk/node-shelf.list"}); isErr || !strings.Contains(got, `"a"`) {
@@ -166,11 +166,11 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) {
// Refused, by name, where the address does not say enough or says the wrong thing.
for address, want := range map[string]string{
"beta.three": "keeps state on each machine it runs on, so a call names the machine: write <node>/beta.three — it runs on desk",
"node-shelf.list": "held once per machine: write <node>/node-shelf.list — it is held on desk",
"desk/mesh-placement.nodes": "held once for the whole mesh",
"bench/beta.three": "beta does not run on bench; it runs on desk",
"nonsense": "is not an address",
"beta.three": "keeps state on each machine it runs on, so a call names the machine: write <node>/beta.three — it runs on desk",
"node-shelf.list": "held once per machine: write <node>/node-shelf.list — it is held on desk",
"desk/mesh-module-assigner.nodes": "held once for the whole mesh",
"bench/beta.three": "beta does not run on bench; it runs on desk",
"nonsense": "is not an address",
} {
got, isErr := call(t, endpoint, "mesh_call", map[string]any{"address": address})
if !isErr || !strings.Contains(got, want) {
@@ -201,7 +201,7 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) {
t.Fatalf("gamma was found before it served: %s", got)
}
mesh.Issue(t, mt.MembershipOf("gamma", "desk", false, nil))
mesh.Placed(t, tools, map[string][]string{"desk": {"alpha", "beta", "delta", "gamma"}, "bench": {"epsilon", "mesh-placement"}})
mesh.Placed(t, tools, map[string][]string{"desk": {"alpha", "beta", "delta", "gamma"}, "bench": {"epsilon", "mesh-module-assigner"}})
late := connect(t, "node-tools", "desk")
stopLate, err := runtime.Run(late, []runtime.Served{{Module: "gamma", Entrypoints: []string{mt.Fixture("env-gamma.serve.mjs")}}},
nil, (&mt.Logs{}).Logf)
+1 -1
View File
@@ -57,7 +57,7 @@ func TestTheConsoleListsAndCallsOverHTTP(t *testing.T) {
}
defer stop()
// What the mesh holds, as the indexer and placement write it (ADR 0571): ghost declares tools on desk and nothing
// What the mesh holds, as the indexer and the assigner write it (ADR 0571): ghost declares tools on desk and nothing
// announces it.
mesh.Placed(t, map[string][]string{"alpha": {"one", "two"}, "beta": {"three"}, "ghost": {"boo"}},
map[string][]string{"desk": {"alpha", "beta", "ghost"}})
@@ -17,7 +17,7 @@ type viewer struct {
}
func (v *viewer) Status() []views.Status {
return []views.Status{{Owner: "mesh-delivery-assigner", Tool: "placement", State: "placement", Revision: 4, Asks: 1}}
return []views.Status{{Owner: "mesh-delivery-assigner", Tool: "assignments", State: "assignments", Revision: 4, Asks: 1}}
}
func (v *viewer) Again(_ context.Context, why string) []error {
@@ -35,7 +35,7 @@ func TestTheConsoleShowsAndAsksAgainTheRunnersFirstViews(t *testing.T) {
v := &viewer{}
var recorded []map[string]any
s := &Surface{src: v, who: "desk.node-tools", Record: func(a map[string]any) error { recorded = append(recorded, a); return nil }}
if got := text(s.discover(context.Background(), "mesh_first_views", nil)); !strings.Contains(got, "mesh-delivery-assigner") || !strings.Contains(got, "placement") {
if got := text(s.discover(context.Background(), "mesh_first_views", nil)); !strings.Contains(got, "mesh-delivery-assigner") || !strings.Contains(got, "assignments") {
t.Errorf("mesh_first_views shows %s", got)
}
got := s.discover(context.Background(), "mesh_first_view_again", map[string]any{"why": "doubted"})
@@ -36,8 +36,8 @@ func TestTheManifestNamesTheFirstFamiliesOneByOne(t *testing.T) {
}
}
// A wait is no consumer (ADR 0367 §2): what it consumes is the four views families of its first views (ADR 0615 §5, issue
// 1966 TLS-5), and nothing else.
wantConsumes := []string{"mesh-delivery-assigner.views.placement", "mesh-delivery-assigner.views.seat-holders",
// 1966 TLS-5) and the two events of the seat that serves the seat list (ADR 0636 §4), and nothing else.
wantConsumes := []string{"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.assignments", "mesh-delivery-assigner.views.seat-holders",
"mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules"}
if !reflect.DeepEqual(m.Consumes, wantConsumes) {
t.Errorf("the tool runner consumes %v, want %v", m.Consumes, wantConsumes)
+7 -7
View File
@@ -6,8 +6,8 @@ package console
import (
"context"
"encoding/json"
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"os"
"strings"
"sync"
@@ -70,15 +70,15 @@ type Surface struct {
// [{module, node}]}], holders_why}`.
const MethodSeats = "mesh/seats"
// seatsAnswer is MethodSeats' answer: mesh-sdk's seat definitions, with each seat's holders by placement.
// seatsAnswer is MethodSeats' answer: mesh-sdk's seat definitions, with each seat's holders by assignment.
func (s *Surface) seatsAnswer() map[string]any {
placed, err := seatHoldersOn(s.src)
x := &index{Placed: placed}
seats := []map[string]any{}
for _, st := range catalogue.DefaultSeats() {
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 {
@@ -88,7 +88,7 @@ func (s *Surface) seatsAnswer() map[string]any {
}
out := map[string]any{"seats": seats}
if err != nil {
out["holders_why"] = "who holds which seat by placement could not be read: " + err.Error()
out["holders_why"] = "who holds which seat by assignment could not be read: " + err.Error()
}
return out
}
@@ -160,7 +160,7 @@ func (s *Surface) HandleContext(ctx context.Context, r Request) *Reply {
return answer(r.ID, map[string]any{})
case MethodSeats:
// The seats' contract and who holds each, for a client on this machine that has no seat definitions of its own
// (the TypeScript `mesh`): read in-process from mesh-sdk and from placement, never asked of anything else
// (the TypeScript `mesh`): read in-process from mesh-sdk and from the assigner, never asked of anything else
// (novox/hq ADR 0571 clauses 4 and 5).
return answer(r.ID, s.seatsAnswer())
case "tools/list":
@@ -341,7 +341,7 @@ func (s *Surface) instructions() string {
"that serves it; what may be called was fixed when this account was issued, so a " +
"refusal means the account, not the tool. The list is what the running modules " +
"answered, plus every role's tools from the mesh's records — the mesh's own verbs " +
"(mesh-delivery.deliveries, mesh-placement.propose …) among them; a module that did not answer " +
"(mesh-delivery.deliveries, mesh-module-assigner.propose …) among them; a module that did not answer " +
"is named in the list's _meta and can still be called by <module>.<tool>."
}
return "The tools of a Novox mesh, reached as " + s.who + ", found by address rather than listed " +
@@ -11,9 +11,9 @@ import (
// 2026-10-05 the controller's push was described as taking nothing, and a push naming one machine
// ran as a push of every machine behind.
func TestAMeshSeatsNodeIsItsOwnArgument(t *testing.T) {
push := Tool{Module: "mesh-placement", Name: "push", Seat: true, Scope: "mesh",
push := Tool{Module: "mesh-module-assigner", Name: "push", Seat: true, Scope: "mesh",
Input: json.RawMessage(`{"type":"object","properties":{"node":{"type":"string"},"behind":{"type":"string"}}}`)}
mesh := target{Address: "mesh-placement.push", Tool: push, Seat: true}
mesh := target{Address: "mesh-module-assigner.push", Tool: push, Seat: true}
props := schemaAsPassed(mesh)["properties"].(map[string]any)
if _, has := props["node"]; !has {
@@ -40,5 +40,11 @@ func TestATimeoutIsNotAFailure(t *testing.T) {
if strings.Contains(text, "controller") {
t.Errorf("%s names the controller: %s", name, text)
}
// The seats were renamed (ADR 0634 §9): no hint names the seat that placed modules or enrolled a machine by its old name.
for _, old := range []string{"mesh-placement", "mesh-enrolment"} {
if strings.Contains(text, old) {
t.Errorf("%s names the seat %s, which is now mesh-module-assigner or mesh-node-enroller: %s", name, old, text)
}
}
}
}
+18 -14
View File
@@ -8,11 +8,13 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
assigner "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seatassigner "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
"github.com/nats-io/nats.go/micro"
"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/meshtest"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
@@ -56,18 +58,20 @@ func TestARuntimeThatAnswersLateIsFoundAndOneThatNeverDoesIsNamed(t *testing.T)
IndexKept, announce.Patience = time.Minute, 2500*time.Millisecond
t.Cleanup(func() { IndexKept, announce.Patience = keptWas, patienceWas })
mesh := mt.New(t)
// What the mesh holds, as the indexer and placement write it (ADR 0571): never the controller's records.
// What the mesh holds, as the indexer and the assigner write it (ADR 0571): never the controller's records.
mesh.Placed(t, map[string][]string{"slack": {"slack_check"}, "nap": {"doze"}, "nextcloud-client": {"nextcloud_check"}},
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)
}
// Who holds which seat, by assignment (ADR 0571 clause 4), as the seat mesh-module-assigner's holder composes it.
placedKV := mesh.Bucket(t, meshtest.SeatHoldersBucket)
holdersKey, err := assigner.SeatHoldersKey("mesh-wants", "")
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(seatassigner.SeatHolders{V: assigner.Version, Seat: "mesh-wants", Scope: "mesh",
Holders: []seatassigner.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()
@@ -95,7 +99,7 @@ func TestARuntimeThatAnswersLateIsFoundAndOneThatNeverDoesIsNamed(t *testing.T)
Subject: "mesh.mod.nextcloud-client.tool.nextcloud_check.desktop"},
})
controller := connect(t, "mesh-placement", "bench")
controller := connect(t, "mesh-module-assigner", "bench")
var toolsAsked atomic.Int32
for verb, output := range map[string]string{
// The controller's records are served here only to see that none is asked (ADR 0571 clauses 4 and 5).
@@ -104,7 +108,7 @@ func TestARuntimeThatAnswersLateIsFoundAndOneThatNeverDoesIsNamed(t *testing.T)
"tools": `{"seats":[]}`,
} {
output, verb := output, verb
stop, err := controller.HandleSubject("mesh.seat.mesh-placement.tool."+verb, func(json.RawMessage) (any, error) {
stop, err := controller.HandleSubject("mesh.seat.mesh-module-assigner.tool."+verb, func(json.RawMessage) (any, error) {
toolsAsked.Add(1)
return map[string]any{"output": output, "ok": true}, nil
})
@@ -154,8 +158,8 @@ func TestARuntimeThatAnswersLateIsFoundAndOneThatNeverDoesIsNamed(t *testing.T)
// The silent one is named, never called missing.
for address, want := range map[string]string{
// A seat whose holder announced nothing is named with its holder by placement, never called missing.
"mesh-wants.read": "the seat mesh-wants is held by mesh-delivery-assigner on bench by placement",
// A seat whose holder announced nothing is named with its holder by assignment, never called missing.
"mesh-wants.read": "the seat mesh-wants is held by mesh-delivery-assigner on bench by assignment",
"sleeper/nap.doze": "nap is assigned to sleeper in the mesh's records, but nothing that answered discovery serves it: discovery did not hear in full from node-tools on sleeper (it answered PING",
"sleeper/nextcloud-client.nextcloud_check": "nextcloud-client on sleeper did not answer discovery: discovery did not hear in full from node-tools on sleeper",
} {
+19 -19
View File
@@ -33,8 +33,8 @@ func meshToSearch(t *testing.T) *index {
"node": "What one machine reported it can do, what it is assigned, and why.",
"nodes": "Every machine the mesh knows, with whether it is converged or adopted.",
} {
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-placement", Seat: "mesh-placement",
Tool: verb, Scope: "mesh", Description: does, Subject: "mesh.seat.mesh-placement.tool." + verb})
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-module-assigner", Seat: "mesh-module-assigner",
Tool: verb, Scope: "mesh", Description: does, Subject: "mesh.seat.mesh-module-assigner.tool." + verb})
}
for tool, does := range map[string]string{
"docker_list": "Every container on this machine: name, image, state, ports.",
@@ -46,7 +46,7 @@ func meshToSearch(t *testing.T) *index {
Description: does, Subject: "mesh.mod.docker.tool." + tool + ".anchor"})
}
x, _, _ := indexOfEndpoints(endpoints)
x.Replaces = replacesOf(catalogue.DefaultSeats(), []recordedModule{{Module: "docker", On: []string{"anchor"}, Tools: true,
x.Replaces = replacesOf(catalogue.Seats(), []recordedModule{{Module: "docker", On: []string{"anchor"}, Tools: true,
Replaces: map[string][]string{"docker_list": {"docker ps"}, "docker_logs": {"docker logs"}, "docker_restart": {"docker restart"}}}})
return x
}
@@ -56,21 +56,21 @@ func meshToSearch(t *testing.T) *index {
func TestSearchFindsTheToolThatReplacesACommandFirst(t *testing.T) {
x := meshToSearch(t)
for query, want := range map[string]string{
"journalctl": "anchor/node-service-manager.journal",
"journalctl -u mesh-placement -n 50": "anchor/node-service-manager.journal",
"sudo journalctl -fu sshd": "anchor/node-service-manager.journal",
"/usr/bin/journalctl": "anchor/node-service-manager.journal",
"logs": "anchor/node-service-manager.journal",
"systemctl status": "anchor/node-service-manager.status",
"systemctl status nats.service": "anchor/node-service-manager.status",
"systemctl restart mesh-placement": "anchor/node-service-manager.restart",
"systemctl --failed": "anchor/node-service-manager.failed",
"docker ps": "anchor/docker.docker_list",
"docker ps -a": "anchor/docker.docker_list",
"docker logs": "anchor/docker.docker_logs",
"docker logs --tail 100 nats": "anchor/docker.docker_logs",
"docker restart nats": "anchor/docker.docker_restart",
"container logs": "anchor/docker.docker_logs",
"journalctl": "anchor/node-service-manager.journal",
"journalctl -u mesh-module-assigner -n 50": "anchor/node-service-manager.journal",
"sudo journalctl -fu sshd": "anchor/node-service-manager.journal",
"/usr/bin/journalctl": "anchor/node-service-manager.journal",
"logs": "anchor/node-service-manager.journal",
"systemctl status": "anchor/node-service-manager.status",
"systemctl status nats.service": "anchor/node-service-manager.status",
"systemctl restart mesh-module-assigner": "anchor/node-service-manager.restart",
"systemctl --failed": "anchor/node-service-manager.failed",
"docker ps": "anchor/docker.docker_list",
"docker ps -a": "anchor/docker.docker_list",
"docker logs": "anchor/docker.docker_logs",
"docker logs --tail 100 nats": "anchor/docker.docker_logs",
"docker restart nats": "anchor/docker.docker_restart",
"container logs": "anchor/docker.docker_logs",
} {
hits := x.search(query)
if len(hits) == 0 {
@@ -111,7 +111,7 @@ func TestASearchByCommandSaysWhatItReplaces(t *testing.T) {
// A synonym is a word, never part of one: `logs` matches through `log`, and finds no `login`.
func TestSearchStillMatchesEveryWord(t *testing.T) {
x := meshToSearch(t)
if hits := x.search("machine converged"); len(hits) != 1 || hits[0].Address != "mesh-placement.nodes" {
if hits := x.search("machine converged"); len(hits) != 1 || hits[0].Address != "mesh-module-assigner.nodes" {
t.Errorf("words in a description: %+v", hits)
}
if hits := x.search("journalctl kubernetes"); len(hits) != 1 || hits[0].Address != "anchor/node-service-manager.journal" {
@@ -0,0 +1,10 @@
package console
import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest"
)
// The seats the tests describe are given to catalogue as the tool runner gives the ones it read from the bus
// (novox/hq ADR 0635 §1); catalogue compiles none.
func init() { catalogue.UseSeats(catalogue.SeatsFromDefinitions(seatlisttest.Definitions())) }
+34 -4
View File
@@ -1,5 +1,5 @@
// Package firstview is the tool runner's picture of the four states it reads of the indexer and the assigner: the assigner's
// placement and seat-holders, the indexer's manifests and modules (novox/hq ADR 0615 §1, §3; ADR 0621 §2; issue 1966 TLS-5).
// assignments and seat-holders, the indexer's manifests and modules (novox/hq ADR 0615 §1, §3; ADR 0621 §2; issue 1966 TLS-5).
//
// **It never reads the owners' state.** At start it asks each owner's first-view tool until answered (the owner answers in a
// runtime of its own, never through this one: ADR 0621 §1), then follows the owner's views family, one event per changed key;
@@ -15,6 +15,9 @@ import (
"sync"
"time"
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"
"git.novox.be/novox/mesh-sdk/go/views"
"github.com/novox/mesh-tools/node-tools/internal/bus"
@@ -26,18 +29,27 @@ const (
Assigner = "mesh-delivery-assigner"
)
// The names the console reads the four states by (`<owner>.<state>`), as the manifest's `consumes` names their views
// families (no seat's client names a holder, novox/hq ADR 0625 §4; TestTheFirstViewsAreTheManifestsViews).
const (
AssignmentsRead = Assigner + ".assignments"
SeatHoldersRead = Assigner + ".seat-holders"
ModulesRead = Indexer + ".modules"
ManifestsRead = Indexer + ".manifests"
)
// Spec is one first view the runner holds.
type Spec struct{ Owner, Tool, State string }
// Specs are the four.
var Specs = []Spec{
{Assigner, "placement", "placement"},
{Assigner, "assignments", "assignments"},
{Assigner, "seat-holders", "seat-holders"},
{Indexer, "manifests", "manifests"},
{Indexer, "modules", "modules"},
}
// Name is the name a state is read by, `<owner>.<state>`: delivery.PlacementRead, delivery.SeatHoldersRead and the indexer's.
// Name is the name a state is read by, `<owner>.<state>`: AssignmentsRead, SeatHoldersRead and the indexer's.
func (s Spec) Name() string { return s.Owner + "." + s.State }
// Views is the four followers.
@@ -135,7 +147,7 @@ func (v *Views) hear(env bus.Envelope) error {
// finding writes or removes the runner's finding of a first view unanswered past its bound, in its own state.
func (v *Views) finding(st views.Status, raised bool) {
key, value, err := views.UnansweredFinding(v.deps.Module, st, time.Now())
key, value, err := conditionOf(views.UnansweredFinding(v.deps.Module, st, time.Now()))
if err != nil {
v.logf("[mesh-tools] the finding of the first view %s is not made: %v", st.Tool, err)
return
@@ -198,3 +210,21 @@ func (v *Views) Again(ctx context.Context, why string) []error {
}
return errs
}
// conditionOf is a finding in the layout of the seat mesh-conditions' state findings (its contract, ADR 0634 §1), about a
// module, a warning.
func conditionOf(f views.Finding) (string, []byte, error) {
key := conditions.Key(scopeModule, f.ID, f.Kind)
v := condseat.FindingsByKeyValue{V: 1, Key: key, Kind: f.Kind, Observer: f.Observer, Severity: conditions.Warning,
Subject: condseat.Subject{Scope: scopeModule, ID: f.ID}, Summary: f.Summary, Headline: f.Headline,
Explanation: f.Explanation, Source: f.Observer + "." + f.Kind, At: f.At.UTC(),
Evidence: []condseat.Evidence{{At: f.At.UTC(), Said: f.Summary}}}
if err := conditions.CheckFinding(v); err != nil {
return "", nil, err
}
raw, err := json.Marshal(v)
return key, raw, err
}
// scopeModule is the scope of a finding about a module.
const scopeModule = "module"
@@ -44,7 +44,10 @@ func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThen
filters := map[string][]string{}
families := 0
for _, c := range manifest.Consumes {
emitter, event, _ := strings.Cut(c, ".views.")
emitter, event, isView := strings.Cut(c, ".views.")
if !isView { // the seat mesh-catalog's events: the seat list's, on EVENTS (cmd/node-tools/seatlist.go)
continue
}
stream := streamOf[emitter]
if stream == "" {
t.Fatalf("the manifest consumes %s, whose views stream the test does not know", c)
@@ -89,7 +92,7 @@ func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThen
t.Cleanup(stop)
// The owners are down: no tool answers, nothing is claimed, and past the bound each view is a finding.
if _, err := v.State(Assigner + ".placement"); err == nil {
if _, err := v.State(Assigner + ".assignments"); err == nil {
t.Fatal("a state its owner has not answered is claimed")
}
mt.Until(t, func() error {
+45 -16
View File
@@ -4,6 +4,8 @@ import (
"context"
"encoding/json"
"errors"
"os"
"slices"
"strings"
"sync"
"testing"
@@ -26,9 +28,9 @@ type owners struct {
}
func newOwners() *owners {
return &owners{state: map[string]map[string]string{"placement": {"anchor": `{"p":1}`}, "seat-holders": {"seat": `{"h":1}`},
return &owners{state: map[string]map[string]string{"assignments": {"anchor": `{"p":1}`}, "seat-holders": {"seat": `{"h":1}`},
"manifests": {"d1": `{"m":1}`}, "modules": {"web": `{"w":1}`}},
rev: map[string]uint64{"placement": 4, "seat-holders": 2, "manifests": 7, "modules": 3}, asked: map[string]int{}, finds: map[string]bool{}}
rev: map[string]uint64{"assignments": 4, "seat-holders": 2, "manifests": 7, "modules": 3}, asked: map[string]int{}, finds: map[string]bool{}}
}
func (o *owners) ask(address string, _ any) (json.RawMessage, error) {
@@ -102,7 +104,7 @@ func TestTheRunnerHoldsTheFourStatesFromTheOwnersFirstViewsAndTheirEvents(t *tes
defer stop()
// Down: nothing is claimed, and past the bound each unanswered view is a finding.
if _, err := v.State("mesh-delivery-assigner.placement"); err == nil || !strings.Contains(err.Error(), "not been answered") {
if _, err := v.State("mesh-delivery-assigner.assignments"); err == nil || !strings.Contains(err.Error(), "not been answered") {
t.Fatalf("a state its owner has not answered is claimed: %v", err)
}
eventually(t, "no finding past the bound", func() bool { o.mu.Lock(); defer o.mu.Unlock(); return len(o.finds) == 4 })
@@ -122,31 +124,31 @@ func TestTheRunnerHoldsTheFourStatesFromTheOwnersFirstViewsAndTheirEvents(t *tes
// Events: the next revision is applied; an old one passed over; a deletion removes; none of it asks again.
asked := func(tool string) int { o.mu.Lock(); defer o.mu.Unlock(); return o.asked[tool] }
before := asked("placement")
o.say(t, "placement", "laptop", `{"p":2}`, 5, false)
o.say(t, "placement", "laptop", `{"p":0}`, 5, false)
o.say(t, "placement", "anchor", "", 6, false)
before := asked("assignments")
o.say(t, "assignments", "laptop", `{"p":2}`, 5, false)
o.say(t, "assignments", "laptop", `{"p":0}`, 5, false)
o.say(t, "assignments", "anchor", "", 6, false)
// (a deleted mark carries no value: Deleted is the encoding's, set below)
if got, _ := v.Entry("mesh-delivery-assigner.placement", "laptop"); string(got) != `{"p":2}` {
if got, _ := v.Entry("mesh-delivery-assigner.assignments", "laptop"); string(got) != `{"p":2}` {
t.Errorf("the next revision is not applied: %q", got)
}
if n := asked("placement"); n != before {
if n := asked("assignments"); n != before {
t.Errorf("an event in order asked the first view again (%d asks, was %d)", n, before)
}
// A gap (revision 9 where 7 was next) asks the first view once more, said in the log.
o.say(t, "placement", "x", `{"p":9}`, 9, false)
eventually(t, "a gap did not ask the first view again", func() bool { return asked("placement") > before })
o.say(t, "assignments", "x", `{"p":9}`, 9, false)
eventually(t, "a gap did not ask the first view again", func() bool { return asked("assignments") > before })
// The status says owner, tool, revision and the asks; the hand ask asks every view again.
var placement views.Status
var assignments views.Status
for _, s := range v.Status() {
if s.State == "placement" {
placement = s
if s.State == "assignments" {
assignments = s
}
}
if placement.Owner != "mesh-delivery-assigner" || placement.Tool != "placement" || placement.Asks < 2 || placement.Answered.IsZero() {
t.Errorf("the status of placement is %+v", placement)
if assignments.Owner != "mesh-delivery-assigner" || assignments.Tool != "assignments" || assignments.Asks < 2 || assignments.Answered.IsZero() {
t.Errorf("the status of assignments is %+v", assignments)
}
manifests := asked("manifests")
if errs := v.Again(context.Background(), "doubted"); len(errs) != 0 {
@@ -166,3 +168,30 @@ func TestTheRunnerHoldsTheFourStatesFromTheOwnersFirstViewsAndTheirEvents(t *tes
t.Errorf("the gap was not said in the log: %v", logs)
}
}
// The four first views are the manifest's `consumes` families, and the console reads each by the name here.
func TestTheFirstViewsAreTheManifestsViews(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Consumes []string `json:"consumes"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
want := map[string]bool{AssignmentsRead: true, 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 here", s.Name())
}
delete(want, s.Name())
if view := s.Owner + ".views." + s.Tool; !slices.Contains(m.Consumes, view) {
t.Errorf("the first view %s has no family in the manifest's consumes %v", view, m.Consumes)
}
}
if len(want) > 0 {
t.Errorf("no first view reads %v", want)
}
}
+41 -26
View File
@@ -13,9 +13,6 @@ package meshtest
import (
"context"
"encoding/json"
"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"
"runtime"
@@ -24,6 +21,12 @@ import (
"testing"
"time"
assigner "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
seatassigner "git.novox.be/novox/mesh-contracts/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/membership"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
@@ -88,8 +91,8 @@ func New(t *testing.T) *Mesh {
js, _ := nc.JetStream()
_ = js.DeleteStream("EVENTS")
// The grants part's state memberships, where a runtime reads what is issued it (ADR 0571 clause 10).
_ = js.DeleteKeyValue(buscompose.GrantsMembershipsBucket)
if _, err := js.CreateKeyValue(&nats.KeyValueConfig{Bucket: buscompose.GrantsMembershipsBucket, History: 1}); err != nil {
_ = js.DeleteKeyValue(membership.GrantsBucket)
if _, err := js.CreateKeyValue(&nats.KeyValueConfig{Bucket: membership.GrantsBucket, History: 1}); err != nil {
t.Fatal(err)
}
if _, err := js.AddStream(&nats.StreamConfig{Name: "EVENTS", Subjects: []string{"mesh.mod.*.event.>"}}); err != nil {
@@ -100,7 +103,7 @@ func New(t *testing.T) *Mesh {
}
// Issue writes a membership where the grants part keeps it, its state memberships, key `<node>.<module>` (novox/hq ADR
// 0571 clause 7 and 10; mesh-sdk buscompose.GrantsMembershipsBucket), the one place a runtime reads it. It writes with
// 0571 clause 7 and 10; mesh-sdk membership.GrantsBucket), the one place a runtime reads it. It writes with
// mesh-sdk's encoder, the grants part's own, never json of this package's type: a value built here by hand agreed with
// the reader's mistake and hid that the part wrote another shape (issue 774).
func (m *Mesh) Issue(t *testing.T, mem bus.Membership) {
@@ -120,13 +123,13 @@ func (m *Mesh) IssueInGrantsWrapped(t *testing.T, mem bus.Membership, published
func (m *Mesh) putGrants(t *testing.T, mem bus.Membership, body []byte) {
t.Helper()
kv, err := m.js.KeyValue(buscompose.GrantsMembershipsBucket)
kv, err := m.js.KeyValue(membership.GrantsBucket)
if err != nil {
if kv, err = m.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: buscompose.GrantsMembershipsBucket, History: 1}); err != nil {
if kv, err = m.js.CreateKeyValue(&nats.KeyValueConfig{Bucket: membership.GrantsBucket, History: 1}); err != nil {
t.Fatal(err)
}
}
if _, err := kv.Put(buscompose.MembershipKey(mem.Node, mem.Module), body); err != nil {
if _, err := kv.Put(membership.GrantsKey(mem.Node, mem.Module), body); err != nil {
t.Fatal(err)
}
}
@@ -134,7 +137,7 @@ func (m *Mesh) putGrants(t *testing.T, mem bus.Membership, body []byte) {
// DropGrants deletes the grants part's state memberships, as a bus that has not made it yet holds none; Issue makes it again.
func (m *Mesh) DropGrants(t *testing.T) {
t.Helper()
if err := m.js.DeleteKeyValue(buscompose.GrantsMembershipsBucket); err != nil {
if err := m.js.DeleteKeyValue(membership.GrantsBucket); err != nil {
t.Fatal(err)
}
}
@@ -150,19 +153,31 @@ 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"
assignerHolder = "mesh-delivery-assigner"
// AssignmentsBucket and SeatHoldersBucket are the assigner's states assignments and seat-holders.
AssignmentsBucket = assignerHolder + "_assignments"
SeatHoldersBucket = assignerHolder + "_seat-holders"
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.
// assignments, 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: assignerHolder + ".assignments", Bucket: AssignmentsBucket},
{Name: indexer + ".modules", Bucket: ModulesBucket},
{Name: indexer + ".manifests", Bucket: ManifestsBucket},
{Name: assignerHolder + ".seat-holders", Bucket: SeatHoldersBucket},
}
}
// Placed writes what the mesh holds as its parts write it, with mesh-sdk's encoders: each module the indexer holds with
// its manifest (tools, the tools it declares; none for a module without), and each node's placement of modules.
// its manifest (tools, the tools it declares; none for a module without), and each node's assignment of modules.
func (m *Mesh) Placed(t *testing.T, tools map[string][]string, on map[string][]string) {
t.Helper()
kv := func(bucket string) nats.KeyValue {
@@ -173,20 +188,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, assigned := kv(ModulesBucket), kv(ManifestsBucket), kv(AssignmentsBucket)
kv(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,17 +210,17 @@ 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 := seatassigner.Assignments{V: assigner.Version, Node: node}
for _, module := range placed {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: module})
p.Modules = append(p.Modules, seatassigner.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.AssignmentsValues(p)
if err != nil {
t.Fatal(err)
}
for key, raw := range values {
if _, err := placement.Put(key, raw); err != nil {
if _, err := assigned.Put(key, raw); err != nil {
t.Fatal(err)
}
}
+11 -7
View File
@@ -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"
)
@@ -38,6 +38,10 @@ func Offers() map[string]provider.Offer {
}}
}
// Provisioner is the module holding the seat mesh-provisioner, whose provisions and plans the helper reads: named from this
// module's manifest (`reads`; TestTheOfferServesThePortTheManifestServes), since no client names a holder (novox/hq ADR 0625 §4).
const Provisioner = "mesh-provisioner"
// busOf is the helper's bus over the runtime's own state: the provider's bucket is its state `bound`, and the
// provisions and plans of the seat mesh-provisioner are the states it reads, by mesh-sdk's names (novox/hq ADR 0570,
// issue 1954 L6-T1). The old assignments are read no more (mesh-sdk 692dbf9, L6-P3).
@@ -47,10 +51,10 @@ func (b busOf) state(bucket string) (string, error) {
switch bucket {
case provider.BucketOf(Module):
return "bound", nil
case provisions.ProvisionsBucket:
return provisions.ProvisionsRead, nil
case provisions.PlansBucket:
return provisions.PlansRead, nil
case provisions.ProvisionsBucketOf(Provisioner):
return provisions.ProvisionsReadOf(Provisioner), nil
case provisions.PlansBucketOf(Provisioner):
return provisions.PlansReadOf(Provisioner), nil
}
return "", fmt.Errorf("%s is not a bucket this provider keeps or reads", bucket)
}
@@ -97,7 +101,7 @@ func (b busOf) Delete(_ context.Context, bucket, key string) error {
func New(conn *bus.Conn, node string, every time.Duration, logf func(string, ...any)) *provider.Provider {
return provider.New(provider.Options{
Node: node,
Module: Module,
Module: Module, Provisioner: Provisioner,
Offers: Offers(),
Bus: busOf{conn: conn},
Every: every,
+11 -11
View File
@@ -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"
@@ -45,14 +45,14 @@ func fill(t *testing.T, template string, entry map[string]any) string {
func TestTheRuntimeAnswersAHandedConsumersMcpEndpointThroughTheHelper(t *testing.T) {
mesh := mt.New(t)
bound := mesh.Bucket(t, "node-tools_bound")
plans := mesh.Bucket(t, provisions.PlansBucket)
provs := mesh.Bucket(t, provisions.ProvisionsBucket)
plans := mesh.Bucket(t, provisions.PlansBucketOf(Provisioner))
provs := mesh.Bucket(t, provisions.ProvisionsBucketOf(Provisioner))
mem := mt.MembershipOf("node-tools", "anchor", false, nil)
mem.State = []bus.StateIssued{
{Name: "bound", Bucket: "node-tools_bound", Writes: true},
{Name: provisions.PlansRead, Bucket: provisions.PlansBucket},
{Name: provisions.ProvisionsRead, Bucket: provisions.ProvisionsBucket},
{Name: provisions.PlansReadOf(Provisioner), Bucket: provisions.PlansBucketOf(Provisioner)},
{Name: provisions.ProvisionsReadOf(Provisioner), Bucket: provisions.ProvisionsBucketOf(Provisioner)},
}
mesh.Issue(t, mem)
@@ -160,8 +160,8 @@ func TestTheOfferServesThePortTheManifestServes(t *testing.T) {
}
}
for _, r := range m.Reads {
if r == "mesh-placement.assignments" {
t.Error("the manifest still reads mesh-placement.assignments, which the sdk's helper no longer reads (issue 1954)")
if r == "mesh-module-assigner.assignments" {
t.Error("the manifest still reads mesh-module-assigner.assignments, which the sdk's helper no longer reads (issue 1954)")
}
}
for _, a := range m.Build.Artifacts {
@@ -176,9 +176,9 @@ func TestTheOfferServesThePortTheManifestServes(t *testing.T) {
declared[n] = true
}
for bucket, want := range map[string]string{
provider.BucketOf(Module): "bound",
provisions.ProvisionsBucket: "mesh-provisioner.provisions",
provisions.PlansBucket: "mesh-provisioner.plans",
provider.BucketOf(Module): "bound",
provisions.ProvisionsBucketOf(Provisioner): "mesh-provisioner.provisions",
provisions.PlansBucketOf(Provisioner): "mesh-provisioner.plans",
} {
got, err := (busOf{}).state(bucket)
if err != nil || got != want || !declared[got] {
@@ -63,7 +63,7 @@ func TestALedgerIsWrittenOnlyOnItsModulesLedgerConnection(t *testing.T) {
t.Fatal(err)
}
}
for _, b := range []string{"m_records", buscompose.GrantsMembershipsBucket} {
for _, b := range []string{"m_records", membership.GrantsBucket} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: b}); err != nil {
t.Fatal(err)
}
@@ -76,8 +76,8 @@ func TestALedgerIsWrittenOnlyOnItsModulesLedgerConnection(t *testing.T) {
raw, _ := json.Marshal(mem)
var sdk membership.Membership
_ = json.Unmarshal(raw, &sdk)
grants, _ := js.KeyValue(ctx, buscompose.GrantsMembershipsBucket)
if _, err := grants.Put(ctx, buscompose.MembershipKey("anchor", "m"), membership.EncodeGrantsMembership(sdk)); err != nil {
grants, _ := js.KeyValue(ctx, membership.GrantsBucket)
if _, err := grants.Put(ctx, membership.GrantsKey("anchor", "m"), membership.EncodeGrantsMembership(sdk)); err != nil {
t.Fatal(err)
}
@@ -70,7 +70,7 @@ func TestABundlesSeatTrafficGoesThroughItsModulesBus(t *testing.T) {
if !strings.Contains(string(first), `"duplicate":false`) || !strings.Contains(string(again), `"duplicate":true`) {
t.Errorf("the same id twice: %s, then %s", first, again)
}
if _, err := asking.Seat("publish", json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-placement","body":{}}`), nil); err == nil {
if _, err := asking.Seat("publish", json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-module-assigner","body":{}}`), nil); err == nil {
t.Error("a module submitted under another's name")
}
got, err := asking.Seat("record", json.RawMessage(`{"key":"d-1"}`), nil)
+3 -1
View File
@@ -71,7 +71,9 @@
"findings"
],
"consumes": [
"mesh-delivery-assigner.views.placement",
"mesh-catalog.seat-definition-changed",
"mesh-catalog.seat-definition-removed",
"mesh-delivery-assigner.views.assignments",
"mesh-delivery-assigner.views.seat-holders",
"mesh-delivery-indexer.views.manifests",
"mesh-delivery-indexer.views.modules"