package views import ( "context" "encoding/json" "errors" "fmt" "strings" "testing" "time" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" stdio "git.novox.be/novox/mesh-sdk/go" ) // mapSource is a state held in memory, as a Source. type mapSource map[string]Entry func (m mapSource) Keys() ([]string, error) { var out []string for k := range m { out = append(out, k) } return out, nil } func (m mapSource) Get(key string) (*Entry, bool, error) { e, ok := m[key] if !ok { return nil, false, nil } return &e, true, nil } // keptSource is a mapSource that keeps each key's history, as a Historian. type keptSource struct { mapSource kept map[string][]Revision } func (k keptSource) History(key string) ([]Revision, error) { return k.kept[key], nil } func markOf(state string, rev uint64, made time.Time) Entry { raw, _ := json.Marshal(Mark{V: Version, State: state, Revision: rev, Created: made, Fill: Fill{Keys: 2, Bytes: 100, MaxBytes: 1000}}) return Entry{Value: raw, Revision: 1} } // The owners' and the readers' side of a first view (ADR 0615 §4-§6, ADR 0621 §2-§3, ADR 0622): one table of cases, // since mesh-delivery is over its test budget. func TestViews(t *testing.T) { made := time.Date(2026, 11, 4, 12, 0, 0, 0, time.UTC) now := func() time.Time { return made.Add(time.Hour) } written := made.Add(-time.Minute) values := mapSource{"a": {Value: json.RawMessage(`{"n":1}`), Revision: 7, Written: written}, "b": {Value: json.RawMessage(`{"n":2}`), Revision: 9}} marks := mapSource{"modules": markOf("modules", 9, made)} t.Run("a first view answers every value at the mark's revision, the mark read first, with each key's written time", func(t *testing.T) { a, err := FirstView("o", "modules", values, marks, "", now()) if err != nil || a.Revision != 9 || !a.Created.Equal(made) || len(a.Values) != 2 || a.Next != "" { t.Fatalf("answer = %+v, %v", a, err) } raw, _ := json.Marshal(a) if back, err := DecodeAnswer(raw); err != nil || !back.Written["a"].Equal(written) || len(back.Written) != 1 { t.Errorf("written times = %v (%v), want a's alone", back.Written, err) } if a, _ := FirstView("o", "modules", values, mapSource{}, "", now()); a.Revision != 0 { t.Errorf("a state not marked answers revision %d, want 0", a.Revision) } }) t.Run("a first view larger than a page is paged by key", func(t *testing.T) { big := mapSource{} for i := range 4 { big[fmt.Sprintf("k%d", i)] = Entry{Value: json.RawMessage(`"` + strings.Repeat("x", PageBytes/3) + `"`), Revision: uint64(i + 1)} } got := map[string]bool{} after := "" for pages := 0; ; pages++ { a, err := FirstView("o", "s", big, nil, after, now()) if err != nil || pages > 4 { t.Fatalf("page %d: %+v %v", pages, a.Next, err) } for k := range a.Values { got[k] = true } if a.Next == "" { break } after = a.Next } if len(got) != 4 { t.Errorf("pages hold %v, want the four keys", got) } }) t.Run("each first-view tool answers its callers alone, never the owner, by the proof and not an argument", func(t *testing.T) { callers := ParseCallers("mesh-grants, node-tools,mesh-delivery-assigner") for caller, allowed := range map[string]bool{"novox.mesh-grants": true, "ace.node-tools": true, "g14.mesh-delivery-assigner": true, "novox.mesh-delivery-indexer": false, "novox.mesh-config": false, "person.jochen": false, stdio.Unproven: false} { if err := Refusal("manifests", callers, caller); (err == nil) != allowed { t.Errorf("%q: err = %v, allowed %v", caller, err, allowed) } } // A caller argument names nobody: a call with no proof is refused. tool := FirstViewTool("mesh-delivery-indexer", "manifests", "modules", "w.", callers, values, marks, now) if _, err := tool.Run(map[string]any{"caller": "novox.mesh-grants"}); err == nil { t.Errorf("an unproven call naming a caller is answered") } }) t.Run("a read tool says the keys and the fill, one key, one key's kept history, or a page of values", func(t *testing.T) { r, err := Read("o", "modules", values, marks, "", false, "", now()) ra, ok := r.(ReadAnswer) if err != nil || !ok || len(ra.Keys) != 2 || ra.Fill == nil || ra.Fill.MaxBytes != 1000 || ra.Revision != 9 { t.Fatalf("read = %+v, %v", r, err) } r, _ = Read("o", "modules", values, marks, "b", false, "", now()) if k := r.(KeyAnswer); !k.Found || k.Revision != 9 { t.Errorf("one key = %+v", k) } r, _ = Read("o", "modules", values, nil, "", false, "", now()) if ra := r.(ReadAnswer); ra.Fill != nil || ra.Words == "" { t.Errorf("an unmarked state says its fill as %+v", ra) } kept := []Revision{{Revision: 3, At: written, Op: "put", Value: json.RawMessage(`{"n":0}`)}, {Revision: 5, At: written.Add(time.Second), Op: "delete", Value: json.RawMessage(`null`)}, {Revision: 7, At: written, Op: "put", Value: json.RawMessage(`{"n":1}`)}} tool := ReadTool("o", "o_read_modules", "modules", "w", keptSource{values, map[string][]Revision{"a": kept}}, marks, now) r, err = tool.Run(map[string]any{"key": "a", "history": true}) raw, _ := json.Marshal(r) var h struct { History []Revision `json:"history"` } if err != nil || json.Unmarshal(raw, &h) != nil || len(h.History) != 3 || h.History[1].Op != "delete" || h.History[2].Revision != 7 || !h.History[0].At.Equal(written) { t.Errorf("a key's history = %s, %v", raw, err) } if r, err = tool.Run(map[string]any{"key": "none", "history": true}); err != nil || r.(HistoryAnswer).History == nil { t.Errorf("a key with nothing kept answers %+v, %v, not an empty history", r, err) } if _, err := tool.Run(map[string]any{"history": true}); err == nil { t.Error("a history naming no key is answered") } if _, err := ReadTool("o", "o_read_modules", "modules", "w", values, marks, now).Run(map[string]any{"key": "a", "history": true}); err == nil { t.Error("a source keeping no history answers one") } }) // A follower whose owner answers from values and marks. follower := func(ask func(context.Context, string) (Answer, error)) (*Follower, *[]string) { var said []string f := &Follower{Owner: "o", Tool: "o.modules", State: "modules", Ask: ask, Bound: time.Nanosecond, Wait: time.Millisecond, Raise: func(s Status) { said = append(said, "raised "+s.Tool) }, Clear: func(s Status) { said = append(said, "cleared") }} return f, &said } answer := func(context.Context, string) (Answer, error) { return FirstView("o", "modules", values, marks, "", now()) } ev := func(rev uint64, key, value string) Event { e := Event{V: Version, Owner: "o", State: "modules", Key: key, Revision: rev, Created: made} if value == "" { e.Deleted = true } else { e.Value = json.RawMessage(value) } return e } t.Run("a reader applies a put and a delete from the events alone, and passes over what it holds", func(t *testing.T) { asks := 0 f, _ := follower(func(ctx context.Context, after string) (Answer, error) { asks++; return answer(ctx, after) }) if err := f.Start(context.Background()); err != nil { t.Fatal(err) } for _, e := range []Event{ev(8, "a", `{"n":0}`), ev(10, "c", `{"n":3}`), ev(11, "a", "")} { if gap, why := f.Apply(e); gap { t.Fatalf("revision %d is a gap: %s", e.Revision, why) } } if _, ok := f.Get("a"); ok || f.Status().Revision != 11 || asks != 1 { t.Errorf("picture = %v at %d after %d asks", f.Values(), f.Status().Revision, asks) } if v, _ := f.Get("c"); string(v) != `{"n":3}` { t.Errorf("c = %s", v) } }) t.Run("a gap, a state made again and a value too large each ask once more", func(t *testing.T) { for name, e := range map[string]Event{ "a revision missed": ev(12, "a", `{}`), "made again": {V: Version, Owner: "o", State: "modules", Key: "a", Revision: 1, Created: made.Add(time.Minute), Value: json.RawMessage(`{}`)}, "too large": {V: Version, Owner: "o", State: "modules", Key: "a", Revision: 10, Created: made, Gap: true}, } { f, _ := follower(answer) _ = f.Start(context.Background()) if gap, _ := f.Apply(e); !gap { t.Errorf("%s: not taken as a gap", name) } } }) t.Run("a reader whose owner is down keeps asking, raises past its bound, and the answer clears it", func(t *testing.T) { down := 3 f, said := follower(func(ctx context.Context, after string) (Answer, error) { if down > 0 { down-- return Answer{}, errors.New("no responders") } return answer(ctx, after) }) if err := f.Start(context.Background()); err != nil { t.Fatal(err) } if strings.Join(*said, ",") != "raised o.modules,cleared" || f.Status().Asks != 4 || f.Status().Raised { t.Errorf("said %v after %d asks", *said, f.Status().Asks) } }) t.Run("an event heard while an ask runs is taken again after its answer", func(t *testing.T) { var f *Follower f, _ = follower(func(ctx context.Context, after string) (Answer, error) { a, err := answer(ctx, after) // the picture read at 9, before revision 10 was written if f.Status().Asks == 2 { f.Apply(ev(10, "d", `{"n":4}`)) } return a, err }) _ = f.Start(context.Background()) _ = f.Again(context.Background(), "test") if v, ok := f.Get("d"); !ok || string(v) != `{"n":4}` || f.Status().Revision != 10 || f.Status().ByHand != 1 { t.Errorf("d = %s, %v at %d", v, ok, f.Status().Revision) } }) t.Run("an event heard by its data asks a gap once more as a gap, never by hand", func(t *testing.T) { asked := make(chan struct{}, 4) f, _ := follower(func(ctx context.Context, after string) (Answer, error) { asked <- struct{}{} return answer(ctx, after) }) _ = f.Start(context.Background()) <-asked raw, _ := ev(12, "a", `{}`).Encode() if err := HearEvent(context.Background(), map[string]*Follower{"modules": f}, raw, t.Logf); err != nil { t.Fatal(err) } select { case <-asked: case <-time.After(3 * time.Second): t.Fatal("a gap heard by its data asked nothing") } if s := f.Status(); s.Asks != 2 || s.ByHand != 0 { t.Errorf("a gap was counted by hand: %+v", s) } if err := HearEvent(context.Background(), nil, []byte("not an event"), t.Logf); err == nil { t.Error("data that is no views event was taken") } }) t.Run("a module's reader answers Keys and Get from its pictures, refuses a state it does not follow or has no first view of, and writes and clears its finding", func(t *testing.T) { acted := 0 fs := &findingsMap{} r := &Reader{Module: "reader", Findings: fs, Bound: time.Nanosecond, Wait: time.Millisecond, Acts: actsFunc(func(HandAct) error { acted++ return nil })} f := r.Follow("o", "modules", "o.indexer_read_modules") down := 2 f.Ask = func(ctx context.Context, after string) (Answer, error) { if down > 0 { down-- return Answer{}, errors.New("no responders") } return answer(ctx, after) } if _, err := r.Keys("o.modules"); err == nil { t.Errorf("a state with no first view is read") } if _, err := r.Keys("o.other"); err == nil { t.Errorf("a state not followed is read") } if err := f.Start(context.Background()); err != nil { t.Fatal(err) } keys, err := r.Keys("o.modules") v, ok, _ := r.Get("o.modules", "b") if err != nil || len(keys) != 2 || !ok || string(v) != `{"n":2}` || len(fs.m) != 0 || fs.puts != 1 || fs.deletes != 1 { t.Errorf("keys %v, b %s, findings %+v", keys, v, fs) } handed := map[string]bool{} if err := r.Watch("o.modules", func(k string, _ []byte, gone bool) { handed[k] = !gone }); err != nil || len(handed) != 2 || !r.Follows("o.modules") || r.Follows("o.x") { t.Errorf("watch handed %v, %v", handed, err) } f.Apply(ev(10, "c", `{"n":3}`)) f.Apply(ev(11, "a", "")) if !handed["c"] || handed["a"] { t.Errorf("a change was not handed to the watch: %v", handed) } if got := r.Tools("reader"); len(got) != 2 || got[0].Name != "reader_first_views" { t.Errorf("tools = %v", got) } again, err := r.Tools("reader")[1].Run(map[string]any{"state": "o.modules"}) if err != nil || len(again.(map[string]any)["views"].([]Status)) != 1 || f.Status().ByHand != 1 || acted != 1 { t.Errorf("asked again by hand: %v, %v", again, err) } }) t.Run("a process's reader follows the read tool its caller names, reads the state as a read-only bucket, each key's written time handed on, and opens no other", func(t *testing.T) { r := &ProcessReader{Module: "reader", Bound: time.Nanosecond, Wait: time.Millisecond, Now: now, Call: func(tool string, body any) (json.RawMessage, error) { if m, _ := body.(map[string]any); tool != "o.o_read_modules" || m["values"] != true { return nil, fmt.Errorf("asked %s with %v", tool, body) } a, err := FirstView("o", "modules", values, marks, "", now()) raw, _ := json.Marshal(map[string]any{"result": a}) return raw, err }} p, err := r.Follow("o", "modules", "o.o_read_modules") if err != nil { t.Fatal(err) } for name, tool := range map[string]string{"another owner's tool": "x.read_more", "no owner in the tool": "read_more"} { if _, err := r.Follow("o", "more", tool); err == nil { t.Fatalf("%s is followed", name) // a picture no owner answers would hold Ready } } if _, err := p.Get(context.Background(), "a"); err == nil { t.Errorf("a picture with no first view is read") } if _, err := r.KeyValue(context.Background(), "o_other"); err == nil { t.Errorf("a bucket not followed is opened") } ctx, cancel := context.WithCancel(context.Background()) defer cancel() r.Ask(ctx, nil) <-r.Ready() kv, err := r.KeyValue(ctx, "o_modules") if err != nil { t.Fatal(err) } e, err := kv.Get(ctx, "a") if err != nil || string(e.Value()) != `{"n":1}` || !e.Created().Equal(written) { t.Fatalf("a = %v, %v", e, err) } if _, err := kv.Put(ctx, "a", nil); !errors.Is(err, ErrReadOnly) { t.Errorf("a write to a picture: %v", err) } w, _ := kv.Watch(ctx, "*") var seen []string for e := range w.Updates() { if e == nil { break } seen = append(seen, e.Key()) } at := made.Add(2 * time.Minute) ch := ev(10, "c", `{"n":3}`) ch.At = at p.Follower().Apply(ch) if e := <-w.Updates(); strings.Join(seen, ",") != "a,b" || e.Key() != "c" || !e.Created().Equal(at) { t.Errorf("watch handed %v then %v", seen, e) } _ = w.Stop() }) t.Run("a reader's again tool records its hand act before it writes the ask, and asks nothing when the act is not taken", func(t *testing.T) { var recorded []HandAct take := errors.New("no responders") acts := actsFunc(func(q HandAct) error { recorded = append(recorded, q) return take }) again := StatusTools("reader", "reader_first_views", "reader_first_view_again", stdio.State(StatusState), acts, now)[1] args := map[string]any{"owner": "o", "state": "modules", "why": "doubted"} if _, err := again.Run(args); err == nil || !strings.Contains(err.Error(), "nothing was asked again") || len(recorded) != 1 { t.Errorf("a hand act the seat did not take: %v, recorded %v", err, recorded) } take = nil // taken: the ask is written next (here refused, for no runtime serves this test) if _, err := again.Run(args); err == nil || !strings.Contains(err.Error(), "hand ask is not written") || len(recorded) != 2 || recorded[1].Why != "doubted" || recorded[1].Verb != "reader_first_view_again" || !strings.Contains(recorded[1].What, "o's modules") { t.Errorf("a hand act taken: %v, recorded %+v", err, recorded) } }) t.Run("an ask through a tool reads the answer bare or in the tool runner's envelope, and its error", func(t *testing.T) { bare := `{"v":1,"owner":"o","state":"s","revision":3,"values":{"k":"1"}}` for name, c := range map[string]struct { answer string rev uint64 err string }{ "bare": {bare, 3, ""}, "in an envelope": {`{"result":` + bare + `}`, 3, ""}, "an error": {`{"error":"not yet"}`, 0, "not yet"}, } { ask := AskVia(func(string, any) (json.RawMessage, error) { return json.RawMessage(c.answer), nil }, "o.first_view") a, err := ask(context.Background(), "") if (c.err == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), c.err)) || a.Revision != c.rev { t.Errorf("%s: %+v, %v", name, a, err) } } }) t.Run("a change not said is said again on a backoff before the next, and the mark never passes it", func(t *testing.T) { var said []string fails := 2 em := Emitter{Owner: "o", Subject: func(e string) string { return "s." + e }, Publish: func(_ context.Context, msg *nats.Msg, id string) error { if fails > 0 && strings.HasSuffix(id, "-1") { fails-- return errors.New("bus away") } said = append(said, id) return nil }, Wait: func(_ context.Context, attempt int) bool { return true }} updates := make(chan jetstream.KeyValueEntry, 2) updates <- kvEntry{key: "a", rev: 1, at: made} updates <- kvEntry{key: "b", rev: 2, at: made} close(updates) errs := make(chan error, 1) em.follow(context.Background(), "s", made, updates, func() (Fill, error) { return Fill{}, nil }, errs) if len(said) != 2 || !strings.HasSuffix(said[0], "-1") || !strings.HasSuffix(said[1], "-2") || fails != 0 { t.Fatalf("said %v (fails left %d): the change at 1 must be said, again, before the one at 2", said, fails) } // A context that ends while a change waits to be said again stops there: nothing after it is said. said, fails = nil, 1 ctx, cancel := context.WithCancel(context.Background()) em.Wait = func(context.Context, int) bool { cancel(); return false } updates = make(chan jetstream.KeyValueEntry, 2) updates <- kvEntry{key: "a", rev: 1, at: made} updates <- kvEntry{key: "b", rev: 2, at: made} em.follow(ctx, "s", made, updates, func() (Fill, error) { return Fill{}, nil }, errs) if len(said) != 0 { t.Fatalf("said %v after the context ended on a change not said", said) } // A change not said past its bound (Bound) is the owner's finding naming the state, cleared once it is said; // one said within the bound raises none. for _, c := range []struct { fails, puts int }{{1, 0}, {3, 1}} { clock := made fs := &findingsMap{} said, fails = nil, c.fails em.Now, em.Findings = func() time.Time { return clock }, fs em.Wait = func(context.Context, int) bool { clock = clock.Add(time.Minute); return true } updates = make(chan jetstream.KeyValueEntry, 1) updates <- kvEntry{key: "a", rev: 1, at: made} close(updates) em.follow(context.Background(), "s", made, updates, func() (Fill, error) { return Fill{}, nil }, nil) if fs.puts != c.puts || fs.deletes != 1 || len(fs.m) != 0 || len(said) != 1 { t.Errorf("%d failures: %d findings written, %d cleared, %d left, said %v", c.fails, fs.puts, fs.deletes, len(fs.m), said) } if c.puts > 0 { f := UnsaidFinding("o", "s", "a", 1, made, "bus away", clock) if f.Kind != UnsaidKind || f.ID != "o.s" || f.Observer != "o" || !strings.Contains(f.Summary, "o has not said the change of s.a at 1") { t.Errorf("the finding is %+v", f) } } } // A finding a run before a restart raised is cleared by the first change said after it. left0 := UnsaidFinding("o", "s", "a", 1, made, "bus away", made) key := left0.Kind + "/" + left0.ID fs := &findingsMap{m: map[string]Finding{key: left0}} said, fails = nil, 0 em.Findings = fs updates = make(chan jetstream.KeyValueEntry, 1) updates <- kvEntry{key: "b", rev: 2, at: made} close(updates) em.follow(context.Background(), "s", made, updates, func() (Fill, error) { return Fill{}, nil }, nil) if _, left := fs.m[key]; left || len(said) != 1 { t.Errorf("after a restart the leftover finding stands (%v), said %v", left, said) } }) t.Run("an event names its state on its subject, and decodes as it was encoded", func(t *testing.T) { if s := StateOfEvent("mesh.stream.mesh-delivery-indexer.event.views.config-candidates"); s != "config-candidates" { t.Errorf("state of subject = %q", s) } for subject, want := range map[string]string{"mesh.mod.o.event.views.modules": "o", "mesh.mod.o.event.views.a.b": "", "mesh.mod.o.event.other": "", "mesh.stream.o.event.views.modules": "", "mesh.mod..event.views.modules": ""} { if got, _ := OwnerOfEvent(subject); got != want { t.Errorf("owner of %s = %q, want %q", subject, got, want) } } raw, err := ev(3, "k", `{"x":1}`).Encode() got, err2 := DecodeEvent(raw) if err != nil || err2 != nil || got.Revision != 3 || string(got.Value) != `{"x":1}` { t.Errorf("round trip = %+v %v %v", got, err, err2) } if _, err := (Event{Owner: "o", State: "s"}).Encode(); err == nil { t.Errorf("an event with no key or revision is encoded") } }) } // findingsMap is a module's own findings state in memory, keyed `/`. type findingsMap struct { m map[string]Finding puts, deletes int } func (f *findingsMap) Put(v Finding) error { if f.m == nil { f.m = map[string]Finding{} } f.m[v.Kind+"/"+v.ID] = v f.puts++ return nil } func (f *findingsMap) Delete(v Finding) error { delete(f.m, v.Kind+"/"+v.ID); f.deletes++; return nil } // actsFunc records a hand act through a test's function. type actsFunc func(HandAct) error func (f actsFunc) RecordHandAct(a HandAct) error { return f(a) } // kvEntry is one change of a bucket as a watch hands it. type kvEntry struct { key string rev uint64 at time.Time } func (k kvEntry) Bucket() string { return "b" } func (k kvEntry) Key() string { return k.key } func (k kvEntry) Value() []byte { return []byte(`{"n":1}`) } func (k kvEntry) Revision() uint64 { return k.rev } func (k kvEntry) Created() time.Time { return k.at } func (k kvEntry) Delta() uint64 { return 0 } func (k kvEntry) Operation() jetstream.KeyValueOp { return jetstream.KeyValuePut }