C7: the node-engine seat's client keeps the wait of every caller's kept manifest-reads calls, ReadsCalls beside Keeper.Unanswered, its bound's end asking once whether the machine is heard (issue 1966, ADR 0668 §3, ADR 0634 §1, ADR 0596 §2; review round 3, findings 1 and 2)
mesh/check checking bb16f99e2d5e on 7fbfd77a76cb | novox/mesh-contracts#3 in group fix/1966-call-made-once
mesh/delivery merged: source.main moved to novox/mesh-contracts@7fbfd77a76cb
mesh/delivery-group group fix/1966-call-made-once merged: every member is merged

The assigner, the builder and the indexer share it instead of two copies; it names no other seat's client (the client check), so each caller writes its finding in mesh-conditions' layout through Raise, stamped with ReadsCalls' clock.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-10 04:54:06 +01:00
parent 11a36eb88c
commit bb16f99e2d
2 changed files with 293 additions and 0 deletions
@@ -99,3 +99,71 @@ func TestTheAnswerAndTheEventAreJudged(t *testing.T) {
t.Errorf("the names are %s %s %s %s", Verb, Event, Consumes, Invoke)
}
}
// ADR 0668 §3, ADR 0596 §2 (review round 3): the wait of a part's kept manifest-reads calls. With Arm, a node unanswered
// has its bound kept, and only its expiry asks whether its machine is heard, once: heard, the finding is written at the
// part's clock and the node asked again; away, none; answered, it is cleared. With no Arm, heard is weighed at each step.
func TestReadsCallsSayAnUnansweredNodeHeard(t *testing.T) {
clock := time.Date(2026, 11, 10, 3, 0, 0, 0, time.UTC)
answers, calls := false, 0
k := &Keeper{Sync: true, Now: func() time.Time { return clock }, Call: func(node, _ string, _ any) (json.RawMessage, error) {
calls++
if answers {
return json.RawMessage(`{"v":1,"node":"` + node + `","manifest-reads":["a.b"],"stated-at":"2026-11-10T03:00:00Z"}`), nil
}
return nil, context.DeadlineExceeded
}}
for name, armed := range map[string]bool{"armed": true, "by its own knowledge": false} {
answers, calls = false, 0
k.Forget("ace")
first := clock
k.AskNode("ace", "the first view")
heard, asked := true, 0
raised, cleared := map[string]UnansweredCall{}, map[string]int{}
bounds := map[string]time.Time{}
c := &ReadsCalls{Keeper: k, Nodes: func() []string { return []string{"ace", "g14"} }, Bound: 5 * time.Minute,
Now: func() time.Time { return clock.Add(time.Second) },
Heard: func(context.Context, string) (bool, error) { asked++; return heard, nil },
Raise: func(_ context.Context, u UnansweredCall) error { raised[u.Node] = u; return nil },
Clear: func(_ context.Context, node string) error { cleared[node]++; delete(raised, node); return nil }}
if armed {
c.Arm = func(_ context.Context, node string, ends time.Time) error { bounds[node] = ends; return nil }
}
c.Step(context.Background())
if armed && (asked != 0 || !bounds["ace"].Equal(first.Add(5*time.Minute)) || len(raised) != 0) {
t.Errorf("%s: a step before the bound asked heard %d times, bounds %v, raised %v", name, asked, bounds, raised)
}
clock = clock.Add(6 * time.Minute)
heard, asked = false, 0
if armed {
c.Expired(context.Background(), "ace")
} else {
c.Step(context.Background())
}
if len(raised) != 0 || asked != 1 {
t.Errorf("%s: away, raised %v, heard asked %d times", name, raised, asked)
}
heard, asked, calls = true, 0, 0
if armed {
c.Expired(context.Background(), "ace")
} else {
c.Step(context.Background())
}
if u, ok := raised["ace"]; !ok || !u.Since.Equal(first) || !u.At.Equal(clock.Add(time.Second)) || asked != 1 || calls != 1 {
t.Errorf("%s: heard past the bound, raised %+v, heard asked %d, called %d", name, raised, asked, calls)
}
answers = true
k.AskNode("ace", "answered")
c.Step(context.Background())
if _, ok := raised["ace"]; ok || cleared["ace"] == 0 {
t.Errorf("%s: answered, raised %v, cleared %v", name, raised, cleared)
}
clock = clock.Add(time.Hour)
}
if CallID("ace") != "manifest-reads.ace" {
t.Errorf("the call's id is %s", CallID("ace"))
}
if n, ok := BoundOf(BoundPrefix + "ace"); !ok || n != "ace" {
t.Errorf("the bound's node is %q %v", n, ok)
}
}