package enginereads import ( "context" "encoding/json" "errors" "strings" "testing" "time" ) // The answer and the event are judged as the contract lays them out and its reader checks: the layout's version, a node // that is a name, no empty capability, a time, and an event's why one of the two. func TestTheAnswerAndTheEventAreJudged(t *testing.T) { const at = "2026-11-05T12:00:00Z" for name, c := range map[string]struct{ body, bad string }{ "whole": {`{"v":1,"node":"ace","manifest-reads":["mesh-config.resolved"],"stated-at":"` + at + `"}`, ""}, "old layout": {`{"v":2,"node":"ace","stated-at":"` + at + `"}`, "version"}, "bad node": {`{"v":1,"node":"a.b","stated-at":"` + at + `"}`, "no node"}, "empty read": {`{"v":1,"node":"ace","manifest-reads":[" "],"stated-at":"` + at + `"}`, "empty reader"}, "no time": {`{"v":1,"node":"ace","stated-at":"yesterday"}`, "RFC 3339"}, } { _, _, err := DecodeAnswer([]byte(c.body)) if (c.bad == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.bad)) { t.Errorf("answer %s: %v, want %q", name, err, c.bad) } } for name, c := range map[string]struct{ body, bad string }{ "whole": {`{"v":1,"node":"ace","why":"started","manifest-reads":["a.b"],"at":"` + at + `"}`, ""}, "returned": {`{"v":1,"node":"ace","why":"returned","at":"` + at + `"}`, ""}, "old layout": {`{"v":2,"node":"ace","why":"started","at":"` + at + `"}`, "version"}, "no node": {`{"v":1,"node":"","why":"started","at":"` + at + `"}`, "no node"}, "another why": {`{"v":1,"node":"ace","why":"bored","at":"` + at + `"}`, "bored"}, "no time": {`{"v":1,"node":"ace","why":"started","at":"yesterday"}`, "RFC 3339"}, } { _, err := DecodeChanged([]byte(c.body)) if (c.bad == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.bad)) { t.Errorf("event %s: %v, want %q", name, err, c.bad) } } // Issue 1966 note 155, ADR 0668 §4: a part whose first call of a node timed out, and whose consumer was bound after // the node's event was said, calls every node once at the bind, and the node is then held as it answered. calls := map[string]int{} k := &Keeper{Sync: true, Call: func(node, _ string, _ any) (json.RawMessage, error) { calls[node]++ if node == "ace" && calls[node] == 1 { return nil, context.DeadlineExceeded } return json.RawMessage(`{"v":1,"node":"` + node + `","manifest-reads":["a.b"],"stated-at":"` + at + `"}`), nil }} k.AskNode("ace", "the first view") if k.Statements()["ace"].Answered { t.Errorf("a call that timed out reads as answered") } k.AtBind(func() []string { return []string{"ace", "g14"} })(time.Unix(0, 0)) if p := k.Present([]string{"ace", "g14"}); len(p["ace"]) != 1 || len(p["g14"]) != 1 || calls["ace"] != 2 || calls["g14"] != 1 { t.Errorf("after the bind: present %v, calls %v", p, calls) } // Review round 1 (ADR 0668 §3): a node whose engine answers nothing reads as its record at the first fault of the call // made once, while the kept call asks on; what the kept call answers then stands. var during Statement f := &Keeper{Sync: true, Fallback: func(string) ([]string, bool) { return []string{"r.s"}, true }, Once: func(string, string, any) (json.RawMessage, error) { return nil, errors.New("nats: no responders available for request") }} f.Call = func(node, _ string, _ any) (json.RawMessage, error) { during = f.Statements()[node] return json.RawMessage(`{"v":1,"node":"` + node + `","manifest-reads":["a.b"],"stated-at":"` + at + `"}`), nil } f.AskNode("g14", "the first view") if during.From != FromRecord || during.Answered || f.Statements()["g14"].From != FromVerb { t.Errorf("while the kept call asked g14 read %+v; after it %+v", during, f.Statements()["g14"]) } // ADR 0668 §3: a node unanswered keeps when it first was not, across asks; past the bound it is unanswered while its // machine is heard, none while away, and none once it answers. clock := time.Date(2026, 11, 10, 3, 0, 0, 0, time.UTC) answers := false u := &Keeper{Sync: true, Now: func() time.Time { return clock }, Call: func(node, _ string, _ any) (json.RawMessage, error) { if answers { return json.RawMessage(`{"v":1,"node":"` + node + `","manifest-reads":["a.b"],"stated-at":"` + at + `"}`), nil } return nil, context.DeadlineExceeded }} first := clock u.AskNode("ace", "the first view") clock = clock.Add(6 * time.Minute) u.AskNode("ace", "asked on") heard := func(string) bool { return true } if got := u.Unanswered([]string{"ace"}, heard, 5*time.Minute, clock); !got["ace"].Equal(first) || len(u.Unanswered([]string{"ace"}, func(string) bool { return false }, 5*time.Minute, clock)) != 0 { t.Errorf("unanswered past the bound: %v", got) } answers = true u.AskNode("ace", "answered") if got := u.Unanswered([]string{"ace"}, heard, 5*time.Minute, clock); len(got) != 0 || !u.Statements()["ace"].UnansweredSince.IsZero() { t.Errorf("an answered node is unanswered: %v", got) } if Verb != "manifest-reads" || Event != "manifest-reads-changed" || Consumes != "node-engine.manifest-reads-changed" || Invoke != "seat:node-engine.manifest-reads" { 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) } }