Files
mesh-controller/internal/link/pages_test.go
T
jochen 175b28ee42
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/314-a-large-answer-is-paged-not-lost delivered: every member is delivered
Page an answer larger than one message of the bus, and keep overviews brief (hq issue 314)
The client library refuses to send a reply over the bus's max_payload, and the
controller only logged it: conditions and status answered nobody for hours on
2026-10-08 while calls said each was answered in 130 ms, and the operator's
channel read nothing. An answer too large is now held under its call and paged
to the caller that asks, on the same subject; a caller that does not page is
told in words, and calls says it. The overviews no longer carry every finding:
conditions and status list each condition with its newest evidence, doctor at
most twenty findings a probe (probe= gives one whole), and the JSON overviews
are sent once, as data, instead of twice.
2026-10-08 12:19:16 +02:00

102 lines
3.7 KiB
Go

package link
import (
"context"
"encoding/json"
"strings"
"testing"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-controller/internal/testbus"
)
// An answer too large for one message is recorded as paged, and a call's record says so: `calls` no
// longer reads "answered" for an answer nobody received (novox/hq issue 314).
func TestAPagedAnswerIsSaidInCalls(t *testing.T) {
l, a := NewCallLog(), newAnswers(t)
big := strings.Repeat("x", 5000)
l.serveCallWithin("mesh-controller", "conditions", nil, "_INBOX.operator.ABCDEFGHIJKLMNOPQRSTUV",
func(context.Context, json.RawMessage) (any, error) { return big, nil }, a.respond, 2048, nil)
got := a.only()
said, _ := got["error"].(string)
paged, _ := got["paged"].(map[string]any)
if !strings.Contains(said, "more than the bus carries") || paged == nil || paged["parts"].(float64) < 3 {
t.Fatalf("answered %v", got)
}
recent, _ := l.Recent()
if len(recent) != 1 || recent[0].Paged == "" || strings.Contains(string(recent[0].Answer), "in full") {
t.Fatalf("kept %+v", recent[0])
}
}
// An answer the bus refuses anyway is said to its caller and to `calls`, never only to the journal.
func TestAnAnswerTheBusRefusesIsSaid(t *testing.T) {
l := NewCallLog()
var sent [][]byte
respond := func(body []byte) error {
if len(body) > 1000 {
return nats.ErrMaxPayload
}
sent = append(sent, body)
return nil
}
l.serveCallWithin("mesh-controller", "status", nil, "_INBOX.x.1",
func(context.Context, json.RawMessage) (any, error) { return strings.Repeat("y", 4000), nil }, respond, 0, nil)
if len(sent) != 1 || !strings.Contains(string(sent[0]), "would not carry it") {
t.Fatalf("the caller was told %q", sent)
}
recent, _ := l.Recent()
if recent[0].Refused == "" {
t.Fatalf("calls does not say the answer was not sent: %+v", recent[0])
}
}
// A part goes only to the caller its answer was paged to, on the verb that answered it.
func TestAPartGoesOnlyToItsCaller(t *testing.T) {
conn, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
defer conn.Close()
l := NewCallLog()
c := l.begin("mesh-controller", "status", nil, "_INBOX.operator.ABCDEFGHIJKLMNOPQRSTUV")
l.page(c, []byte(strings.Repeat("z", 5000)), 2048)
sub, err := conn.Subscribe("pages.test", func(m *nats.Msg) { l.servePage("mesh-controller", "status", m, nil) })
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
_, err = askPart(context.Background(), conn, "pages.test", Paged{Call: c.ID, Parts: 3}, 0)
if err == nil || !strings.Contains(err.Error(), "another caller") {
t.Fatalf("a part was handed to a caller it was not paged to: %v", err)
}
_, err = askPart(context.Background(), conn, "pages.test", Paged{Call: "call-0-0", Parts: 3}, 0)
if err == nil || !strings.Contains(err.Error(), "not held here") {
t.Fatalf("a part of nothing held: %v", err)
}
}
// A part that cannot be had fails the whole answer, naming it — never a shorter answer.
func TestAMissingPartFailsTheWhole(t *testing.T) {
conn, err := nats.Connect(testbus.URL(t))
if err != nil {
t.Fatal(err)
}
defer conn.Close()
sub, err := conn.Subscribe("pages.none", func(m *nats.Msg) { _ = m.Respond([]byte(`{"error":"gone"}`)) })
if err != nil {
t.Fatal(err)
}
defer sub.Unsubscribe()
first, _ := json.Marshal(map[string]any{"error": "too large", "paged": Paged{Call: "call-1-1", Bytes: 10, Parts: 2}})
_, err = Whole(context.Background(), conn, "pages.none", first)
if err == nil || !strings.Contains(err.Error(), "part 0") || !strings.Contains(err.Error(), "gone") {
t.Fatalf("a missing part read as %v", err)
}
if plain, err := Whole(context.Background(), conn, "pages.none", []byte(`{"result":1}`)); err != nil ||
string(plain) != `{"result":1}` {
t.Fatalf("an answer that fits was changed: %s %v", plain, err)
}
}