CAT-V4, CAT-C10: the vault's retire and the config events and renames verb match the seat definitions of mesh-contracts
retire takes the definition's warrant (the hand act's id, naming node, module, name and ask) and answers retired; own-retired, renamed, retiring and retired carry exactly the definition's bodies on its subjects. The whole-module tests decode what the module published strictly into the generated types. The modules require mesh-contracts, to be pinned with the group (a local replace in an uncommitted go.work until then). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -104,7 +104,7 @@ func notServed(err error) bool {
|
||||
}
|
||||
|
||||
// toolVault asks the seat mesh-vault to retire a secret: the hand act that warrants it is recorded first, by this
|
||||
// module, naming the node, module and secret; the vault reads it back by its id.
|
||||
// module, naming the node, module, secret and the ask of the Approve; the vault reads it back by its id (`warrant`).
|
||||
type toolVault struct {
|
||||
ask func(key string, body any) (json.RawMessage, error)
|
||||
recorder handacts.Recorder
|
||||
@@ -112,7 +112,7 @@ type toolVault struct {
|
||||
|
||||
// Retire records the warrant and calls the vault's verb.
|
||||
func (v toolVault) Retire(r RetireRequest) error {
|
||||
taken, err := v.recorder.Record(handacts.Request{What: r.Node + " " + r.Module + " " + r.Name,
|
||||
taken, err := v.recorder.Record(handacts.Request{What: r.Node + " " + r.Module + " " + r.Name + " " + r.Ask,
|
||||
Why: "the operator approved retiring " + r.Module + "'s secret " + r.Name + " on " + r.Node + " (ask " + r.Ask + ")",
|
||||
Cause: "the operator's Approve of ask " + r.Ask, Verb: "mesh-vault.retire"})
|
||||
if err != nil {
|
||||
@@ -127,7 +127,6 @@ func (v toolVault) Retire(r RetireRequest) error {
|
||||
if json.Unmarshal(answer, &rec) != nil || rec.Act.ID == "" {
|
||||
return fmt.Errorf("mesh-hand-acts recorded the warrant and named no act")
|
||||
}
|
||||
_, err = v.ask(VaultRetireAsk, map[string]any{"node": r.Node, "module": r.Module, "name": r.Name, "act": rec.Act.ID,
|
||||
"ask": r.Ask})
|
||||
_, err = v.ask(VaultRetireAsk, map[string]any{"node": r.Node, "module": r.Module, "name": r.Name, "warrant": rec.Act.ID})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -272,6 +272,23 @@ func hearingDelivery(c *Config) {
|
||||
}
|
||||
}
|
||||
|
||||
// seatRename is one open rename as the seat's definition answers it: module, from, to, at and the nodes holding it open.
|
||||
type seatRename struct {
|
||||
Module string `json:"module"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
At time.Time `json:"at"`
|
||||
Open []string `json:"open"`
|
||||
}
|
||||
|
||||
func seatRenames(rows []RenameRow) []seatRename {
|
||||
out := make([]seatRename, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, seatRename{Module: r.Module, From: r.From, To: r.To, At: r.At, Open: r.Open})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Tools is the seat's verb and the module's own tools of the switch, as the runtime serves them.
|
||||
func Tools(c *Config) []stdio.Tool {
|
||||
renames := stdio.Tool{Name: Seat + "." + RenamesVerb,
|
||||
@@ -283,7 +300,7 @@ func Tools(c *Config) []stdio.Tool {
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
m, _ := a["module"].(string)
|
||||
rows, err := c.Renames(strings.TrimSpace(m), true)
|
||||
return map[string]any{"renames": rows}, err
|
||||
return map[string]any{"renames": seatRenames(rows)}, err
|
||||
}}
|
||||
return append(switchTools(c), renames, stdio.Tool{
|
||||
Name: Seat + "." + Verb,
|
||||
|
||||
@@ -6,7 +6,7 @@ package main
|
||||
// (its Value, whole), and opens a **rename window** per node: until that node's node-engine has applied a build of the
|
||||
// module that reads the new name, the old name still answers with the new value (Resolve). A window closes on the
|
||||
// status view's `delivered` event and, for a node that was away, on the node-engine seat's `manifest-reads-changed`
|
||||
// (Windows). Its state is `renames`, key `<module>.<to>`; what it says is `events.renamed` on the stream `config-events`.
|
||||
// (Windows). Its state is `renames`, key `<module>.<to>`; what it says is `renamed` on the stream `config-events`.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -20,11 +20,11 @@ import (
|
||||
// The state, the stream's events and the bounds this part keeps.
|
||||
const (
|
||||
RenamesState = "renames"
|
||||
// EventRenamed is `renamed`, EventRetiring `retiring` and EventRetired `retired`, the family `config.events`
|
||||
// (design 26): each carried by the stream StreamName and kept seven days.
|
||||
EventRenamed = "events.renamed"
|
||||
EventRetiring = "events.retiring"
|
||||
EventRetired = "events.retired"
|
||||
// EventRenamed is `renamed`, EventRetiring `retiring` and EventRetired `retired`, the family `config.events` of the
|
||||
// seat's definition (design 26), under the holder's subjects: each carried by the stream StreamName, kept seven days.
|
||||
EventRenamed = "renamed"
|
||||
EventRetiring = "retiring"
|
||||
EventRetired = "retired"
|
||||
StreamName = "config-events"
|
||||
// HeldFinding is how long a window or a retirement may stay open before it is a finding naming the node.
|
||||
HeldFinding = 7 * 24 * time.Hour
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
@@ -343,6 +344,33 @@ func (p *player) event(key string, body any) {
|
||||
}
|
||||
}
|
||||
|
||||
// conforms decodes what the module published under key strictly into the seat definition's type (a field the type does
|
||||
// not know is a fault here, though a reader drops it) and checks the subject the runtime publishes it on is the one the
|
||||
// definition names: CAT-V4, the definition's fixtures matched by a real event.
|
||||
func (p *player) conforms(key string, into any, subject func(holder string) string) {
|
||||
p.t.Helper()
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
found := false
|
||||
for _, s := range p.said {
|
||||
if s.Key != key {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
dec := json.NewDecoder(strings.NewReader(string(s.Body)))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(into); err != nil {
|
||||
p.t.Errorf("the published %s %s does not match the seat's definition: %v", key, s.Body, err)
|
||||
}
|
||||
if got, want := "mesh.mod."+Module+".event."+s.Key, subject(Module); got != want {
|
||||
p.t.Errorf("%s is published on %s, the definition names %s", key, got, want)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
p.t.Errorf("%s was never published", key)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *player) publishedCount(key string) int {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
@@ -533,6 +561,8 @@ func TestConfigWholeRenamesAndRetiresAgainstTheBus(t *testing.T) {
|
||||
})
|
||||
eventually(t, "renamed is said", func() bool { return p.publishedCount(EventRenamed) == 1 })
|
||||
|
||||
p.conforms(EventRenamed, &meshconfig.RenamedHolderEvent{}, meshconfig.HolderSubjectRenamed)
|
||||
|
||||
// A refused move writes its finding: dock declares both names.
|
||||
p.manifestIn("dock", dock)
|
||||
p.buildIn("g14", "dock", fp1, dock)
|
||||
@@ -569,6 +599,19 @@ func TestConfigWholeRenamesAndRetiresAgainstTheBus(t *testing.T) {
|
||||
t.Fatalf("the window after the restart: %s", raw)
|
||||
}
|
||||
|
||||
// The seat's verb answers the open window in the definition's shape (CAT-C10).
|
||||
raw, err = p2.call(Seat+"."+RenamesVerb, map[string]any{"module": "player"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var open meshconfig.RenamesAnswer
|
||||
dec := json.NewDecoder(strings.NewReader(string(raw)))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&open); err != nil || len(open.Renames) != 1 || open.Renames[0].To != "level" ||
|
||||
strings.Join(open.Renames[0].Open, ",") != "g14,novox" {
|
||||
t.Fatalf("renames answered %s (%v), want player's level open on g14 and novox", raw, err)
|
||||
}
|
||||
|
||||
// g14 applies the build that reads the new name; the delivery says so; its window closes, novox's stays.
|
||||
p2.buildIn("g14", "player", fp2, newPlayer)
|
||||
p2.mu.Lock()
|
||||
@@ -619,5 +662,7 @@ func TestConfigWholeRenamesAndRetiresAgainstTheBus(t *testing.T) {
|
||||
_, still := p2.get(ValuesState, "module.g14.radio.band")
|
||||
return !still && p2.publishedCount(EventRetired) == 1
|
||||
})
|
||||
p2.conforms(EventRetiring, &meshconfig.RetiringHolderEvent{}, meshconfig.HolderSubjectRetiring)
|
||||
p2.conforms(EventRetired, &meshconfig.RetiredHolderEvent{}, meshconfig.HolderSubjectRetired)
|
||||
eventually(t, "the retirement is dropped", func() bool { _, ok := p2.get(RetiringState, "radio.band"); return !ok })
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ module mesh-config
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/go v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102191058-f02f142af128
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
],
|
||||
"emits": [
|
||||
"config-changed",
|
||||
"events.renamed",
|
||||
"events.retiring",
|
||||
"events.retired"
|
||||
"renamed",
|
||||
"retiring",
|
||||
"retired"
|
||||
],
|
||||
"uses": [
|
||||
"operator-channel"
|
||||
@@ -110,7 +110,9 @@
|
||||
{
|
||||
"name": "config-events",
|
||||
"events": [
|
||||
"events.**"
|
||||
"renamed",
|
||||
"retiring",
|
||||
"retired"
|
||||
],
|
||||
"max-age-days": 7
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user