Say a first merge that waits past ten minutes for mesh-delivery's word, with probe D17 (issue 1146, AC8)

This commit is contained in:
2026-10-19 00:19:03 +02:00
parent 4e12fc19b7
commit 39ae7de6d3
3 changed files with 86 additions and 0 deletions
+5
View File
@@ -120,6 +120,11 @@ var probeRegistry = []probe{
{ID: probeDeliveriesID, Asserts: "no delivery is held past its state's bound unsaid: mesh-delivery's " +
"`stalled`, each with the transition its table lets healer H2 take", From: "ADR 0239",
Kind: kindDeliveryStalled, Phase: 3, run: probeDeliveries},
// A first merge into a repository the mesh builds nothing from, kept waiting for mesh-delivery's word on the
// operator's Approve (novox/hq ADR 0432, issue 1146): said past a bound, never only in a log line (ADR 0426 §3, §8).
{ID: probeFirstBuildsID, Asserts: "no first merge into a repository the mesh builds nothing from waits more than ten " +
"minutes for mesh-delivery's announcement of the operator's Approve of its head", From: "ADR 0432, issue 1146",
Kind: kindFirstBuildUnasked, Phase: 3, run: probeFirstBuilds},
// The delivery budgets (novox/hq ADR 0282 decision 7): the newest delivery of each class within its budget,
// read from mesh-delivery's `times`; a measurement said, never a delivery held.
{ID: probeBudgetsID, Asserts: "the newest delivery of a leaf module ran on every machine within three minutes of " +
+49
View File
@@ -8,6 +8,7 @@ import (
"strings"
"time"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
@@ -161,3 +162,51 @@ func firstBuildsOf(ctx context.Context, inv catalogueReader, m link.SourceMoved,
// Not announced yet, or the bus or the store could not be asked: the caller waits or tries again, and asks nothing.
return nil, "", err
}
// probeFirstBuildsID is the probe that reads the first merges waiting for mesh-delivery's word.
const probeFirstBuildsID = "D17"
// kindFirstBuildUnasked is what a first merge waiting too long for mesh-delivery's word raises.
const kindFirstBuildUnasked = "first-build-unasked"
// firstBuildWaitBound is how long a first merge waits for mesh-delivery's announcement of the operator's Approve before
// it is said: a bound in the sense of ADR 0426 §3, not a delay. Nothing waits on it: the word's arrival asks the builds
// at once, and the merge is said, never dropped, once it is past this (issue 1146).
const firstBuildWaitBound = 10 * time.Minute
// firstBuildObservations are the conditions of the first merges waiting past the bound, one for each; a merge answered is
// no longer listed as waiting, so its condition clears with the next look.
func firstBuildObservations(waiting []inventory.BatchedMerge, now time.Time) []conditions.Observation {
var out []conditions.Observation
for _, w := range waiting {
k := announcedOf(w)
if !k.FirstBuild || now.Sub(w.Heard) <= firstBuildWaitBound {
continue
}
repository := k.Owner + "/" + k.Repo
for_ := now.Sub(w.Heard).Round(time.Minute)
out = append(out, conditions.Observation{Scope: conditions.ScopeMerge,
ID: fmt.Sprintf("%s_%.8s", repository, k.Commit), Kind: kindFirstBuildUnasked, Severity: conditions.Warning,
Summary: fmt.Sprintf("the merge %.8s of %s adds modules the mesh builds nothing from yet, and has waited %s for "+
"mesh-delivery's announcement of the operator's Approve of its head: no build is asked", k.Commit, repository, for_),
Said: fmt.Sprintf("%s at %s waits for mesh-delivery's word on pull request %d (head %.8s)", repository, k.Commit,
k.Number, k.HeadSHA),
Headline: fmt.Sprintf("The first build of %s is not asked: no Approve was announced", repository),
Explanation: fmt.Sprintf("The merge %.8s into %s adds modules the mesh builds nothing from yet. It registers them "+
"only when mesh-delivery announces that it merged the pull request on your Approve of that head, and no such "+
"announcement has come in %s. The merge is kept, and the builds are asked the moment the announcement is kept.",
k.Commit, repository, for_),
Resolved: "mesh-delivery announces the merge, or a hand `build` registers the modules",
})
}
return out
}
// probeFirstBuilds is D17: no first merge waits past its bound for mesh-delivery's word unsaid.
func probeFirstBuilds(ctx context.Context, d *doctor) ([]conditions.Observation, error) {
waiting, err := d.open.inventory.WaitingMerges(ctx)
if err != nil {
return nil, err
}
return firstBuildObservations(waiting, time.Now().UTC()), nil
}
+32
View File
@@ -2,12 +2,15 @@ package main
import (
"context"
"encoding/json"
"errors"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
@@ -72,3 +75,32 @@ func TestAFirstMergeIntoANewRepositoryAsksItsModulesBuildsOnlyOnTheApprove(t *te
})
}
}
// 1146/AC8: a first merge waiting past the bound for mesh-delivery's word is said, naming the repository, the commit and
// the missing word; one inside the bound, one that is no first merge, and one answered (no longer waiting) are not.
func TestAFirstMergeWaitingPastItsBoundIsSaidAndClearsWhenAnswered(t *testing.T) {
now := time.Date(2026, 10, 19, 1, 0, 0, 0, time.UTC)
waiting := func(commit string, first bool, heard time.Time) inventory.BatchedMerge {
event, _ := json.Marshal(keptMerge{SourceMoved: link.SourceMoved{Owner: "novox", Repo: "mesh-delivery", Base: "main",
Commit: commit, Number: 1, HeadSHA: "a1a1a1a1a1a1"}, FirstBuild: first})
return inventory.BatchedMerge{Repository: "novox/mesh-delivery", Branch: "main", Commit: commit, Heard: heard, Event: event}
}
got := firstBuildObservations([]inventory.BatchedMerge{
waiting("0177ea7a00", true, now.Add(-firstBuildWaitBound-time.Minute)),
waiting("1111111100", true, now.Add(-time.Minute)),
waiting("2222222200", false, now.Add(-time.Hour)),
}, now)
if len(got) != 1 {
t.Fatalf("1146/AC8: %d conditions raised, want the one merge past the bound: %+v", len(got), got)
}
o := got[0]
if o.Kind != kindFirstBuildUnasked || o.Scope != conditions.ScopeMerge || o.Severity != conditions.Warning ||
!strings.Contains(o.Summary, "novox/mesh-delivery") || !strings.Contains(o.Summary, "0177ea7a") ||
!strings.Contains(o.Summary, "announcement of the operator's Approve") {
t.Fatalf("1146/AC8: the condition is %+v", o)
}
// Answered, the merge is no longer listed as waiting: nothing is observed, and the condition clears.
if cleared := firstBuildObservations(nil, now); len(cleared) != 0 {
t.Fatalf("1146/AC8: an answered merge still raises %+v", cleared)
}
}