D12: the watchman serves the watchdogs seat's presence with its declared machine, answering that machine's online and since in the seat's answer (issue 1966, ADR 0596 §2; review round 3, finding 1)
The builder and the indexer ask it whether a node's machine is heard instead of reading the watchman's state; key and prefix stay for the whole read. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
@@ -105,6 +106,35 @@ var keyArgs = map[string]any{"type": "object", "properties": map[string]any{
|
||||
"key": map[string]any{"type": "string", "description": "one key only"},
|
||||
"prefix": map[string]any{"type": "string", "description": "only the keys that begin with it"}}}
|
||||
|
||||
// presenceArgs are the seat's input of presence, machine, beside the watchman's own key and prefix.
|
||||
var presenceArgs = map[string]any{"type": "object", "properties": map[string]any{
|
||||
"machine": map[string]any{"type": "string", "description": "one machine: whether the watchman hears it and since when"},
|
||||
"key": map[string]any{"type": "string", "description": "one key only"},
|
||||
"prefix": map[string]any{"type": "string", "description": "only the keys that begin with it"}}}
|
||||
|
||||
// presenceOf answers the seat's presence of one machine (meshwatchdogs.PresenceAnswer): online and since as the state
|
||||
// presence holds it under the machine's key; a machine it holds no word of is not online, with no since (novox/hq ADR
|
||||
// 0596 §2: a part asks this verb, never reads the state).
|
||||
func presenceOf(st state, machine string) (any, error) {
|
||||
e, err := st.Get(meshwatchdogs.PresenceByNodeKey(machine))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("presence %s cannot be read: %w", machine, err)
|
||||
}
|
||||
answer := meshwatchdogs.PresenceAnswer{Machine: machine}
|
||||
if e == nil {
|
||||
return answer, nil
|
||||
}
|
||||
v, err := meshwatchdogs.DecodePresenceByNodeValue(e.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("presence %s does not read: %w", machine, err)
|
||||
}
|
||||
answer.Online = v.Online
|
||||
if !e.At.IsZero() {
|
||||
answer.Since = e.At.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
// Tools are the seat's verbs and the module's tools, over the states states gives and the program asked through ask.
|
||||
func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, error)) []stdio.Tool {
|
||||
read := func(name string) func(map[string]any) (any, error) {
|
||||
@@ -141,9 +171,15 @@ func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, err
|
||||
Description: "What stopped moving: every key of the watchman's stuck bucket, <bucket>.<key> with what it waited for since " +
|
||||
"when, and whether it moved since (never deleted). A key not moved is also the watchman's finding " +
|
||||
"delivery.<key>.stuck, a node's presence machine.<node>.silent. Give key for one, prefix to narrow. Read-only."},
|
||||
{Name: verb(VerbPresence), Input: keyArgs, Run: read("presence"),
|
||||
{Name: verb(VerbPresence), Input: presenceArgs, Run: func(args map[string]any) (any, error) {
|
||||
if machine := str(args, "machine"); machine != "" {
|
||||
return presenceOf(states("presence"), machine)
|
||||
}
|
||||
return read("presence")(args)
|
||||
},
|
||||
Description: "Who is online on the bus as the watchman hears it: each node's agent (<node>) and each delivery part's instance " +
|
||||
"(part.<part>.<node>), online or not since the entry's time. Give key for one, prefix to narrow. Read-only."},
|
||||
"(part.<part>.<node>), online or not since the entry's time. Give machine for one machine's online and since, " +
|
||||
"key for one key, prefix to narrow. Read-only."},
|
||||
{Name: verb(VerbDeadLetters), Run: dead,
|
||||
Description: "What a consumer gave up on, kept in DEAD_LETTERS (novox/hq ADR 0564 §5, ADR 0264, ADR 0376): newest first, " +
|
||||
"for one consumer (its name or <stream>.<consumer>), at most limit; one whole by id (a restricted one's body " +
|
||||
|
||||
@@ -4,9 +4,12 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
"mesh-delivery-watchman/internal/socket"
|
||||
@@ -27,7 +30,7 @@ func (f fakeState) Get(key string) (*stdio.StateEntry, error) {
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return &stdio.StateEntry{Key: key, Value: json.RawMessage(v), Revision: 3}, nil
|
||||
return &stdio.StateEntry{Key: key, Value: json.RawMessage(v), Revision: 3, At: time.Date(2026, 11, 10, 3, 0, 0, 0, time.UTC)}, nil
|
||||
}
|
||||
|
||||
// 1952/WM1, ADR 0564 §12: every state the manifest keeps is read by a tool, every act the program starts by hand is
|
||||
@@ -51,7 +54,8 @@ func TestEveryStateAndActHasATool(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
asked := []socket.Ask{}
|
||||
states := map[string]fakeState{"findings": {"machine.ace.silent": `{"v":1}`, "core.doctor.self-check-silent": `{"v":1}`}}
|
||||
states := map[string]fakeState{"findings": {"machine.ace.silent": `{"v":1}`, "core.doctor.self-check-silent": `{"v":1}`},
|
||||
"presence": {"ace": `{"v":1,"online":true}`, "g14": `{"v":1,"online":false}`, "part.mesh-delivery-builder.ace": `{"v":1,"online":true}`}}
|
||||
tools := Tools(func(name string) state { return states[name] }, func(a socket.Ask) (json.RawMessage, error) {
|
||||
asked = append(asked, a)
|
||||
if a.Act != "" {
|
||||
@@ -126,6 +130,31 @@ func TestEveryStateAndActHasATool(t *testing.T) {
|
||||
t.Fatal("a key not held is answered")
|
||||
}
|
||||
})
|
||||
// Issue 1966, ADR 0596 §2 (review round 3): presence with the seat's machine answers that machine's online and since in
|
||||
// the seat's declared answer; one the watchman holds no word of is not online; without it, the keys as before.
|
||||
t.Run("presence answers one machine as the seat declares", func(t *testing.T) {
|
||||
for machine, want := range map[string]meshwatchdogs.PresenceAnswer{
|
||||
"ace": {Machine: "ace", Online: true, Since: "2026-11-10T03:00:00Z"},
|
||||
"g14": {Machine: "g14", Since: "2026-11-10T03:00:00Z"},
|
||||
"novox": {Machine: "novox"},
|
||||
} {
|
||||
out, err := served[Seat+"."+VerbPresence].Run(map[string]any{"machine": machine})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, _ := json.Marshal(out)
|
||||
var got meshwatchdogs.PresenceAnswer
|
||||
if err := json.Unmarshal(b, &got); err != nil || !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("presence of %s = %s (%v), want %+v", machine, b, err, want)
|
||||
}
|
||||
}
|
||||
out, _ := served[Seat+"."+VerbPresence].Run(map[string]any{"prefix": "part."})
|
||||
b, _ := json.Marshal(out)
|
||||
var whole meshwatchdogs.PresenceAnswer
|
||||
if err := json.Unmarshal(b, &whole); err != nil || whole.Holds != 3 || len(whole.Keys) != 1 || whole.State != "presence" {
|
||||
t.Errorf("presence whole = %s (%v)", b, err)
|
||||
}
|
||||
})
|
||||
t.Run("dead-letters and resync ask the program", func(t *testing.T) {
|
||||
if _, err := served[Seat+"."+VerbDeadLetters].Run(map[string]any{"consumer": "ace_web", "limit": float64(3), "id": float64(7)}); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in New Issue
Block a user