Files
mesh-delivery/modules/mesh-delivery-view/internal/view/asks_test.go
T
jschoubben 0de534c692 Names N3 (view): the status view names its own buckets from its credential's module
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
2026-11-12 19:14:55 +01:00

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
}