The hand-act recorder records through the seat alone, and the watched reader's reads are proved

The controller's hand-act and warranted are gone (issue 1953 L5b) and the bus refuses them, so the recorder no longer
falls back to them: an act the seat does not take is an error saying nothing was recorded. And ADR 0580 clause 1's
reader is shown reading a build by key each time and a manifest once per digest.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-01 09:53:17 +01:00
parent db4eb3bbf4
commit 8696aa9ba4
3 changed files with 41 additions and 38 deletions
@@ -5,7 +5,9 @@ import (
"strings"
"testing"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/compose/frombus"
"git.novox.be/novox/mesh-sdk/go/compose/fromruntime"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
@@ -135,6 +137,26 @@ func TestTheProvisionedMeshHandsAPlanWithItsProvision(t *testing.T) {
t.Errorf("%s: data items %v held %v: %v", row.name, items, iheld, err)
}
}
// ADR 0580 clause 1: the watched reader reads a state it composes from out of the copy, a build by key each time it is
// asked, and a manifest once per digest.
byKey := map[string]int{}
ws := stdio.WatchedStatesOver(func(state string, handle func(stdio.StateChange) error) error {
byKey[state+" watched"]++
return handle(stdio.StateChange{Key: "placement." + Node, Op: stdio.OpPut, Value: []byte(`{}`), Current: true})
}, func(state, key string) ([]byte, bool, error) { byKey[state]++; return []byte(key), true, nil })
w := fromruntime.Watched{WatchedStates: ws}
for range 2 {
_, _, _ = w.Get(frombus.BuildsRead, "restic."+fp)
_, _, _ = w.Get(frombus.ManifestsRead, "sha256-d")
if _, found, _ := w.Get(frombus.PlacementRead, "placement."+Node); !found {
t.Error("the watched reader does not answer the placement from its copy")
}
}
if byKey[frombus.BuildsRead] != 2 || byKey[frombus.ManifestsRead] != 1 || byKey[frombus.PlacementRead] != 0 ||
byKey[frombus.PlacementRead+" watched"] != 1 {
t.Errorf("the watched reader read %v, want a build by key each time, a manifest once, the placement watched once", byKey)
}
}
func planned[P any](plans map[string]P, module string) bool { _, ok := plans[module]; return ok }
+11 -28
View File
@@ -1,8 +1,7 @@
// Package handacts is the hand-act log's contract (novox/hq ADR 0565 clauses 10 and 11, issue 1953): the layout of an act,
// moved unchanged from the controller's (mesh-controller internal/link HandAct) so one reader reads both logs alike; the
// names of the seat mesh-hand-acts and of what its holder keeps; and the recorder every module records through, which
// asks the seat and, while no holder answers, the controller's verb until the controller hands its lease away, saying
// which log took the act.
// asks the seat alone: the controller's hand-act and warranted are gone (issue 1953 L5b) and the bus refuses them.
package handacts
import (
@@ -95,13 +94,13 @@ type Taken struct {
Answer json.RawMessage `json:"answer,omitempty"`
}
// Recorder records acts for a module (ADR 0565 clause 11): through the seat mesh-hand-acts, and while the seat has no
// holder that answers, through the controller's verb until the controller hands its lease away (the release's order
// gives no seat's holder before its callers; 00-plan.md, "Missing" 1).
// Recorder records acts for a module (ADR 0565 clause 11) through the seat mesh-hand-acts alone. An act the seat does
// not take is an error that says nothing was recorded, never a quiet success.
type Recorder struct {
// Ask calls a seat verb by its address (`seat:<seat>.<verb>`): stdio.Ask through the runtime.
Ask func(key string, body any) (json.RawMessage, error)
// LeaseAway says the controller's lease is handed away: then nothing falls back to it. Nil is never handed away.
// LeaseAway is no longer read: nothing falls back to the controller. Kept so a caller that sets it builds; it goes
// once no module sets it.
LeaseAway func() bool
}
@@ -121,38 +120,22 @@ func (r Recorder) Record(q Request) (Taken, error) {
body[k] = v
}
}
controller := map[string]any{"what": q.What, "why": q.Why, "cause": q.Cause}
if q.Condition != "" {
controller["condition"] = q.Condition
}
if q.Cause == "" {
controller["cause"] = q.Verb
}
return r.either("hand-act", body, "hand-act", controller)
return r.record("hand-act", body)
}
// RecordWarrant records what the asking module did on the operator's warrant for its ask.
func (r Recorder) RecordWarrant(asker, ask string) (Taken, error) {
body := map[string]any{"asker": asker, "ask": ask}
return r.either("warranted", body, "warranted", body)
return r.record("warranted", body)
}
func (r Recorder) either(verb string, body any, controllerVerb string, controllerBody any) (Taken, error) {
func (r Recorder) record(verb string, body any) (Taken, error) {
answer, err := r.Ask("seat:"+Seat+"."+verb, body)
if err == nil {
return Taken{Log: LogSeat, Answer: answer}, nil
}
if !NoResponder(err) {
return Taken{}, fmt.Errorf("%s.%s refused: %w", Seat, verb, err)
if NoResponder(err) {
return Taken{}, fmt.Errorf("%s has no holder that answers: nothing was recorded (%w)", Seat, err)
}
if r.LeaseAway != nil && r.LeaseAway() {
return Taken{}, fmt.Errorf("%s has no holder that answers, and the controller handed its lease away: nothing "+
"was recorded (%w)", Seat, err)
}
answer, cerr := r.Ask("seat:mesh-controller."+controllerVerb, controllerBody)
if cerr != nil {
return Taken{}, fmt.Errorf("%s has no holder that answers (%v), and the controller's %s refused too: %w", Seat, err,
controllerVerb, cerr)
}
return Taken{Log: LogController, Answer: answer}, nil
return Taken{}, fmt.Errorf("%s.%s refused, nothing was recorded: %w", Seat, verb, err)
}
+8 -10
View File
@@ -8,9 +8,8 @@ import (
"testing"
)
// ADR 0565 clauses 10 and 11 (issue 1953 L5-S2, S4): an act reads the controller's entries unchanged, and a recorder
// records through the seat, through the controller's verb only while the seat has no holder and the lease is the
// controller's, saying which log took it, and refuses to record nowhere.
// ADR 0565 clauses 10 and 11 (issue 1953 L5-S2, S4, L5b): an act reads the controller's entries unchanged, and a
// recorder records through the seat alone, the controller's verbs being gone, and says loudly when nothing was recorded.
func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) {
// The shape of mesh-controller internal/link HandAct, as its bucket keeps an entry (handacts.go at 0fbb5ef3).
raw := `{"id":"act-1","at":"2026-10-28T09:00:00Z","by":"person.jochen","verb":"hand-act record","why":"a stuck consumer",` +
@@ -39,14 +38,13 @@ func TestAnActIsRecordedInTheLogThatAnswers(t *testing.T) {
warrants bool
}{
{name: "the seat answers", log: LogSeat, asked: "seat:mesh-hand-acts.hand-act"},
{name: "no holder yet", seat: noResponders, log: LogController, asked: "seat:mesh-hand-acts.hand-act,seat:mesh-controller.hand-act"},
{name: "no holder, the lease handed away", seat: noResponders, away: true, refused: "handed its lease away",
{name: "no holder: nothing recorded, the controller never asked", seat: noResponders, refused: "nothing was recorded",
asked: "seat:mesh-hand-acts.hand-act"},
{name: "the seat refuses", seat: errors.New("why is required"), refused: "refused", asked: "seat:mesh-hand-acts.hand-act"},
{name: "neither answers", seat: noResponders, ctrl: noResponders, refused: "refused too",
asked: "seat:mesh-hand-acts.hand-act,seat:mesh-controller.hand-act"},
{name: "a warrant without a holder", seat: noResponders, warrants: true, log: LogController,
asked: "seat:mesh-hand-acts.warranted,seat:mesh-controller.warranted"},
{name: "the seat refuses", seat: errors.New("why is required"), refused: "refused, nothing was recorded",
asked: "seat:mesh-hand-acts.hand-act"},
{name: "a warrant without a holder", seat: noResponders, warrants: true, refused: "nothing was recorded",
asked: "seat:mesh-hand-acts.warranted"},
{name: "a warrant recorded", warrants: true, log: LogSeat, asked: "seat:mesh-hand-acts.warranted"},
} {
var asked []string
r := Recorder{LeaseAway: func() bool { return c.away }, Ask: func(key string, body any) (json.RawMessage, error) {