Files
mesh-sdk/go/views/views_test.go
T
jschoubben dae61959b9 J3 (lead's a): go/views names no module; its caller gives the owner's read tool and views stream
ProcessReader.Follow(owner, state, tool, stream) takes the owner's read tool and its views stream as the owner's
declaration names them, and refuses another owner's tool or a second stream for one owner; OwnerReadTool, StreamOf,
IndexerStream and AssignerStream are gone, and viewstest takes the same two values. The second instance of a module
name in shared code (the earlier review finding), so the package test now refuses a module's name in go/views.

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

402 lines
16 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 follows the read tool and views stream 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", "o-views")
if err != nil {
t.Fatal(err)
}
for name, c := range map[string][2]string{"another owner's tool": {"x.read_more", "o-views"}, "no stream": {"o.read_more", ""},
"a second stream for the owner": {"o.read_more", "o-other-views"}} {
if _, err := r.Follow("o", "more", c[0], c[1]); 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 through mesh-hand-acts before it writes the ask, and asks nothing when the seat does not take it", func(t *testing.T) {
var recorded []handacts.Request
take := errors.New("no responders")
acts := handacts.Recorder{Ask: func(key string, body any) (json.RawMessage, error) {
if key != "seat:mesh-hand-acts.hand-act" {
return nil, fmt.Errorf("asked %s", key)
}
raw, _ := json.Marshal(body)
var q handacts.Request
_ = json.Unmarshal(raw, &q)
recorded = append(recorded, q)
if take != nil {
return nil, take
}
return json.RawMessage(`{"log":"mesh-hand-acts"}`), nil
}}
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("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 }