Record a drill in mesh-hand-acts and count S15 over both logs, so the healers no longer refuse a drill (issue 1953)

This commit is contained in:
2026-10-29 22:27:23 +01:00
parent d34645361d
commit 45db0e650e
8 changed files with 137 additions and 22 deletions
+3 -2
View File
@@ -25,7 +25,7 @@ again, `fortnight` ends when S15's oldest counted act leaves the fortnight and c
each expiry; no timer runs in the process. A wait still running is on the bus across a restart; one that expired while
no instance watched is judged at the start, once the lease is first read.
S15: it watches the controller's hand-act log and writes `healer-wanted` for a cause repaired by hand twice within a
S15: it hears the seat mesh-hand-acts' `act-recorded` and watches the controller's hand-act log and writes `healer-wanted` for a cause repaired by hand twice within a
fortnight; a drill and a person's decision by design never count.
## What it keeps
@@ -41,5 +41,6 @@ fortnight; a drill and a person's decision by design never count.
- `mesh-healers.healers`: the registry, the acts of the last days, the would-acts, the brake, whether it acts and why;
`recount=true` counts the hand-act log again.
- `mesh-healers.heal`: one act now on an open condition, by key and why.
- `mesh-healers.drill`: refused until the hand-act log's home is delivered (issue 1953), naming `mesh-controller.drill`.
- `mesh-healers.drill`: records a drill in the hand-act log of the seat `mesh-hand-acts` as `hand-act drill` (the
controller's `drill` while that seat has no holder and the controller holds its lease). S15 never counts a drill.
- `healers_findings`, `healers_would`, `healers_wakes`: the reads of its buckets.
@@ -27,6 +27,7 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/health"
)
@@ -48,6 +49,8 @@ const (
ActedEvent = health.EventHealerActed
// ActedSubject is where the seat says it, which its consumers hear (catalogue.SeatSays).
ActedSubject = "mesh.seat." + Seat + ".event." + ActedEvent
// RecordedEvent is the hand-act log's seat event, each act recorded there (ADR 0565 §11).
RecordedEvent = handacts.Seat + "." + handacts.EventRecorded
// The events it hears: the keeper's, on the seat mesh-conditions.
RaisedEvent = "mesh-conditions.condition-raised"
ChangedEvent = "mesh-conditions.condition-changed"
@@ -10,6 +10,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/handacts"
"git.novox.be/novox/mesh-sdk/go/health"
)
@@ -379,9 +380,14 @@ func TestACauseRepairedByHandTwiceIsAHealerWanted(t *testing.T) {
if keys := w.store.keys(FindingsState); len(keys) != 0 {
t.Fatalf("one act is a finding: %v", keys)
}
act("a2", "broker consumer-reset", "consumer-behind", 24*time.Hour)
// The second of the cause is the seat's own act, heard as act-recorded: one in each log counts two (ADR 0565 §11).
seatAct, _ := json.Marshal(HandAct{ID: "a2", At: w.now.Add(-24 * time.Hour), By: "person.jochen",
Verb: "broker consumer-reset", Why: "x", Cause: "consumer-behind"})
w.h.ActRecorded(seatAct)
// A drill in each log counts none.
act("a3", "hand-act drill", "drill", time.Hour)
act("a4", "hand-act drill", "drill", 2*time.Hour)
drill, _ := json.Marshal(HandAct{ID: "a4", At: w.now.Add(-2 * time.Hour), Verb: "hand-act drill", Why: "x", Cause: "drill"})
w.h.ActRecorded(drill)
act("a5", "conditions silence", "operator-answer", time.Hour)
act("a6", "conditions silence", "operator-answer", time.Hour)
act("a7", "hand-act record", "relogin", 20*24*time.Hour)
@@ -402,7 +408,7 @@ func TestACauseRepairedByHandTwiceIsAHealerWanted(t *testing.T) {
if keys := w.store.keys(FindingsState); len(keys) != 0 {
t.Errorf("past the fortnight the finding stays: %v", keys)
}
w.h.HandActHeard("a2", "delete", nil)
w.h.HandActHeard("a5", "delete", nil)
if len(w.h.acts) != 7 {
t.Errorf("a deleted entry is kept: %d", len(w.h.acts))
}
@@ -436,14 +442,53 @@ func TestTheVerbsAndReads(t *testing.T) {
if a["acting"] != true || len(a["acts"].([]Attempt)) != 1 || len(a["healers"].([]map[string]any)) != 3 {
t.Errorf("healers answered %v", a)
}
var drill error
for _, tool := range Tools(w.h) {
if tool.Name == DrillTool {
_, drill = tool.Run(map[string]any{})
}
// drill records into mesh-hand-acts as a drill; while that seat has no holder, into the controller's drill until
// the lease is handed away (ADR 0565 §11).
drills := []struct {
name string
seatGone bool
leaseAway bool
args map[string]any
log, err string
}{
{"into the seat", false, true, map[string]any{"what": "stopped records", "why": "does it raise"}, handacts.LogSeat, ""},
{"no holder: the controller's", true, false, map[string]any{"what": "stopped records", "why": "x"}, handacts.LogController, ""},
{"no holder, lease away", true, true, map[string]any{"what": "stopped records", "why": "x"}, "", "handed its lease away"},
{"says nothing", false, true, map[string]any{}, "", "says what"},
}
if drill == nil || !strings.Contains(drill.Error(), "mesh-controller.drill") {
t.Errorf("drill answered %v", drill)
for _, d := range drills {
x := newWorld()
if d.leaseAway {
x.handAway()
} else {
x.h.LeaseHeard([]byte(`{"instance":"novox/1","epoch":5}`), true, nil)
}
var asked []string
x.h.Ask = func(key string, body any) (json.RawMessage, error) {
raw, _ := json.Marshal(body)
asked = append(asked, key+" "+string(raw))
if d.seatGone && strings.HasPrefix(key, "seat:mesh-hand-acts.") {
return nil, errors.New("nats: no responders available for request")
}
return json.RawMessage(`{}`), nil
}
var got any
var err error
for _, tool := range Tools(x.h) {
if tool.Name == DrillTool {
got, err = tool.Run(d.args)
}
}
switch {
case d.err != "":
if err == nil || !strings.Contains(err.Error(), d.err) {
t.Errorf("drill %s: %v", d.name, err)
}
case err != nil || got.(map[string]any)["log"] != d.log:
t.Errorf("drill %s: %v, %v", d.name, got, err)
case d.log == handacts.LogSeat && !strings.Contains(asked[0], `"verb":"hand-act drill"`):
t.Errorf("drill %s asked %v", d.name, asked)
}
}
if _, err := w.h.read(WouldState, ""); err != nil {
t.Error(err)
@@ -84,6 +84,20 @@ func main() {
h.HandActHeard(c.Key, c.Op, c.Value)
return nil
})
// S15 over both logs: the seat's act-recorded on a durable consumer, beside the controller's bucket's watch.
go func() {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
time.Sleep(wait)
err := stdio.Subscribe(RecordedEvent, func(e stdio.Envelope) error {
h.ActRecorded(e.Body)
return nil
})
if err == nil {
return
}
logf("[%s] does not hear %s yet (%v); asking again in %s", Module, RecordedEvent, err, wait)
}
}()
for _, event := range []string{RaisedEvent, ChangedEvent} {
go func(event string) {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
@@ -48,8 +48,8 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) {
got, want []string
}{
{"emits (healer-acted is the seat's, said on its subject)", m.Emits, nil},
{"consumes", m.Consumes, []string{RaisedEvent, ChangedEvent}},
{"invokes", m.Invokes, []string{AssertTool}},
{"consumes", m.Consumes, []string{RaisedEvent, ChangedEvent, RecordedEvent}},
{"invokes", m.Invokes, []string{AssertTool, "seat:mesh-hand-acts.hand-act", "seat:mesh-controller.drill"}},
{"reads", m.Reads, []string{LeaseRead, ConditionsRead, HandActsRead}},
} {
if !slices.Equal(c.got, c.want) {
+43 -6
View File
@@ -13,6 +13,7 @@ import (
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/handacts"
)
// Verbs are the seat's verbs, as the manifest claims them.
@@ -55,12 +56,14 @@ func Tools(h *Healers) []stdio.Tool {
return h.Heal(strArg(a, "key"), strArg(a, "why"), stdio.CallerOf(a))
}},
{Name: DrillTool,
Description: "A drill: something broken on purpose to see the mesh raise and clear it, recorded in the hand-act " +
"log. Refused until the log's home is delivered (issue 1953): the controller's drill records it meanwhile.",
Input: obj(map[string]any{"why": map[string]any{"type": "string"}}),
Run: func(map[string]any) (any, error) {
return nil, errors.New("a drill is recorded in the hand-act log, whose one writer is the controller until its " +
"home is delivered (issue 1953): use mesh-controller.drill, which records it and S15 never counts")
Description: "Record a drill: something broken on purpose to see the mesh raise and clear it, with what it " +
"tests, in the hand-act log of the seat mesh-hand-acts (the controller's drill while that seat has no " +
"holder and the controller holds its lease). A drill is never a repair: S15 never counts it.",
Input: obj(map[string]any{"what": map[string]any{"type": "string", "description": "what was done on purpose"},
"why": map[string]any{"type": "string", "description": "what the drill tests"},
"condition": map[string]any{"type": "string", "description": "the condition it is meant to raise, if any"}}),
Run: func(a map[string]any) (any, error) {
return h.Drill(strArg(a, "what"), strArg(a, "why"), strArg(a, "condition"))
}},
{Name: FindingsTool,
Description: "What the healers find of themselves (their bucket findings): the brake, their blindness, and " +
@@ -81,6 +84,40 @@ func Tools(h *Healers) []stdio.Tool {
}
}
// Drill records a drill (ADR 0565 §11, which ends ADR 0564 Limits 1's refusal): through the seat mesh-hand-acts as
// the act `hand-act drill`, and while that seat has no holder that answers and the controller still holds its lease,
// through the controller's own `drill`, which records it as a drill too. It answers which log took it.
func (h *Healers) Drill(what, why, condition string) (any, error) {
if what == "" || why == "" {
return nil, errors.New("a drill says what was done on purpose and what it tests: nothing was recorded")
}
if h.Ask == nil {
return nil, errors.New("the hand-act log cannot be reached from here: nothing was recorded")
}
body := map[string]any{"what": what, "why": why, "verb": handacts.VerbDrill}
controller := map[string]any{"what": what, "why": why}
if condition != "" {
body["condition"], controller["condition"] = condition, condition
}
answer, err := h.Ask("seat:"+handacts.Seat+".hand-act", body)
if err == nil {
return map[string]any{"log": handacts.LogSeat, "answer": answer}, nil
}
if !handacts.NoResponder(err) {
return nil, fmt.Errorf("%s.hand-act refused the drill: %w", handacts.Seat, err)
}
if acting, _ := h.Acting(); acting {
return nil, fmt.Errorf("%s has no holder that answers, and the controller handed its lease away: nothing was "+
"recorded (%w)", handacts.Seat, err)
}
answer, cerr := h.Ask("seat:mesh-controller.drill", controller)
if cerr != nil {
return nil, fmt.Errorf("%s has no holder that answers (%v), and the controller's drill refused too: %w", handacts.Seat,
err, cerr)
}
return map[string]any{"log": handacts.LogController, "answer": answer}, nil
}
// Heal is the verb heal: one condition judged now by its healer.
func (h *Healers) Heal(key, why, caller string) (any, error) {
if key == "" || why == "" {
@@ -15,6 +15,8 @@ import (
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/handacts"
)
// Within is how far back a repeated cause counts.
@@ -66,6 +68,16 @@ func personsDecision(a HandAct) bool {
return len(causes) == 0 || slices.Contains(causes, a.Cause)
}
// ActRecorded is one act the seat mesh-hand-acts recorded, heard as act-recorded: counted beside the controller's
// log's, under a key of its own log, so an act is counted once whichever log holds it (ADR 0565 §11).
func (h *Healers) ActRecorded(body []byte) {
var a HandAct
if json.Unmarshal(body, &a) != nil || a.ID == "" {
return
}
h.HandActHeard(handacts.LogSeat+":"+a.ID, "put", body)
}
// HandActHeard is one change of the hand-act log: kept or forgotten, and every cause counted again.
func (h *Healers) HandActHeard(key, op string, value []byte) {
h.mu.Lock()
+5 -2
View File
@@ -15,10 +15,13 @@
],
"consumes": [
"mesh-conditions.condition-raised",
"mesh-conditions.condition-changed"
"mesh-conditions.condition-changed",
"mesh-hand-acts.act-recorded"
],
"invokes": [
"mesh-grants.grants_assert"
"mesh-grants.grants_assert",
"seat:mesh-hand-acts.hand-act",
"seat:mesh-controller.drill"
],
"reads": [
"mesh-controller.lease",