package views import ( "context" "encoding/json" "errors" "fmt" "strings" "testing" "time" 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 } 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) } values := mapSource{"a": {Value: json.RawMessage(`{"n":1}`), Revision: 7}, "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", 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) } 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, 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) } }) // 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 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) } 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") } }) }