Files
mesh-sdk/go/views/views_test.go
T
jschoubben 315d7e9b8c J3 (A2g, A2h): a process's pictures of its owners' states are go/views', and a first view says when each key was written
ProcessReader and Picture (A2g's mesh-delivery sdk/firstview at c6c6687) and the test owner (viewstest) move into
go/views, so the builder, checker, teller, view and watchman read the indexer's and the assigner's states through the
one shared follower. The first view's answer carries each key's written time as the bus keeps it, and
Follower.Changed hands it on, so a reader's want time is the owner's write, not its own start (A2h's request).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:22:20 +01:00

366 lines
14 KiB
Go

package views
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/handacts"
)
// 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) }
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, 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 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: handacts.Recorder{Ask: func(string, any) (json.RawMessage, error) {
acted++
return json.RawMessage(`{"log":"mesh-hand-acts"}`), 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 reads its owner's 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 != "mesh-delivery-indexer.indexer_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 := r.Follow("mesh-delivery-indexer", "modules")
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(), "mesh-delivery-indexer_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, "mesh-delivery-indexer_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("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("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")
}
})
}
// findingsMap is a module's own findings state in memory.
type findingsMap struct {
m map[string][]byte
puts, deletes int
}
func (f *findingsMap) Put(k string, v []byte) error {
if f.m == nil {
f.m = map[string][]byte{}
}
f.m[k] = v
f.puts++
return nil
}
func (f *findingsMap) Delete(k string) error { delete(f.m, k); f.deletes++; return nil }