Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/delivery_test.go
T
jochen 01bde68f96
mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
mesh-issues: an issue knows whether its fix is delivered, says it ready to verify, and a fix stuck after its merge is said (hq ADR 0281)
The operator approved measuring from the delivery's own events whether each fix an
issue names reached the machines; delivered is not resolved, and a merged fix that
does not arrive is said on the issue and the operator's channel instead of found.
2026-10-10 18:12:52 +02:00

247 lines
8.3 KiB
Go

package main
import (
"encoding/json"
"strings"
"sync"
"testing"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
type notified struct {
mu sync.Mutex
said []string
clear []string
err error
}
func (n *notified) notify(key, severity, summary, headline, explanation, resolved string, clear bool) error {
n.mu.Lock()
defer n.mu.Unlock()
if n.err != nil {
return n.err
}
if clear {
n.clear = append(n.clear, key)
} else {
n.said = append(n.said, key+" "+severity+" "+summary)
}
return nil
}
func transition(t *testing.T, h *harness, number int, to, why, mergedAs string, at time.Time) {
t.Helper()
raw, _ := json.Marshal(map[string]any{"repository": "novox/mesh-catalog", "number": number, "to": to, "why": why,
"merged_as": mergedAs, "at": at})
if err := h.OnTransition(stdio.Envelope{Key: "mesh-delivery.transition", Body: raw}); err != nil {
t.Fatal(err)
}
}
func countEvents(h *harness, event string) int {
n := 0
for _, e := range h.said() {
if e.Event == event {
n++
}
}
return n
}
// The delivery line follows what the delivery says, as a measured fact: not merged, merged, held and why,
// delivered; an issue not yet resolved is said ready to verify once, and stays unresolved.
func TestAnIssueKnowsWhetherItsFixIsDelivered(t *testing.T) {
h := newHarness(t)
n := h.open(t, "the counter skips a number")
if _, err := h.Move(MoveAsk{Number: n, Status: StatusLocated, LocatedIn: []string{"mesh-catalog modules/records"},
FixedBy: []string{"mesh-catalog#9"}}); err != nil {
t.Fatal(err)
}
line := func() string {
v, err := h.View(n)
if err != nil {
t.Fatal(err)
}
return v.Delivery
}
if got := line(); got != "nothing heard from the delivery of mesh-catalog#9" {
t.Fatal(got)
}
at := time.Date(2026, 10, 10, 12, 0, 0, 0, time.UTC)
transition(t, h, 9, "ready", "", "", at)
if got := line(); got != "fix not merged: mesh-catalog#9" {
t.Fatal(got)
}
transition(t, h, 9, "published", "", "abc", at)
if got := line(); got != "fix merged, not delivered: mesh-catalog#9" {
t.Fatal(got)
}
transition(t, h, 9, "held", "dnsmasq waits for a person's push on two machines", "abc", at)
if got := line(); got != "held: mesh-catalog#9: dnsmasq waits for a person's push on two machines" {
t.Fatal(got)
}
rows, _ := h.List(ListAsk{})
if rows.Rows[0].Delivery != line() {
t.Fatalf("%+v", rows.Rows[0])
}
for i := 0; i < 2; i++ {
transition(t, h, 9, "delivered", "", "abc", at)
}
if got := line(); got != "delivered to every machine it should reach" {
t.Fatal(got)
}
is := get(t, h, n)
if is.Status != StatusLocated || countEvents(h, EventReadyToVerify) != 1 {
t.Fatalf("%s, %d ready-to-verify", is.Status, countEvents(h, EventReadyToVerify))
}
var verify []string
for _, nt := range is.Notes {
if strings.HasPrefix(nt.Text, "Ready to verify:") {
verify = append(verify, nt.Text)
}
}
if len(verify) != 1 || !strings.Contains(verify[0], "Delivered is not resolved") {
t.Fatalf("%q", verify)
}
// Covered by a later merge counts as delivered, and says so; a resolved issue is not said ready to verify.
h2 := newHarness(t)
m := h2.open(t, "x")
resolve(t, h2, m, "mesh-catalog#9")
transition(t, h2, 9, "covered", "", "abc", at)
if v, _ := h2.View(m); !strings.HasPrefix(v.Delivery, "delivered to every machine it should reach (by a later merge") ||
countEvents(h2, EventReadyToVerify) != 0 {
t.Fatalf("%q", v.Delivery)
}
}
// A fix merged and not delivered past the bound is said once on the issue and the operator's channel, and
// cleared there when it is delivered; one that fails to be said is said at the next sweep.
func TestAStuckFixIsSaidOnceAndClearedWhenDelivered(t *testing.T) {
h := newHarness(t)
n := h.open(t, "the counter skips a number")
resolve(t, h, n, "mesh-catalog#9")
note := &notified{}
h.Notify = note.notify
merged := time.Date(2026, 10, 10, 9, 0, 0, 0, time.UTC) // three hours before the harness's clock
transition(t, h, 9, "held", "waits for a person", "abc", merged)
note.err = errStuck
if said, err := h.Sweep(); err != nil || said != 0 {
t.Fatalf("%d %v", said, err)
}
note.err = nil
for i := 0; i < 2; i++ {
if _, err := h.Sweep(); err != nil {
t.Fatal(err)
}
}
stuck := 0
for _, nt := range get(t, h, n).Notes {
if strings.HasPrefix(nt.Text, "Stuck: the fix mesh-catalog#9") {
stuck++
}
}
if stuck != 1 || len(note.said) != 1 || !strings.HasPrefix(note.said[0], "issue-tracker.fix-stuck.365 warning") {
t.Fatalf("%d notes, said %q", stuck, note.said)
}
transition(t, h, 9, "delivered", "", "abc", merged)
if len(note.clear) != 1 {
t.Fatalf("not cleared: %q", note.clear)
}
// Under the bound nothing is said.
h2 := newHarness(t)
m := h2.open(t, "y")
resolve(t, h2, m, "mesh-catalog#9")
n2 := &notified{}
h2.Notify = n2.notify
transition(t, h2, 9, "delivering", "", "abc", time.Date(2026, 10, 10, 11, 30, 0, 0, time.UTC))
if said, _ := h2.Sweep(); said != 0 || len(n2.said) != 0 {
t.Fatal("said stuck under the bound")
}
h2.settings.StuckAfter = "10m"
if said, _ := h2.Sweep(); said != 1 {
t.Fatal("the setting's bound was not read")
}
}
var errStuck = stdioErr("the operator's channel did not answer")
type stdioErr string
func (e stdioErr) Error() string { return string(e) }
// A fix is named while the issue stays located: a move to the same status changes only what it holds.
func TestAFixIsNamedWhileTheIssueStaysLocated(t *testing.T) {
h := newHarness(t)
n := h.open(t, "x")
if _, err := h.Move(MoveAsk{Number: n, Status: StatusLocated, LocatedIn: []string{"mesh-catalog modules/records"}}); err != nil {
t.Fatal(err)
}
if _, err := h.Move(MoveAsk{Number: n, Status: StatusLocated}); err == nil {
t.Fatal("a move that changes nothing was taken")
}
if _, err := h.Move(MoveAsk{Number: n, Status: StatusLocated, FixedBy: []string{"mesh-catalog#9"}}); err != nil {
t.Fatal(err)
}
if is := get(t, h, n); is.Status != StatusLocated || is.Closed != nil || len(is.FixedBy) != 1 {
t.Fatalf("%+v", is)
}
}
// Transitions out of order never take a delivered fix back, and a delivered fix is never said stuck; a clear
// that fails is tried again; a fix no longer named is not swept; an empty answer from the delivery is asked again.
func TestDeliveredStaysDeliveredAndClearsAreRetried(t *testing.T) {
h := newHarness(t)
n := h.open(t, "x")
resolve(t, h, n, "mesh-catalog#9")
note := &notified{}
h.Notify = note.notify
merged := time.Date(2026, 10, 10, 9, 0, 0, 0, time.UTC)
transition(t, h, 9, "held", "waits", "abc", merged)
if said, _ := h.Sweep(); said != 1 {
t.Fatal("not said stuck")
}
note.err = errStuck
transition(t, h, 9, "delivered", "", "abc", merged.Add(time.Hour))
transition(t, h, 9, "held", "an older word", "abc", merged.Add(30*time.Minute)) // out of order
if v, _ := h.View(n); v.Delivery != "delivered to every machine it should reach" {
t.Fatal(v.Delivery)
}
if len(note.clear) != 0 || !get(t, h, n).StuckOpen {
t.Fatal("cleared while the channel failed")
}
note.err = nil
if said, _ := h.Sweep(); said != 0 || len(note.clear) != 1 || get(t, h, n).StuckOpen {
t.Fatalf("said %d, cleared %q", said, note.clear)
}
// A fix no longer named is not swept, and its warning is cleared.
h2 := newHarness(t)
m := h2.open(t, "y")
if _, err := h2.Move(MoveAsk{Number: m, Status: StatusLocated, LocatedIn: []string{"mesh-catalog modules/records"}, FixedBy: []string{"mesh-catalog#9"}}); err != nil {
t.Fatal(err)
}
n2 := &notified{}
h2.Notify = n2.notify
transition(t, h2, 9, "held", "waits", "abc", merged)
_, _ = h2.Sweep()
if _, err := h2.Move(MoveAsk{Number: m, Status: StatusLocated, FixedBy: []string{"mesh-catalog#10"}}); err != nil {
t.Fatal(err)
}
if said, _ := h2.Sweep(); said != 0 || len(n2.said) != 1 || len(n2.clear) != 1 {
t.Fatalf("said %d %q, cleared %q", said, n2.said, n2.clear)
}
if _, err := h2.Move(MoveAsk{Number: m, Status: StatusLocated, FixedBy: []string{"mesh-catalog#10"}}); err == nil {
t.Fatal("a move that changes nothing was taken")
}
// The delivery knows nothing yet: not kept, asked again at the next move.
asks := 0
h2.Deliveries = func(string, string) (Delivery, error) { asks++; return Delivery{}, nil }
for i := 0; i < 2; i++ {
_ = h2.OnMoved(stdio.Envelope{Body: json.RawMessage(`{"number":365}`)})
}
if v, _ := h2.View(m); asks != 2 || v.Delivery != "nothing heard from the delivery of mesh-catalog#10" {
t.Fatalf("%d asks, %q", asks, v.Delivery)
}
}