J2: the tool runner tells a subscribed bundle how its module's consumer stands, as mesh/heartbeat (stdio.OnHeartbeat), at each idle beat and once Missed (ADR 0615, ADR 0621, issue 1966)

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 14:10:25 +01:00
parent d71cd5cef6
commit f87c9720ed
5 changed files with 241 additions and 11 deletions
@@ -19,6 +19,9 @@ while IFS= read -r line; do
*'"method":"tools/list"'*)
printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[]}}\n' "$id"
printf '{"jsonrpc":"2.0","id":"s1","method":"mesh/subscribe","params":{"pattern":"#"}}\n' ;;
*'"method":"mesh/heartbeat"'*)
params=$(printf '%s' "$line" | sed -n 's/.*"params":\(.*\)}$/\1/p')
printf '{"jsonrpc":"2.0","id":"h1","method":"mesh/publish","params":%s}\n' "$params" ;;
*'"method":"mesh/event"'*)
case "$ANSWER" in
error) printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32000,"message":"Unexpected end of JSON input"}}\n' "$id" ;;
@@ -30,11 +33,18 @@ done
`
type subscribing struct {
mu sync.Mutex
deliver func(json.RawMessage) error
mu sync.Mutex
deliver func(json.RawMessage) error
beat func(json.RawMessage)
published []json.RawMessage
}
func (b *subscribing) Publish(json.RawMessage) error { return nil }
func (b *subscribing) Publish(p json.RawMessage) error {
b.mu.Lock()
b.published = append(b.published, p)
b.mu.Unlock()
return nil
}
func (b *subscribing) Ask(json.RawMessage) (json.RawMessage, error) { return nil, nil }
func (b *subscribing) State(string, json.RawMessage) (json.RawMessage, error) { return nil, nil }
func (b *subscribing) Log(string, json.RawMessage) (json.RawMessage, error) { return nil, nil }
@@ -53,10 +63,11 @@ func (b *subscribing) BusStateWatch(string, json.RawMessage, func(json.RawMessag
return func() {}, nil
}
func (b *subscribing) Begin(string) (string, func()) { return "", func() {} }
func (b *subscribing) Subscribe(d func(json.RawMessage) error) error {
func (b *subscribing) Subscribe(d func(json.RawMessage) error, beat func(json.RawMessage)) error {
b.mu.Lock()
defer b.mu.Unlock()
b.deliver = d
b.beat = beat
return nil
}
@@ -104,3 +115,45 @@ func TestAnEventIsTakenByAnyAnswerThatIsNotAnError(t *testing.T) {
func (b *subscribing) LogWatch(json.RawMessage, func(json.RawMessage) error) (func(), error) {
return func() {}, nil
}
// The runtime's word on the module's consumer between events reaches the child that subscribed as `mesh/heartbeat`,
// unchanged (stdio.OnHeartbeat reads it in the bundle).
func TestTheWordOnTheConsumerBetweenEventsReachesTheBundleThatSubscribed(t *testing.T) {
entry := filepath.Join(t.TempDir(), "bundle")
if err := os.WriteFile(entry, []byte(answeringBundle), 0o755); err != nil {
t.Fatal(err)
}
b := &subscribing{}
l, err := Start("plex", entry, append(os.Environ(), "ANSWER=result"), b, t.Logf)
if err != nil {
t.Fatal(err)
}
t.Cleanup(l.Stop)
var beat func(json.RawMessage)
for i := 0; beat == nil; i++ {
b.mu.Lock()
beat = b.beat
b.mu.Unlock()
if i > 100 {
t.Fatal("the bundle never subscribed")
}
time.Sleep(20 * time.Millisecond)
}
word := json.RawMessage(`{"at":"2026-11-04T13:00:00Z","missed":true,"why":"the bus ended it"}`)
beat(word)
for i := 0; ; i++ {
b.mu.Lock()
got := b.published
b.mu.Unlock()
if len(got) == 1 {
if string(got[0]) != string(word) {
t.Fatalf("the bundle was told %s, not %s", got[0], word)
}
return
}
if i > 100 {
t.Fatal("the bundle was told nothing")
}
time.Sleep(20 * time.Millisecond)
}
}