Files
mesh-delivery/modules/mesh-delivery-view/internal/view/asks_test.go
T
jschoubben 2fe8d01e86 Join item 1: the rewrite map, asks/asker, the vault's types and compose-candidate to mesh-contracts
mesh-sdk holds no go/asks, go/asker, vault answers or compose-candidate types (A1d, J1): each module now names the
operator-channel contract and client, the channel benches (mesh-sdk/go/channel), mesh-vault's contract, and node-engine's
contract and client (a1-progress.md, the A1d and J1 rewrite maps).
- assigner, teller, view: delivery.Ask.Ask and AssignerAsk.Ask are the router's ask as JSON; the refusals mesh-sdk
  dropped (an id other than AskingID(key, asking), no option) are each reader's and writer's: assign.EncodeAsk/DecodeAsk
  (askjson.go), the teller's routerAskOf (teller.go) and the view's routerAsk (view.go); the teller refuses an option
  the asking does not offer and one whose level is not acknowledge (viewanswers.go), the two refusals ViewAnswerFits
  gave. Row: subtest of TestTheHoldAskIsValid (another id, no option, no ask; each written before).
- enrolment, grants: the vault's verbs and answers from meshvault; asks' conditions from the contract and client.
- checker, assigner's judge: compose-candidate from nodeengine/nodeengineclient (nec.Own for "node").
- mesh-grants' user-lists golden: the node-engine's users now subscribe to mesh-delivery-view's settled (mesh-sdk's
  NodeAgentHears, A7's ENG-8 retry).
go.mod: each module requires the mesh-contracts modules it imports (and nodeengineclient, which mesh-sdk imports) at the
zero version, for the group pin.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 15:05:12 +01:00

198 lines
7.7 KiB
Go

package view
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
const askHeadA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
// askEntry is a key of the gate's asks as the gate writes it (mesh-sdk go/delivery EncodeAsk, MergeAskKey): the router's ask
// whole, under the id its key gives.
func askEntry(t *testing.T, group, headline, words string) Entry {
t.Helper()
heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}}
key, err := delivery.MergeAskKey(group, heads)
if err != nil {
t.Fatal(err)
}
a := oc.Ask{ID: delivery.AskID(key), Headline: headline, Explanation: words, Who: occ.Operator, Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held",
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
v, err := delivery.EncodeAsk(delivery.Ask{Group: group, Heads: heads, Ask: rawAsk(t, a)})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.AsksBucket, Key: key, Value: v, At: t0}
}
// 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, delivery.AskID(key)) }
func answerToAsking(t *testing.T, key, id string) Entry {
t.Helper()
v, err := delivery.EncodeAnswer(delivery.Answer{Option: "approve", Warrant: id + "@2026-10-18T12:00:00Z", Words: "the operator, via telegram, chose Approve"})
if err != nil {
t.Fatal(err)
}
return Entry{Bucket: delivery.AnswersBucket, Key: key, Value: v, At: t0}
}
func TestAnAskWithNoAnswerIsListedAndAnAnsweredOneIsNot(t *testing.T) { // 1345/AC1
m := New()
e := askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges one pull request.\nReview: approved.")
fill(m, e)
s := m.Snapshot()
if len(s.Asks) != 1 {
t.Fatalf("asks = %+v, want the one open ask", s.Asks)
}
a := s.Asks[0]
if a.Key != e.Key || a.Group != "feat/x" || a.Headline != "Merge into main: mesh-delivery#12" || !strings.Contains(a.Words, "Merges one pull request.") ||
a.How != "answer on the phone" || len(a.Pulls) != 1 || !strings.Contains(a.Pulls[0], "novox/mesh-delivery#12") || !strings.Contains(a.Pulls[0], askHeadA[:12]) {
t.Errorf("ask = %+v", a)
}
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: delivery.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: delivery.AsksBucket, Key: e.Key, Deleted: true})
if got := m.Snapshot().Asks; len(got) != 0 {
t.Errorf("a withdrawn ask is listed: %+v", got)
}
}
func TestAnAnswerHeardBeforeItsAskStillHidesTheAsk(t *testing.T) {
m := New()
e := askEntry(t, "feat/x", "Merge", "Merges.")
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()
fill(m, Entry{Bucket: delivery.AsksBucket, Key: "merge.g.0123456789ab", Value: []byte("{"), At: t0})
s := m.Snapshot()
if len(s.Asks) != 0 || len(s.Unreadable) != 1 || s.Unreadable[0].Bucket != delivery.AsksBucket {
t.Errorf("snapshot = %+v", s)
}
}
func TestTableAndPageShowTheAsks(t *testing.T) { // 1345/AC2
m := New()
fill(m, askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges one pull request."))
var out bytes.Buffer
Table(&out, m.Snapshot())
for _, want := range []string{"ASKS", "Merge into main: mesh-delivery#12", "Merges one pull request.", "answer on the phone", "novox/mesh-delivery#12"} {
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", "Merge into main: mesh-delivery#12", "Merges one pull request.", "answer on the phone"} {
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().Snapshot())
if strings.Contains(out.String(), "ASKS") {
t.Errorf("the table shows an ASKS section with no ask:\n%s", out.String())
}
}
func TestTheSnapshotsJSONCarriesTheAsks(t *testing.T) { // 1345/AC2
m := New()
fill(m, askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges one pull request."))
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 != "answer on the phone" {
t.Errorf("json %s: %+v %v", raw, back, err)
}
}
func TestAHaveOfARemovedModuleIsShownRemovedNotLeftBehind(t *testing.T) { // 1370: the node-engine writes phase removed
m := New()
fill(m, haveE(t, "novox", "mesh-delivery", have(t, "b1", delivery.PhaseRemoved, delivery.HealthOK, ""), t0),
haveE(t, "novox", "stray", have(t, "b2", delivery.PhaseRunning, delivery.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)
}
}
func TestAnAnswerToAnEarlierAskingDoesNotHideTheNextAsking(t *testing.T) { // R2-1 of review round 2
m := New()
e := askEntry(t, "feat/x", "Merge into main: mesh-delivery#12", "Merges one pull request.")
fill(m, e, answerEntry(t, e.Key))
if got := m.Snapshot().Asks; len(got) != 0 {
t.Fatalf("asking 1 is answered and listed: %+v", got)
}
// The gate rewrites the key at asking 2; only asking 1's answer exists.
heads := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: askHeadA}}
a := oc.Ask{ID: delivery.AskingID(e.Key, 2), Headline: "Merge into main: mesh-delivery#12", Explanation: "Merges one pull request.", Who: occ.Operator,
Expires: occ.FormatTime(t0.Add(23 * time.Hour)), OnExpiry: "the merge stays held",
Options: []oc.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: occ.Approve, Binds: "sha256:00"}}}
v, err := delivery.EncodeAsk(delivery.Ask{Group: "feat/x", Heads: heads, Asking: 2, Ask: rawAsk(t, a)})
if err != nil {
t.Fatal(err)
}
fill(m, Entry{Bucket: delivery.AsksBucket, Key: e.Key, Value: v, At: t0})
if got := m.Snapshot().Asks; len(got) != 1 {
t.Fatalf("asking 2 is not listed while only asking 1's answer exists: %+v", got)
}
fill(m, answerToAsking(t, e.Key, a.ID))
if got := m.Snapshot().Asks; len(got) != 0 {
t.Errorf("asking 2 is answered and listed: %+v", got)
}
}
// rawAsk is the router's ask as the JSON a gate's or 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
}