Its states answers, reach and waits (viewstate WaitsBucket, ReachBucket, ViewAnswersBucket), its first views and its marks were named from the constant ViewModule; they are now `<cred.Module>_<state>` (viewstate.BucketsOf), and the model is made for its module. Builds: design 47 G A-296 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
157 lines
5.5 KiB
Go
157 lines
5.5 KiB
Go
package view
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
|
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
|
)
|
|
|
|
// answerEntry is the teller's answer to the first asking of the key; answerToAsking to the asking with this router id. The
|
|
// answer's warrant is `<router id>@<time>` (teller.go warrantID).
|
|
func answerEntry(t *testing.T, key string) Entry {
|
|
return answerToAsking(t, key, meshdeliveryclient.AskID(key))
|
|
}
|
|
|
|
func answerToAsking(t *testing.T, key, id string) Entry {
|
|
t.Helper()
|
|
v, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "release", Warrant: id + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Release"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return Entry{Bucket: AnswersBucket, Key: key, Value: v, At: t0}
|
|
}
|
|
|
|
func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/AC1
|
|
m := New(viewstate.ViewModule)
|
|
e, a := holdAsk(t)
|
|
fill(m, e)
|
|
s := m.Snapshot()
|
|
if len(s.Asks) != 1 || s.Asks[0].Key != e.Key || s.Asks[0].Headline != a.Headline || s.Asks[0].How != AnswerInTheView {
|
|
t.Fatalf("asks = %+v, want the one open ask", s.Asks)
|
|
}
|
|
fill(m, answerEntry(t, e.Key))
|
|
if got := m.Snapshot().Asks; len(got) != 0 {
|
|
t.Errorf("an answered ask is listed: %+v", got)
|
|
}
|
|
// An answer taken away leaves the ask open again; an ask taken away leaves the list.
|
|
fill(m, Entry{Bucket: AnswersBucket, Key: e.Key, Deleted: true})
|
|
if got := m.Snapshot().Asks; len(got) != 1 {
|
|
t.Errorf("asks after the answer was taken away = %+v", got)
|
|
}
|
|
fill(m, Entry{Bucket: AssignerAsksBucket, Key: e.Key, Deleted: true})
|
|
if got := m.Snapshot().Asks; len(got) != 0 {
|
|
t.Errorf("a withdrawn ask is listed: %+v", got)
|
|
}
|
|
}
|
|
|
|
// The gate's asks go with the Approve per merge (design 47 G §431, §512): a value under the gate's former state is no ask
|
|
// of the view's, and nothing names it unreadable.
|
|
func TestTheGatesMergeAsksAreNoAsksOfTheView(t *testing.T) {
|
|
m := New(viewstate.ViewModule)
|
|
fill(m, Entry{Bucket: "mesh-delivery-gate_asks", Key: "merge.g.0123456789ab", Value: []byte(`{"v":1,"group":"g"}`), At: t0},
|
|
Entry{Bucket: "mesh-delivery-gate_asks", Key: "merge.h.0123456789ab", Value: []byte("{"), At: t0})
|
|
if s := m.Snapshot(); len(s.Asks) != 0 || len(s.Unreadable) != 0 {
|
|
t.Errorf("snapshot = %+v", s)
|
|
}
|
|
}
|
|
|
|
func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) {
|
|
m := New(viewstate.ViewModule)
|
|
e, _ := holdAsk(t)
|
|
fill(m, answerEntry(t, e.Key), e)
|
|
if got := m.Snapshot().Asks; len(got) != 0 {
|
|
t.Errorf("asks = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestAnUnreadableAskIsNamedAndNotListed(t *testing.T) { // never dropped silently
|
|
m := New(viewstate.ViewModule)
|
|
e, _ := holdAsk(t)
|
|
fill(m, Entry{Bucket: AssignerAsksBucket, Key: e.Key, Value: []byte("{"), At: t0})
|
|
s := m.Snapshot()
|
|
if len(s.Asks) != 0 || len(s.Unreadable) != 1 || s.Unreadable[0].Bucket != AssignerAsksBucket {
|
|
t.Errorf("snapshot = %+v", s)
|
|
}
|
|
}
|
|
|
|
func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2
|
|
m := New(viewstate.ViewModule)
|
|
e, a := holdAsk(t)
|
|
fill(m, e)
|
|
var out bytes.Buffer
|
|
Table(&out, m.Snapshot())
|
|
for _, want := range []string{"ASKS", a.Headline, a.Explanation, AnswerInTheView} {
|
|
if !strings.Contains(out.String(), want) {
|
|
t.Errorf("the table lacks %q:\n%s", want, out.String())
|
|
}
|
|
}
|
|
rec := httptest.NewRecorder()
|
|
Web(m).ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
|
|
for _, want := range []string{"ASKS", a.Headline, AnswerInTheView} {
|
|
if !strings.Contains(rec.Body.String(), want) {
|
|
t.Errorf("the page lacks %q:\n%s", want, rec.Body.String())
|
|
}
|
|
}
|
|
// No ask, no section.
|
|
out.Reset()
|
|
Table(&out, New(viewstate.ViewModule).Snapshot())
|
|
if strings.Contains(out.String(), "ASKS") {
|
|
t.Errorf("the table shows an ASKS section with no ask:\n%s", out.String())
|
|
}
|
|
raw, err := json.Marshal(m.Snapshot())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var back struct {
|
|
Asks []struct {
|
|
Headline string `json:"headline"`
|
|
How string `json:"how"`
|
|
} `json:"asks"`
|
|
}
|
|
if err := json.Unmarshal(raw, &back); err != nil || len(back.Asks) != 1 || back.Asks[0].How != AnswerInTheView {
|
|
t.Errorf("json %s: %+v %v", raw, back, err)
|
|
}
|
|
}
|
|
|
|
func TestAHaveOfARemovedModuleIsShownRemovedNotLeftBehind(t *testing.T) { // 1370: the node-engine writes phase removed
|
|
m := New(viewstate.ViewModule)
|
|
fill(m, haveE(t, "novox", "mesh-delivery", have(t, "b1", nodeengineclient.PhaseRemoved, nodeengineclient.HealthOK, ""), t0),
|
|
haveE(t, "novox", "stray", have(t, "b2", nodeengineclient.PhaseRunning, nodeengineclient.HealthOK, ""), t0))
|
|
s := m.Snapshot()
|
|
if r := find(t, s, "novox", "mesh-delivery"); r.Status != StatusRemoved {
|
|
t.Errorf("row = %+v, want status %q", r, StatusRemoved)
|
|
}
|
|
if r := find(t, s, "novox", "stray"); r.Status != StatusLeftBehind {
|
|
t.Errorf("row = %+v, want left behind", r)
|
|
}
|
|
var out bytes.Buffer
|
|
Table(&out, s)
|
|
line := ""
|
|
for _, l := range strings.Split(out.String(), "\n") {
|
|
if strings.Contains(l, "mesh-delivery") {
|
|
line = l
|
|
}
|
|
}
|
|
if !strings.Contains(line, "removed") || strings.Contains(line, "left behind") {
|
|
t.Errorf("table line %q", line)
|
|
}
|
|
}
|
|
|
|
// rawAsk is the router's ask as the JSON an assigner's value carries (mesh-sdk keeps it raw, ADR 0627 section 4).
|
|
func rawAsk(t *testing.T, a oc.Ask) json.RawMessage {
|
|
t.Helper()
|
|
b, err := json.Marshal(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|