seatlist: an event heard before the seat ever answered reads the whole list (issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 14:32:19 +01:00
parent e0d2298f58
commit b81622842b
2 changed files with 57 additions and 0 deletions
+18
View File
@@ -345,6 +345,16 @@ func (r *Reader) dropLocked(seat string) {
// heard takes one of the seat's events: a trunk definition moved, so that seat is asked again, or went, so it is dropped.
// An error is returned so the bus offers the event again.
func (r *Reader) heard(key string, body json.RawMessage) error {
r.mu.Lock()
unread := !r.spoken
r.mu.Unlock()
if unread {
// the seat was silent when the reader started and speaks now: read the whole list, not only the seat named
if err := r.Reload(); err != nil {
return err
}
return nil
}
var c Changed
if err := json.Unmarshal(body, &c); err != nil || c.Seat == "" {
return nil
@@ -352,6 +362,14 @@ func (r *Reader) heard(key string, body json.RawMessage) error {
if c.Source != "" && c.Source != SourceTrunk {
return nil
}
r.mu.Lock()
spoken := r.spoken
r.mu.Unlock()
if !spoken {
// The seat was silent when this reader started: its event says it speaks now, so the whole list is read, not this
// seat alone, and the reader is heard (MayAct) from here on.
return r.Reload()
}
if strings.HasSuffix(key, "."+RemovedEvent) {
r.mu.Lock()
r.dropLocked(c.Seat)
+39
View File
@@ -218,3 +218,42 @@ func TestTheTwoToolsReadTheHeldListAndReadItAgainForTheirCallers(t *testing.T) {
t.Fatalf("again: %v %+v", err, r.Status())
}
}
// A reader started while the seat was silent reads the whole list at the first event the seat sends, and then acts on any module.
func TestAReaderStartedWhileTheSeatWasSilentReadsTheWholeListAtItsFirstEvent(t *testing.T) {
owner := &fakeOwner{down: true, table: map[string]string{"node-b": def("node-b", "node", "put"), "node-c": def("node-c", "node", "put")}}
r, err := Start(Config{Bus: owner, Bootstrap: Bootstrapped()})
if err != nil {
t.Fatal(err)
}
if r.Status().Heard || r.MayAct("node-b") {
t.Fatal("the silent seat was heard")
}
owner.down = false
if err := owner.say(ChangedEvent, "node-b", SourceTrunk); err != nil {
t.Fatal(err)
}
if !r.Status().Heard || !r.MayAct("node-b") || !r.Has("node-c") {
t.Fatalf("the event did not read the whole list: %+v", r.Status())
}
}
// A reader started while the seat was silent is heard at the seat's changed event: the whole list is read then (ADR 0636 §1).
func TestAReaderStartedWhileTheSeatWasSilentIsHeardAtItsChangedEvent(t *testing.T) {
owner := &fakeOwner{down: true}
r, err := Start(Config{Bus: owner, Bootstrap: Bootstrapped()})
if err != nil {
t.Fatal(err)
}
if r.Status().Heard || r.MayAct("node-files") {
t.Fatal("the silent seat must leave the reader unheard")
}
owner.down = false
owner.table = map[string]string{"node-a": def("node-a", "node", "put"), "node-b": def("node-b", "node", "put")}
if err := owner.say(ChangedEvent, "node-a", SourceTrunk); err != nil {
t.Fatal(err)
}
if st := r.Status(); !st.Heard || st.LastError != "" || !r.MayAct("node-files") || !r.Has("node-a") || !r.Has("node-b") {
t.Fatalf("after the changed event: %+v, node-b held %v: want heard and the whole list read", st, r.Has("node-b"))
}
}