assigner: a reads statement older than a reads-changed heard does not stand (issue 1975)

The assigner keeps each reads-changed event it hears beside its statement (enginereads.Keeper.HeardChanged),
reads a node as the event says while its statement is behind it, and asks again until the statement is as new.

Builds: issue 1975 note 4 (the lead's handover; no design line)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 10:48:08 +01:00
parent bf36f84445
commit 3ec05dda1f
2 changed files with 23 additions and 0 deletions
@@ -569,6 +569,10 @@ func hearReadsChanged(ctx context.Context, js jetstream.JetStream, hub *process.
logf("%s holds what is not %s, and %s is asked again all the same: %v", m.Subject(), enginereads.Event, node, err)
reads.AskNode(node, enginereads.Event+" that does not read")
default:
// What the event says is kept first: a statement older than it does not stand while the node is asked again.
if e, derr := enginereads.DecodeChanged(m.Data()); derr == nil {
reads.HeardChanged(e)
}
reads.AskNode(node, enginereads.Event)
}
}
@@ -317,6 +317,25 @@ func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) {
if asked["ace"] != 2 {
t.Errorf("ace, back online, was asked %d times, want 2", asked["ace"])
}
// Issue 1975: ace's engine answered its bootstrapped seats, then stated the full list and said so; an answer older
// than that event does not stand: the rules read what the event says, and the node is asked until it is as new.
stale := true
bootstrapped := func(node, _ string, _ any) (json.RawMessage, error) {
if stale {
stale = false
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"seat:a"}, StatedAt: "2026-11-12T01:14:36.498Z"})
}
return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"seat:a", "seat:b"}, StatedAt: "2026-11-12T01:14:39.001Z"})
}
rk := &enginereads.Keeper{Call: bootstrapped, Sync: true}
rk.AskNode("ace", "the assigner started")
rk.HeardChanged(nodeengine.ManifestReadsChangedEvent{V: 1, Node: "ace", Why: enginereads.ReadsStarted, At: "2026-11-12T01:14:39.002Z", ManifestReads: []string{"seat:a", "seat:b"}})
rp := &passer{w: w, reads: rk, online: map[string]bool{"ace": true}}
rn := w.nodes(nil, nil)
rp.engineReads(rn)
if !slices.Equal(rn.Reads["ace"], []string{"seat:a", "seat:b"}) {
t.Errorf("the rules read the statement older than the event heard: %v", rn.Reads["ace"])
}
// ADR 0668 §3 (review round 1): a node whose engine answers nothing while its machine is heard is asked again at each
// pass once its ask ended, and past five minutes is the finding call-unanswered, cleared once it answers; none is
// written while its machine is away.