package main // Tests of novox/hq issue 483: an issue whose fix is delivered stays in fixing, and a claim nobody works on is // never noticed. The issues the operator found (231, 402, 457, …) named their fix the way the live tracker keeps // it: a link of kind pull-request, by the worker, while the issue stayed in fixing, with fixed-by given only at // the resolve (issue-tracker.get 457, 2026-10-11). import ( "encoding/json" "strings" "testing" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) // asJSON is a verb's answer as the tool runner hands it on: what a caller reads, whatever the Go types are. func asJSON(t *testing.T, v any) map[string]any { t.Helper() raw, err := json.Marshal(v) if err != nil { t.Fatal(err) } var m map[string]any if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } return m } // linkPR links a pull request the way the live issues do: kind pull-request, target #. func linkPR(t *testing.T, h *harness, n uint64, target string) { t.Helper() if _, err := h.call(t, "link", map[string]any{"number": n, "kind": "pull-request", "target": target}); err != nil { t.Fatal(err) } } func rowsOf(t *testing.T, h *harness, args map[string]any) []map[string]any { t.Helper() v, err := h.call(t, "list", args) if err != nil { t.Fatal(err) } var out []map[string]any for _, r := range asJSON(t, v)["rows"].([]any) { out = append(out, r.(map[string]any)) } return out } // Part 1: when every pull request linked to an issue in fixing is delivered, the issue gains one note naming // them, when each was delivered, and its worker, and the event ready-to-verify names the worker. func TestALinkedFixDeliveredIsNotedOnceNamingItsWorker(t *testing.T) { h := newHarness(t) now := clocked(h) n := h.open(t, "a build fails during the registry's clean-up", "mesh-controller") if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-457@shanks"}); err != nil { t.Fatal(err) } linkPR(t, h, n, "mesh-catalog#225") linkPR(t, h, n, "mesh-catalog#226") active := get(t, h, n).Worker.Active merged := now.Add(-20 * time.Minute) transition(t, h, 225, "published", "", "abc", merged) if v, _ := h.View(n); v.Delivery != "fix merged, not delivered: mesh-catalog#225" { t.Fatalf("a linked pull request's delivery is not measured: %q", v.Delivery) } delivered := now.Add(-10 * time.Minute) transition(t, h, 225, "delivered", "", "abc", delivered) if countEvents(h, EventReadyToVerify) != 0 { t.Fatal("said delivered while mesh-catalog#226 is not") } transition(t, h, 226, "delivered", "", "def", delivered.Add(time.Minute)) transition(t, h, 226, "delivered", "", "def", delivered.Add(time.Minute)) is := get(t, h, n) var said []string for _, nt := range is.Notes { if strings.HasPrefix(nt.Text, "Ready to verify:") { said = append(said, nt.Text) } } if len(said) != 1 { t.Fatalf("want one note, got %q", said) } for _, want := range []string{"mesh-catalog#225", "mesh-catalog#226", delivered.Format(time.RFC3339), delivered.Add(time.Minute).Format(time.RFC3339), "fix-457@shanks"} { if !strings.Contains(said[0], want) { t.Fatalf("the note does not say %q: %s", want, said[0]) } } if is.Status != StatusFixing || is.Worker == nil || is.Worker.By != "fix-457@shanks" { t.Fatalf("the tracker's own note changed the issue's work: %+v", is) } var ev map[string]any for _, e := range h.said() { if e.Event == EventReadyToVerify { ev = e.Body } } if countEvents(h, EventReadyToVerify) != 1 || ev["worker"] != "fix-457@shanks" { t.Fatalf("%d ready-to-verify, last %v", countEvents(h, EventReadyToVerify), ev) } // The tracker's own note keeps no worker current: the worker's activity is still its own last act. if !is.Worker.Active.Equal(active) { t.Fatalf("the worker was made active by the tracker's note: %v", is.Worker.Active) } } // Part 2: past the bound, an issue in fixing whose every linked fix is delivered is warned on the operator's // channel, once, naming the issue and its worker; the warning clears when the issue leaves fixing. func TestDeliveredNotResolvedIsWarnedAfterTheBoundAndClearedWhenItLeavesFixing(t *testing.T) { h := newHarness(t) now := clocked(h) note := ¬ified{} h.Notify = note.notify n := h.open(t, "x", "mesh-catalog modules/records") if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-x@g14"}); err != nil { t.Fatal(err) } linkPR(t, h, n, "mesh-catalog#9") transition(t, h, 9, "delivered", "", "abc", *now) *now = now.Add(59 * time.Minute) if _, err := h.Sweep(); err != nil || len(note.said) != 0 { t.Fatalf("warned under the bound: %q %v", note.said, err) } if rows := rowsOf(t, h, map[string]any{"flag": "delivered-not-resolved"}); len(rows) != 1 || rows[0]["delivered-not-resolved"] != true || rows[0]["worker"] != "fix-x@g14" { t.Fatalf("list flag delivered-not-resolved: %v", rows) } *now = now.Add(2 * time.Minute) for i := 0; i < 2; i++ { if _, err := h.Sweep(); err != nil { t.Fatal(err) } } key := "issue-tracker.delivered-not-resolved.365" if len(note.said) != 1 || !strings.HasPrefix(note.said[0], key+" warning ") || !strings.Contains(note.said[0], "fixed and delivered, not resolved") || !strings.Contains(note.said[0], "fix-x@g14") || !strings.Contains(note.said[0], "365") { t.Fatalf("said %q", note.said) } if v := asJSON(t, mustView(t, h, n)); v["delivered-not-resolved"] != true { t.Fatalf("get does not say it: %v", v["delivered-not-resolved"]) } if _, err := h.Move(MoveAsk{Number: n, Status: StatusResolved, FixedBy: []string{"mesh-catalog#9"}}); err != nil { t.Fatal(err) } if err := h.OnMoved(envelopeOf(n)); err != nil { t.Fatal(err) } if len(note.clear) != 1 || note.clear[0] != key { t.Fatalf("not cleared when it left fixing: %q", note.clear) } if rows := rowsOf(t, h, map[string]any{"flag": "delivered-not-resolved"}); len(rows) != 0 { t.Fatalf("a resolved issue still listed: %v", rows) } // The setting's bound is read. h2 := newHarness(t) now2 := clocked(h2) n2 := ¬ified{} h2.Notify = n2.notify // Set as the operator's settings file gives it. if err := json.Unmarshal([]byte(`{"delivered-not-resolved-after": "10m"}`), &h2.settings); err != nil { t.Fatal(err) } m := h2.open(t, "y", "mesh-catalog modules/records") if _, err := h2.Move(MoveAsk{Number: m, Status: StatusFixing, By: "fix-y@g14"}); err != nil { t.Fatal(err) } linkPR(t, h2, m, "mesh-catalog#9") transition(t, h2, 9, "delivered", "", "abc", *now2) *now2 = now2.Add(11 * time.Minute) if _, err := h2.Sweep(); err != nil || len(n2.said) != 1 { t.Fatalf("the setting's bound was not read: %q %v", n2.said, err) } } // A warning the channel did not take is said at the next sweep, and an issue's own status is what clears it at // the sweep too, when the moved event was not heard. func TestDeliveredNotResolvedIsRetriedAndClearedAtTheSweep(t *testing.T) { h := newHarness(t) now := clocked(h) note := ¬ified{err: errStuck} h.Notify = note.notify n := h.open(t, "x", "mesh-catalog modules/records") if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-x@g14"}); err != nil { t.Fatal(err) } linkPR(t, h, n, "mesh-catalog#9") transition(t, h, 9, "delivered", "", "abc", *now) *now = now.Add(2 * time.Hour) _, _ = h.Sweep() note.err = nil _, _ = h.Sweep() if len(note.said) != 1 { t.Fatalf("not said at the next sweep: %q", note.said) } if _, err := h.Move(MoveAsk{Number: n, Status: StatusLocated}); err != nil { t.Fatal(err) } _, _ = h.Sweep() if len(note.clear) != 1 { t.Fatalf("not cleared at the sweep: %q", note.clear) } } // Part 3: an issue in fixing with no open linked pull request and nothing done on it for a day is an abandoned issue, // naming its worker; a new worker takes it over without a reason, and the tracker says it was abandoned. func TestAnAbandonedIssueIsFlaggedAndTakenOverWithoutAReason(t *testing.T) { h := newHarness(t) now := clocked(h) n := h.open(t, "claimed and left", "hq") if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "build-424@shanks"}); err != nil { t.Fatal(err) } busy := h.open(t, "claimed, with a pull request in review", "hq") if _, err := h.Move(MoveAsk{Number: busy, Status: StatusFixing, By: "fix-b@g14"}); err != nil { t.Fatal(err) } linkPR(t, h, busy, "mesh-catalog#9") transition(t, h, 9, "ready", "its merge", "", *now) *now = now.Add(23 * time.Hour) if rows := rowsOf(t, h, map[string]any{"flag": "abandoned"}); len(rows) != 0 { t.Fatalf("stale before the day: %v", rows) } *now = now.Add(2 * time.Hour) rows := rowsOf(t, h, map[string]any{"flag": "abandoned"}) if len(rows) != 1 || rows[0]["number"] != float64(n) || rows[0]["worker"] != "build-424@shanks" || rows[0]["abandoned"] != true { t.Fatalf("want only %d flagged naming its worker, got %v", n, rows) } if v := asJSON(t, mustView(t, h, n)); v["abandoned"] != true { t.Fatalf("get does not say it: %v", v) } if v := asJSON(t, mustView(t, h, busy)); v["abandoned"] == true { t.Fatal("an issue with an open pull request is flagged") } m, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-483@shanks"}) if err != nil { t.Fatalf("an abandoned issue was not taken over without a reason: %v", err) } if a := asJSON(t, m); a["abandoned"] != true || !strings.Contains(m.Said, "abandoned") { t.Fatalf("the answer does not say it was abandoned: %v", a) } is := get(t, h, n) last := is.Notes[len(is.Notes)-1].Text if !strings.Contains(last, "abandoned") || !strings.Contains(last, "build-424@shanks") { t.Fatalf("the note does not say it was abandoned: %s", last) } for _, e := range h.said() { if e.Event == EventMoved && e.Body["took-over-from"] == "build-424@shanks" && e.Body["abandoned"] != true { t.Fatalf("the moved event does not say it was abandoned: %v", e.Body) } } } // Part 4: list refuses a flag it does not know, naming the two it does. func TestListRefusesAnUnknownFlag(t *testing.T) { h := newHarness(t) _, err := h.call(t, "list", map[string]any{"flag": "stuck"}) if err == nil || !strings.Contains(err.Error(), "delivered-not-resolved") || !strings.Contains(err.Error(), "abandoned") { t.Fatalf("want refused naming the flags, got %v", err) } } // The sweep asks the delivery of a linked pull request it never heard of, a few per sweep, so an issue whose // pull request merged before it was linked (as the operator's thirteen did) is measured at all. func TestTheSweepAsksALinkedPullRequestNeverHeardOf(t *testing.T) { h := newHarness(t) now := clocked(h) n := h.open(t, "x", "mesh-catalog modules/records") if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-x@g14"}); err != nil { t.Fatal(err) } linkPR(t, h, n, "mesh-catalog#209") var asked []string h.Deliveries = func(repo, number string) (Delivery, error) { asked = append(asked, repo+"#"+number) return Delivery{State: "delivered", Since: now.Add(-3 * time.Hour)}, nil } if _, err := h.Sweep(); err != nil { t.Fatal(err) } if len(asked) != 1 || asked[0] != "mesh-catalog#209" { t.Fatalf("asked %q", asked) } if v, _ := h.View(n); v.Delivery != "delivered to every machine it should reach" || countEvents(h, EventReadyToVerify) != 1 { t.Fatalf("%q, %d ready-to-verify", v.Delivery, countEvents(h, EventReadyToVerify)) } if _, err := h.Sweep(); err != nil || len(asked) != 1 { t.Fatalf("asked again once heard: %q %v", asked, err) } } func envelopeOf(n uint64) stdio.Envelope { raw, _ := json.Marshal(map[string]any{"number": n}) return stdio.Envelope{Key: "mesh-issues.moved", Body: raw} } func mustView(t *testing.T, h *harness, n uint64) *View { t.Helper() v, err := h.View(n) if err != nil { t.Fatal(err) } return v } // A pull request closed without merging is withdrawn: mesh-delivery says it `stopped` with no merged_as (its // `closed` event). It is not a fix merged and stuck, not delivered, and not an open pull request: the operator is // never told it is merged and not delivered, and a worker with nothing else in review holds an abandoned issue. func TestAPullRequestClosedUnmergedIsWithdrawn(t *testing.T) { for _, via := range []string{"transition", "checks"} { t.Run(via, func(t *testing.T) { h := newHarness(t) now := clocked(h) note := ¬ified{} h.Notify = note.notify n := h.open(t, "x", "mesh-catalog modules/records") if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: "fix-x@g14"}); err != nil { t.Fatal(err) } linkPR(t, h, n, "mesh-catalog#9") closed := now.Add(-time.Hour) if via == "transition" { transition(t, h, 9, "ready", "its merge", "", closed.Add(-time.Hour)) transition(t, h, 9, "stopped", "the pull request was closed without merging", "", closed) } else { h.Deliveries = func(string, string) (Delivery, error) { return Delivery{State: "stopped", Why: "the pull request was closed without merging", Since: closed}, nil } } *now = now.Add(25 * time.Hour) for i := 0; i < 2; i++ { if _, err := h.Sweep(); err != nil { t.Fatal(err) } } if len(note.said) != 0 { t.Fatalf("a pull request closed unmerged was said to the operator: %q", note.said) } is := get(t, h, n) if len(is.Fixes) != 1 || is.Fixes[0].Merged || is.Fixes[0].MergedAt != nil { t.Fatalf("a pull request closed unmerged is kept as merged: %+v", is.Fixes) } v := asJSON(t, mustView(t, h, n)) if v["delivered-not-resolved"] == true || countEvents(h, EventReadyToVerify) != 0 { t.Fatalf("a withdrawn pull request reads as delivered: %v", v) } if v["abandoned"] != true { t.Fatalf("a withdrawn pull request reads as open, so the claim is not stale: %v", v) } if d, _ := v["delivery"].(string); !strings.HasPrefix(d, "fix withdrawn: mesh-catalog#9") { t.Fatalf("delivery %q", d) } // A second fix linked and delivered is all the issue waits for: the withdrawn one is no fix. linkPR(t, h, n, "mesh-catalog#10") transition(t, h, 10, "delivered", "", "abc", *now) if v := mustView(t, h, n); !v.DeliveredNotResolved || v.Delivery != "delivered to every machine it should reach" { t.Fatalf("%+v %q", v.DeliveredNotResolved, v.Delivery) } }) } } // One pull request linked to several issues is measured on each, and each is warned and cleared on its own. func TestOnePullRequestLinkedToSeveralIssues(t *testing.T) { h := newHarness(t) now := clocked(h) note := ¬ified{} h.Notify = note.notify var ns []uint64 for _, w := range []string{"fix-a@g14", "fix-b@shanks"} { n := h.open(t, "x "+w, "mesh-catalog modules/records") if _, err := h.Move(MoveAsk{Number: n, Status: StatusFixing, By: w}); err != nil { t.Fatal(err) } linkPR(t, h, n, "mesh-catalog#9") ns = append(ns, n) } transition(t, h, 9, "delivered", "", "abc", *now) if countEvents(h, EventReadyToVerify) != 2 { t.Fatalf("%d ready-to-verify", countEvents(h, EventReadyToVerify)) } *now = now.Add(2 * time.Hour) if _, err := h.Sweep(); err != nil { t.Fatal(err) } if len(note.said) != 2 || !strings.Contains(strings.Join(note.said, "\n"), "fix-a@g14") || !strings.Contains(strings.Join(note.said, "\n"), "fix-b@shanks") { t.Fatalf("said %q", note.said) } if _, err := h.Move(MoveAsk{Number: ns[0], Status: StatusResolved, FixedBy: []string{"mesh-catalog#9"}}); err != nil { t.Fatal(err) } if err := h.OnMoved(envelopeOf(ns[0])); err != nil { t.Fatal(err) } if _, err := h.Sweep(); err != nil { t.Fatal(err) } if len(note.clear) != 1 || note.clear[0] != notResolvedKeyOf(ns[0]) || len(note.said) != 2 { t.Fatalf("cleared %q, said %q", note.clear, note.said) } } func notResolvedKeyOf(n uint64) string { return "issue-tracker.delivered-not-resolved." + keyOf(n) }