The tracker derives a fix's delivered from the delivery view's event, not the gate's gone states (issue 1946)
This commit is contained in:
@@ -62,17 +62,17 @@ func TestALinkedFixDeliveredIsNotedOnceNamingItsWorker(t *testing.T) {
|
||||
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)
|
||||
transition(t, h, 225, "published", "", "abc1234", 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)
|
||||
transition(t, h, 225, "delivered", "", "abc1234", 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))
|
||||
transition(t, h, 226, "delivered", "", "def1234", delivered.Add(time.Minute))
|
||||
transition(t, h, 226, "delivered", "", "def1234", delivered.Add(time.Minute))
|
||||
is := get(t, h, n)
|
||||
var said []string
|
||||
for _, nt := range is.Notes {
|
||||
@@ -119,7 +119,7 @@ func TestDeliveredNotResolvedIsWarnedAfterTheBoundAndClearedWhenItLeavesFixing(t
|
||||
t.Fatal(err)
|
||||
}
|
||||
linkPR(t, h, n, "mesh-catalog#9")
|
||||
transition(t, h, 9, "delivered", "", "abc", *now)
|
||||
transition(t, h, 9, "delivered", "", "abc1234", *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)
|
||||
@@ -169,7 +169,7 @@ func TestDeliveredNotResolvedIsWarnedAfterTheBoundAndClearedWhenItLeavesFixing(t
|
||||
t.Fatal(err)
|
||||
}
|
||||
linkPR(t, h2, m, "mesh-catalog#9")
|
||||
transition(t, h2, 9, "delivered", "", "abc", *now2)
|
||||
transition(t, h2, 9, "delivered", "", "abc1234", *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)
|
||||
@@ -188,7 +188,7 @@ func TestDeliveredNotResolvedIsRetriedAndClearedAtTheSweep(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
linkPR(t, h, n, "mesh-catalog#9")
|
||||
transition(t, h, 9, "delivered", "", "abc", *now)
|
||||
transition(t, h, 9, "delivered", "", "abc1234", *now)
|
||||
*now = now.Add(2 * time.Hour)
|
||||
_, _ = h.Sweep()
|
||||
note.err = nil
|
||||
@@ -355,7 +355,7 @@ func TestAPullRequestClosedUnmergedIsWithdrawn(t *testing.T) {
|
||||
}
|
||||
// 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)
|
||||
transition(t, h, 10, "delivered", "", "abc1234", *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)
|
||||
}
|
||||
@@ -378,7 +378,7 @@ func TestOnePullRequestLinkedToSeveralIssues(t *testing.T) {
|
||||
linkPR(t, h, n, "mesh-catalog#9")
|
||||
ns = append(ns, n)
|
||||
}
|
||||
transition(t, h, 9, "delivered", "", "abc", *now)
|
||||
transition(t, h, 9, "delivered", "", "abc1234", *now)
|
||||
if countEvents(h, EventReadyToVerify) != 2 {
|
||||
t.Fatalf("%d ready-to-verify", countEvents(h, EventReadyToVerify))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
)
|
||||
|
||||
// R6-C (ADR 0539 §17): the gate keeps no state after the merge, so an issue's fix is delivered when the delivery view says its
|
||||
// source revision reached the machines (`mesh-delivery-view.delivered`: {revision, group, heads}), matched to the fix by the
|
||||
// group it merged in or by the commit its merge landed as. The same revision twice is once.
|
||||
|
||||
func mergedFix(t *testing.T, h *harness, number int, group, mergedAs string) {
|
||||
t.Helper()
|
||||
body := map[string]any{"repository": "novox/mesh-catalog", "number": number, "to": "merged", "merged_as": mergedAs,
|
||||
"at": time.Date(2026, 10, 24, 12, 0, 0, 0, time.UTC)}
|
||||
if group != "" {
|
||||
body["group"] = group
|
||||
}
|
||||
raw, _ := json.Marshal(body)
|
||||
if err := h.OnTransition(stdio.Envelope{Key: "mesh-delivery-gate.transition", Body: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func viewSays(t *testing.T, h *harness, revision int, group string, heads map[string]string) {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(map[string]any{"revision": revision, "group": group, "heads": heads})
|
||||
if err := h.OnDelivered(stdio.Envelope{Key: DeliveredEvent, Body: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func fixIssue(t *testing.T, h *harness, pr string) uint64 {
|
||||
t.Helper()
|
||||
n := h.open(t, "a fix to deliver "+pr)
|
||||
fixedAtFixing(t, h, n, pr)
|
||||
return n
|
||||
}
|
||||
|
||||
func TestAFixIsDeliveredWhenTheViewSaysTheGroupThatMovedTheSourceReachedTheMachines(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
in := fixIssue(t, h, "mesh-catalog#20")
|
||||
other := fixIssue(t, h, "mesh-catalog#21")
|
||||
mergedFix(t, h, 20, "feat/r6", "aaaaaaaaaaaa1111")
|
||||
mergedFix(t, h, 21, "feat/other", "bbbbbbbbbbbb2222")
|
||||
if v, _ := h.View(in); v.Delivery != "fix merged, not delivered: mesh-catalog#20" {
|
||||
t.Fatalf("before the view speaks: %q", v.Delivery)
|
||||
}
|
||||
viewSays(t, h, 7, "feat/r6", map[string]string{"novox/mesh-sdk": "cccccccccccc3333"})
|
||||
if v, _ := h.View(in); v.Delivery != "delivered to every machine it should reach" {
|
||||
t.Fatalf("the group's fix after the view spoke: %q", v.Delivery)
|
||||
}
|
||||
if v, _ := h.View(other); v.Delivery != "fix merged, not delivered: mesh-catalog#21" {
|
||||
t.Fatalf("a fix of another group was delivered by a revision that is not its own: %q", v.Delivery)
|
||||
}
|
||||
if n := countEvents(h, EventReadyToVerify); n != 1 {
|
||||
t.Fatalf("%d ready-to-verify, wanted one", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFixIsDeliveredWhenTheViewSaysTheHeadItsMergeLandedAs(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
n := fixIssue(t, h, "mesh-catalog#22")
|
||||
mergedFix(t, h, 22, "", "dddddddddddd4444eeee")
|
||||
viewSays(t, h, 8, "some-other-group", map[string]string{"novox/mesh-sdk": "dddddddddddd4444eeee"})
|
||||
if v, _ := h.View(n); v.Delivery != "fix merged, not delivered: mesh-catalog#22" {
|
||||
t.Fatalf("a head of another repository delivered it: %q", v.Delivery)
|
||||
}
|
||||
viewSays(t, h, 9, "some-other-group", map[string]string{"novox/mesh-catalog": "dddddddddddd"})
|
||||
if v, _ := h.View(n); v.Delivery != "delivered to every machine it should reach" {
|
||||
t.Fatalf("the head its merge landed as: %q", v.Delivery)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheSameRevisionTwiceIsOnceAndTheGatesOwnDeliveredIsNoWord(t *testing.T) {
|
||||
h := newHarness(t)
|
||||
n := fixIssue(t, h, "mesh-catalog#23")
|
||||
// The gate keeps no state after the merge: a transition to delivered, from an older gate, says nothing.
|
||||
raw, _ := json.Marshal(map[string]any{"repository": "novox/mesh-catalog", "number": 23, "to": "delivered", "merged_as": "ffffffffffff5555"})
|
||||
if err := h.OnTransition(stdio.Envelope{Key: "mesh-delivery-gate.transition", Body: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v, _ := h.View(n); v.Delivery != "nothing heard from the delivery of mesh-catalog#23" {
|
||||
t.Fatalf("the gate's delivered counted: %q", v.Delivery)
|
||||
}
|
||||
mergedFix(t, h, 23, "feat/x", "ffffffffffff5555")
|
||||
for range 2 {
|
||||
viewSays(t, h, 10, "feat/x", nil)
|
||||
}
|
||||
if is := get(t, h, n); len(is.Delivered) != 1 || countEvents(h, EventReadyToVerify) != 1 {
|
||||
t.Fatalf("delivered %v, %d ready-to-verify", is.Delivered, countEvents(h, EventReadyToVerify))
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
// Whether an issue's fix is delivered (novox/hq ADR 0281). The tracker hears mesh-delivery's `transition` event
|
||||
// for every pull request an issue's fixed-by names and keeps, per pull request, the state the delivery last said
|
||||
// — merged or not, held and why, covered by a later merge, delivered. From that it derives one line for `get` and
|
||||
// Whether an issue's fix is delivered (novox/hq ADR 0281, cut by ADR 0539 §17). The tracker hears the gate's `transition`
|
||||
// event for every pull request an issue's fixed-by names and keeps, per pull request, the state the gate last said — merged
|
||||
// or not, held and why, closed — and the delivery view's `delivered` event for what reached the machines: the gate keeps no
|
||||
// state after the merge, so a source revision's delivered state is the view's, said once per revision with the group that
|
||||
// moved it and the merged heads. From that it derives one line for `get` and
|
||||
// `list`: a measured fact, never set by a verb. Delivered is not resolved: when every fix of an issue not yet
|
||||
// resolved is delivered, the issue gains a note and the event `ready-to-verify`, and an agent proves its
|
||||
// acceptance live before resolving it. A fix merged and not delivered past a bound is said on the issue and to
|
||||
@@ -41,6 +43,11 @@ type Fix struct {
|
||||
// no module; Opened when its pull request was first heard (novox/hq ADR 0313 §2, §4).
|
||||
Class string `json:"class,omitempty"`
|
||||
Opened *time.Time `json:"opened,omitempty"`
|
||||
// Repository, Group and MergedAs are what the gate said of its merge, which the view's `delivered` event is matched to:
|
||||
// the repository (owner/name), the group that moved the source, and the commit the merge landed as.
|
||||
Repository string `json:"repository,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
MergedAs string `json:"merged-as,omitempty"`
|
||||
}
|
||||
|
||||
// heardFix is what the delivery said of a pull request at one moment: its state, why, whether it is merged, and
|
||||
@@ -54,6 +61,10 @@ type heardFix struct {
|
||||
Class string
|
||||
Opened *time.Time
|
||||
MergedAt *time.Time
|
||||
// Repository, Group and MergedAs: see Fix.
|
||||
Repository string
|
||||
Group string
|
||||
MergedAs string
|
||||
}
|
||||
|
||||
// news says whether what was heard adds anything to what a fix keeps.
|
||||
@@ -64,13 +75,15 @@ func (h heardFix) news(f *Fix, kept string) bool {
|
||||
if h.Opened != nil && (f.Opened == nil || !f.Opened.Equal(*h.Opened)) {
|
||||
return true
|
||||
}
|
||||
if h.Group != "" && f.Group != h.Group || h.MergedAs != "" && f.MergedAs != h.MergedAs || h.Repository != "" && f.Repository != h.Repository {
|
||||
return true
|
||||
}
|
||||
return h.MergedAt != nil && (f.MergedAt == nil || !f.MergedAt.Equal(*h.MergedAt))
|
||||
}
|
||||
|
||||
// reached says whether a delivery's state says the change reached the machines (mesh-delivery's Reached).
|
||||
func reached(state string) bool {
|
||||
return state == "delivered" || state == "covered" || state == "retired"
|
||||
}
|
||||
// reached says whether a fix's state says the change reached the machines: only the view's `delivered` event says it
|
||||
// (OnDelivered); the gate's states end at the merge.
|
||||
func reached(state string) bool { return state == "delivered" }
|
||||
|
||||
// Whether a pull request is merged is what the delivery says of the pull request itself — `merged_as` on a
|
||||
// transition, the pull request's `merged` in checks — never read from the state: `stopped` is also what a pull
|
||||
@@ -79,7 +92,9 @@ func reached(state string) bool {
|
||||
|
||||
// withdrawn says whether a fix is a pull request closed without merging: no fix at all, neither awaiting delivery,
|
||||
// delivered, nor open.
|
||||
func withdrawn(f *Fix) bool { return f != nil && f.State == "stopped" && !f.Merged }
|
||||
func withdrawn(f *Fix) bool {
|
||||
return f != nil && (f.State == "closed" || f.State == "stopped") && !f.Merged
|
||||
}
|
||||
|
||||
// liveFixes is an issue's fixes less those withdrawn: what its delivery, readiness to verify, the stuck sweep and a
|
||||
// abandoned issue are measured on. fixPullRequests still names the withdrawn ones, so a pull request reopened is heard.
|
||||
@@ -138,17 +153,15 @@ func DeliveryLine(is *Issue) string {
|
||||
return ""
|
||||
}
|
||||
var held, failed, merged, open, unheard []string
|
||||
covered := false
|
||||
for _, pr := range prs {
|
||||
f := fixOf(is, pr)
|
||||
switch {
|
||||
case contains(is.Delivered, pr):
|
||||
covered = covered || (f != nil && f.State == "covered")
|
||||
case f == nil:
|
||||
unheard = append(unheard, pr)
|
||||
case f.State == "held":
|
||||
held = append(held, strings.TrimSpace(pr+": "+f.Why))
|
||||
case f.State == "failed" || f.State == "stopped":
|
||||
case f.State == "failed" || f.State == "stopped" || f.State == "closed":
|
||||
failed = append(failed, strings.TrimSpace(pr+" "+f.State+": "+f.Why))
|
||||
case f.Merged:
|
||||
merged = append(merged, pr)
|
||||
@@ -167,8 +180,6 @@ func DeliveryLine(is *Issue) string {
|
||||
return "fix not merged: " + strings.Join(open, ", ")
|
||||
case len(unheard) > 0:
|
||||
return "nothing heard from the delivery of " + strings.Join(unheard, ", ")
|
||||
case covered:
|
||||
return "delivered to every machine it should reach (by a later merge that contains it)"
|
||||
}
|
||||
return "delivered to every machine it should reach"
|
||||
}
|
||||
@@ -215,6 +226,15 @@ func (t *Tracker) recordFix(pr string, h heardFix, only uint64) ([]uint64, error
|
||||
if h.Class != "" {
|
||||
f.Class = h.Class
|
||||
}
|
||||
if h.Repository != "" {
|
||||
f.Repository = h.Repository
|
||||
}
|
||||
if h.Group != "" {
|
||||
f.Group = h.Group
|
||||
}
|
||||
if h.MergedAs != "" {
|
||||
f.MergedAs = h.MergedAs
|
||||
}
|
||||
if h.Opened != nil {
|
||||
o := h.Opened.UTC()
|
||||
f.Opened = &o
|
||||
@@ -374,17 +394,22 @@ func (t *Tracker) OnTransition(e stdio.Envelope) error {
|
||||
Class string `json:"class"`
|
||||
Opened *time.Time `json:"opened"`
|
||||
MergedAt *time.Time `json:"merged_at"`
|
||||
Group string `json:"group"`
|
||||
}
|
||||
if err := json.Unmarshal(e.Body, &body); err != nil || body.To == "" || body.Number <= 0 {
|
||||
return nil
|
||||
}
|
||||
if body.To == "delivered" {
|
||||
return nil // the gate keeps no state after the merge; what reached the machines is the view's (OnDelivered)
|
||||
}
|
||||
at := body.At.UTC()
|
||||
if at.IsZero() {
|
||||
at = t.now()
|
||||
}
|
||||
merged := body.MergedAs != ""
|
||||
changed, err := t.recordFix(prKey(body.Repository, body.Number), heardFix{State: body.To, Why: body.Why, Merged: merged,
|
||||
At: at, Class: body.Class, Opened: nonZero(body.Opened), MergedAt: nonZero(body.MergedAt)}, 0)
|
||||
At: at, Class: body.Class, Opened: nonZero(body.Opened), MergedAt: nonZero(body.MergedAt),
|
||||
Repository: strings.ToLower(body.Repository), Group: body.Group, MergedAs: body.MergedAs}, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -396,6 +421,90 @@ func (t *Tracker) OnTransition(e stdio.Envelope) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeliveredEvent is the view's `delivered` (mesh-delivery-view, ADR 0539 §17 and design 47 R6): said once per source revision
|
||||
// when every want, have and assignment is at the fingerprint and healthy.
|
||||
const DeliveredEvent = "mesh-delivery-view.delivered"
|
||||
|
||||
// OnDelivered is the handler of the view's `delivered` event: {revision, group, heads}. Every merged fix of a pull request the
|
||||
// group moved, or whose merge is the head the revision holds for its repository, reached the machines at that revision. The same
|
||||
// revision twice changes nothing (a delivered fix stays delivered), and a fix is matched by the group it merged in or by its own
|
||||
// merge commit, so a revision that carries a later head of a repository does not cover an earlier merge the tracker never heard
|
||||
// delivered: that waits for its own revision's event.
|
||||
func (t *Tracker) OnDelivered(e stdio.Envelope) error {
|
||||
var body struct {
|
||||
Revision uint64 `json:"revision"`
|
||||
Group string `json:"group"`
|
||||
Heads map[string]string `json:"heads"`
|
||||
// At is when the view judged the revision delivered, when it says so; else this tracker's moment of hearing it.
|
||||
At time.Time `json:"at"`
|
||||
}
|
||||
if err := json.Unmarshal(e.Body, &body); err != nil || (body.Group == "" && len(body.Heads) == 0) {
|
||||
return nil
|
||||
}
|
||||
all, err := t.allIssues()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
at := body.At.UTC()
|
||||
if at.IsZero() {
|
||||
at = t.now()
|
||||
}
|
||||
var changed []uint64
|
||||
for n, is := range all {
|
||||
for _, pr := range liveFixes(is) {
|
||||
f := fixOf(is, pr)
|
||||
if f == nil || !f.Merged || contains(is.Delivered, pr) || !deliveredBy(f, body.Group, body.Heads) {
|
||||
continue
|
||||
}
|
||||
pr := pr
|
||||
if _, err := t.change(n, Act{Op: "delivery", By: Seat}, func(i *Issue) error {
|
||||
g := fixOf(i, pr)
|
||||
if g == nil || contains(i.Delivered, pr) {
|
||||
return nil
|
||||
}
|
||||
g.State, g.Why, g.Since = "delivered", fmt.Sprintf("source revision %d reached the machines", body.Revision), at
|
||||
i.Delivered = append(i.Delivered, pr)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(changed) == 0 || changed[len(changed)-1] != n {
|
||||
changed = append(changed, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(changed, func(i, j int) bool { return changed[i] < changed[j] })
|
||||
for _, n := range changed {
|
||||
if err := t.afterFix(n); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deliveredBy says whether a merged fix is one a delivered revision names: the group it merged in is the group that moved the
|
||||
// source, or its merge commit is the head the revision holds for its repository.
|
||||
func deliveredBy(f *Fix, group string, heads map[string]string) bool {
|
||||
if group != "" && f.Group == group {
|
||||
return true
|
||||
}
|
||||
for repository, head := range heads {
|
||||
if f.MergedAs != "" && strings.EqualFold(repository, f.Repository) && sameCommitIn(f.MergedAs, head) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// sameCommitIn is whether two commits are one, however either is abbreviated (at least seven characters).
|
||||
func sameCommitIn(a, b string) bool {
|
||||
a, b = strings.ToLower(a), strings.ToLower(b)
|
||||
if len(a) > len(b) {
|
||||
a, b = b, a
|
||||
}
|
||||
return len(a) >= 7 && strings.HasPrefix(b, a)
|
||||
}
|
||||
|
||||
// Delivery is a pull request's delivery as mesh-delivery's `checks` answers it; a test stubs the asking.
|
||||
type Delivery struct {
|
||||
State string
|
||||
|
||||
@@ -52,6 +52,19 @@ func transition(t *testing.T, h *harness, number int, to, why, mergedAs string,
|
||||
// transitionOn is a delivery's transition said under an event's name.
|
||||
func transitionOn(t *testing.T, h *harness, event string, number int, to, why, mergedAs string, at time.Time) {
|
||||
t.Helper()
|
||||
if to == "delivered" || to == "covered" || to == "retired" {
|
||||
// The gate keeps no state after the merge (ADR 0539 §17): what reached the machines is the view's `delivered`, matched
|
||||
// to the fix by the commit its merge landed as. The fix is first heard merged, as it is in the real flow.
|
||||
if mergedAs == "" {
|
||||
mergedAs = "abc1234"
|
||||
}
|
||||
transitionOn(t, h, event, number, "merged", why, mergedAs, at)
|
||||
raw, _ := json.Marshal(map[string]any{"revision": 1, "heads": map[string]string{"novox/mesh-catalog": mergedAs}, "at": at})
|
||||
if err := h.OnDelivered(stdio.Envelope{Key: DeliveredEvent, Body: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
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: event, Body: raw}); err != nil {
|
||||
@@ -93,11 +106,11 @@ func TestAnIssueKnowsWhetherItsFixIsDelivered(t *testing.T) {
|
||||
if got := line(); got != "fix not merged: mesh-catalog#9" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
transition(t, h, 9, "published", "", "abc", at)
|
||||
transition(t, h, 9, "published", "", "abc1234", 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)
|
||||
transition(t, h, 9, "held", "dnsmasq waits for a person's push on two machines", "abc1234", at)
|
||||
if got := line(); got != "held: mesh-catalog#9: dnsmasq waits for a person's push on two machines" {
|
||||
t.Fatal(got)
|
||||
}
|
||||
@@ -106,7 +119,7 @@ func TestAnIssueKnowsWhetherItsFixIsDelivered(t *testing.T) {
|
||||
t.Fatalf("%+v", rows.Rows[0])
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
transition(t, h, 9, "delivered", "", "abc", at)
|
||||
transition(t, h, 9, "delivered", "", "abc1234", at)
|
||||
}
|
||||
if got := line(); got != "delivered to every machine it should reach" {
|
||||
t.Fatal(got)
|
||||
@@ -124,15 +137,6 @@ func TestAnIssueKnowsWhetherItsFixIsDelivered(t *testing.T) {
|
||||
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
|
||||
@@ -144,7 +148,7 @@ func TestAStuckFixIsSaidOnceAndClearedWhenDelivered(t *testing.T) {
|
||||
note := ¬ified{}
|
||||
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)
|
||||
transition(t, h, 9, "held", "waits for a person", "abc1234", merged)
|
||||
note.err = errStuck
|
||||
if said, err := h.Sweep(); err != nil || said != 0 {
|
||||
t.Fatalf("%d %v", said, err)
|
||||
@@ -164,7 +168,7 @@ func TestAStuckFixIsSaidOnceAndClearedWhenDelivered(t *testing.T) {
|
||||
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)
|
||||
transition(t, h, 9, "delivered", "", "abc1234", merged)
|
||||
if len(note.clear) != 1 {
|
||||
t.Fatalf("not cleared: %q", note.clear)
|
||||
}
|
||||
@@ -174,7 +178,7 @@ func TestAStuckFixIsSaidOnceAndClearedWhenDelivered(t *testing.T) {
|
||||
fixedAtFixing(t, h2, m, "mesh-catalog#9")
|
||||
n2 := ¬ified{}
|
||||
h2.Notify = n2.notify
|
||||
transition(t, h2, 9, "delivering", "", "abc", time.Date(2026, 10, 10, 11, 30, 0, 0, time.UTC))
|
||||
transition(t, h2, 9, "delivering", "", "abc1234", 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")
|
||||
}
|
||||
@@ -218,13 +222,13 @@ func TestDeliveredStaysDeliveredAndClearsAreRetried(t *testing.T) {
|
||||
note := ¬ified{}
|
||||
h.Notify = note.notify
|
||||
merged := time.Date(2026, 10, 10, 9, 0, 0, 0, time.UTC)
|
||||
transition(t, h, 9, "held", "waits", "abc", merged)
|
||||
transition(t, h, 9, "held", "waits", "abc1234", 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
|
||||
transition(t, h, 9, "delivered", "", "abc1234", merged.Add(time.Hour))
|
||||
transition(t, h, 9, "held", "an older word", "abc1234", 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)
|
||||
}
|
||||
@@ -243,7 +247,7 @@ func TestDeliveredStaysDeliveredAndClearsAreRetried(t *testing.T) {
|
||||
}
|
||||
n2 := ¬ified{}
|
||||
h2.Notify = n2.notify
|
||||
transition(t, h2, 9, "held", "waits", "abc", merged)
|
||||
transition(t, h2, 9, "held", "waits", "abc1234", merged)
|
||||
_, _ = h2.Sweep()
|
||||
if _, err := h2.Move(MoveAsk{Number: m, Status: StatusLocated, FixedBy: []string{"mesh-catalog#10"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -284,7 +288,7 @@ func TestAResolvedIssueEndsItsStuckWarningAndIsNeverSaidStuck(t *testing.T) {
|
||||
fixedAtFixing(t, h, n, "mesh-catalog#9")
|
||||
note := ¬ified{}
|
||||
h.Notify = note.notify
|
||||
transition(t, h, 9, "failed", "the gate failed on one machine", "abc", merged)
|
||||
transition(t, h, 9, "failed", "the gate failed on one machine", "abc1234", merged)
|
||||
if said, err := h.Sweep(); err != nil || said != 1 || len(note.said) != 1 {
|
||||
t.Fatalf("not said stuck: %d %v %q", said, err, note.said)
|
||||
}
|
||||
@@ -314,7 +318,7 @@ func TestAResolvedIssueEndsItsStuckWarningAndIsNeverSaidStuck(t *testing.T) {
|
||||
fixedAtFixing(t, h, n, "mesh-catalog#9")
|
||||
note := ¬ified{err: errStuck}
|
||||
h.Notify = note.notify
|
||||
transition(t, h, 9, "failed", "the gate failed", "abc", merged)
|
||||
transition(t, h, 9, "failed", "the gate failed", "abc1234", merged)
|
||||
if _, err := h.change(n, Act{Op: "warned", By: Seat}, func(i *Issue) error {
|
||||
i.Fixes[0].StuckSaid, i.StuckOpen = true, true
|
||||
return nil
|
||||
@@ -331,7 +335,7 @@ func TestAResolvedIssueEndsItsStuckWarningAndIsNeverSaidStuck(t *testing.T) {
|
||||
}
|
||||
m := h.open(t, "y")
|
||||
fixedAtFixing(t, h, m, "mesh-catalog#10")
|
||||
transition(t, h, 10, "failed", "the gate failed", "abc", merged)
|
||||
transition(t, h, 10, "failed", "the gate failed", "abc1234", merged)
|
||||
if _, err := h.Move(MoveAsk{Number: m, Status: StatusResolved, FixedBy: []string{"mesh-catalog#10"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -38,10 +38,17 @@ func (c *fixClock) later(d time.Duration) time.Time { c.at = c.at.Add(d); return
|
||||
// delivery says one transition of a pull request as mesh-delivery says it, with what a fix's time is read by.
|
||||
func (c *fixClock) delivery(t *testing.T, number int, to, class string, opened, mergedAt *time.Time) {
|
||||
t.Helper()
|
||||
delivered := to == "delivered"
|
||||
if delivered {
|
||||
// What reached the machines is the view's `delivered` (ADR 0539 §17): the fix is heard merged, then delivered.
|
||||
to = "merged"
|
||||
}
|
||||
body := map[string]any{"repository": "novox/mesh-catalog", "number": number, "to": to, "at": c.at}
|
||||
if mergedAt != nil {
|
||||
if mergedAt != nil || delivered {
|
||||
body["merged_as"] = "m" + strings.Repeat("a", 11)
|
||||
body["merged_at"] = *mergedAt
|
||||
if mergedAt != nil {
|
||||
body["merged_at"] = *mergedAt
|
||||
}
|
||||
}
|
||||
if class != "" {
|
||||
body["class"] = class
|
||||
@@ -53,6 +60,12 @@ func (c *fixClock) delivery(t *testing.T, number int, to, class string, opened,
|
||||
if err := c.OnTransition(stdio.Envelope{Key: "mesh-delivery-gate.transition", Body: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if delivered {
|
||||
view, _ := json.Marshal(map[string]any{"revision": number, "heads": map[string]string{"novox/mesh-catalog": "m" + strings.Repeat("a", 11)}, "at": c.at})
|
||||
if err := c.OnDelivered(stdio.Envelope{Key: DeliveredEvent, Body: view}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func at(t time.Time) *time.Time { return &t }
|
||||
|
||||
@@ -161,6 +161,7 @@ func hearOpened(t *Tracker) {
|
||||
for _, event := range transitionEvents {
|
||||
hearings = append(hearings, hearing{event, t.OnTransition})
|
||||
}
|
||||
hearings = append(hearings, hearing{DeliveredEvent, t.OnDelivered})
|
||||
for _, s := range hearings {
|
||||
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
|
||||
time.Sleep(wait)
|
||||
|
||||
@@ -110,7 +110,7 @@ func TestItDeclaresAndHoldsTheIssueTrackerSeatOnceForTheMesh(t *testing.T) {
|
||||
}
|
||||
// The duplicate check hears the tracker's own opens and asks the agent module on its node (ADR 0278 §1, §5);
|
||||
// the import and a reservation read the record through the records module (issue 424).
|
||||
if !reflect.DeepEqual(m.Consumes, []string{"mesh-issues." + EventOpened, "mesh-issues." + EventMoved, "mesh-delivery-gate.transition"}) ||
|
||||
if !reflect.DeepEqual(m.Consumes, []string{"mesh-issues." + EventOpened, "mesh-issues." + EventMoved, "mesh-delivery-gate.transition", DeliveredEvent}) ||
|
||||
!reflect.DeepEqual(m.Invokes, []string{"claude-code.claude_code_judge", "seat:mesh-delivery.checks", "seat:operator-channel.notify",
|
||||
"gitea.gitea_pull_reviews", "records.records_status", "records.records_list", "records.records_read", "mesh-knowledge.knowledge_highest"}) {
|
||||
t.Fatalf("consumes %v, invokes %v", m.Consumes, m.Invokes)
|
||||
|
||||
@@ -472,11 +472,8 @@ func TestAnIssueIsReadyOnlyWhenWhatItDependsOnIsDelivered(t *testing.T) {
|
||||
if asked != 1 || ready(h, w) {
|
||||
t.Fatalf("ready before its fix is delivered (asked %d)", asked)
|
||||
}
|
||||
tr := stdio.Envelope{Key: "mesh-delivery-gate.transition", Body: json.RawMessage(`{"repository":"novox/mesh-catalog","number":9,"to":"delivered"}`)}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := h.OnTransition(tr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deliverNine(t, h)
|
||||
}
|
||||
if !ready(h, w) || !reflect.DeepEqual(get(t, h, dep).Delivered, []string{"mesh-catalog#9"}) {
|
||||
t.Fatal("not ready once delivered")
|
||||
@@ -771,10 +768,7 @@ func TestReadyIsSaidOnceFollowsDuplicatesAndAskingFailsLoudly(t *testing.T) {
|
||||
if got := pullRequests(get(t, h, b)); !reflect.DeepEqual(got, []string{"mesh-catalog#9"}) {
|
||||
t.Fatalf("%v", got)
|
||||
}
|
||||
tr := stdio.Envelope{Body: json.RawMessage(`{"repository":"novox/mesh-catalog","number":9,"to":"delivered"}`)}
|
||||
if err := h.OnTransition(tr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deliverNine(t, h)
|
||||
readies := 0
|
||||
for _, e := range h.said() {
|
||||
if e.Event == EventReady {
|
||||
@@ -824,3 +818,17 @@ func TestAVerdictNamingIssueZeroIsNone(t *testing.T) {
|
||||
t.Fatalf("%+v", is)
|
||||
}
|
||||
}
|
||||
|
||||
// deliverNine says pull request 9 of mesh-catalog merged, then delivered by the view's event (ADR 0539 §17).
|
||||
func deliverNine(t *testing.T, h *harness) {
|
||||
t.Helper()
|
||||
merged := stdio.Envelope{Key: "mesh-delivery-gate.transition", Body: json.RawMessage(
|
||||
`{"repository":"novox/mesh-catalog","number":9,"to":"merged","merged_as":"abc1234abc"}`)}
|
||||
if err := h.OnTransition(merged); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
view := stdio.Envelope{Key: DeliveredEvent, Body: json.RawMessage(`{"revision":3,"heads":{"novox/mesh-catalog":"abc1234abc"}}`)}
|
||||
if err := h.OnDelivered(view); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"consumes": [
|
||||
"mesh-issues.opened",
|
||||
"mesh-issues.moved",
|
||||
"mesh-delivery-gate.transition"
|
||||
"mesh-delivery-gate.transition",
|
||||
"mesh-delivery-view.delivered"
|
||||
],
|
||||
"invokes": [
|
||||
"claude-code.claude_code_judge",
|
||||
|
||||
Reference in New Issue
Block a user