GATE A-24, §172, §431, §592, §593, §613: the gate keeps no merge states; deliveries, show, groups and stalled answer the delivery records

Removes holder, table, approval, ready, onestatus, pullkey, checks, group_merge, the effects, the forge reads, the teller's
and the view's reads, kvOperator, kvPulls, delivery_merge_again and their tests. deliveries, show, groups and stalled answer
the records as kept (stalled: a record open past a day, and an act owed unanswered past 15 minutes, read from the record's
acts); the switch reads the old section deliveries and says approval-carried for each unperformed merges key. The manifest
drops pulls, asks, reads, the forge, teller and view consumes and invokes, and the events group and merged.

Builds: design 47 G §172, §431, §592, §593, §613, A-24, A-35 (G-3, G-9, G-16, G-17, G-20)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 22:10:42 +01:00
parent 05fcebf9c5
commit 7cc4a33cc5
89 changed files with 619 additions and 16970 deletions
@@ -1,32 +0,0 @@
package main
import (
"testing"
"time"
)
const (
cat = "novox/mesh-catalog"
headCC = "cc00000000000001"
headDu = "dd00000000000002"
)
var atCC = time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
// A pull request closed unmerged is closed. Since issue 1318 (ADR 0503 §3) the controller is no longer asked to let go
// of what its checks built, and since ADR 0539 §17 the gate asks it nothing at all.
func TestAPullRequestClosedUnmergedIsClosedAndAsksNoOne(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), atCC)
cc := pr(cat, 101, headCC, "feat/claude-code", "modules/claude-code/module.json")
w.h.PullUpdated(cc)
w.h.PullClosed(cc)
if s := w.state(IDOf(cat, headCC)); s != Closed {
t.Fatalf("closed unmerged it is %s", s)
}
du := pr(cat, 102, headDu, "feat/dunst", "modules/dunst/module.json")
w.h.PullUpdated(du)
w.h.PullClosed(du)
if s := w.state(IDOf(cat, headDu)); s != Closed {
t.Fatalf("a closed delivery is %s", s)
}
}
@@ -1,221 +0,0 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// The tests of novox/hq ADR 0505 "How it is checked" for the gate: a refusal the gate is already answering is no
// stall yet (§4), and a delivery whose cure is its author's new head is its author's for two hours (§5).
// ADR 0505 §4, §5: the two bounds are settings with their decided defaults.
func TestTheBoundsAreSettingsWithTheirDefaults(t *testing.T) {
read := func(body string) (Settings, error) {
p := filepath.Join(t.TempDir(), "settings.json")
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
t.Fatal(err)
}
return readSettings(p)
}
h := &Holder{}
if h.refusalFloor() != 10*time.Minute || h.authorBound() != 2*time.Hour {
t.Fatalf("no settings: floor %s, author %s", h.refusalFloor(), h.authorBound())
}
s, err := read(`{"refusal-stall-floor-minutes": 5, "author-new-head-minutes": 60}`)
if err != nil {
t.Fatal(err)
}
h.Settings = func() Settings { return s }
if h.refusalFloor() != 5*time.Minute || h.authorBound() != time.Hour {
t.Fatalf("set: floor %s, author %s", h.refusalFloor(), h.authorBound())
}
s, _ = read(`{}`)
if h.refusalFloor() != 10*time.Minute || h.authorBound() != 2*time.Hour {
t.Fatalf("empty file: floor %s, author %s", h.refusalFloor(), h.authorBound())
}
if _, err := read(`{"author-new-head-minutes": 0}`); err == nil {
t.Fatalf("a bound of zero minutes was taken")
}
}
// ADR 0505 §4: a repository's last measured check time is its newest verdict's (`took`, as mesh-controller
// cmd/mesh-builder main.go writes it: a duration rounded to the second); none, and the floor alone holds.
func TestTheLastMeasuredCheckTimeIsTheNewestVerdictsOfItsRepository(t *testing.T) {
h := &Holder{}
h.init()
at := time.Date(2026, 10, 22, 10, 0, 0, 0, time.UTC)
h.deliveries["novox/mesh-catalog@a"] = &Delivery{ID: "novox/mesh-catalog@a", Repository: "novox/mesh-catalog",
Check: &Verdict{At: at, Took: "4m0s"}}
h.deliveries["novox/mesh-catalog@b"] = &Delivery{ID: "novox/mesh-catalog@b", Repository: "novox/mesh-catalog",
Check: &Verdict{At: at.Add(time.Hour), Took: "7m30s"}}
h.deliveries["novox/hq@c"] = &Delivery{ID: "novox/hq@c", Repository: "novox/hq",
Check: &Verdict{At: at.Add(2 * time.Hour), Took: "1m0s"}}
if got := h.lastCheckTime("novox/mesh-catalog"); got != 7*time.Minute+30*time.Second {
t.Fatalf("the newer of two: %s", got)
}
if got := h.answeredRefusalBound("novox/mesh-catalog"); got != 15*time.Minute {
t.Fatalf("twice 7m30s: %s", got)
}
if got := h.answeredRefusalBound("novox/hq"); got != 10*time.Minute {
t.Fatalf("twice one minute is under the floor: %s", got)
}
if got := h.answeredRefusalBound("novox/mesh-sdk"); got != 10*time.Minute {
t.Fatalf("no verdict: %s", got)
}
}
// refusedForAMovedTrunk is a lone delivery approved and refused by the merge tool because its trunk moved and its check
// was asked again (gitea merge.ts:147, errAskedAgain).
func refusedForAMovedTrunk(t *testing.T) (*world, string) {
t.Helper()
w := announced(t)
id := IDOf("novox/app", readyHead)
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errAskedAgain
approve(w)
if len(w.forge.merges) != 1 || w.delivery(id).Ready.RefusedSince == nil {
t.Fatalf("the merge was not asked and refused: %v %+v", w.forge.merges, w.delivery(id).Ready)
}
return w, id
}
func linesFor(w *world, id string) []StalledLine {
var out []StalledLine
for _, l := range w.h.Stalled() {
if l.ID == id {
out = append(out, l)
}
}
return out
}
// ADR 0505 §4: with no answer, the refusal is said at twice the repository's last check time, at least ten minutes.
func TestATrunkMovedRefusalIsStalledAtTwiceTheLastCheckTimeAtLeastTheFloor(t *testing.T) {
w, id := refusedForAMovedTrunk(t)
w.later(9 * time.Minute)
if got := linesFor(w, id); len(got) != 0 {
t.Fatalf("said before the floor: %+v", got)
}
w.later(2 * time.Minute)
if got := linesFor(w, id); len(got) != 1 {
t.Fatalf("not said past the floor of ten minutes: %+v", got)
}
w, id = refusedForAMovedTrunk(t)
w.h.mu.Lock()
w.h.deliveries[id].Check.Took = "7m0s" // its repository's last check ran seven minutes: the bound is fourteen
w.h.mu.Unlock()
w.later(11 * time.Minute)
if got := linesFor(w, id); len(got) != 0 {
t.Fatalf("said at eleven minutes, under twice the last check time: %+v", got)
}
w.later(4 * time.Minute)
if got := linesFor(w, id); len(got) != 1 {
t.Fatalf("not said past twice the last check time: %+v", got)
}
}
// ADR 0505 §4: any other refusal is said at once, as issue 1115 has it.
func TestAnyOtherRefusalIsStalledAtOnce(t *testing.T) {
w := announced(t)
id := IDOf("novox/app", readyHead)
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = refusedWords("novox/app#7")
approve(w)
if got := linesFor(w, id); len(got) != 1 {
t.Fatalf("a missing status refusal is not said at once: %+v", got)
}
}
// rejectedBeforeItsAnswer is a delivery approved and then rejected: its check failed after the ask (as in events_test.go),
// its pull request opened by author as the forge's record says, when given.
func rejectedBeforeItsAnswer(t *testing.T, author string) (*world, string) {
t.Helper()
w := newWorld(t)
w.op.auto = false
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
p.Author = author
w.h.PullUpdated(p)
w.settleAll()
id := IDOf("novox/app", readyHead)
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
// Judged again (the table's recheck, which the checker's verdict on a new trunk makes): proposed once more.
w.h.mu.Lock()
d := w.h.deliveries[id]
if _, err := Apply(d, EvRecheck, true, Facts{Now: w.h.Now(), Why: "a second look", By: "tester"}); err != nil {
w.h.mu.Unlock()
t.Fatal(err)
}
d.Check = nil
w.h.mu.Unlock()
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "fail", "t1t1t1t1t1t1"))
w.settleAll()
// A check that fails before the operator answers withdraws the ask (novox/hq ADR 0661 §5, ADR 0441 §1).
if open := w.op.open(); len(open) != 0 {
t.Fatalf("the ask stands after its check failed: %v", open)
}
w.h.React()
if w.state(id) != Rejected {
t.Fatalf("not rejected: %s", w.state(id))
}
return w, id
}
// ADR 0505 §5: a rejected delivery is told on its pull request at once, and reaches the operator only after two hours
// with no new head, in words naming its author (its branch, and the session that pushed it) and needing a new head —
// never "stop it", never "the operator's".
func TestARejectedDeliveryIsItsAuthorsForTwoHours(t *testing.T) {
w, id := rejectedBeforeItsAnswer(t, "quiet-deliveries")
said := false
for _, c := range w.forge.comments {
said = said || strings.Contains(c, "not merged")
}
if !said {
t.Fatalf("its author was not told on the pull request at once: %v", w.forge.comments)
}
w.later(2*time.Hour - time.Minute)
if got := linesFor(w, id); len(got) != 0 {
t.Fatalf("the operator's before two hours: %+v", got)
}
w.later(2 * time.Minute)
got := linesFor(w, id)
if len(got) != 1 {
t.Fatalf("not the operator's after two hours with no new head: %+v", got)
}
l := got[0]
if l.Needs != NeedsNewHead || l.Author != "the author of feat/x (quiet-deliveries)" || l.Branch != "feat/x" {
t.Fatalf("the line does not name its author and the new head it needs: %+v", l)
}
for _, never := range []string{"operator's", "stop it"} {
if strings.Contains(l.Says+l.H2, never) {
t.Fatalf("the line says %q: %+v", never, l)
}
}
if !strings.Contains(l.Says, "needs a new head from the author of feat/x (quiet-deliveries)") {
t.Fatalf("the words do not say a new head is needed from its author: %q", l.Says)
}
}
// ADR 0505 §5: with no author in the forge's record, the branch alone names its author.
func TestARejectedDeliveryWithNoAuthorIsNamedByItsBranch(t *testing.T) {
w, id := rejectedBeforeItsAnswer(t, "")
w.later(2*time.Hour + time.Minute)
if got := linesFor(w, id); len(got) != 1 || got[0].Author != "the author of feat/x" {
t.Fatalf("not named by its branch: %+v", got)
}
}
// ADR 0505 §5: a new head within the two hours ends it: the rejected head is never the operator's.
func TestANewHeadWithinTwoHoursKeepsTheRejectedHeadFromTheOperator(t *testing.T) {
w, id := rejectedBeforeItsAnswer(t, "quiet-deliveries")
w.later(time.Hour)
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
w.settleAll()
w.later(2 * time.Hour)
if got := linesFor(w, id); len(got) != 0 {
t.Fatalf("the superseded head reached the operator: %+v", got)
}
}
@@ -1,155 +0,0 @@
package main
import (
"encoding/json"
"reflect"
"strings"
"testing"
"time"
)
// No check gates a merge (novox/hq ADR 0661), the gate's part: no approved merge waits for another delivery's running check,
// and no turn or stalled line holds it (§1); the merge judges the operator's Approve alone, so an Approve given before a trunk
// move merges onto the new trunk with no wait for its carry (§3); a failure after the Approve is said once on the pull
// request and as a stalled line naming the merge it follows, and refuses nothing (§5).
const otherHead = "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
// approvedLone is a lone delivery into oneRepo whose check passed, approved by the operator (the world approves at once).
func approvedLone(w *world) string {
w.t.Helper()
p := pr(oneRepo, 1, oneHead, "feat/one")
requiresCheck(w, p)
w.h.PullUpdated(p)
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
return IDOf(oneRepo, oneHead)
}
// sayingFailed is the forge's word that oneHead's check failed on the trunk commit given (a carry's line).
func sayingFailed(trunk string) ForgeStatusEvent {
e := checkStatus(oneRepo, oneHead, "failure", trunk)
e.Description = "failed " + oneHead[:12] + " on " + trunk[:12] + " | a test failed"
return e
}
func lineOf(w *world, id string) *StalledLine {
for _, l := range w.h.Stalled() {
if l.ID == id {
return &l
}
}
return nil
}
func TestNoCheckGatesAMergeAtTheGate(t *testing.T) {
for name, run := range map[string]func(t *testing.T){
// §1: a running check of other heads on the same repository holds nothing, and no stalled line is raised for it.
"an approved merge into a repository under a running check merges at once": func(t *testing.T) {
w := newWorld(t)
putCheckerVerdict(w, "running", headOf(oneRepo, 9, otherHead))
id := approvedLone(w)
w.settleAll()
if w.op.count() != 1 || len(w.forge.merges) != 1 {
t.Fatalf("asked %d times, merges %v: want asked once and merged at once (%s)", w.op.count(), w.forge.merges, w.logs())
}
w.h.PullMerged(merged(oneRepo, 1, oneHead, strings.Repeat("c", 40))) // the seat git says the merge too
w.later(time.Hour)
w.settleAll()
if l := lineOf(w, id); l != nil {
t.Fatalf("a stalled line for a merge a running check once held: %+v", l)
}
},
"the delivery's own running verdict does not hold it": func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
id := approvedLone(w)
w.settleAll()
putCheckerVerdict(w, "running", headOf(oneRepo, 1, oneHead)) // its own heads judged again (a carry)
for _, k := range w.op.open() {
w.op.answer(k, OptionApprove)
}
w.settleAll()
if d := w.delivery(id); len(w.forge.merges) != 1 || d.Ready == nil {
t.Fatalf("merges %v, run %+v: its own check held it", w.forge.merges, d.Ready)
}
},
// §3 (and ADR 0548 §8): the ask stands through a trunk move; the Approve given then merges onto the new trunk on the
// verdict judged against the old one, with no wait for its carry and no second ask.
"an Approve given after a trunk move merges at once, its carry after": func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
id := approvedLone(w)
w.settleAll()
if w.op.count() != 1 || len(w.forge.merges) != 0 {
t.Fatalf("asked %d times, merges %v: want asked and not merged yet", w.op.count(), w.forge.merges)
}
w.h.PullMerged(merged(oneRepo, 5, otherHead, oneTrunk2)) // a merge at the forge moves the trunk
w.settleAll()
for _, k := range w.op.open() {
w.op.answer(k, OptionApprove)
}
w.settleAll()
d := w.delivery(id)
if len(w.forge.merges) != 1 || w.op.count() != 1 || d.State != Ready && d.State != Merged {
t.Fatalf("merges %v, asked %d times, %s (waits %q): want merged at once on the old verdict (%s)", w.forge.merges,
w.op.count(), d.State, d.Ready.Waits, w.logs())
}
},
// §5: a failure after the Approve, a carry's, is said once on the pull request and as one stalled line naming the merge;
// it refuses nothing, and a later pass clears the line.
"a failure after the Approve is said once and refuses nothing": func(t *testing.T) {
w := newWorld(t)
id := approvedLone(w)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("merges %v: want merged at once", w.forge.merges)
}
before := len(w.forge.comments)
w.h.CommitStatus(sayingFailed(oneTrunk2))
w.settleAll()
w.h.CommitStatus(sayingFailed(oneTrunk2))
w.settleAll()
said := 0
for _, c := range w.forge.comments[before:] {
if strings.Contains(c, "failed after the Approve") {
said++
}
}
if said != 1 {
t.Fatalf("said %d times on the pull request, want once: %v", said, w.forge.comments[before:])
}
l := lineOf(w, id)
if l == nil || !strings.Contains(l.Says, "after the operator's Approve") || !strings.Contains(l.Says, oneHead[:12]) {
t.Fatalf("the stalled line = %+v", l)
}
if d := w.delivery(id); d.State == Rejected || len(w.forge.merges) != 1 {
t.Fatalf("%s, merges %v: the failure refused the approved merge", d.State, w.forge.merges)
}
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk2))
w.settleAll()
if l := lineOf(w, id); l != nil {
t.Fatalf("a later pass left the stalled line: %+v", l)
}
},
} {
t.Run(name, run)
}
}
// Issue 1794: the gate keeps no mark of an own ask of a check, so no verdict of another check of the head can clear one while
// the gate's own is in flight: since issue 1318 the gate asks no check, only the checker's verdict at the heads is read, and a
// run holds no field that marks an ask of one.
func TestTheGateAsksNoCheckOfItsOwnSoNoMarkOfOneCanBeClearedByAnother(t *testing.T) {
typ := reflect.TypeOf(ReadyRun{})
for i := 0; i < typ.NumField(); i++ {
if f := typ.Field(i).Name; strings.HasPrefix(f, "OwnAsk") || strings.HasPrefix(f, "OwnCheck") {
t.Errorf("ReadyRun holds %s: a mark of a check the gate asked itself, which it no longer asks", f)
}
}
for _, key := range []string{"own_asked_at", "own_ask"} {
if raw, _ := json.Marshal(ReadyRun{}); strings.Contains(string(raw), key) {
t.Errorf("a run is kept with %s", key)
}
}
}
@@ -1,835 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// A merge into main waits for the operator's Approve (novox/hq ADR 0398 §3, issue 948, ADR 0421).
//
// This owner writes the ask into its bucket asks (novox/hq ADR 0437 §6) as soon as a pull request's checks pass:
// Approve or Decline, each option binding the act "merge these heads". The teller publishes it to the operator on the
// router and, on a warrant that holds the ask (its asker, the ask's digest, an option it offered, in time, and the act
// that option bound), writes the answer into its bucket answers. This owner reads the answer and acts on it only while
// the heads of the key it was written under are still its group's heads; it asks the router nothing.
//
// - Asked once per head. A group is asked once, for all its members' heads together, by the first member that reaches
// this point; the Approve covers every member at those heads.
// - A push after the ask ends it, said once on the pull request: the new head is asked on its own.
// - A Decline merges nothing and is said once on the pull request.
// - An ask that ended without a choice (expired, cancelled, refused by the router) is asked again when the router says so.
// - The ask, its state and the warrant's words are kept on the delivery's ReadyRun and shown by `show`.
//
// The terminal Approve (ADR 0398 §3) is issue 953; today the proving channel is the operator's phone.
// Module is this module's name: the one the bus names its buckets and events under (novox/hq ADR 0437 §3). Seat is
// the seat it claims, whose name stays `mesh-delivery` (ADR 0437 §11).
const (
Module = "mesh-delivery-gate"
Seat = "mesh-delivery"
)
// The two answers, both at the level approve.
const (
OptionApprove = "approve"
OptionDecline = "decline"
)
// AskLasts is how long the operator has to answer: a day less a margin, under the router's bound for an approval.
const AskLasts = occ.ApproveLasts - 10*time.Minute
// Operator is how this owner asks the operator: through the keys of its bucket asks and the teller's answers. kvOperator in
// the running module.
type Operator interface {
// Ask writes the ask under its key (mdc.MergeAskKey); it writes nothing when that key holds the same ask already.
Ask(key string, a Ask) error
// Cancel takes the ask at a key away, which the teller reads as the ask withdrawn.
Cancel(key string) error
// Where is the teller's answer to the ask at a key, or nil when there is none.
Where(key string) (*Answer, error)
}
// askGroup is the name a delivery's ask and merge are kept under: its group's, or, for a lone pull request, a group of one
// under its branch name (novox/hq ADR 0437 §6).
func askGroup(d *Delivery) string {
switch {
case d.Group != "":
return d.Group
case d.Branch != "":
return d.Branch
}
return d.ID
}
// headsString is the heads an act names, as an act's value: "<id>=<sha>" for each, sorted, joined by commas.
func headsString(heads map[string]string) string {
var out []string
for id, sha := range heads {
out = append(out, id+"="+sha)
}
sort.Strings(out)
return strings.Join(out, ",")
}
// mergeAct is what an answer binds: this ask, the answer, and the heads it covers.
func mergeAct(ask, option string, heads map[string]string) occ.Act {
return occ.Act{"verb": Seat + ".merge", "ask": ask, "option": option, "heads": headsString(heads)}
}
var unshownPath = regexp.MustCompile(`[/\\@~$]|\d+\.\d+\.\d+|[A-Za-z0-9+/=_]{24,}|(?i)\b[a-z0-9-]+\.[a-z]{2,12}\b`)
// phone is a line the operator's phone may be shown: what the router refuses in an ask's words (a path, an address, a
// host-like name, a long unbroken word) is left out, and the line is cut to n characters.
func phone(s string, n int) string {
s = strings.Join(strings.Fields(unshownPath.ReplaceAllString(s, "…")), " ")
if r := []rune(s); len(r) > n {
return string(r[:n-1]) + "…"
}
return s
}
// issuesSaid is the issues linked to the deliveries' pull requests in the issue tracker, as the ask names them.
func issuesSaid(ds []*Delivery) string {
seen := map[int]bool{}
var out []string
for _, d := range ds {
if d.IssuesUnread != "" {
out = append(out, pullSaid(d)+"'s unread (the issue tracker did not answer)")
}
for _, n := range d.Issues {
if !seen[n] {
seen[n] = true
out = append(out, "issue "+strconv.Itoa(n))
}
}
}
if len(out) == 0 {
return "none linked"
}
return strings.Join(out, ", ")
}
func repoName(repository string) string {
_, name, ok := strings.Cut(repository, "/")
if !ok {
return repository
}
return name
}
func pullSaid(d *Delivery) string { return fmt.Sprintf("%s#%d", repoName(d.Repository), d.Number) }
// touchesSaid is the modules the delivery's check says its change touches, for the ask; the gate plans no delivery.
func touchesSaid(d *Delivery) string {
if d.Check == nil || len(d.Check.Modules) == 0 {
return "no module the check named"
}
return strings.Join(d.Check.Modules, ", ")
}
// checkClause is the check's state as the ask's summary says it after the pull request's or the group's name: sep and
// "passed its check", or ": its check has no verdict yet: it is running (since 21:05 UTC)" and the like.
func checkClause(c checkSaid, sep string) string {
if c.passed() {
return sep + c.Words
}
return ": " + c.Words
}
// approveCaveat is what an Approve does beyond the merge while the check has not passed: it merges although it failed (the check gates
// no merge, check-gates-merge false). Empty once it passed; an ask is made only with a verdict.
func approveCaveat(c checkSaid) string {
switch c.Key {
case checkSaidPass:
return ""
case checkSaidFail:
return " although its check failed"
}
return ""
}
// mergeHeadline opens the ask's headline: "Merge into main: ", and "Check failed - merge into main: " once the check failed,
// since a merge is not gated on the check and the headline is the phone's only warning (issue 1166). A passed or still
// running check keeps the plain headline. The prefix leads, so phone's cut of a long headline never drops it.
func mergeHeadline(c checkSaid) string {
if c.Key == checkSaidFail {
return "Check failed \u2014 merge into main: "
}
return "Merge into main: "
}
// composeMergeAsk is the ask for a delivery, or for a group's members together.
// check is the state of the check at the heads it covers (checkAt): the ask says "passed its check" only when it did, and
// otherwise that Approve merges before the check ends, or although it failed (issue 1733).
func composeMergeAsk(id string, d *Delivery, group string, members []*Delivery, heads map[string]string, check checkSaid,
now time.Time) oc.Ask {
var a oc.Ask
a.ID, a.Who, a.Expires = id, occ.Operator, occ.FormatTime(now.Add(AskLasts))
a.OnExpiry = "nothing is merged; the ask is made again"
a.About = "delivery." + phone(repoName(d.Repository), 40) + "." + strconv.Itoa(d.Number)
a.About = strings.ReplaceAll(a.About, " ", "-")
var reaches, details []string
for _, m := range members {
touches := touchesSaid(m)
reaches = append(reaches, touches)
details = append(details, fmt.Sprintf("%s %q at %s: issues: %s; touches: %s",
pullSaid(m), m.Title, shortOf(m.Commit, 12), issuesSaid([]*Delivery{m}), touches))
}
if group == "" {
a.Headline = phone(mergeHeadline(check)+pullSaid(d)+" "+d.Title, occ.HeadlineLength)
summary := fmt.Sprintf("%s %s%s. Approve merges it into main at %s%s; Decline leaves it unmerged.",
pullSaid(d), d.Title, checkClause(check, " "), shortOf(d.Commit, 12), approveCaveat(check))
a.Explanation = strings.Join([]string{
phone(summary, occ.SummaryLength),
phone("Issues: "+issuesSaid(members), occ.PointLength),
phone("Touches: "+reaches[0], occ.PointLength),
}, "\n")
} else {
var names []string
for _, m := range members {
names = append(names, pullSaid(m))
}
a.Headline = phone(mergeHeadline(check)+"group "+group+" ("+strings.Join(names, ", ")+")", occ.HeadlineLength)
caveat := approveCaveat(check)
if caveat != "" {
caveat = "," + caveat
}
summary := fmt.Sprintf("The group %s, %d pull requests%s. Approve merges all of them into main in one pass, at the heads named%s; Decline leaves them unmerged.",
group, len(members), checkClause(check, ", "), caveat)
a.Explanation = strings.Join([]string{
phone(summary, occ.SummaryLength),
phone("Issues: "+issuesSaid(members), occ.PointLength),
phone("Touches: "+strings.Join(reaches, "; "), occ.PointLength),
}, "\n")
}
a.Details = phone(strings.Join(details, "\n"), 1500)
if len(details) > 0 { // keep the lines apart: phone joined them
a.Details = strings.Join(func() []string {
var out []string
for _, l := range details {
out = append(out, phone(l, 400))
}
return out
}(), "\n")
}
for _, o := range []struct{ id, label, does string }{
{OptionApprove, "Approve", "the pull request is merged into main at the head named (a group: all its members, in one pass)"},
{OptionDecline, "Decline", "nothing is merged"},
} {
binds, _ := occ.ActDigest(mergeAct(id, o.id, heads))
a.Options = append(a.Options, oc.Option{ID: o.id, Label: o.label, Does: o.does, Level: occ.Approve, Binds: binds})
}
return a
}
// askMembers are the deliveries one ask covers: the group's members not yet merged, by id, or the delivery.
func (h *Holder) askMembers(d *Delivery) []*Delivery {
g := h.groups[d.Group]
if d.Group == "" || g == nil {
return []*Delivery{d}
}
var out []*Delivery
seen := map[string]bool{}
ids := append([]string(nil), g.Members...)
sort.Strings(ids)
for _, id := range ids {
if o := h.deliveries[id]; o != nil && !seen[id] && o.MergedAs == "" && !o.State.Final() {
seen[id] = true
out = append(out, o)
}
}
if len(out) == 0 {
return []*Delivery{d}
}
return out
}
// covering is the ready run holding an ask (pending or approved) that covers d at its head: its own, or a group
// sibling's, whose ask named d's head too.
func (h *Holder) covering(d *Delivery) *ReadyRun {
has := func(o *Delivery) *ReadyRun {
if o != nil && o.Ready != nil && (o.Ready.AskID != "" || o.Ready.Approved != nil) && o.Ready.Heads[d.ID] == d.Commit {
return o.Ready
}
return nil
}
if r := has(d); r != nil {
return r
}
if g := h.groups[d.Group]; d.Group != "" && g != nil {
for _, id := range g.Members {
if r := has(h.deliveries[id]); r != nil {
return r
}
}
}
return nil
}
func headsEqual(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if b[k] != v {
return false
}
}
return true
}
// headsHold is whether an ask's heads are still the heads of d's group: every member now at the head asked, and every
// head asked that is no member now a member already merged.
func (h *Holder) headsHold(d *Delivery, c *ReadyRun) bool {
now := map[string]bool{}
for _, m := range h.askMembers(d) {
now[m.ID] = true
if c.Heads[m.ID] != m.Commit {
return false
}
}
for id := range c.Heads {
if !now[id] {
if o := h.deliveries[id]; o == nil || o.MergedAs == "" {
return false
}
}
}
return true
}
// endStaleAsks ends every ask whose heads are no longer its group's: a push to any member ends the approval (ADR 0398
// §3), and says so once on each pull request it covered (ADR 0421 §6). An ask for a head a newer one replaced ends too.
// A Decline stands, and is not ended. An ask none of whose deliveries can still merge by it is taken away after
// (endAsksOfFinished). Under the lock; the cancels run with the look's asks.
func (h *Holder) endStaleAsks() {
ending := map[string]bool{}
for _, d := range h.deliveries {
r := d.Ready
if r == nil || r.Merged != nil || d.MergedAs != "" || r.declined() || (r.AskID == "" && r.Approved == nil) || len(r.Heads) == 0 {
continue
}
id, before := r.AskID, len(h.cancels)
switch {
case d.State.Final():
h.cancels = append(h.cancels, h.dropAsk(r.AskID, "its head "+shortOf(d.Commit, 12)+" is no longer the pull request's head, or the pull request closed"))
case !h.headsHold(d, r):
h.cancels = append(h.cancels, h.dropAsk(r.AskID, "a head it covers moved on: the heads it covers are no longer the group's heads"))
case r.Approved == nil && r.AskID != "" && r.AskCheck != "" && h.checkMoved(d, r.AskCheck):
// The check gates the ask, so it was made on a pass: once that pass is gone (a failure, or a check of these heads
// running again), the ask not yet answered is withdrawn in words that say so (novox/hq ADR 0661 §5, ADR 0441 §1).
h.cancels = append(h.cancels, h.withdrawAsk(r.AskID, "the check at its heads is now: "+h.checkAt(h.askMembers(d)).Words))
case r.Approved == nil && r.AskID != "" && !r.AskedAt.IsZero() && !h.Now().Before(r.AskedAt.Add(AskLasts)):
// The ask lapsed at the router with nobody's answer, and nothing in the gate's inputs says so: it is taken away and
// written again, which the teller asks afresh (ADR 0441 §1).
h.cancels = append(h.cancels, h.withdrawAsk(r.AskID, "nobody answered in "+AskLasts.String()))
}
if id != "" && (len(h.cancels) > before || h.withdrawn[id]) {
ending[id] = true
}
}
h.endAsksOfFinished(ending)
}
// endAsksOfFinished takes away every ask that stands in the gate's asks while none of the deliveries it was asked for can still
// be merged by it: each merged (on the operator's Approve or by another path) or ended. The ask is about a merge, so it ends
// once that merge cannot follow from it, however the deliveries got there (issue 774: on 2026-10-31 two pull requests the lead
// merged once their checks were green left their asks standing for a day, and the teller's asks held two of the router's three
// places, so a newer ask was refused for want of room). It reads what the gate keeps (AskedKey, Written), so an ask whose
// taking away failed is taken away again at the next look. An ask another end of this look takes away (ending, withdrawn) is
// left to it. Nothing is said on a pull request: its merge or its end said it. Under the lock.
func (h *Holder) endAsksOfFinished(ending map[string]bool) {
standing, live := map[string]bool{}, map[string]bool{}
for _, d := range h.deliveries {
r := d.Ready
if r == nil || r.AskedKey == "" {
continue
}
if r.Written {
standing[r.AskedKey] = true
}
if d.MergedAs == "" && !d.State.Final() {
live[r.AskedKey] = true
}
}
keys := make([]string, 0, len(standing))
for key := range standing {
if !live[key] && !ending[key] && !h.withdrawn[key] {
keys = append(keys, key)
}
}
sort.Strings(keys)
for _, key := range keys {
for _, d := range h.deliveries {
if r := d.Ready; r != nil && r.AskID == key && r.Approved == nil {
r.AskID, r.Ask, r.Heads = "", nil, nil
h.keep(d)
}
}
h.Logf("[mesh-delivery] the operator's ask %s is taken away: every pull request it was asked for is merged or ended", key)
if h.Operator == nil {
continue
}
h.cancels = append(h.cancels, func() {
if err := h.Operator.Cancel(key); err != nil {
h.Logf("[mesh-delivery] the operator's ask %s could not be taken away, and is taken away at the next look (%v)", key, err)
return
}
h.mu.Lock()
h.markWritten(key, false)
h.mu.Unlock()
})
}
}
// checkMoved is whether the check at the heads of d's ask reached a verdict its pending ask does not say: it passed or failed
// since, or the pass it said is gone. Under the lock.
func (h *Holder) checkMoved(d *Delivery, said string) bool {
now := h.checkAt(h.askMembers(d)).Key
if now == said {
return false
}
return now == checkSaidPass || now == checkSaidFail || said == checkSaidPass || said == checkSaidFail
}
// approval is what a delivery that may merge waits on for the operator: empty once its head is approved, and the ask
// to make when none is made yet. Under the lock.
func (h *Holder) approval(d *Delivery, now time.Time) (string, func()) {
r := d.Ready
c := h.covering(d)
if c != nil {
if c.Approved != nil {
r.Approved, r.Warrant, r.AskID, r.Ask, r.Heads = c.Approved, c.Warrant, c.AskID, c.Ask, c.Heads
return "", nil
}
if !c.AskedAt.IsZero() {
h.lookAt(c.AskedAt.Add(AskLasts), "the operator's Approve, unanswered until the ask lapses")
}
return "the operator's Approve is asked (" + c.AskID + ")", nil
}
if r.Refused != "" {
return "the operator's answer was refused here: " + r.Refused, nil
}
if h.Operator == nil {
return "the operator cannot be asked from here", nil
}
members := h.askMembers(d)
if waits := h.writeWaits(d, members, now); waits != "" {
return waits, nil // asked only once the forge account can carry the merge out (writes.go, issue 1945)
}
if read := h.issuesToRead(members); read != nil {
return "the issues linked to its pull request are read from the issue tracker", read
}
group := ""
if len(members) > 1 {
group = d.Group
}
heads := map[string]string{}
var covered []mdc.Head
for _, m := range members {
heads[m.ID] = m.Commit
covered = append(covered, mdc.Head{Repository: m.Repository, Number: m.Number, Head: m.Commit})
}
name := askGroup(d)
id, err := mdc.MergeAskKey(name, covered)
if err != nil {
return "the operator cannot be asked: " + err.Error(), nil
}
r.Asks++
// Each asking at these heads has its own router id (mdc.AskingID): the router keeps an ask by its id and drops a second ask
// under it as a redelivery, so an ask written again at unchanged heads (a lapse, or a status changing) is
// the next asking, with a new id (review round 1, G2; issue 1370).
asking := 1
for _, m := range members {
if m.Ready != nil && m.Ready.AskedKey == id {
asking = max(asking, m.Ready.Asking+1)
}
}
check := h.checkAt(members)
ask := composeMergeAsk(mdc.AskingID(id, asking), d, group, members, heads, check, now)
taken := []*Delivery{}
var reads []memberRead
for _, m := range members {
h.runOf(m, now)
m.Ready.AskID, m.Ready.Ask, m.Ready.Heads, m.Ready.AskedAt, m.Ready.Asks = id, &ask, heads, now, max(m.Ready.Asks, r.Asks)
m.Ready.AskedKey, m.Ready.Asking, m.Ready.AskCheck = id, asking, check.Key
m.Ready.Declined, m.Ready.AnsweredAsk = nil, "" // a new ask: an answer to an earlier one does not stand on it
h.keep(m)
taken = append(taken, m)
reads = append(reads, readOf(m, m.Commit))
}
h.Logf("[mesh-delivery] %s (%s): the operator is asked to Approve its merge into main (%s, ask %d)",
d.ID, check.Words, id, r.Asks)
gid := ""
if group != "" {
gid = d.Group
}
return "the operator's Approve is asked (" + id + ")", func() {
// A group is not asked while a member's required status is not a success at its head (issue 1275): the group
// says which, and is asked once every member may merge.
var holdMember, holdSays string
var readErr error
for _, m := range reads {
if gid == "" {
break
}
says, forge, err := h.notASuccess(m)
if err != nil {
readErr = err
break
}
if says != "" && !forge { // the forge's merge answer is weighed at the merge, where it is asked again (group_merge.go)
holdMember, holdSays = m.id, says
break
}
}
undo := func() {
for _, m := range taken {
if m.Ready != nil && m.Ready.AskID == id {
m.Ready.AskID, m.Ready.Ask, m.Ready.Heads = "", nil, nil
h.keep(m)
}
}
}
// An earlier asking of this key may still stand in the bucket: not written again, it is taken away.
stale := func() {
h.mu.Lock()
standing := h.standing(id)
h.mu.Unlock()
if standing {
if err := h.Operator.Cancel(id); err != nil {
h.Logf("[mesh-delivery] the operator's ask %s could not be withdrawn (%v)", id, err)
return
}
h.mu.Lock()
h.markWritten(id, false)
h.mu.Unlock()
}
}
// Nothing is asked that the forge account could not merge (writes.go, issue 1945): said at the next look.
if says := h.cannotWrite(reads); says != "" {
stale()
h.mu.Lock()
defer h.mu.Unlock()
undo()
h.Logf("[mesh-delivery] %s (%s): the operator is not asked: %s", d.ID, check.Words, says)
h.lookAt(h.Now(), "an ask not made: the forge account cannot write a repository it covers")
return
}
if readErr != nil || holdSays != "" {
stale()
h.mu.Lock()
defer h.mu.Unlock()
undo()
if g := h.groups[gid]; g != nil && holdSays != "" {
if g.Hold == nil || g.Hold.Kind != "unasked" || g.Hold.Says != holdSays || g.Hold.Member != holdMember {
h.Logf("[mesh-delivery] group %s is not asked for its Approve: %s waits on a required status: %s", gid, holdMember, holdSays)
g.Hold = &GroupHold{Kind: "unasked", Member: holdMember, Says: holdSays, Since: h.Now()}
h.keepGroup(g)
}
}
if readErr != nil {
h.Logf("[mesh-delivery] group %s: the required statuses cannot be read, so the operator is not asked yet: %v", gid, readErr)
}
h.again("a group's Approve held for a required status")
return
}
if g := func() *Group { h.mu.Lock(); defer h.mu.Unlock(); return h.groups[gid] }(); g != nil && g.Hold != nil && g.Hold.Kind == "unasked" {
h.mu.Lock()
g.Hold = nil
h.keepGroup(g)
h.mu.Unlock()
}
toAsk, err := gateAsk(name, covered, asking, ask)
if err == nil {
err = h.Operator.Ask(id, toAsk)
}
if err == nil {
h.mu.Lock()
h.markWritten(id, true)
h.mu.Unlock()
} else {
h.mu.Lock()
defer h.mu.Unlock()
undo()
h.again("the operator's Approve, which could not be asked")
h.Logf("[mesh-delivery] the operator could not be asked about %s (%v); asked again in %s", d.ID, err, askAgainAfter)
}
}
}
// dropAsk forgets a pending ask on every delivery it covers and takes it back from the router; under the lock. What it
// says is said once on each pull request it covered (ADR 0421 §6): the Approve is ended, and why.
func (h *Holder) dropAsk(id, why string) func() {
var said []func()
seen := map[string]bool{}
var covered []*Delivery
for _, d := range h.deliveries {
if d.Ready != nil && d.Ready.AskID == id {
covered = append(covered, d)
}
}
sort.Slice(covered, func(i, j int) bool { return covered[i].ID < covered[j].ID })
for _, d := range covered {
d.Ready.AskID, d.Ready.Ask, d.Ready.Heads = "", nil, nil
if d.Ready.Merged == nil {
d.Ready.Approved, d.Ready.Warrant = nil, ""
}
h.keep(d)
if k := fmt.Sprintf("%s#%d", d.Repository, d.Number); !seen[k] && d.Number != 0 {
seen[k] = true
said = append(said, h.sayOn(d, "mesh-delivery: the operator's Approve of this pull request's merge into main is ended: "+why+
". Nothing is merged on it. The operator is asked again once the checks pass at the new head."))
}
}
h.Logf("[mesh-delivery] the operator's ask %s is cancelled: %s", id, why)
return func() {
for _, s := range said {
s()
}
if h.Operator == nil {
return
}
if err := h.Operator.Cancel(id); err != nil {
h.Logf("[mesh-delivery] the operator's ask %s could not be cancelled (%v)", id, err)
return
}
h.mu.Lock()
h.markWritten(id, false)
h.mu.Unlock()
}
}
// withdrawAsk takes back an ask nobody answered, on every delivery it covers, so the next look asks again with what is
// known now (issue 1079). Unlike dropAsk it ends no Approve and says nothing on a pull request: there was none. Under the
// lock; the router's cancel runs with the look's asks.
func (h *Holder) withdrawAsk(id, why string) func() {
for _, d := range h.deliveries {
if d.Ready != nil && d.Ready.AskID == id && d.Ready.Approved == nil {
d.Ready.AskID, d.Ready.Ask, d.Ready.Heads = "", nil, nil
h.keep(d)
}
}
// The key is not taken away: the next look writes it again with the next asking, if the group may be asked, and takes it away
// only if it may not (readyAsks, withdrawnKeys), so the key is never absent between the two.
h.withdrawn[id] = true
h.Logf("[mesh-delivery] the operator's ask %s is withdrawn to be asked again: %s", id, why)
return func() {}
}
// withdrawnKeys are, after a look, the closures that take away the asks withdrawn and not written again by it. Under the lock.
func (h *Holder) withdrawnKeys() []func() {
var out []func()
for key := range h.withdrawn {
delete(h.withdrawn, key)
again := false
for _, d := range h.deliveries {
again = again || (d.Ready != nil && d.Ready.AskID == key)
}
if again || h.Operator == nil {
continue
}
key := key
out = append(out, func() {
if err := h.Operator.Cancel(key); err != nil {
h.Logf("[mesh-delivery] the operator's ask %s could not be withdrawn (%v)", key, err)
return
}
h.mu.Lock()
h.markWritten(key, false)
h.mu.Unlock()
})
}
return out
}
// pendingAsks are the asks waiting for an answer, by id; under the lock.
func (h *Holder) pendingAsks() []string {
seen := map[string]bool{}
var out []string
for _, d := range h.deliveries {
if r := d.Ready; r != nil && r.AskID != "" && r.Approved == nil && !r.declined() && !seen[r.AskID] {
seen[r.AskID] = true
out = append(out, r.AskID)
}
}
sort.Strings(out)
return out
}
// hearAsks reads the teller's answer to every pending ask and applies each that is there.
func (h *Holder) hearAsks() {
if h.Operator == nil {
return
}
h.mu.Lock()
keys := h.pendingAsks()
h.mu.Unlock()
for _, key := range keys {
answer, err := h.Operator.Where(key)
if err != nil {
h.Logf("[mesh-delivery] the teller's answer to the ask %s cannot be read: %v", key, err)
continue
}
if answer == nil {
continue
}
h.mu.Lock()
said := h.answered(key, *answer)
h.mu.Unlock()
for _, s := range said {
s()
}
}
}
// answered applies the teller's answer to the ask at a key to the deliveries it covers; under the lock. The teller has
// held the warrant to the ask (novox/hq ADR 0437 §6); this owner acts on the answer only if the key it was written under is
// still the key of the heads asked: an answer to an ask taken away, or to other heads, merges nothing (ADR 0441 §1). It
// returns what to say on the pull requests.
func (h *Holder) answered(key string, ans Answer) []func() {
var covered []*Delivery
for _, d := range h.deliveries {
if d.Ready != nil && d.Ready.AskID == key {
covered = append(covered, d)
}
}
if len(covered) == 0 {
h.Logf("[mesh-delivery] the answer to the ask %s names no ask that is asked now: nothing is merged on it", key)
return nil
}
sort.Slice(covered, func(i, j int) bool { return covered[i].ID < covered[j].ID })
first, now := covered[0].Ready, h.Now()
refuse := func(why string) {
h.Logf("[mesh-delivery] REFUSED the answer to the ask %s: %s", key, why)
for _, d := range covered {
d.Ready.AskID, d.Ready.Ask, d.Ready.Heads = "", nil, nil
d.Ready.Refused = "was answered, and the answer does not hold here: " + clip(oneLine(why), 300)
h.keep(d)
}
}
var heads []mdc.Head
for id, sha := range first.Heads {
m := h.deliveries[id]
if m == nil {
refuse("the head " + id + " it covers is no delivery of this owner")
return nil
}
heads = append(heads, mdc.Head{Repository: m.Repository, Number: m.Number, Head: sha})
}
if want, err := mdc.MergeAskKey(askGroup(covered[0]), heads); err != nil || want != key {
refuse(fmt.Sprintf("its key %s is not the key %s of the heads asked", key, want))
return nil
}
// The answer is to the asking asked now: the teller names the router id it was given in the warrant's id (`<ask id>@<time>`),
// and an answer to an earlier asking is never taken for a later one (issue 1370).
if now, at, ok := strings.Cut(ans.Warrant, "@"); ok && at != "" && now != mdc.AskingID(key, first.Asking) {
h.Logf("[mesh-delivery] the answer to the ask %s was given to an earlier asking (%s), not the one asked now (%s): not taken",
key, now, mdc.AskingID(key, first.Asking))
return nil
}
if ans.Option != OptionApprove && ans.Option != OptionDecline {
refuse("it chose " + ans.Option + ", which the ask did not offer")
return nil
}
var said []func()
for _, d := range covered {
r := d.Ready
r.Warrant = ans.Words
r.Repeats = 0 // the operator chose: whatever the tool refused before is asked afresh (issue 1106)
h.mergeAgainNow(d) // the operator chose: the merge is asked at once
switch ans.Option {
case OptionApprove:
r.Approved, r.Declined, r.AnsweredAsk = &now, nil, key
h.Logf("[mesh-delivery] %s: the operator approved its merge into main (%s)", d.ID, r.Warrant)
case OptionDecline:
r.Declined, r.AnsweredAsk = &now, key
h.Logf("[mesh-delivery] %s: the operator declined its merge into main (%s)", d.ID, r.Warrant)
said = append(said, h.sayOn(d, fmt.Sprintf("mesh-delivery: **not merged.** The merge of this pull request into main at %s was "+
"declined: %s. A new head asks the operator again.", d.Commit, r.Warrant)))
}
h.keep(d)
}
return said
}
// gateAsk is the gate's ask for a group at heads: the router's ask as its JSON in the value's ask, after the checks
// mesh-sdk no longer makes for it (novox/hq ADR 0627 §4): the ask's id is the one its group, heads and asking give, and
// it offers at least one option.
func gateAsk(group string, heads []mdc.Head, asking int, ask oc.Ask) (Ask, error) {
raw, err := json.Marshal(ask)
if err != nil {
return Ask{}, err
}
a := Ask{Group: group, Heads: heads, Asking: asking, Ask: raw}
if _, err := askOf(a); err != nil {
return Ask{}, err
}
return a, nil
}
// askOf reads the router's ask a gate ask carries and holds it to what the gate owes: the id its group, heads and asking
// give, and at least one option (occ.Carried). kvOperator.Ask holds every ask it writes to it, too.
func askOf(a Ask) (oc.Ask, error) {
key, err := mdc.MergeAskKey(a.Group, a.Heads)
if err != nil {
return oc.Ask{}, fmt.Errorf("an ask needs its group and at least one pull request's head: %w", err)
}
return occ.Carried(a.Ask, mdc.AskingID(key, a.Asking))
}
// kvOperator is the operator over the runtime: the ask is a key of the gate's bucket asks, the answer the teller's, read
// through its tool teller_answers.
type kvOperator struct{}
func (kvOperator) Ask(key string, a Ask) error {
if _, err := askOf(a); err != nil {
return err
}
raw, err := EncodeAsk(a)
if err != nil {
return err
}
if e, err := stdio.State("asks").Get(key); err == nil && e != nil {
if kept, err := DecodeAsk(e.Value); err == nil && SameAsk(kept, a) {
return nil // the same ask is there: the teller has it
}
}
_, err = stdio.State("asks").Put(key, json.RawMessage(raw))
return err
}
func (kvOperator) Cancel(key string) error { return stdio.State("asks").Delete(key) }
// Where reads the teller's answer through its tool (ADR 0596 §3), never its bucket.
func (kvOperator) Where(key string) (*Answer, error) { return answersOf(stdio.Ask, key) }
// standing is whether an ask is written under a key in the gate's asks, as this owner last knew. Under the lock.
func (h *Holder) standing(key string) bool {
for _, d := range h.deliveries {
if d.Ready != nil && d.Ready.AskedKey == key && d.Ready.Written {
return true
}
}
return false
}
// markWritten keeps whether an ask stands under a key, on every delivery that asked it. Under the lock.
func (h *Holder) markWritten(key string, written bool) {
for _, d := range h.deliveries {
if d.Ready != nil && d.Ready.AskedKey == key && d.Ready.Written != written {
d.Ready.Written = written
h.keep(d)
}
}
}
@@ -1,523 +0,0 @@
package main
import (
"encoding/json"
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// **A merge into main waits for the operator's Approve** (novox/hq ADR 0398 §3, issue 948): a marked, green delivery
// into main is asked of the operator, and merged only after an Approve whose warrant holds for that ask.
// markedGreen is a pull request into main, announced, judged green and marked ready at its head; the operator is not
// answering by himself (w.op.auto is off).
func markedGreen(t *testing.T) *world {
w := newWorld(t)
w.op.auto = false
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
p.Title = "Fix the apostrophe (issue 948, issue 12)"
w.h.PullUpdated(p)
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
return w
}
func TestAMarkedGreenDeliveryIntoMainIsAskedAndNotMergedUntilApproved(t *testing.T) {
w := markedGreen(t)
id := IDOf("novox/app", readyHead)
if len(w.forge.merges) != 0 {
t.Fatalf("merged before the operator's Approve: %v", w.forge.merges)
}
open := w.op.open()
if len(open) != 1 {
t.Fatalf("the operator is asked %v; wanted exactly one ask", open)
}
a := w.op.ask(open[0])
if !strings.HasPrefix(a.Headline, "Merge into main: app#7") || a.Who != occ.Operator ||
len(a.Options) != 2 || a.Options[0].ID != OptionApprove || a.Options[1].ID != OptionDecline {
t.Fatalf("the ask is not the merge ask: %+v", a)
}
for _, want := range []string{"issue 948", "issue 12", "Touches: "} {
if !strings.Contains(a.Explanation, want) {
t.Fatalf("the ask does not say %q: %s", want, a.Explanation)
}
}
if err := occ.Glance(a.Explanation); err != nil {
t.Fatalf("the ask does not read at a glance: %v", err)
}
// Ticks and minutes pass without an answer: not merged, not asked again.
for range 3 {
w.later(2 * time.Minute)
w.settleAll()
}
if len(w.forge.merges) != 0 || w.op.count() != 1 {
t.Fatalf("while waiting: merges %v, asks %d", w.forge.merges, w.op.count())
}
if d := w.delivery(id); d.Ready.AskID != open[0] || !strings.Contains(d.Ready.Waits, "Approve") {
t.Fatalf("the ask is not kept on the delivery: %+v", d.Ready)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 || w.forge.merges[0] != "novox/app#7@"+readyHead {
t.Fatalf("after the Approve it was merged %v; wanted once, at the marked head", w.forge.merges)
}
d := w.delivery(id)
if d.Ready.Approved == nil || !strings.Contains(d.Ready.Warrant, "chose Approve") {
t.Fatalf("the warrant is not kept: %+v", d.Ready)
}
w.h.PullMerged(merged("novox/app", 7, readyHead, "m7m7m7m7m7m7"))
w.settleAll()
var last Transition
for _, tr := range w.delivery(id).Transitions {
if tr.Event == EvMerged {
last = tr
}
}
if !strings.Contains(last.By, "approved: the operator, via telegram") {
t.Fatalf("the merge's transition does not name the operator's Approve: %+v", last)
}
}
// AC3 of issue 1348: an answer naming old heads merges nothing. The teller's answer to an ask that was withdrawn when the head
// moved is still in its bucket; the gate acts on the key of the heads asked now alone.
func TestAnAnswerNamingOldHeadsMergesNothing(t *testing.T) {
w := markedGreen(t)
old := w.op.open()[0]
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
w.settleAll()
w.op.answer(old, OptionApprove) // the teller wrote it after the ask was taken away
w.settleAll()
if len(w.forge.merges) != 0 || len(w.merges.puts) != 0 {
t.Fatalf("an answer to old heads merged: %v, handed %d", w.forge.merges, len(w.merges.puts))
}
if d := w.delivery(IDOf("novox/app", readyHead)); d.Ready != nil && d.Ready.Approved != nil {
t.Fatalf("an answer to old heads approved the old delivery: %+v", d.Ready)
}
}
// An answer under a key that is not the key of the heads the ask covers is refused and said: the gate acts on its own
// ask's key alone (the heads hash is the key's last part).
func TestAnAnswerUnderAKeyThatIsNotTheHeadsKeyIsRefused(t *testing.T) {
w := markedGreen(t)
id := IDOf("novox/app", readyHead)
key := w.op.open()[0]
w.h.mu.Lock()
w.h.deliveries[id].Ready.Heads[id] = strings.Repeat("9", 40) // the heads no longer give the key
w.h.mu.Unlock()
w.op.answer(key, OptionApprove)
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("an answer under another key merged: %v", w.forge.merges)
}
d := w.delivery(id)
if !strings.Contains(d.Ready.Refused, "does not hold") || d.Ready.Approved != nil {
t.Fatalf("the refused answer is not kept: %+v", d.Ready)
}
if lines := w.h.Stalled(); len(lines) != 1 || !strings.Contains(lines[0].Says, "answer") {
t.Fatalf("a refused answer is not said loudly: %+v", lines)
}
}
func TestADeclinedMergeIsNotMergedAndSaidOnce(t *testing.T) {
w := markedGreen(t)
w.op.answer(w.op.open()[0], OptionDecline)
for range 3 {
w.later(2 * time.Minute)
w.settleAll()
}
if len(w.forge.merges) != 0 || w.op.count() != 1 {
t.Fatalf("declined, yet merges %v and asks %d", w.forge.merges, w.op.count())
}
if len(w.forge.comments) != 1 || !strings.Contains(w.forge.comments[0], "declined") ||
!strings.Contains(w.forge.comments[0], "chose Decline") {
t.Fatalf("the Decline is not said once on the pull request, with its warrant: %v", w.forge.comments)
}
}
func TestAPushAfterTheAskCancelsItAndTheNewHeadIsAskedOnItsOwnWhenItsChecksPass(t *testing.T) {
w := markedGreen(t)
old := w.op.open()[0]
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.cancelled[0] != old || len(w.op.open()) != 0 {
t.Fatalf("the ask was not cancelled: cancelled %v, open %v", w.op.cancelled, w.op.open())
}
if len(w.forge.merges) != 0 || w.op.count() != 1 {
t.Fatalf("merges %v, asks %d", w.forge.merges, w.op.count())
}
if d := w.delivery(IDOf("novox/app", readyHead)); d.Ready.AskID != "" {
t.Fatalf("the cancelled ask is still kept as waiting: %+v", d.Ready)
}
// The new head is asked afresh once its checks pass.
w.h.Checked(judgedAt("novox/app", 7, pushed, "pass", "t1t1t1t1t1t1"))
w.settleAll()
if open := w.op.open(); len(open) != 1 || open[0] == old {
t.Fatalf("the new head was not asked afresh: %v", open)
}
// Issue 774 (2026-10-31): a pull request merged by another path than the Approve (the lead merged it once its check was
// green) left its ask in the gate's asks for a day, and the teller's ask held one of the router's three places. An ask whose
// every delivery is merged or ended is taken away at the next look, saying nothing on the pull request.
t.Run("774: an ask whose pull request merged without it is taken away", func(t *testing.T) {
w := markedGreen(t)
ask := w.op.open()[0]
comments := len(w.forge.comments)
w.h.PullMerged(merged("novox/app", 7, readyHead, "m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1"))
w.settleAll()
if len(w.op.open()) != 0 || len(w.op.cancelled) != 1 || w.op.cancelled[0] != ask {
t.Fatalf("the ask of a merged pull request stands: open %v, taken away %v", w.op.open(), w.op.cancelled)
}
if d := w.delivery(IDOf("novox/app", readyHead)); d.Ready.AskID != "" || d.Ready.Written {
t.Errorf("the merged delivery still keeps its ask: %+v", d.Ready)
}
w.later(2 * time.Minute)
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.count() != 1 || len(w.forge.comments) != comments {
t.Errorf("taken away %v, asks %d, comments %v: once, asking nothing again, saying nothing", w.op.cancelled, w.op.count(), w.forge.comments[comments:])
}
})
}
// The ask lapses at the router after AskLasts with nobody's answer, and the gate has no word of it: it takes its key away and
// writes it again, once, which the teller asks afresh (it asks again under the same id when the gate writes its key again).
func TestAnUnansweredAskIsWrittenAgainOnceWhenItLapses(t *testing.T) {
w := markedGreen(t)
first := w.op.open()[0]
w.later(AskLasts + time.Minute)
w.settleAll()
if w.op.count() != 2 || len(w.op.open()) != 1 || w.op.open()[0] != first || len(w.op.cancelled) != 0 {
t.Fatalf("a lapsed ask was not written again once, over the same key: %d asks, open %v, taken away %v", w.op.count(), w.op.open(), w.op.cancelled)
}
// Review round 1, G2: the router keeps an ask by its id and drops a second under it, so the second asking has an id of its own.
again := w.op.byKey[first]
againAsk := w.op.ask(first)
if again.Asking != 2 || againAsk.ID != mdc.AskingID(first, 2) || againAsk.ID == mdc.AskID(first) {
t.Fatalf("the second asking is %d under the id %s; wanted asking 2 under %s", again.Asking, againAsk.ID, mdc.AskingID(first, 2))
}
for range 3 {
w.later(2 * time.Minute)
w.settleAll()
}
if w.op.count() != 2 || len(w.forge.merges) != 0 {
t.Fatalf("asked more than once again: %d asks, merges %v", w.op.count(), w.forge.merges)
}
}
func TestAGroupIsAskedOnceForAllItsHeadsAndMergedInOnePassOnOneApprove(t *testing.T) {
w := newWorld(t)
w.op.auto = false
const ctlHead, catHead = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
w.h.Checked(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
w.forge.open = []OpenPull{openPull("novox/mesh-controller", 1, ctlHead),
openPull("novox/mesh-catalog", 2, catHead)}
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
if open := w.op.open(); len(open) != 1 || len(w.forge.merges) != 0 {
t.Fatalf("a group is asked once before any merge: open %v, merges %v", w.op.open(), w.forge.merges)
}
key := w.op.open()[0]
a := w.op.ask(key)
if !strings.Contains(a.Headline, "group feat") || !strings.Contains(a.Details, "mesh-controller#1") ||
!strings.Contains(a.Details, "mesh-catalog#2") {
t.Fatalf("the ask does not name the group and its members: %+v", a)
}
w.op.answer(key, OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 || w.op.count() != 1 {
t.Fatalf("both members were not merged in one pass on the one Approve: merges %v, asks %d", w.forge.merges, w.op.count())
}
}
// A push to a group member without a mark ends the group's ask (ADR 0398 §3, issue 948; review round 1): the Approve
// that follows would be for heads the group no longer has.
func TestAPushToAGroupMemberEndsTheGroupsAskAndApproveAndSaysSoOnEveryMember(t *testing.T) {
w := newWorld(t)
w.op.auto = false
const ctlHead, catHead, catPushed = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3"
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
w.h.Checked(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
w.forge.open = []OpenPull{openPull("novox/mesh-controller", 1, ctlHead),
openPull("novox/mesh-catalog", 2, catHead)}
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
first := w.op.open()
if len(first) != 1 {
t.Fatalf("asked %v", first)
}
// The catalog member is pushed; its new head has no mark yet.
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catPushed, "feat/x", "modules/app/module.json"))
w.forge.open = []OpenPull{openPull("novox/mesh-controller", 1, ctlHead),
{Owner: "novox", Repo: "mesh-catalog", Number: 2, Base: "main", HeadSHA: catPushed}}
w.h.SeeOpenPulls()
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.cancelled[0] != first[0] {
t.Fatalf("the group's ask was not cancelled by the push: %v", w.op.cancelled)
}
w.op.answer(first[0], OptionApprove) // a late answer to the cancelled ask
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catPushed, "pass", "t3t3t3t3t3t3"))
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-2", Verdict: "pass", Summary: "both compose again"})
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("merged on an Approve of heads the group no longer has: %v", w.forge.merges)
}
}
// mergedSaid is the module events `merged` the world has said.
func mergedSaid(w *world) []map[string]any {
w.mu.Lock()
defer w.mu.Unlock()
var out []map[string]any
for _, m := range w.said {
if m["event-kind"] == "merged" {
out = append(out, m)
}
}
return out
}
// Issue 1025: a merge on the operator's Approve is announced once, with the warrant's ask, option and heads.
func TestIssue1025AMergeOnTheApproveIsAnnouncedWithItsWarrant(t *testing.T) {
w := markedGreen(t)
id := IDOf("novox/app", readyHead)
open := w.op.open()
if len(open) != 1 {
t.Fatalf("asks %v", open)
}
if got := mergedSaid(w); len(got) != 0 {
t.Fatalf("announced before the merge: %v", got)
}
d := w.delivery(id)
wantHeads := map[string]string{}
for k, v := range d.Ready.Heads {
wantHeads[k] = v
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
got := mergedSaid(w)
if len(got) != 1 {
t.Fatalf("announced %d times; wanted once: %v", len(got), got)
}
m := got[0]
if m["owner"] != "novox" || m["repo"] != "app" || m["number"] != float64(7) || m["head_sha"] != readyHead ||
m["ask_id"] != mdc.AskID(open[0]) || m["asker"] != tellerModule || m["option"] != OptionApprove {
t.Fatalf("the event does not name the pull request and its warrant: %v", m)
}
hs, _ := m["heads"].(map[string]any)
if len(hs) == 0 || len(hs) != len(wantHeads) {
t.Fatalf("heads %v; wanted %v", hs, wantHeads)
}
for k, v := range wantHeads {
if hs[k] != v {
t.Fatalf("heads %v; wanted %v", hs, wantHeads)
}
}
if g, ok := m["group"]; ok && g != "" {
t.Fatalf("a lone delivery names group %v", g)
}
w.later(2 * time.Minute)
w.settleAll()
if n := len(mergedSaid(w)); n != 1 {
t.Fatalf("announced %d times after more ticks", n)
}
}
// Issue 1025: a declined merge announces nothing.
func TestIssue1025ADeclinedMergeAnnouncesNothing(t *testing.T) {
w := markedGreen(t)
open := w.op.open()
w.op.answer(open[0], OptionDecline)
w.settleAll()
if got := mergedSaid(w); len(got) != 0 || len(w.forge.merges) != 0 {
t.Fatalf("declined, yet merges %v, announced %v", w.forge.merges, got)
}
}
// Issue 1025: a merge of a delivery in a group carries the group id and the heads the group's Approve was bound to.
func TestIssue1025AMergeInAGroupCarriesTheGroupAndItsHeads(t *testing.T) {
w := newWorld(t)
w.op.auto = false
const ctlHead, catHead = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
ctl, cat := IDOf("novox/mesh-controller", ctlHead), IDOf("novox/mesh-catalog", catHead)
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
w.h.Checked(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
w.forge.open = []OpenPull{openPull("novox/mesh-controller", 1, ctlHead),
openPull("novox/mesh-catalog", 2, catHead)}
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
open := w.op.open()
if len(open) != 1 {
t.Fatalf("asks %v", open)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
got := mergedSaid(w)
if len(got) != 2 {
t.Fatalf("announced %d; wanted each member's merge once, from the one pass: %v", len(got), got)
}
for _, m := range got {
hs, _ := m["heads"].(map[string]any)
if m["group"] != "feat/x" || m["ask_id"] != mdc.AskID(open[0]) || m["asker"] != tellerModule || m["option"] != OptionApprove ||
hs[ctl] != ctlHead || hs[cat] != catHead || len(hs) != 2 {
t.Fatalf("the group's merge event: %v", m)
}
}
}
// Issue 1025: the announcement is owed and kept, so it is said after a restart, and once.
func TestIssue1025AnOwedMergedIsSaidAfterARestartAndOnce(t *testing.T) {
w := newWorld(t)
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
w.h.PullUpdated(p)
id := IDOf("novox/app", readyHead)
w.h.mu.Lock()
d := w.h.deliveries[id]
d.Owed = append(d.Owed, Effect{Kind: EffectEmit, Event: "merged", Line: `{"owner":"novox","repo":"app","number":7,"head_sha":"` + readyHead + `"}`, Since: w.now})
if err := w.store.PutDelivery(d); err != nil {
t.Fatal(err)
}
w.h.mu.Unlock()
again := newWorldOver(t, w.store, w.forge, w.now.Add(time.Minute))
again.settleAll()
again.later(time.Hour)
again.settleAll()
if n := len(mergedSaid(again)); n != 1 {
t.Fatalf("said %d times after the restart; wanted once", n)
}
}
// Review round 1, G2: an answer given to the first asking is not taken for the second, and the second's Approve is announced
// under the second asking's id.
func TestAnAnswerToAnEarlierAskingIsNotTakenAndTheMergeNamesTheAnsweredAsking(t *testing.T) {
w := markedGreen(t)
key := w.op.open()[0]
w.later(AskLasts + time.Minute)
w.settleAll() // asked again: the second asking
w.op.mu.Lock()
w.op.answers[key] = &Answer{Option: OptionApprove, Warrant: mdc.AskingID(key, 1) + "@2026-10-06T12:00:00Z", Words: "the operator, via telegram, chose Approve"}
w.op.mu.Unlock()
w.settleAll()
if len(w.forge.merges) != 0 || len(w.merges.puts) != 0 {
t.Fatalf("an answer to the first asking merged on the second: %v", w.forge.merges)
}
w.op.answer(key, OptionApprove) // the answer to the asking asked now
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("the answer to the second asking did not merge: %v", w.forge.merges)
}
say := mergedSaid(w)
if len(say) != 1 || say[0]["ask_id"] != mdc.AskingID(key, 2) {
t.Fatalf("the merge is announced %v; wanted the ask id of asking 2, %s", say, mdc.AskingID(key, 2))
}
}
// 2026-10-29 (hq#495): a group declined under one ask, a member pushed, and the group asked and approved again at the new
// heads: the member whose head did not move kept the earlier Decline beside the new Approve, and nothing was handed, with
// no word. An answer stands only for the ask it answers; whichever member is pushed, both merge on the new Approve.
func TestADeclineOfAnEarlierAskDoesNotHoldTheApproveOfTheNext(t *testing.T) {
const ctlHead, catHead, pushed = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3"
for _, push := range []struct {
repo string
number int
trunk string
}{{"novox/mesh-catalog", 2, "t3t3t3t3t3t3"}, {"novox/mesh-controller", 1, "t1t1t1t1t1t1"}} {
t.Run(push.repo, func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
heads := map[string]string{"novox/mesh-controller": ctlHead, "novox/mesh-catalog": catHead}
paths := map[string]string{"novox/mesh-controller": "internal/a.go", "novox/mesh-catalog": "modules/app/module.json"}
numbers := map[string]int{"novox/mesh-controller": 1, "novox/mesh-catalog": 2}
trunks := map[string]string{"novox/mesh-controller": "t1t1t1t1t1t1", "novox/mesh-catalog": "t3t3t3t3t3t3"}
round := func(check string) {
var open []OpenPull
for _, repo := range []string{"novox/mesh-controller", "novox/mesh-catalog"} {
w.h.PullUpdated(pr(repo, numbers[repo], heads[repo], "feat/x", paths[repo]))
w.h.Checked(judgedAt(repo, numbers[repo], heads[repo], "pass", trunks[repo]))
open = append(open, openPull(repo, numbers[repo], heads[repo]))
}
w.forge.open = open
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: check, Verdict: "pass", Summary: "both compose"})
w.settleAll()
}
round("check-1")
w.op.answer(w.op.open()[0], OptionDecline)
w.settleAll()
heads[push.repo] = pushed
round("check-2")
open := w.op.open()
if len(open) != 1 {
t.Fatalf("the group at its new heads is not asked once: %v", open)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("approved at the new heads, yet merged %v; stalled %+v", w.forge.merges, w.h.Stalled())
}
})
}
}
// mesh-sdk no longer refuses an ask whose id is another's or that offers no option (novox/hq ADR 0627 §4, ruling 1): the gate
// holds both, when it composes its ask and when kvOperator writes it.
func TestTheGateRefusesAnAskWithAnotherIdOrNoOption(t *testing.T) {
heads := []mdc.Head{{Repository: "novox/app", Number: 7, Head: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
key, err := mdc.MergeAskKey("feat/g", heads)
if err != nil {
t.Fatal(err)
}
option := oc.Option{ID: OptionApprove, Label: "Approve", Level: occ.Approve}
for _, c := range []struct {
name string
asking int
ask oc.Ask
refuse string
}{
{"the first asking's id", 1, oc.Ask{ID: mdc.AskingID(key, 1), Options: []oc.Option{option}}, ""},
{"the second asking's id", 2, oc.Ask{ID: mdc.AskingID(key, 2), Options: []oc.Option{option}}, ""},
// The refusals are operator-channel's client's (occ.Carried), held to the id the gate's key and asking give.
{"another id", 1, oc.Ask{ID: "another-id", Options: []oc.Option{option}}, "the id it was kept under"},
{"the first asking's id at the second asking", 2, oc.Ask{ID: mdc.AskingID(key, 1), Options: []oc.Option{option}}, "the id it was kept under"},
{"no option", 1, oc.Ask{ID: mdc.AskingID(key, 1)}, "offers no option"},
} {
a, err := gateAsk("feat/g", heads, c.asking, c.ask)
if c.refuse == "" {
if err != nil {
t.Errorf("%s: refused: %v", c.name, err)
}
continue
}
if err == nil || !strings.Contains(err.Error(), c.refuse) {
t.Errorf("%s: composed (%v), wanted a refusal saying %q", c.name, err, c.refuse)
}
// The same ask written by hand, past gateAsk, is refused where kvOperator writes it.
raw, _ := json.Marshal(c.ask)
if err := (kvOperator{}).Ask(key, Ask{Group: "feat/g", Heads: heads, Asking: c.asking, Ask: raw}); err == nil || !strings.Contains(err.Error(), c.refuse) {
t.Errorf("%s: kvOperator wrote it (%v, %v)", c.name, err, a)
}
}
// A value that carries no router's ask is refused before it is written.
if err := (kvOperator{}).Ask(key, Ask{Group: "feat/g", Heads: heads, Asking: 1, Ask: json.RawMessage(`"an ask"`)}); err == nil ||
!strings.Contains(err.Error(), "carries no operator-channel ask") {
t.Errorf("a value carrying no ask: %v", err)
}
}
@@ -1,311 +0,0 @@
package main
import (
"errors"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// fakeMerges is the gate's bucket merges with the forge bridge behind it. Every pull request the gate writes in the state
// merging is merged on the fake forge, as the bridge's merge does (mesh-delivery modules/mesh-delivery-forge-bridge
// internal/bridge acts.go MergeActs): the forge's merge tool is asked at the head with the approved heads of the whole
// group, `<repository>@<full sha>` sorted, and what the fake forge answers is written on FORGE as the bridge writes it, a
// merged message or a refused one with the forge's words. With manual set the bridge says nothing.
type fakeMerges struct {
mu sync.Mutex
forge *fakeForge
h *Holder
values map[string]Merge
puts []Merge
manual bool
failed error
// said are the messages the bridge wrote on FORGE, in order.
said []forgemsg.Forge
}
func newFakeMerges(f *fakeForge) *fakeMerges {
return &fakeMerges{forge: f, values: map[string]Merge{}}
}
func (m *fakeMerges) Get(group string) (*Merge, error) {
m.mu.Lock()
defer m.mu.Unlock()
if v, ok := m.values[group]; ok {
return &v, nil
}
return nil, nil
}
func (m *fakeMerges) Put(group string, v Merge) error {
if _, err := EncodeMerge(v); err != nil {
return err
}
m.mu.Lock()
if m.failed != nil {
defer m.mu.Unlock()
return m.failed
}
m.values[group] = v
m.puts = append(m.puts, v)
manual := m.manual
m.mu.Unlock()
if manual {
return nil
}
for _, p := range v.Pulls {
if p.State != mdc.MergeMerging {
continue
}
owner, repo, _ := strings.Cut(p.Repository, "/")
var approved []string
for key, sha := range v.Approved {
repository, _, _ := strings.Cut(key, "@")
approved = append(approved, repository+"@"+sha)
}
sortStrings(approved)
f := m.forge
f.mu.Lock()
f.merges = append(f.merges, owner+"/"+repo+"#"+itoa(p.Number)+"@"+p.Head.Head)
f.approvedHeads = append(f.approvedHeads, approved)
err := f.refuseRepo[owner+"/"+repo]
if err == nil {
err = f.refuse
}
f.mu.Unlock()
msg := forgemsg.Forge{Repository: p.Repository, Number: p.Number, Head: p.Head.Head}
if err != nil {
msg.Kind, msg.Act, msg.Words = forgemsg.ForgeRefused, "merge", err.Error()
} else {
msg.Kind, msg.Merge, msg.Root = forgemsg.ForgeMerged, strings.Repeat("c", 40), "root"
msg.Trees = map[string]string{"app": "tree"}
}
m.say(msg)
}
return nil
}
// say writes a message on FORGE and has the gate hear it, as the runtime does.
func (m *fakeMerges) say(msg forgemsg.Forge) {
body, err := forgemsg.EncodeForge(msg)
if err != nil {
panic(err)
}
event, err := forgemsg.ForgeEvent(msg.Repository, msg.Kind)
if err != nil {
panic(err)
}
m.mu.Lock()
m.said = append(m.said, msg)
m.mu.Unlock()
m.h.Forged("mesh-delivery-forge-bridge."+event, body)
}
// last is the newest value written for a group.
func (m *fakeMerges) last(group string) Merge {
m.mu.Lock()
defer m.mu.Unlock()
return m.values[group]
}
// fakeOld is the gate's old states merges and owed, which only the switch reads.
type fakeOld struct {
mu sync.Mutex
states map[string]map[string]oldEntry
}
type oldEntry struct {
raw []byte
revision uint64
}
func (o *fakeOld) put(state, key string, raw []byte, revision uint64) {
o.mu.Lock()
defer o.mu.Unlock()
if o.states[state] == nil {
o.states[state] = map[string]oldEntry{}
}
o.states[state][key] = oldEntry{raw: raw, revision: revision}
}
func (o *fakeOld) Keys(state string) ([]string, error) {
o.mu.Lock()
defer o.mu.Unlock()
var out []string
for k := range o.states[state] {
out = append(out, k)
}
sort.Strings(out)
return out, nil
}
func (o *fakeOld) Get(state, key string) ([]byte, uint64, error) {
o.mu.Lock()
defer o.mu.Unlock()
e, ok := o.states[state][key]
if !ok {
return nil, 0, nil
}
return e.raw, e.revision, nil
}
// fakeSource is the gate's bucket source with its one key.
type fakeSource struct {
mu sync.Mutex
main *Source
moves []Source
}
func (s *fakeSource) Move(update func(prev *Source) (Source, bool)) error {
s.mu.Lock()
defer s.mu.Unlock()
next, write := update(s.main)
if !write {
return nil
}
if _, err := mdc.EncodeSource(next); err != nil {
return err
}
s.main = &next
s.moves = append(s.moves, next)
return nil
}
// Read is a branch's source key as held, and its revision: the number of writes. The fake holds the key of MergeBranch only.
func (s *fakeSource) Read(branch string) (*Source, uint64, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.main == nil || branch != MergeBranch {
return nil, 0, nil
}
held := *s.main
return &held, uint64(len(s.moves)), nil
}
// Branches are the branches whose source key the fake holds.
func (s *fakeSource) Branches() ([]string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.main == nil {
return nil, nil
}
return []string{MergeBranch}, nil
}
func sortStrings(s []string) { sort.Strings(s) }
func itoa(n int) string { return strconv.Itoa(n) }
// fakeOwed is the seat's event owed with the forge bridge behind it: every act said is performed on the fake forge, as the
// bridge performs it through the seat git (mesh-delivery modules/mesh-delivery-forge-bridge internal/bridge owed.go), once
// for each id, and answered as owed.ended performed unless silent is set. With down set nothing can be said (the bus is
// away); the forge's own refusals are the bridge's to say, not the gate's to retry.
type fakeOwed struct {
mu sync.Mutex
forge *fakeForge
h *Holder
down bool
silent bool
// keys are the latest act said under each key, in the gate's working form, writes every key said in order, and said
// every event in order.
keys map[string]Owed
writes []string
said []seat.Owed
ids map[string]bool
}
func newFakeOwed(f *fakeForge) *fakeOwed {
return &fakeOwed{forge: f, keys: map[string]Owed{}, ids: map[string]bool{}}
}
func (o *fakeOwed) Say(v seat.Owed) error {
if _, _, _, switched := mdc.ParseSwitchedActID(v.ID); switched {
o.mu.Lock()
defer o.mu.Unlock()
o.said = append(o.said, v) // the bridge counts it against what the old bridge performed
return nil
}
key, _, found := strings.Cut(v.ID, "@")
if !found {
return fmt.Errorf("the act's id %q holds no @", v.ID)
}
repository, number, head, _, err := mdc.ParseOwedKey(key)
if err != nil {
return err
}
if repository != v.Repository || int64(number) != v.Number || head != v.Head && v.Commit != head {
return fmt.Errorf("the act %s names %s#%d at %s", v.ID, v.Repository, v.Number, v.Head)
}
o.mu.Lock()
if o.down {
defer o.mu.Unlock()
return errors.New("the bus is away")
}
o.said = append(o.said, v)
if o.ids[v.ID] {
// Said again: the bridge performs it once and answers it again from what it performed.
silent, h := o.silent, o.h
o.mu.Unlock()
if !silent && h != nil {
return h.OwedEnded(owedEndedBody(v.ID, forgemsg.OwedPerformed, ""))
}
return nil
}
o.ids[v.ID] = true
working := Owed{Act: v.Act, Context: v.Context, State: v.State, Target: v.Target, Text: v.Text}
switch v.Act {
case seat.OwedActStatus:
working.Text = v.Description
case seat.OwedActNote:
working.Text = v.Line
}
o.keys[key] = working
o.writes = append(o.writes, key)
silent, h := o.silent, o.h
o.mu.Unlock()
owner, repo, _ := strings.Cut(repository, "/")
f := o.forge
switch v.Act {
case seat.OwedActNote:
_ = f.Note(owner, repo, head, v.Line)
case seat.OwedActView:
_ = f.View(owner, repo, number, v.Text)
case seat.OwedActStatus:
_ = f.Status(owner, repo, head, v.Context, v.State, v.Description, v.Target)
case seat.OwedActComment:
_ = f.Comment(owner, repo, number, v.Text)
}
if !silent && h != nil {
return h.OwedEnded(owedEndedBody(v.ID, forgemsg.OwedPerformed, ""))
}
return nil
}
// owedEndedBody is the forge bridge's owed.ended for one of the gate's ids.
func owedEndedBody(id, outcome, words string) []byte {
raw, err := forgemsg.EncodeOwedEnded(forgemsg.OwedEnded{Of: seat.Seat, ID: id, Outcome: outcome, Words: words,
At: time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)})
if err != nil {
panic(err)
}
return raw
}
// acts are the keys written whose act is of a kind, in order.
func (o *fakeOwed) acts(kind string) []string {
o.mu.Lock()
defer o.mu.Unlock()
var out []string
for _, k := range o.writes {
if _, _, _, act, err := mdc.ParseOwedKey(k); err == nil && actKind(act) == kind {
out = append(out, k)
}
}
return out
}
@@ -5,8 +5,6 @@ package main
import (
"git.novox.be/novox/mesh-sdk/go/buscalls"
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"bufio"
"context"
"encoding/json"
@@ -15,16 +13,12 @@ import (
"io"
"os"
"os/exec"
"reflect"
"strconv"
"strings"
"sync"
"testing"
"time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
stdio "git.novox.be/novox/mesh-sdk/go"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
@@ -133,8 +127,7 @@ type runtime struct {
// operatorCancels are the ids of the asks taken back from the router, in order (issue 1079).
operatorCancels []string
// owedSaid are the subjects of the seat events owed the gate said, in order (W11.17).
owedSaid []string
openPulls []OpenPull
owedSaid []string
}
type askedOfMesh struct {
@@ -358,43 +351,10 @@ func (r *runtime) answer(m rpcMessage) {
}
r.mu.Lock()
r.asked = append(r.asked, askedOfMesh{Key: a.Key, Body: a.Body, At: time.Now()})
pulls := append([]OpenPull{}, r.openPulls...)
r.mu.Unlock()
switch a.Key {
case "":
return nil, errors.New("an ask with no key")
case "seat:git.open-pulls":
var open []gitseat.OpenPull
for _, p := range pulls {
open = append(open, seatOpenPull(p))
}
return gitseat.OpenPullsAnswer{Pulls: open}, nil
case "seat:git.pull":
number, _ := a.Body["number"].(float64)
repository, _ := a.Body["repository"].(string)
for _, p := range pulls {
if p.Owner+"/"+p.Repo == repository && p.Number == int(number) {
return gitseat.PullAnswer{Pull: &gitseat.Pull{Repository: repository, Number: int64(p.Number), Base: p.Base,
Head: p.HeadSHA, State: gitseat.PullStateOpen}, Merging: &gitseat.Merging{Mergeable: true, Protected: true,
Required: []string{"mesh/check"}, Writes: true}}, nil
}
}
return gitseat.PullAnswer{Error: "no such pull request"}, nil
case "seat:git.statuses":
// Green: mesh/check a success at the head asked, as the seat answers.
commit, _ := a.Body["commit"].(string)
return gitseat.StatusesAnswer{Commit: commit, Statuses: []gitseat.Status{{Context: "mesh/check", State: "success"}}}, nil
case "mesh-delivery-teller.teller_answers":
key, _ := a.Body["key"].(string)
r.b.mu.Lock()
raw, found := r.b.answers[key]
r.b.mu.Unlock()
if !found {
return map[string]any{"key": key, "found": false}, nil
}
return map[string]any{"key": key, "found": true, "answer": json.RawMessage(raw)}, nil
case "mesh-delivery-view.view_reach":
return map[string]any{"reach": []json.RawMessage{}}, nil
case "gitea.gitea_note_append", "gitea.gitea_delivery_view", "gitea.gitea_commit_status", "gitea.gitea_merge_pull_request",
"gitea.gitea_add_comment":
return toolAnswer(map[string]any{}), nil
@@ -557,377 +517,3 @@ func (b *testBus) publish(t *testing.T, subject string, body any) time.Time {
}
return time.Now()
}
func TestAPassingVerdictAsksTheApproveAndItsApproveMergesWithinSecondsOverARealBus(t *testing.T) {
b := newTestBus(t)
ctx := context.Background()
rt := b.start(t)
evctx, cancel := context.WithCancel(ctx)
defer cancel()
rt.deliverEvents(evctx)
// The module has started when it hears what it follows.
rt.untilListening()
for _, k := range followed {
if strings.Contains(k, "labeled") || strings.Contains(k, "commented") {
t.Fatalf("the module follows %s: a label or a comment starts nothing here (ADR 0421 §4)", k)
}
}
// A pull request into main is announced, and the controller's verdict on its head passes.
const head = "1111111111111111111111111111111111111111"
announced := pr("novox/app", 7, head, "fix/x", "src/a.go")
announced.BaseSHA = "a1a1a1a1a1a1"
announced.Title = "Fix the apostrophe (issue 975)"
b.publish(t, "mesh.seat.git.event.pull-updated", seatUpdated(announced))
rt.until("the pull request's key to hold its head", eventWithin, func() bool {
e, err := b.kvs["pulls"].Get(ctx, "novox.app.7")
var k PullKey
return err == nil && json.Unmarshal(e.Value(), &k) == nil && k.Head == head && k.Trunk == "a1a1a1a1a1a1"
})
b.checkPasses(t, rt, "fix/x", "a1a1a1a1a1a1", judgedPull{"novox/app", 7, head})
// The description holds no review line and nothing is waited for: the ask is written into the gate's bucket asks within
// seconds of the verdict, with no label, comment or mark written by anyone (the gate reads no description, ADR 0493 §6, ADR 0659).
edit := time.Now()
var key string
rt.until("the ask to be written", eventWithin, func() bool { key = onlyAsk(b); return key != "" })
ask, err := askOf(askAt(t, b, key))
if err != nil {
t.Fatal(err)
}
t.Logf("the ask was written %s after the verdict", time.Since(edit))
if strings.Contains(strings.ToLower(ask.Explanation), "review") || ask.ID != mdc.AskID(key) {
t.Fatalf("the ask names a review or has the wrong id: %s", ask.Explanation)
}
if len(rt.asksOf("gitea.gitea_merge_pull_request")) != 0 {
t.Fatalf("merged before the Approve")
}
// The pull request's key holds its head, the trunk commit it was asked against and the verdict.
rt.until("the pull request's key to hold head, trunk and verdict", eventWithin, func() bool {
e, err := b.kvs["pulls"].Get(ctx, "novox.app.7")
var k PullKey
return err == nil && json.Unmarshal(e.Value(), &k) == nil && k == PullKey{Head: head, Trunk: "a1a1a1a1a1a1", Verdict: "pass"}
})
// What the transitions owe the forge is written as owed keys; the gate calls no tool of the forge that writes (issue 1349).
for _, tool := range []string{"gitea.gitea_commit_status", "gitea.gitea_delivery_view", "gitea.gitea_note_append", "gitea.gitea_add_comment"} {
if n := len(rt.asksOf(tool)); n != 0 {
t.Fatalf("the gate asked %s %d times: the bridge writes to the forge", tool, n)
}
}
rt.until("the acts owed to be said as the seat's event owed", eventWithin, func() bool {
rt.mu.Lock()
defer rt.mu.Unlock()
return len(rt.owedSaid) > 0 && strings.HasSuffix(rt.owedSaid[0], ".7")
})
// The operator approves; the teller holds the warrant and writes the answer: the merge is handed to the bridge within
// seconds, with the Approve's heads, and the gate asks the forge nothing.
approved := b.answer(t, key, OptionApprove)
var handed Merge
rt.until("the merge to be handed to the bridge", eventWithin, func() bool { handed = mergeAt(b, "fix/x"); return len(handed.Pulls) > 0 })
if len(handed.Pulls) != 1 || handed.Pulls[0].Head.Head != head || handed.Pulls[0].Number != 7 || handed.Pulls[0].State != mdc.MergeMerging ||
handed.Approved[mdc.ApprovedKey("novox/app", 7)] != head || handed.Ask != key {
t.Fatalf("the merge handed is %+v; wanted pull request 7 at the approved head, merging, under the ask %s", handed, key)
}
t.Logf("the merge was handed %s after the answer", time.Since(approved))
if n := len(rt.asksOf("gitea.gitea_merge_pull_request")); n != 0 {
t.Fatalf("the gate asked the forge to merge %d times: the bridge merges", n)
}
// The bridge says it merged; the mesh's source then holds the merge, in one write.
b.forgeSays(t, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/app", Number: 7, Head: head, Merge: strings.Repeat("b", 40),
Root: "r", Trees: map[string]string{"app": "t"}})
// The forge's own word that the pull request merged follows the bridge's (its webhook): the delivery ends merged.
b.publish(t, "mesh.seat.git.event.pull-merged", seatMerged(merged("novox/app", 7, head, strings.Repeat("b", 40))))
rt.until("source.main to hold the merge", eventWithin, func() bool {
e, err := b.kvs["source"].Get(ctx, mdc.SourceKey(MergeBranch))
if err != nil {
return false
}
src, err := mdc.DecodeSource(e.Value())
return err == nil && src.Group == "fix/x" && src.Heads["novox/app"] == strings.Repeat("b", 40)
})
// The seat's verb source answers it from the bucket, with its revision on the bus.
said := strings.ReplaceAll(rt.call(t, Seat+".source", map[string]any{}), `\"`, `"`)
if !strings.Contains(said, `{"head":"`+strings.Repeat("b", 40)+`","repository":"novox/app","trunk":"main"}`) || !strings.Contains(said, `"revision":`) {
t.Fatalf("source answers %s", said)
}
// The delivery ends merged (ADR 0539 §17), and the gate asked the controller nothing (R6-C3).
rt.until("the delivery to be merged", eventWithin, func() bool {
e, err := b.kvs["deliveries"].Get(ctx, kvKey(IDOf("novox/app", head)))
var d Delivery
return err == nil && json.Unmarshal(e.Value(), &d) == nil && d.State == Merged
})
rt.noControllerAsk()
// W11.16 (acceptance 34, 39): the delivery is traced in one record of the ledger, which says its fill; the gate is stopped
// and started again over the same bus and ledger: the record is whole and opened once, the merge is not handed again, and
// the ask of the Approve already given is not asked again.
recordsNow := func(r *runtime) map[string]any {
var said struct {
Result struct {
Content []struct{ Text string } `json:"content"`
} `json:"result"`
}
raw := r.call(t, "gate_records", map[string]any{})
if err := json.Unmarshal([]byte(raw), &said); err != nil || len(said.Result.Content) == 0 {
t.Fatalf("gate_records answered %s", raw)
}
var out map[string]any
if err := json.Unmarshal([]byte(said.Result.Content[0].Text), &out); err != nil {
t.Fatalf("gate_records says %s", said.Result.Content[0].Text)
}
return out
}
before := recordsNow(rt)
if kept, _ := before["kept"].(float64); kept != 1 || before["fill"] == nil || before["fill"] == "" {
t.Fatalf("one delivery is traced in %v records, fill %v", before["kept"], before["fill"])
}
merges := len(rt.mergesSaid())
_ = rt.in.Close()
_ = rt.cmd.Wait()
rt2 := b.start(t)
evctx2, cancel2 := context.WithCancel(ctx)
defer cancel2()
rt2.deliverEvents(evctx2)
rt2.untilListening()
after := recordsNow(rt2)
if !reflect.DeepEqual(before["records"], after["records"]) || after["kept"] != before["kept"] {
t.Fatalf("the record changed over a restart: %v then %v", before["records"], after["records"])
}
rt2.until("the restarted gate to hold the delivery merged", eventWithin, func() bool {
e, err := b.kvs["deliveries"].Get(ctx, kvKey(IDOf("novox/app", head)))
var d Delivery
return err == nil && json.Unmarshal(e.Value(), &d) == nil && d.State == Merged
})
if n := len(rt2.mergesSaid()); n != merges || len(onlyAskKeys(b)) > 1 {
t.Fatalf("the restarted gate said the merge %d more times (%d before), or asked again: %v", n, merges, onlyAskKeys(b))
}
// Quiet follows: nothing asks the forge or the controller again with no event to say so.
quiet := len(rt.asked)
time.Sleep(1500 * time.Millisecond)
rt.mu.Lock()
more := len(rt.asked) - quiet
rt.mu.Unlock()
if more > 3 {
t.Fatalf("%d asks of the mesh in a quiet second and a half after the merge: something polls", more)
}
}
// onlyAskKeys are the keys of every ask in the gate's bucket asks.
func onlyAskKeys(b *testBus) []string {
keys, _ := b.kvs["asks"].Keys(context.Background())
return keys
}
// onlyAsk is the key of the one ask in the gate's bucket asks, or empty.
func onlyAsk(b *testBus) string {
keys, err := b.kvs["asks"].Keys(context.Background())
if err != nil || len(keys) != 1 {
return ""
}
return keys[0]
}
// askNow is the ask under a key at this moment; false in the instant a rewrite has taken the key away and not yet written it.
func askNow(b *testBus, key string) (Ask, bool) {
e, err := b.kvs["asks"].Get(context.Background(), key)
if err != nil {
return Ask{}, false
}
a, err := DecodeAsk(e.Value())
return a, err == nil
}
func askNowOrFatal(t *testing.T, b *testBus, key string) Ask {
t.Helper()
a, ok := askNow(b, key)
if !ok {
t.Fatalf("no ask under %s to answer", key)
}
return a
}
func askAt(t *testing.T, b *testBus, key string) Ask {
t.Helper()
e, err := b.kvs["asks"].Get(context.Background(), key)
if err != nil {
t.Fatalf("the ask %s: %v", key, err)
}
a, err := DecodeAsk(e.Value())
if err != nil {
t.Fatal(err)
}
return a
}
func mergeAt(b *testBus, group string) Merge {
e, err := b.kvs["merges"].Get(context.Background(), group)
if err != nil {
return Merge{}
}
m, _ := DecodeMerge(e.Value())
return m
}
// answer is the teller's answer to an ask, written under the ask's own key in its bucket answers.
func (b *testBus) answer(t *testing.T, key, option string) time.Time {
t.Helper()
// The warrant's id is the router id of the asking answered and the time (the teller's warrantID).
asked, err := askOf(askNowOrFatal(t, b, key))
if err != nil {
t.Fatal(err)
}
raw, err := mdc.EncodeAnswer(Answer{Option: option, Warrant: asked.ID + "@2026-10-20T03:00:00Z", Words: "the operator, via telegram, chose " + option})
if err != nil {
t.Fatal(err)
}
// The teller writes the answer under the ask's key and says it as the event answered (ADR 0596 §2).
b.mu.Lock()
b.answers[key] = raw
b.mu.Unlock()
b.publish(t, "mesh.mod.mesh-delivery-teller.event.answered", map[string]any{"key": key, "ask": asked.ID, "answer": json.RawMessage(raw)})
return time.Now()
}
// forgeSays is the forge bridge's message on FORGE, as its stream carries it.
func (b *testBus) forgeSays(t *testing.T, f forgemsg.Forge) {
t.Helper()
raw, err := forgemsg.EncodeForge(f)
if err != nil {
t.Fatal(err)
}
event, err := forgemsg.ForgeEvent(f.Repository, f.Kind)
if err != nil {
t.Fatal(err)
}
if _, err := b.js.Publish(context.Background(), "mesh.mod."+forgeBridgeModule+".event."+event, raw); err != nil {
t.Fatal(err)
}
}
// **The acceptance of one group, one Approve** (novox/hq ADR 0418 §4, ADR 0421, ADR 0441 §5, design 47 milestone 3, issues 978
// and 1022): a group of two pull requests on one branch name, in two repositories, with green checks, gets one ask of the
// operator — no label, comment or mark written by anyone — and on the Approve the members are handed to the bridge one at a time,
// the second after the first merged, each with the approved heads of both.
func TestAGroupOfTwoWithGreenChecksGetsOneAskAndOnItsApproveBothMergeOneAfterTheOtherOverARealBus(t *testing.T) {
b := newTestBus(t)
ctx := context.Background()
rt := b.start(t)
evctx, cancel := context.WithCancel(ctx)
defer cancel()
rt.deliverEvents(evctx)
rt.untilListening()
const ctlHead, catHead = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
for _, p := range []PullEvent{pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"),
pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json")} {
p.BaseSHA = "a1a1a1a1a1a1"
b.publish(t, "mesh.seat.git.event.pull-updated", seatUpdated(p))
}
// Both members pass at the checker, whose one verdict covers both heads: the group is asked once, no composed check is asked
// of the controller (issue 1318).
b.checkPasses(t, rt, "feat/x", "a1a1a1a1a1a1", judgedPull{"novox/mesh-controller", 1, ctlHead}, judgedPull{"novox/mesh-catalog", 2, catHead})
var key string
rt.until("the operator to be asked once for the group", 10*time.Second, func() bool { key = onlyAsk(b); return key != "" })
time.Sleep(500 * time.Millisecond)
if keys, _ := b.kvs["asks"].Keys(ctx); len(keys) != 1 {
t.Fatalf("the operator was asked %d times for one group", len(keys))
}
if g := askAt(t, b, key).Group; g != "feat/x" {
t.Fatalf("the ask is for the group %q", g)
}
if len(rt.asksOf("gitea.gitea_merge_pull_request")) != 0 || mergeAt(b, "feat/x").Group != "" {
t.Fatalf("merged before the Approve")
}
approved := b.answer(t, key, OptionApprove)
// One member is handed, with both heads approved, within seconds of the answer; the second only after the bridge says the
// first merged.
var first Merge
rt.until("the first member to be handed", eventWithin, func() bool { first = mergeAt(b, "feat/x"); return len(first.Pulls) > 0 })
if len(first.Pulls) != 1 || first.Pulls[0].State != mdc.MergeMerging || len(first.Approved) != 2 ||
first.Approved[mdc.ApprovedKey("novox/mesh-controller", 1)] != ctlHead || first.Approved[mdc.ApprovedKey("novox/mesh-catalog", 2)] != catHead {
t.Fatalf("the first merge handed is %+v", first)
}
t.Logf("the first member was handed %s after the answer", time.Since(approved))
time.Sleep(500 * time.Millisecond)
if again := mergeAt(b, "feat/x"); len(again.Pulls) != 1 {
t.Fatalf("the second member was handed before the first merged: %+v", again.Pulls)
}
one := first.Pulls[0]
b.forgeSays(t, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: one.Repository, Number: one.Number, Head: one.Head.Head,
Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
var both Merge
rt.until("the second member to be handed", eventWithin, func() bool { both = mergeAt(b, "feat/x"); return len(both.Pulls) == 2 })
if both.Pulls[0].State != mdc.MergeMerged || both.Pulls[1].State != mdc.MergeMerging || both.Pulls[1].Repository == one.Repository {
t.Fatalf("the second merge handed is %+v", both.Pulls)
}
two := both.Pulls[1]
b.forgeSays(t, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: two.Repository, Number: two.Number, Head: two.Head.Head,
Merge: strings.Repeat("b", 40), Root: "r", Trees: map[string]string{"x": "t"}})
rt.until("source.main to hold both merges", eventWithin, func() bool {
e, err := b.kvs["source"].Get(ctx, mdc.SourceKey(MergeBranch))
if err != nil {
return false
}
src, err := mdc.DecodeSource(e.Value())
return err == nil && src.Group == "feat/x" && len(src.Heads) == 2
})
if keys, _ := b.kvs["asks"].Keys(ctx); len(keys) != 1 {
t.Fatalf("a second ask was made: %v", keys)
}
// The forge's own word that each pull request merged follows the bridge's (its webhook), after the second was handed (the
// checker judges the members still open by the trunk the first merge moved, which this test does not play): both members
// end merged, the group with them, and the controller was never called (R6-C3).
b.publish(t, "mesh.seat.git.event.pull-merged", seatMerged(merged(one.Repository, one.Number, one.Head.Head, strings.Repeat("a", 40))))
b.publish(t, "mesh.seat.git.event.pull-merged", seatMerged(merged(two.Repository, two.Number, two.Head.Head, strings.Repeat("b", 40))))
for _, id := range []string{IDOf("novox/mesh-controller", ctlHead), IDOf("novox/mesh-catalog", catHead)} {
id := id
rt.until(id+" to be merged", eventWithin, func() bool {
e, err := b.kvs["deliveries"].Get(ctx, kvKey(id))
var d Delivery
return err == nil && json.Unmarshal(e.Value(), &d) == nil && d.State == Merged
})
}
rt.until("the group to read merged", eventWithin, func() bool {
e, err := b.kvs["groups"].Get(ctx, kvKey("feat/x"))
var g Group
return err == nil && json.Unmarshal(e.Value(), &g) == nil && g.Said == "merged"
})
rt.noControllerAsk()
}
func mustDecode[T any](t *testing.T, raw []byte) T {
t.Helper()
var v T
if err := json.Unmarshal(raw, &v); err != nil {
t.Fatal(err)
}
return v
}
// eventWithin is how long a test waits for one step of the gate's path over the real bus. On an idle machine each step
// takes milliseconds (the tests log it), but a slow, loaded build node ran the ask step in 6.69 s against the old 5 s
// (issue 1404). A failed first ask is asked again only after askAgainAfter (30 s, react.go), and 20 s deliberately does not
// cover that: a failed first ask still fails the test.
const eventWithin = 20 * time.Second
// mergesSaid are the writes of the gate's state merges, the merges it handed to the forge bridge, in order (the old form of
// the merge: eb131beb, the merge as an owed act, is left out of the join).
func (r *runtime) mergesSaid() []string {
ctx := context.Background()
kv := r.b.kvs["merges"]
keys, err := kv.Keys(ctx)
if err != nil {
return nil
}
var out []string
for _, k := range keys {
hist, err := kv.History(ctx, k)
if err != nil {
continue
}
for _, e := range hist {
out = append(out, k+"@"+strconv.FormatUint(e.Revision(), 10))
}
}
return out
}
@@ -1,142 +0,0 @@
package main
import (
"encoding/json"
"strings"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// ctxGate and ctxRepo are the two statuses mesh/check replaced: only a base that a revert gives them again requires them
// (unchecked.go mergeChecks), so only tests of that name them.
const (
ctxGate = "mesh/merge-gate"
ctxRepo = "mesh/repo-check"
)
// shimCheckedKey is the key a test hears a verdict under: said as the checker's (see Checked).
const shimCheckedKey = "mesh-controller.checked"
// CheckedEvent is a verdict as these tests state it. The gate follows no verdict of the controller any more (issue 1318,
// ADR 0524 §5): the shim below says it the way the mesh says it now, as the head's mesh/check and the checker's verdict at
// the heads, so the table, the Approve and the merges are exercised on the one path they have.
type CheckedEvent struct {
Owner string
Repo string
Number int
Commit string
Verdict string
Summary string
ID string
On string
Report string
Gate *struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
Modules []string `json:"modules,omitempty"`
Dependents []string `json:"dependents,omitempty"`
}
RepoCheck *struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
}
Group string
Members []struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Commit string `json:"commit"`
Trunk string `json:"trunk,omitempty"`
}
Trunk string
Beside map[string]string
Took string
}
// Checked takes a verdict as the checker's: the head's mesh/check, and the checker's verdict at the heads its Approve covers.
func (h *Holder) Checked(c CheckedEvent) {
h.mu.Lock()
defer h.mu.Unlock()
state := map[string]string{"pass": "success", "warning": "warning", "fail": "failure", "error": "error"}[c.Verdict]
if state == "" {
state = "error"
}
if c.Group != "" {
g := h.groups[c.Group]
if g == nil {
return
}
named := map[string]string{}
for _, m := range c.Members {
if m.Trunk != "" {
named[strings.ToLower(m.Owner+"/"+m.Repo)] = m.Trunk
}
}
h.putChecker(h.membersOf(g), c.Verdict, named)
return
}
repository := strings.ToLower(c.Owner + "/" + c.Repo)
id := IDOf(repository, c.Commit)
if h.deliveries[id] == nil && c.Number != 0 {
d := &Delivery{ID: id, Repository: repository, Commit: c.Commit, Number: c.Number, Created: h.Now()}
if t, err := Apply(d, EvAnnounced, false, Facts{Now: h.Now(), Why: "its verdict arrived before its head was heard"}); err == nil {
h.moved(d, []Transition{t})
}
}
d := h.deliveries[id]
if d == nil {
return
}
words := map[string]string{"success": "pass", "warning": "pass", "failure": "failed: " + c.Summary, "error": "error"}[state]
line := words + " " + shortOf(c.Commit, 12)
if c.Trunk != "" {
line += " on " + shortOf(c.Trunk, 12) // a verdict that names no trunk commit is judged against none
}
h.takeCheckStatus(repository, c.Commit, state, line+" | "+c.Summary)
if d := h.deliveries[id]; d != nil && state == "success" && d.Ready != nil && d.Ready.Merged == nil {
h.mergeAgainNow(d) // as the forge's success status event does (CommitStatus)
}
if d := h.deliveries[id]; d != nil && d.Check != nil && c.Beside != nil {
d.Check.Beside, d.Check.Took = c.Beside, c.Took
}
if d := h.deliveries[id]; d != nil {
h.putChecker(h.askMembers(d), c.Verdict, nil)
}
}
// putChecker keeps the checker's verdict at these members' heads, judged on each repository's trunk the members name or, for
// none named, the one the check line named.
func (h *Holder) putChecker(members []*Delivery, verdict string, named map[string]string) {
state := "pass"
switch verdict {
case "pass", "warning":
default:
state = "fail"
}
hash, err := mdc.HeadsHash(headsOf(members))
if err != nil {
return
}
trunks := map[string]string{}
for _, d := range members {
if d.Check != nil && d.Check.Trunk != "" {
trunks[strings.ToLower(d.Repository)] = d.Check.Trunk
}
if d.TrunkNow != "" && trunks[strings.ToLower(d.Repository)] == "" {
trunks[strings.ToLower(d.Repository)] = d.TrunkNow
}
}
for repository, trunk := range named {
trunks[repository] = trunk
}
value, _ := json.Marshal(map[string]any{"v": 1, "group": "g", "heads": hash, "trunk": trunks, "state": state})
if h.checker == nil {
h.checker = map[string]checkerVerdict{}
}
var v checkerVerdict
_ = json.Unmarshal(value, &v)
h.checker[hash] = v
h.Poke()
}
var _ = time.Second
@@ -1,60 +0,0 @@
//go:build integration
package main
import (
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"context"
"encoding/json"
"testing"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// A pull request and the head the checker judged, for checkPasses.
type judgedPull struct {
repository string
number int
head string
}
// checkPasses says on the bus what the mesh says of heads the checker passed (issue 1302, ADR 0447): its verdict at these
// heads in its verdicts bucket, each pull request's base requiring mesh/check and the forge's word that its bridge set it, the
// line naming the head and the trunk commit it judged. The gate follows no verdict of the controller (issue 1318).
func (b *testBus) checkPasses(t *testing.T, rt *runtime, group, trunk string, pulls ...judgedPull) {
t.Helper()
var heads []mdc.Head
trunks := map[string]string{}
for _, p := range pulls {
heads = append(heads, mdc.Head{Repository: p.repository, Number: p.number, Head: p.head})
trunks[p.repository] = trunk
}
hash, err := mdc.HeadsHash(heads)
if err != nil {
t.Fatal(err)
}
verdict, _ := json.Marshal(map[string]any{"v": 1, "group": group, "heads": hash, "trunk": trunks, "state": "pass"})
if _, err := b.kvs[checkerVerdicts].Put(context.Background(), group+"."+hash, verdict); err != nil {
t.Fatal(err)
}
for _, p := range pulls {
owner, repo := splitRepository(p.repository)
line := "pass " + p.head[:12] + " on " + trunk[:12] + " | unit 3 run, 0 kept"
rt.mu.Lock()
found := false
for i := range rt.openPulls {
o := &rt.openPulls[i]
if o.Owner == owner && o.Repo == repo && o.Number == p.number {
o.Required, o.Statuses, o.Checks, found = []string{ctxCheck}, []string{ctxCheck}, map[string]HeadCheck{ctxCheck: {State: "success", Description: line}}, true
}
}
if !found {
rt.openPulls = append(rt.openPulls, OpenPull{Owner: owner, Repo: repo, Number: p.number, Base: "main", HeadSHA: p.head,
Required: []string{ctxCheck}, Statuses: []string{ctxCheck}, Checks: map[string]HeadCheck{ctxCheck: {State: "success", Description: line}}})
}
rt.mu.Unlock()
b.publish(t, "mesh.seat.git.event.forge-status", gitseat.ForgeStatusEvent{Repository: owner + "/" + repo, Commit: p.head,
Context: ctxCheck, State: "success", Description: line})
}
}
@@ -1,283 +0,0 @@
package main
import (
"errors"
"fmt"
"sort"
"strings"
"time"
)
// The `checks` verb (novox/hq ADR 0239): what the mesh's checks said of a pull request's head, or of one
// commit. Two records answer it, and neither alone is enough. The forge keeps each status — the newest of
// each context, a description clipped to what it shows, and the protection that decides whether the merge
// button works. This owner keeps the verdict whole as the controller said it: each layer's summary
// unclipped ("0 of 4 compose" is not lost to a 140-character description), the build seat's ask that ran
// it, the machine it ran on, and its report. So the forge is asked for its statuses and this owner joins
// its own verdict to the two statuses that verdict set. Nothing is written: a read, by anyone.
// Context names of the statuses this owner can say more of than the forge keeps.
const (
// mesh/check, the one status main requires, is ctxCheck (onestatus.go); the controller's mesh/merge-gate and hq's
// mesh/repo-check are gone with the merge gate (ADR 0524 §5, ADR 0527 §2).
ctxDelivery = "mesh/delivery"
ctxGroup = "mesh/delivery-group"
)
// CheckStatus is one status of the commit, with what this owner knows of it beyond the forge.
type CheckStatus struct {
Name string `json:"name"`
State string `json:"state"`
Description string `json:"description,omitempty"`
SetAt string `json:"set_at,omitempty"`
SetBy string `json:"set_by,omitempty"`
Link string `json:"link,omitempty"`
// From the verdict that set it, when this owner holds it: the build seat's ask, the machine that ran it,
// when it was heard, and the layer's verdict in full.
Build string `json:"build,omitempty"`
CheckedOn string `json:"checked_on,omitempty"`
CheckedAt string `json:"checked_at,omitempty"`
Verdict string `json:"verdict,omitempty"`
// Source says where the status was read: the forge, or — the forge unreachable — this owner's verdict.
Source string `json:"source"`
}
// ChecksAnswer is the `checks` verb's answer.
type ChecksAnswer struct {
Repository string `json:"repository"`
Commit string `json:"commit"`
Pull *ForgePull `json:"pull,omitempty"`
Delivery *ChecksOf `json:"delivery,omitempty"`
Statuses []CheckStatus `json:"statuses"`
// Verdict is the merge check's verdict whole, as this owner heard it for this commit.
Verdict *Verdict `json:"verdict,omitempty"`
// Judged is what the verdict judged: the head merged into the trunk commit (novox/hq ADR 0323 §3); OutOfDate
// why it no longer counts, when its trunk has moved since.
Judged string `json:"judged,omitempty"`
OutOfDate string `json:"out_of_date,omitempty"`
// Merge is whether the branch's protection lets the commit merge; Mergeable nil when it is not known.
Merge ForgeMerge `json:"merge"`
// Forge is why the forge could not be read, when it could not.
Forge string `json:"forge,omitempty"`
Says string `json:"says"`
}
// ChecksOf is the delivery of the commit, in brief: `show` has it whole.
type ChecksOf struct {
ID string `json:"id"`
State State `json:"state"`
Since string `json:"since"`
Waits string `json:"waits,omitempty"`
Group string `json:"group,omitempty"`
// MergedAs is the commit its merge landed as on the trunk, once merged: what the delivery view's `delivered` event is
// matched to a fix by (ADR 0539 clause 17).
MergedAs string `json:"merged_as,omitempty"`
// Opened and MergedAt are what the issue tracker measures a fix's time by (novox/hq ADR 0313): when its pull request
// was first heard, and when it merged.
Opened string `json:"opened,omitempty"`
MergedAt string `json:"merged_at,omitempty"`
}
// Checks is the `checks` verb: a repository and a pull request's number, or a repository and a commit.
func (h *Holder) Checks(repository string, number int, commit string) (*ChecksAnswer, error) {
repository = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(repository), ".git"))
owner, repo, ok := strings.Cut(repository, "/")
if !ok || owner == "" || repo == "" || strings.Contains(repo, "/") {
return nil, fmt.Errorf("%q is not owner/repository", repository)
}
commit = strings.ToLower(strings.TrimSpace(commit))
if number < 0 {
return nil, fmt.Errorf("%d is not a pull request's number", number)
}
if number == 0 && commit == "" {
return nil, errors.New("name a pull request's number or a commit")
}
if commit != "" && (len(commit) < 7 || strings.Trim(commit, "0123456789abcdef") != "") {
return nil, fmt.Errorf("%q is not a commit: seven or more hexadecimal characters", commit)
}
// What this owner knows first, for the base whose protection is read; the forge then says the head.
base := ""
if d := h.deliveryOf(repository, number, commit); d != nil {
base = d.Base
}
a := &ChecksAnswer{Repository: repository, Commit: commit}
fc, err := h.Forge.Statuses(owner, repo, number, commit, base)
if err != nil {
a.Forge = err.Error()
} else {
a.Commit, a.Pull = fc.Commit, fc.Pull
}
d := h.deliveryOf(repository, number, a.Commit)
if a.Commit == "" && d != nil {
a.Commit = d.Commit // the forge away: the newest head this owner holds for the pull request
}
if d != nil {
a.Delivery = &ChecksOf{ID: d.ID, State: d.State, Since: d.Since.UTC().Format(time.RFC3339), Waits: d.waits,
Group: d.Group, MergedAs: d.MergedAs}
if !d.opened.IsZero() {
a.Delivery.Opened = d.opened.UTC().Format(time.RFC3339)
}
if !d.MergedAt.IsZero() {
a.Delivery.MergedAt = d.MergedAt.UTC().Format(time.RFC3339)
}
if d.Check != nil && sameCommit(d.Commit, a.Commit) {
a.Verdict = d.Check
a.Judged, a.OutOfDate = d.Check.Judged(), d.outOfDate()
}
}
if fc != nil {
for _, s := range fc.Statuses {
a.Statuses = append(a.Statuses, CheckStatus{Name: s.Context, State: s.State, Description: s.Description,
SetAt: firstOf(s.UpdatedAt, s.CreatedAt), SetBy: s.Creator, Link: s.TargetURL, Source: "the forge"})
}
a.Merge = fc.Merge
} else {
// The forge away: the statuses the verdict set, as the forge holder sets them from it, said as such.
if v := a.Verdict; v != nil {
a.Statuses = append(a.Statuses, CheckStatus{Name: ctxCheck, State: forgeStateOf(v.Gate), Source: "mesh-delivery's verdict; the forge could not be read"})
}
a.Merge = ForgeMerge{Branch: firstOf(base, "main"), Says: "not known: the forge could not be read, and only it holds the protection"}
}
for i := range a.Statuses {
h.addWhatIsKnown(&a.Statuses[i], a.Verdict, d)
}
sort.SliceStable(a.Statuses, func(i, j int) bool { return a.Statuses[i].Name < a.Statuses[j].Name })
a.Says = a.says()
return a, nil
}
// checksDelivery is the part of a delivery `checks` reads, copied under the lock.
type checksDelivery struct {
Delivery
waits string
groupSaid string
opened time.Time
}
// deliveryOf is the delivery of a commit — its head, or the commit it merged as — else, for a pull request,
// its newest head that was not superseded, else its newest.
func (h *Holder) deliveryOf(repository string, number int, commit string) *checksDelivery {
h.mu.Lock()
defer h.mu.Unlock()
var found *Delivery
if commit != "" {
for _, d := range h.deliveries {
if strings.EqualFold(d.Repository, repository) && (sameCommit(d.Commit, commit) || sameCommit(d.MergedAs, commit)) {
found = d
break
}
}
} else if number > 0 {
for _, d := range h.deliveries {
if !strings.EqualFold(d.Repository, repository) || d.Number != number {
continue
}
if found == nil || newerHead(d, found) {
found = d
}
}
}
if found == nil {
return nil
}
c := &checksDelivery{Delivery: *found, waits: h.waitsFor(found), opened: h.openedOf(found)}
if found.Check != nil {
v := *found.Check
c.Check = &v
}
if g := h.groups[found.Group]; g != nil {
state, why := GroupState(g, h.membersOf(g))
c.groupSaid = state + ": " + why
if g.Check != nil && g.Check.Verdict != "" {
c.groupSaid += "; composed check " + g.Check.Verdict + " — " + g.Check.Summary
}
}
return c
}
// newerHead is whether d is a pull request's head to read before e: one not superseded first, then the newest.
func newerHead(d, e *Delivery) bool {
if (d.State == Closed) != (e.State == Closed) {
return e.State == Closed
}
return d.Created.After(e.Created)
}
// addWhatIsKnown joins this owner's verdict to the status it set, and the delivery to its own statuses.
func (h *Holder) addWhatIsKnown(s *CheckStatus, v *Verdict, d *checksDelivery) {
switch s.Name {
case ctxCheck:
if v != nil && v.Status == ctxCheck {
s.Verdict = strings.ToUpper(firstOf(v.Gate, "error")) + " — " + v.Summary
if !v.At.IsZero() {
s.CheckedAt = v.At.UTC().Format(time.RFC3339)
}
}
case ctxDelivery:
if d != nil {
s.Verdict = string(d.State)
if d.waits != "" {
s.Verdict += ": waits for " + d.waits
}
}
case ctxGroup:
if d != nil && d.groupSaid != "" {
s.Verdict = d.groupSaid
}
}
}
// forgeStateOf is the forge state the forge's holder sets for a verdict (the gitea module's pulls.ts): a
// warning is a pass with a note, an error never a success.
func forgeStateOf(verdict string) string {
switch verdict {
case "pass", "warning":
return "success"
case "fail":
return "failure"
}
return "error"
}
func (a *ChecksAnswer) says() string {
what := a.Repository + "@" + shortOf(a.Commit, 8)
if a.Pull != nil {
what = fmt.Sprintf("%s#%d at %s", a.Repository, a.Pull.Number, shortOf(a.Commit, 8))
}
var parts []string
for _, s := range a.Statuses {
parts = append(parts, s.Name+" "+s.State)
}
statuses := "no status"
if len(parts) > 0 {
statuses = strings.Join(parts, ", ")
}
merge := a.Merge.Says
switch {
case a.Merge.Mergeable == nil:
merge = "mergeable: not known — " + merge
case *a.Merge.Mergeable:
merge = "mergeable — " + merge
default:
merge = "NOT mergeable — " + merge
}
if a.Judged != "" {
what += " (" + a.Judged + ")"
}
if a.OutOfDate != "" {
merge += "; its verdict is " + a.OutOfDate
}
return what + ": " + statuses + "; " + merge
}
func firstOf(s ...string) string {
for _, x := range s {
if x != "" {
return x
}
}
return ""
}
@@ -1,226 +0,0 @@
package main
import (
"strings"
"testing"
)
// The `checks` verb (novox/hq ADR 0239): what the mesh's checks said of a pull request's head or a commit —
// the forge's statuses, the verdict whole as the controller said it, and whether the base's protection lets
// it merge.
// checkedAs is a verdict as the checker says it on the head's mesh/check.
func checkedAs(t *testing.T, repo string, number int, sha, gate, gateSaid string) CheckedEvent {
t.Helper()
owner, name, _ := strings.Cut(repo, "/")
return CheckedEvent{Owner: owner, Repo: name, Number: number, Commit: sha, Verdict: gate, Summary: gateSaid, ID: "build-" + sha[:8]}
}
// statusesAsTheForgeSets puts the status the forge's bridge sets from a verdict: clipped, as the forge keeps it.
func statusesAsTheForgeSets(w *world, repo, sha string, c CheckedEvent) {
owner, name, _ := strings.Cut(repo, "/")
_ = w.forge.Status(owner, name, sha, ctxCheck, forgeStateOf(c.Verdict), clip(c.Verdict+": "+c.Summary, 40), "https://forge.invalid/pr")
}
func statusNamed(a *ChecksAnswer, name string) *CheckStatus {
for i := range a.Statuses {
if a.Statuses[i].Name == name {
return &a.Statuses[i]
}
}
return nil
}
func TestChecksOfAPullRequestSayEveryStatusTheVerdictWholeAndWhetherItMerges(t *testing.T) {
w := newWorld(t)
w.forge.required = []string{ctxCheck}
w.forge.pulls["novox/app#7"] = ForgePull{Number: 7, Title: "a change", State: "open", Base: "main", Head: "feat/x", HeadSHA: head}
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
said := "0 of 4 compose: anchor refuses app's manifest — a provision nothing provides, and three more machines alike"
c := checkedAs(t, "novox/app", 7, head, "fail", said)
w.h.Checked(c)
w.settleAll()
statusesAsTheForgeSets(w, "novox/app", head, c)
a, err := w.h.Checks("novox/app", 7, "")
if err != nil {
t.Fatal(err)
}
if a.Commit != head || a.Pull == nil || a.Pull.Number != 7 || a.Forge != "" {
t.Fatalf("the head read is %q, pull %+v, forge %q", a.Commit, a.Pull, a.Forge)
}
gate := statusNamed(a, ctxCheck)
if gate == nil || gate.State != "failure" || gate.Source != "the forge" || gate.SetBy != "mesh-admin" {
t.Fatalf("the check's status is %+v", gate)
}
// The forge clipped it; the verdict says it whole.
if strings.Contains(gate.Description, "three more machines") {
t.Fatalf("the fake forge did not clip: %q", gate.Description)
}
if !strings.Contains(gate.Verdict, "FAIL — failed: 0 of 4 compose") || !strings.Contains(gate.Verdict, "three more machines") {
t.Fatalf("the check's verdict is %q", gate.Verdict)
}
if gate.CheckedAt == "" {
t.Fatal("the check says no time it was heard")
}
if d := statusNamed(a, ctxDelivery); d == nil || !strings.HasPrefix(d.Verdict, "rejected: waits for a new head") {
t.Fatalf("the delivery's status is %+v", d)
}
if a.Verdict == nil || !strings.Contains(a.Verdict.Summary, "three more machines") {
t.Fatalf("the verdict whole is %+v", a.Verdict)
}
if a.Delivery == nil || a.Delivery.ID != IDOf("novox/app", head) || a.Delivery.State != Rejected {
t.Fatalf("the delivery is %+v", a.Delivery)
}
if a.Merge.Mergeable == nil || *a.Merge.Mergeable || len(a.Merge.Blocking) != 1 || a.Merge.Blocking[0].Context != ctxCheck {
t.Fatalf("the merge is %+v", a.Merge)
}
if !strings.Contains(a.Says, "novox/app#7 at aaaaaaaa") || !strings.Contains(a.Says, "NOT mergeable") {
t.Fatalf("it says %q", a.Says)
}
// The forge was asked for the pull request, with the base this owner knew.
if len(w.forge.asked) != 1 || w.forge.asked[0] != "novox/app#7@ base=main" {
t.Fatalf("the forge was asked %v", w.forge.asked)
}
}
func TestANewHeadIsReadWithItsOwnVerdictNeverTheOlderOnes(t *testing.T) {
w := newWorld(t)
w.forge.required = []string{ctxCheck}
newer := "bbbbbbbbbbbb2222"
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x"))
w.h.Checked(checkedAs(t, "novox/app", 7, head, "fail", "0 of 4 compose"))
w.h.PullUpdated(pr("novox/app", 7, newer, "feat/x"))
w.forge.pulls["novox/app#7"] = ForgePull{Number: 7, State: "open", Base: "main", HeadSHA: newer}
a, err := w.h.Checks("novox/app", 7, "")
if err != nil {
t.Fatal(err)
}
if a.Commit != newer || a.Delivery == nil || a.Delivery.ID != IDOf("novox/app", newer) || a.Delivery.State != Proposed {
t.Fatalf("the new head reads as %q, %+v", a.Commit, a.Delivery)
}
if a.Verdict != nil {
t.Fatalf("the older head's verdict was given for the newer: %+v", a.Verdict)
}
if a.Merge.Mergeable == nil || *a.Merge.Mergeable || len(a.Merge.Blocking) != 1 || a.Merge.Blocking[0].State != "missing" {
t.Fatalf("a head not checked yet merges as %+v", a.Merge)
}
// Passed, it merges.
c := checkedAs(t, "novox/app", 7, newer, "pass", "every machine composes")
w.h.Checked(c)
statusesAsTheForgeSets(w, "novox/app", newer, c)
a, _ = w.h.Checks("novox/app", 7, "")
if a.Merge.Mergeable == nil || !*a.Merge.Mergeable || !strings.Contains(a.Says, "mergeable") || strings.Contains(a.Says, "NOT") {
t.Fatalf("a head that passed: %+v, says %q", a.Merge, a.Says)
}
if g := statusNamed(a, ctxCheck); g == nil || !strings.HasPrefix(g.Verdict, "PASS — pass ") || !strings.Contains(g.Verdict, "every machine composes") {
t.Fatalf("its check: %+v", g)
}
}
func TestACommitIsReadByTheStartOfItsShaAndAMergeByItsDelivery(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x"))
c := checkedAs(t, "novox/app", 7, head, "pass", "every machine composes")
w.h.Checked(c)
statusesAsTheForgeSets(w, "novox/app", head, c)
a, err := w.h.Checks("Novox/App.git", 0, head[:8])
if err != nil {
t.Fatal(err)
}
if a.Commit != head || a.Verdict == nil || a.Delivery == nil || a.Pull != nil {
t.Fatalf("by the start of its sha: %q, %+v, %+v", a.Commit, a.Verdict, a.Delivery)
}
// No protection: nothing is required, so it merges.
if a.Merge.Mergeable == nil || !*a.Merge.Mergeable {
t.Fatalf("with no protection: %+v", a.Merge)
}
mergedAs := "dddddddddddd9999"
w.h.PullMerged(merged("novox/app", 7, head, mergedAs))
_ = w.forge.Status("novox", "app", mergedAs, ctxDelivery, "pending", "published", "")
a, err = w.h.Checks("novox/app", 0, mergedAs)
if err != nil {
t.Fatal(err)
}
if a.Delivery == nil || a.Delivery.ID != IDOf("novox/app", head) {
t.Fatalf("the merge commit's delivery is %+v", a.Delivery)
}
if a.Verdict != nil {
t.Fatalf("the head's verdict was given for the merge commit: %+v", a.Verdict)
}
if s := statusNamed(a, ctxDelivery); s == nil || s.State != "pending" || s.Verdict == "" {
t.Fatalf("the merge's delivery status: %+v", s)
}
}
func TestWithTheForgeAwayTheVerdictIsSaidAndWhetherItMergesIsNotKnown(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x"))
w.h.Checked(checkedAs(t, "novox/app", 7, head, "warning", "every machine composes, with a note"))
w.forge.down = true
a, err := w.h.Checks("novox/app", 7, "")
if err != nil {
t.Fatal(err)
}
if a.Forge == "" || a.Commit != head || a.Verdict == nil {
t.Fatalf("with the forge away: forge %q, commit %q, verdict %+v", a.Forge, a.Commit, a.Verdict)
}
gate := statusNamed(a, ctxCheck)
if gate == nil || gate.State != "success" || !strings.Contains(gate.Source, "could not be read") {
t.Fatalf("the status from the verdict: %+v", gate)
}
if a.Merge.Mergeable != nil || !strings.Contains(a.Says, "mergeable: not known") {
t.Fatalf("whether it merges, with the forge away: %+v, %q", a.Merge, a.Says)
}
// The protection unreadable: the statuses are said, and whether it merges is not known.
w.forge.down, w.forge.protectionDown = false, true
w.forge.pulls["novox/app#7"] = ForgePull{Number: 7, Base: "main", HeadSHA: head}
a, _ = w.h.Checks("novox/app", 7, "")
if a.Merge.Mergeable != nil || a.Forge != "" {
t.Fatalf("with the protection unreadable: %+v", a.Merge)
}
}
func TestChecksRefuseWhatNamesNoCommit(t *testing.T) {
w := newWorld(t)
for _, c := range []struct {
repo string
number int
commit string
says string
}{
{"novox/app", 0, "", "number or a commit"},
{"app", 7, "", "owner/repository"},
{"novox/app/x", 7, "", "owner/repository"},
{"novox/app", 0, "abc", "not a commit"},
{"novox/app", 0, "zzzzzzzzzz", "not a commit"},
{"novox/app", -1, "", "not a pull request's number"},
} {
if _, err := w.h.Checks(c.repo, c.number, c.commit); err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s #%d %q: %v", c.repo, c.number, c.commit, err)
}
}
if len(w.forge.asked) != 0 {
t.Fatalf("the forge was asked for what names nothing: %v", w.forge.asked)
}
}
func TestANumberIsTakenAsANumberOrAsText(t *testing.T) {
for _, c := range []struct {
in any
want int
ok bool
}{{float64(168), 168, true}, {"168", 168, true}, {"#168", 168, true}, {nil, 0, true}, {"", 0, true},
{"x", 0, false}, {1.5, 0, false}} {
got, err := intArg(map[string]any{"number": c.in}, "number")
if (err == nil) != c.ok || got != c.want {
t.Errorf("%v → %d %v", c.in, got, err)
}
}
}
@@ -1,34 +0,0 @@
package main
import "time"
// lastCheckTime is how long the newest answered check of a repository ran, as its verdict says it (`took`): the
// repository's last measured check time (novox/hq ADR 0505 §4). Zero when no verdict of the repository says it.
// Under the lock.
func (h *Holder) lastCheckTime(repository string) time.Duration {
var newest *Verdict
var took time.Duration
for _, d := range h.deliveries {
v := d.Check
if d.Repository != repository || v == nil || v.Took == "" {
continue
}
t, err := time.ParseDuration(v.Took)
if err != nil || t <= 0 {
continue
}
if newest == nil || v.At.After(newest.At) {
newest, took = v, t
}
}
return took
}
// answeredRefusalBound is how long a refusal the gate is already answering waits before it is said as stalled: twice
// the repository's last measured check time, at least the floor (ADR 0505 §4). Under the lock.
func (h *Holder) answeredRefusalBound(repository string) time.Duration {
if b := 2 * h.lastCheckTime(repository); b > h.refusalFloor() {
return b
}
return h.refusalFloor()
}
@@ -67,7 +67,7 @@ func conditionKeyOf(id string) string { return "mesh-delivery-gate.delivery." +
// conditionSaid is what a raised condition said that, when it changes, raises it again: its severity and what it needs.
// The words of a line hold durations, which change at every look and are no reason to tell the operator again.
func conditionSaid(l StalledLine) string { return severityOfLine(l) + "|" + l.H2 + "|" + l.Needs }
func conditionSaid(l StalledLine) string { return severityOfLine(l) + "|" + l.H2 }
func severityOfLine(l StalledLine) string {
if l.Severity == occ.SeverityUrgent {
@@ -79,10 +79,6 @@ func severityOfLine(l StalledLine) string {
// conditionOf is a stalled line as the condition the operator is told.
func conditionOf(l StalledLine) oc.Condition {
headline := "stalled: " + l.ID
if strings.HasPrefix(l.Says, "group ") {
group, _, _ := strings.Cut(strings.TrimPrefix(l.Says, "group "), " ")
headline = "group " + group + " is not merged whole"
}
if r := []rune(headline); len(r) > occ.HeadlineLength {
headline = string(r[:occ.HeadlineLength-1]) + "…"
}
@@ -109,16 +105,9 @@ func (h *Holder) sayConditions() {
}
h.said = said
}
h.mu.Lock()
if at := h.nextStall(h.Now()); !at.IsZero() {
h.lookAt(at, "a delivery whose stalled line, or its severity, comes due with no event")
h.lookAt(at, "a record whose stalled line comes due with no event")
}
forgeRead := h.forgeRead
waitingOnForge := map[string]bool{}
for id := range h.unchecked {
waitingOnForge[id] = true
}
h.mu.Unlock()
lines := map[string]StalledLine{}
for _, l := range h.Stalled() {
// One condition for an id: the most severe of its lines.
@@ -146,13 +135,11 @@ func (h *Holder) sayConditions() {
h.Logf("[mesh-delivery] %s is said to the operator as %s: %s", id, severityOfLine(l), l.Says)
h.said[key], changed = said, true
}
// Cleared only on what was read: not before the forge's open pull requests were read once since the gate started (the
// heads waiting on the forge are kept in memory alone), and not while the forge still shows the head open and waiting:
// its line is gone only because its bound runs again from a later first sight, and it comes back with the same words.
// Cleared when the line is gone: what it raised is kept in the bucket conditions, so a gate that restarts clears what ended.
var gone []string
for key := range h.said {
id := strings.TrimSuffix(strings.TrimPrefix(key, "mesh-delivery-gate.delivery."), ".stalled")
if _, ok := lines[id]; !ok && forgeRead && !waitingOnForge[id] {
if _, ok := lines[id]; !ok {
gone = append(gone, key)
}
}
@@ -194,36 +181,30 @@ func (h *Holder) sayConditions() {
// sayTries is how many looks in a row a condition that could not be said is tried again on the clock.
const sayTries = 10
// nextStall is the earliest time after now when a delivery's stalled line comes due, or its severity changes, with nothing
// else happening: a state's bound, the author's bound of a rejected head, a head never announced, a refusal the gate
// answers, and an out-of-date refusal turning urgent (1275/AC6). Zero when none. Under the lock.
// nextStall is the earliest time after now when a record's stalled line comes due with nothing else happening: a record open
// past its bound, an act unanswered past its. Zero when none.
func (h *Holder) nextStall(now time.Time) time.Time {
var next time.Time
if h.Records == nil {
return next
}
consider := func(at time.Time) {
if at.After(now) && (next.IsZero() || at.Before(next)) {
next = at
}
}
for _, d := range h.deliveries {
if d.State.Final() {
continue
kept, err := h.Records.Kept()
if err != nil {
return next
}
for _, r := range kept {
if r.Ended == nil {
consider(r.Opened.Add(recordOpenBound).Add(time.Second))
}
if b, bounded := boundOf(d); bounded && b.For > 0 {
consider(boundSince(d).Add(b.For).Add(time.Second)) // pastBound is strictly after the bound
}
r := d.Ready
if r == nil || r.Merged != nil || d.MergedAs != "" {
continue
}
if d.Base == "" && d.Number != 0 {
consider(r.Seen.Add(headHeardBound))
}
if r.Failed != "" && d.State == Rejected {
consider(d.Since.Add(h.authorBound()))
}
if r.RefusedSince != nil {
consider(r.RefusedSince.Add(h.answeredRefusalBound(d.Repository)))
consider(r.RefusedSince.Add(outOfDateFor))
for _, a := range r.Acts {
if a.Outcome == "" {
consider(a.At.Add(owedBound).Add(time.Second))
}
}
}
return next
@@ -1,100 +0,0 @@
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
)
// A config value held or dropped for a pull request asks its check again (novox/hq ADR 0502 clause 5, issue 1826).
//
// The controller's composed check lays the approved values held for a pull request (mesh-config's `held`) when it
// composes, so a check run before a value was held, or after it was dropped, judged another mesh. mesh-config
// raises `config-changed` with the action hold or drop and the pull request; this owner then asks that pull
// request's check, or its group's composed check, again, the way it asks a moved head: its own path, with the
// reason in the delivery's transition, and not the `recheck` verb, which stays an exception (ADR 0421 §12).
// ConfigChangedEvent is the event this owner follows from mesh-config.
const ConfigChangedEvent = "mesh-config.config-changed"
// ConfigChange is mesh-config's `config-changed` as far as this owner reads it.
type ConfigChange struct {
Action string `json:"action"`
Pull string `json:"pull,omitempty"`
Head string `json:"head,omitempty"`
Key string `json:"key,omitempty"`
Level struct {
Node string `json:"node,omitempty"`
Module string `json:"module,omitempty"`
} `json:"level"`
}
var pullNamed = regexp.MustCompile(`^([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)#([0-9]+)$`)
// ConfigChanged takes a hold or a drop of a value for a pull request: every live delivery of that pull request has its
// check asked again. Anything else mesh-config says (a value set, a resolution, the merge's set) asks nothing.
func (h *Holder) ConfigChanged(c ConfigChange) {
if c.Action != "hold" && c.Action != "drop" {
return
}
m := pullNamed.FindStringSubmatch(strings.TrimSpace(c.Pull))
if m == nil {
return
}
number, _ := strconv.Atoi(m[3])
repository := strings.ToLower(m[1] + "/" + m[2])
verb := "held for"
if c.Action == "drop" {
verb = "dropped from"
}
why := fmt.Sprintf("a config value was %s this pull request: %s of %s on %s (mesh-config)", verb, c.Key,
orWord(c.Level.Module, "a module"), orWord(c.Level.Node, "a node"))
h.mu.Lock()
var ids []string
for id, d := range h.deliveries {
if d.Number == number && strings.EqualFold(d.Repository, repository) && d.MergedAs == "" && !d.State.Final() {
ids = append(ids, id)
}
}
h.mu.Unlock()
for _, id := range ids {
h.askAgainForConfig(id, why)
}
}
func orWord(s, word string) string {
if s == "" {
return word
}
return s
}
// askAgainForConfig asks one delivery's check again: a rejected or ready one is proposed again, as any judged again
// head is; a proposed one has its pending ask replaced. Its group's composed check goes out of date, and is asked
// once a member is judged again.
func (h *Holder) askAgainForConfig(id, why string) {
h.mu.Lock()
d := h.deliveries[id]
if d == nil {
h.mu.Unlock()
return
}
probe := *d
probe.Transitions = append([]Transition(nil), d.Transitions...)
_, refused := Apply(&probe, EvRecheck, true, Facts{Now: h.Now(), Why: why, By: byOwner})
proposed := d.State == Proposed
h.mu.Unlock()
switch {
case refused == nil:
h.Logf("[mesh-delivery] %s: its check could not be asked again for a config value: %v", id, checkerRefusal(id))
case proposed:
h.askProposedAgain(id, why)
}
}
// askProposedAgain says why a proposed delivery's check is not asked again for a config value: the checker's (issue 1302), and
// whether a held config value is weighed is its composition's. Nothing is asked of the controller (issue 1318).
func (h *Holder) askProposedAgain(id, why string) {
h.Logf("[mesh-delivery] %s: a held config value changed; %v", id, checkerRefusal(id))
}
@@ -1,394 +0,0 @@
package main
import (
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
// What a transition owes outside the state (novox/hq ADR 0239 decision 5), each by its owner: the event on
// the bus (said by this module), a line on the commit's note and the pull request's view and statuses (by
// the forge's holder, asked through its tools). Owed, never lost: kept with the delivery before anything is
// tried, and tried again until done.
// transitionEvent is what `mesh-delivery-gate.transition` says: no secret, no address. opened is when its pull
// request was first heard (zero when not known); with its class and its merge's moment it is what the issue
// tracker measures a fix's time by (novox/hq ADR 0313 §2, §4).
func transitionEvent(d *Delivery, t Transition, opened time.Time) map[string]any {
e := map[string]any{"id": d.ID, "repository": d.Repository, "commit": d.Commit, "from": t.From, "to": t.To,
"event": t.Event, "why": t.Why, "at": t.At}
if t.By != "" {
e["by"] = t.By
}
if d.Number > 0 {
e["number"] = d.Number
}
if d.Group != "" {
e["group"] = d.Group
}
if d.MergedAs != "" {
e["merged_as"] = d.MergedAs
}
if !opened.IsZero() {
e["opened"] = opened.UTC()
}
if !d.MergedAt.IsZero() {
e["merged_at"] = d.MergedAt.UTC()
}
return e
}
// openedOf is when a delivery's pull request was first heard: the earliest head of it this owner holds, never
// one made from the forge's word at its merge; zero when none is held or it has no pull request. Under the lock.
func (h *Holder) openedOf(d *Delivery) time.Time {
if d.Number <= 0 {
return time.Time{}
}
var first time.Time
for _, c := range h.deliveries {
if c.FromForge || c.Number != d.Number || !strings.EqualFold(c.Repository, d.Repository) || c.Created.IsZero() {
continue
}
if first.IsZero() || c.Created.Before(first) {
first = c.Created
}
}
if first.IsZero() && !d.FromForge {
first = d.Created
}
return first
}
// noteLine is one transition as the commit's note keeps it.
func noteLine(d *Delivery, t Transition) string {
by := ""
if t.By != "" {
by = " by " + t.By
}
line := fmt.Sprintf("%s delivery %s: %s -> %s (%s%s): %s", t.At.UTC().Format(time.RFC3339), d.ID, orNothing(t.From),
t.To, t.Event, by, t.Why)
if d.Group != "" && (t.To == Proposed || t.To == Merged) {
line += "; group " + d.Group
}
return oneLine(line)
}
func oneLine(s string) string { return strings.Join(strings.Fields(s), " ") }
// statusOf is the commit status `mesh/delivery`: where the delivery stands, in the forge's words. It says the merge's
// states only (ADR 0539 §17): whether a merged change reached the machines is the view's.
func statusOf(d *Delivery) (string, string) {
why := ""
if len(d.Transitions) > 0 {
why = d.Transitions[len(d.Transitions)-1].Why
}
switch d.State {
case Proposed:
return "pending", "checking"
case Ready:
return "success", "ready: it is merged on the Approve"
case Rejected:
return "failure", clip("rejected: "+why, 140)
case Merged:
return "success", clip("merged: "+d.sourceSaid(), 140)
case Held:
return "pending", clip("held for a person: "+d.HeldWhy, 140)
case Closed:
return "warning", clip("closed: "+why, 140)
}
return "pending", string(d.State)
}
// sourceSaid names the source revision a merge moved: the trunk commit of its repository that source.main now holds
// (ADR 0539 §17: the gate's last word on a change; what the machines run is the view's).
func (d *Delivery) sourceSaid() string {
if d.MergedAs == "" {
return "not on the trunk"
}
return "source.main moved to " + d.Repository + "@" + shortOf(d.MergedAs, 12)
}
// viewMarker is how the forge's holder finds the one view comment of a pull request.
const viewMarker = "<!-- mesh-delivery:view -->"
// ViewBody is a delivery's view on its pull request, written from the delivery whole.
func ViewBody(d *Delivery, g *Group) string {
var b strings.Builder
fmt.Fprintf(&b, "%s\n**Delivery** `%s` — **%s** since %s\n\n", viewMarker, d.ID, d.State, d.Since.UTC().Format(time.RFC3339))
if d.HeldWhy != "" && d.State == Held {
fmt.Fprintf(&b, "Held for a person: %s — the merge reached the trunk without a passing check.\n\n", d.HeldWhy)
}
if d.MergedAs != "" {
fmt.Fprintf(&b, "**Source** — %s. Whether it reached the machines is `mesh-delivery-view`'s, derived from "+
"`source.main`, the wants and the haves.\n\n", d.sourceSaid())
}
if g != nil {
fmt.Fprintf(&b, "**Group** `%s`: %s\n", g.ID, strings.Join(g.Members, ", "))
if g.Check != nil && g.Check.Verdict != "" {
fmt.Fprintf(&b, "- composed together: %s — %s\n", g.Check.Verdict, g.Check.Summary)
}
b.WriteString("\n")
}
b.WriteString("**Transitions**\n")
from := 0
if len(d.Transitions) > 12 {
from = len(d.Transitions) - 12
}
for _, t := range d.Transitions[from:] {
fmt.Fprintf(&b, "- %s %s → %s (%s): %s\n", t.At.UTC().Format("2006-01-02 15:04"), orNothing(t.From), t.To, t.Event, t.Why)
}
b.WriteString("\nThe commit's note under `refs/notes/mesh-plan` keeps every transition of this change up to its merge: `git log --notes=mesh-plan`.\n")
return b.String()
}
// Flush does what is owed, kept deliveries first: a delivery not yet kept says nothing.
func (h *Holder) Flush() {
type job struct {
id string
group bool
e Effect
body string
repo string
num int
head string
url string
}
h.mu.Lock()
now := h.Now()
for id := range h.dirty {
if d := h.deliveries[id]; d != nil {
h.keep(d)
}
}
for id := range h.dirtyGroup {
if g := h.groups[id]; g != nil {
h.keepGroup(g)
}
}
var jobs []job
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids)
// An act said and not yet answered by its owed.ended waits for that answer; at this owner's start it is said once more,
// under the same id (design 47: every unanswered act said again at its start).
again := !h.saidAtStart
h.saidAtStart = true
waiting := func(e Effect) bool {
return e.Next != nil && now.Before(*e.Next) || e.Said != nil && !again
}
for _, id := range ids {
d := h.deliveries[id]
if h.dirty[id] {
continue
}
for i, e := range d.Owed {
if waiting(e) {
continue
}
if e.ID == "" {
// The id is kept before the act is said, so its owed.ended finds it however soon it is heard.
if e.ID = actIDOf(e, d.Repository, d.Number, d.Commit, d.HTMLURL, false); e.ID != "" {
d.Owed[i].ID = e.ID
h.keep(d)
}
}
j := job{id: id, e: e, repo: d.Repository, num: d.Number, head: d.Commit, url: d.HTMLURL}
if e.Kind == EffectView {
j.body = ViewBody(d, h.groups[d.Group])
}
jobs = append(jobs, j)
}
}
for id, g := range h.groups {
if h.dirtyGroup[id] {
continue
}
for i, e := range g.Owed {
if waiting(e) {
continue
}
if e.ID == "" {
if e.ID = actIDOf(e, "", 0, "", "", true); e.ID != "" {
g.Owed[i].ID = e.ID
h.keepGroup(g)
}
}
jobs = append(jobs, job{id: id, group: true, e: e})
}
}
h.mu.Unlock()
for _, j := range jobs {
said, err := h.do(j.e, j.repo, j.num, j.head, j.body, j.url, j.group)
h.mu.Lock()
// Why it is given up, when it is: a failure of the bus tried retryLimit times, the last ones retryCap apart.
dropped := ""
if err != nil && j.e.Tries+1 >= retryLimit {
dropped = fmt.Sprintf("tried %d times since %s", j.e.Tries+1, j.e.Since.UTC().Format(time.RFC3339))
}
owed := func(list []Effect) []Effect {
for i, o := range list {
if o.Kind == j.e.Kind && o.Commit == j.e.Commit && o.Context == j.e.Context && o.Line == j.e.Line &&
o.Since.Equal(j.e.Since) {
if err == nil && said != "" {
// Said: it stays until its owed.ended is heard (OwedEnded).
at := h.Now()
if o.Said != nil {
at = *o.Said
}
list[i].ID, list[i].Said, list[i].Next, list[i].Tries = said, &at, nil, 0
return list
}
if err == nil || dropped != "" {
return append(list[:i:i], list[i+1:]...)
}
list[i].Tries++
if o.Kind != EffectStatus || !j.group {
list[i].Last = err.Error() // a group's status names its member in Last, which stays
}
next := h.Now().Add(retryAfter(list[i].Tries))
list[i].Next = &next
return list
}
}
return list
}
if j.group {
if g := h.groups[j.id]; g != nil {
g.Owed = owed(g.Owed)
h.keepGroup(g)
}
} else if d := h.deliveries[j.id]; d != nil {
d.Owed = owed(d.Owed)
h.keep(d)
}
// Counted for delivery_status, so a window's writes can be judged against its tries (novox/hq issue 407).
h.tried.last = h.Now()
switch {
case err == nil:
h.tried.done++
case dropped != "":
h.tried.givenUp++
default:
h.tried.failed++
}
if dropped != "" {
h.refuse(fmt.Errorf("%s: its owed %s given up — %s: %v", j.id, j.e.Kind, dropped, err))
}
h.mu.Unlock()
if err != nil && dropped == "" && j.e.Tries%10 == 0 {
h.Logf("[mesh-delivery] %s: %s not done yet (%v); tried again in %s", j.id, j.e.Kind, err,
retryAfter(j.e.Tries+1))
}
}
h.owedAgain()
}
// owedAgain says when what is still owed is tried again: an effect the forge or the bus could not take, at the time
// retryAfter gave it, and a delivery or group not yet kept, after retryFirst. Flush is the bounded retry of both.
func (h *Holder) owedAgain() {
h.mu.Lock()
defer h.mu.Unlock()
for id := range h.dirty {
if h.deliveries[id] != nil {
h.lookAt(h.Now().Add(retryFirst), "a delivery not yet kept, kept again before it is said")
}
}
for id := range h.dirtyGroup {
if h.groups[id] != nil {
h.lookAt(h.Now().Add(retryFirst), "a group not yet kept, kept again before it is said")
}
}
for _, d := range h.deliveries {
for _, e := range d.Owed {
if e.Next != nil {
h.lookAt(*e.Next, "an owed effect the forge or the bus could not take")
}
}
}
for _, g := range h.groups {
for _, e := range g.Owed {
if e.Next != nil {
h.lookAt(*e.Next, "an owed effect the forge or the bus could not take")
}
}
}
}
// An owed effect that fails is tried again later each time: retryFirst after the first failure, doubling up
// to retryCap, and given up after retryLimit tries — a little over two days — saying so (novox/hq issue 313).
const (
retryFirst = 10 * time.Second
retryCap = time.Hour
retryLimit = 60
)
// retryAfter is how long after its tries-th failure an effect is tried again.
func retryAfter(tries int) time.Duration {
wait := retryFirst
for i := 1; i < tries && wait < retryCap; i++ {
wait *= 2
}
return min(wait, retryCap)
}
// do is one owed effect: said on the bus, or said as the seat's event owed, which the forge bridge performs; said is the
// act's id when it was said as owed, and empty when nothing waits for an answer.
func (h *Holder) do(e Effect, repository string, number int, head, body, url string, group bool) (said string, err error) {
switch e.Kind {
case EffectEmit:
var payload any
if err := json.Unmarshal([]byte(e.Line), &payload); err != nil {
return "", nil // nothing sayable: dropped
}
event := "transition"
if group {
event = "group"
}
if e.Event != "" {
event = e.Event
}
return "", h.Emit(event, payload)
case EffectView:
if number == 0 {
return "", nil
}
}
page := url
if group && e.Kind == EffectStatus {
// A group's status names its member's repository and page in Last.
repository, number, page = memberOfLast(e.Last)
head = e.Commit
}
act, value, num, hd := owedOf(e, repository, number, head, body, page)
if act == "" {
return "", nil
}
return h.owe(repository, num, hd, act, value, e.Since, actSays(e), e.ID)
}
// actIDOf is the id an effect is said under as owed, as do says it; empty for an effect said otherwise or not at all.
func actIDOf(e Effect, repository string, number int, head, url string, group bool) string {
switch {
case e.Kind == EffectEmit, e.Kind == EffectView && number == 0:
return ""
case group && e.Kind == EffectStatus:
repository, number, url = memberOfLast(e.Last)
head = e.Commit
}
act, _, num, hd := owedOf(e, repository, number, head, "", url)
if act == "" {
return ""
}
key, err := mdc.OwedKey(repository, num, hd, act)
if err != nil {
return ""
}
return actID(key, e.Since, actSays(e))
}
@@ -1,62 +0,0 @@
package main
import (
"strings"
"testing"
"time"
)
// R6-C4 (ADR 0539 §17): the delivery's status and view say the merge's six states and nothing after the merge: no delivery plan,
// no machine step, no covered or walk line; a merged delivery's view names the source revision its merge moved.
func TestStatusAndViewSayEachOfTheSixStatesWithNoPlanAndNoMachineStep(t *testing.T) {
at := time.Date(2026, 10, 24, 12, 0, 0, 0, time.UTC)
base := Delivery{ID: "novox/app@c0ffee000000", Repository: "novox/app", Commit: "c0ffee000000", Number: 7, Since: at,
Transitions: []Transition{{At: at, From: Proposed, To: Rejected, Event: EvRefused, Why: "the gate said fail"}}}
for _, c := range []struct {
state State
mutate func(d *Delivery)
status string
desc string
inView []string
keepOut []string
}{
{Proposed, nil, "pending", "checking", nil, nil},
{Ready, nil, "success", "ready: it is merged on the Approve", nil, nil},
{Rejected, nil, "failure", "rejected: the gate said fail", nil, nil},
{Merged, func(d *Delivery) { d.MergedAs = "bbbbbbbbbbbb1111" }, "success",
"merged: source.main moved to novox/app@bbbbbbbbbbbb", []string{"**Source**", "source.main moved to novox/app@bbbbbbbbbbbb", "mesh-delivery-view"}, nil},
{Held, func(d *Delivery) { d.MergedAs, d.HeldWhy = "bbbbbbbbbbbb1111", "merged without a passing check" }, "pending",
"held for a person: merged without a passing check", []string{"Held for a person: merged without a passing check"}, nil},
{Closed, nil, "warning", "closed: the gate said fail", nil, nil},
} {
d := base
d.State = c.state
if c.mutate != nil {
c.mutate(&d)
}
state, desc := statusOf(&d)
if state != c.status || desc != c.desc {
t.Errorf("%s: the status is %s %q, wanted %s %q", c.state, state, desc, c.status, c.desc)
}
view := ViewBody(&d, nil)
if !strings.Contains(view, "**"+string(c.state)+"**") {
t.Errorf("%s: the view does not say the state: %s", c.state, view)
}
for _, want := range c.inView {
if !strings.Contains(view, want) {
t.Errorf("%s: the view lacks %q: %s", c.state, want, view)
}
}
for _, never := range []string{"**Delivery plan**", "**Machines**", "tier ", "receives", "walk", "Covered", "plans go", "mesh-delivery.release"} {
if strings.Contains(view, never) {
t.Errorf("%s: the view still says %q: %s", c.state, never, view)
}
}
}
// A merge not on the trunk names no source revision.
d := base
d.State = Merged
if s := d.sourceSaid(); s != "not on the trunk" {
t.Errorf("a merged delivery with no merge commit says %q", s)
}
}
@@ -1,110 +0,0 @@
package main
import (
"fmt"
"slices"
"strings"
)
// Events that start a step (novox/hq ADR 0406 §1, issue 975, ADR 0421 issue 1022).
//
// This owner acts on events: a verdict, a commit status, an Approve. The operator is asked within the event that makes
// the Approve possible (the checks passing), with no label or comment written by anyone: the forge's label and comment
// events start nothing here.
// ForgeStatusEvent is the forge module's `forge.status`: a commit's status was set.
type ForgeStatusEvent struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
SHA string `json:"sha"`
Context string `json:"context"`
State string `json:"state"`
// Description is the status's words (the seat git's forge.status carries them, issue 1302): mesh/check's
// line names the head and the trunk it judged.
Description string `json:"description,omitempty"`
}
// wantForge says the forge's open pull requests are to be read at the next look: a check may have answered.
func (h *Holder) wantForge() {
h.mu.Lock()
h.forgeWanted = true
h.mu.Unlock()
}
// takeForgeWanted answers whether the forge's open pull requests are to be read, and takes the want.
func (h *Holder) takeForgeWanted() bool {
h.mu.Lock()
defer h.mu.Unlock()
wanted := h.forgeWanted
h.forgeWanted = false
return wanted
}
// AnswersChanged takes a change of the teller's answers: the answers to the asks pending are read at the look it asks for.
func (h *Holder) AnswersChanged() {
h.Poke()
}
// CommitStatus takes a commit status set on the forge: one of the merge checks (mesh/check and, on a
// base that still requires it after a revert, the two it replaced) says a
// check started, answered, or was put back, so the forge's open pull requests are read to see which heads still wait on
// one (SeeOpenPulls). A status of any other context shows nothing the merge needs.
//
// A merge check turning success for the head an approved, ready, not-yet-merged delivery's merge was refused at is
// itself an event that decides whether it may merge (issue 1144): the merge tool may have refused it on a check still
// pending, which this status answers. The merge is asked again at once (mergeAgainNow), under the lock, so the next
// look asks it; no timer does this (issue 1115). A status of another context, another commit or a state other than
// success changes nothing.
func (h *Holder) CommitStatus(e ForgeStatusEvent) {
if !slices.Contains(mergeChecks, e.Context) {
return
}
h.wantForge()
h.mu.Lock()
defer h.mu.Unlock()
if e.Context == ctxCheck && (e.Description != "" || e.State == "pending") {
// mesh/check is the verdict on its base (issue 1302): taken at once, as the controller's `checked` was.
h.takeCheckStatus(e.Owner+"/"+e.Repo, e.SHA, e.State, e.Description)
}
if e.State != "success" {
h.withdrawForStatus(e) // a status not a success takes back an ask not yet answered (novox/hq ADR 0661 §5)
return
}
repository := strings.ToLower(e.Owner + "/" + e.Repo)
for _, d := range h.deliveries {
if !strings.EqualFold(d.Repository, repository) || d.Ready == nil || d.Ready.Merged != nil || d.MergedAs != "" ||
d.State.Final() {
continue
}
head := d.Commit
if sha := d.Ready.Heads[d.ID]; sha != "" {
head = sha
}
if sameCommit(head, e.SHA) {
h.causeChanged(d) // its approved head's check turned success on the forge: the merge is asked again at once, the count anew
}
}
}
// withdrawForStatus takes back the ask of an Approve nobody answered yet when a merge check of a head it covers is no
// longer a success (issue 1275, AC5): the answer to a withdrawn ask merges nothing, and the operator is asked again once
// every member may merge. Under the lock.
func (h *Holder) withdrawForStatus(e ForgeStatusEvent) {
repository := strings.ToLower(e.Owner + "/" + e.Repo)
done := map[string]bool{}
for _, d := range h.deliveries {
r := d.Ready
if !strings.EqualFold(d.Repository, repository) || r == nil || r.AskID == "" || r.Approved != nil || r.declined() || done[r.AskID] {
continue
}
head := d.Commit
if sha := r.Heads[d.ID]; sha != "" {
head = sha
}
if sameCommit(head, e.SHA) {
done[r.AskID] = true
h.cancels = append(h.cancels, h.withdrawAsk(r.AskID, fmt.Sprintf("%s of %s is %s at its head %s", e.Context, d.ID, e.State, shortOf(head, 12))))
h.Poke()
}
}
}
@@ -1,175 +0,0 @@
package main
import (
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"reflect"
"encoding/json"
"strings"
"testing"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// **A verdict and an Approve are acted on as events** (novox/hq ADR 0406 §1, issue 975, ADR 0421 §1, issue 1022): the
// operator is asked within the event that makes the Approve possible, and the merge within the Approve's event, never at
// the next tick or the next minute, and with no label or comment written by anyone.
// hear is the runtime handing this owner one event, as handle takes it.
func hear(t *testing.T, w *world, key string, body any) {
t.Helper()
raw, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
if key == shimCheckedKey {
// The gate follows no verdict of the controller any more (issue 1318): a test says it as the checker's.
var c CheckedEvent
if err := json.Unmarshal(raw, &c); err != nil {
t.Fatal(err)
}
w.h.Checked(c)
} else if err := handle(w.h, stdio.Envelope{Key: key, Body: raw}); err != nil {
t.Fatal(err)
}
w.h.React() // the look the event pokes, taken by the holder's worker
}
// greenAnnounced is a pull request into main, announced, the operator not answering by himself.
func announced(t *testing.T) *world {
w := newWorld(t)
w.op.auto = false
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
return w
}
// Issue 1022, design 47 acceptance test 6: the Approve is asked within the event of the checks passing, with no ready
// mark, label or comment written by anyone.
func TestAPassingVerdictAsksTheOperatorAtOnceWithNoMark(t *testing.T) {
w := announced(t)
if w.op.count() != 0 {
t.Fatalf("asked before the checks passed: %d", w.op.count())
}
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
if w.op.count() != 1 {
t.Fatalf("a pull request whose checks passed asked the operator %d times, wanted once, with no mark", w.op.count())
}
if len(w.forge.comments) != 0 || len(w.forge.calls) != 0 {
t.Fatalf("something was written on the forge to ask: %v", w.forge.calls)
}
// Asked once for the head, however often it is looked at again.
hear(t, w, gitEvent(gitseat.EventForgeStatus), gitseat.ForgeStatusEvent{Repository: "novox/app", Commit: readyHead, Context: "mesh/merge-gate", State: "success"})
if w.op.count() != 1 {
t.Fatalf("asked %d times for one head", w.op.count())
}
}
func TestAFailedVerdictAsksNothing(t *testing.T) {
w := announced(t)
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "fail", "t1t1t1t1t1t1"))
if w.op.count() != 0 {
t.Fatalf("a pull request whose checks failed asked the operator %d times", w.op.count())
}
}
// approve is the operator's Approve of the one open ask, heard as the router's `decided` event.
func approve(w *world) {
open := w.op.open()
w.op.answer(open[0], OptionApprove)
w.settleAll()
}
func TestAnApproveHeardAsAnEventMergesAtOnceAtTheApprovedHead(t *testing.T) {
w := announced(t)
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
if len(w.op.open()) != 1 || len(w.forge.merges) != 0 {
t.Fatalf("asks %v, merges %v", w.op.open(), w.forge.merges)
}
approve(w)
if len(w.forge.merges) != 1 || w.forge.merges[0] != "novox/app#7@"+readyHead {
t.Fatalf("the Approve, heard as an event, merged %v; wanted once at the approved head", w.forge.merges)
}
if got := w.forge.approvedHeads[0]; len(got) != 1 || got[0] != "novox/app@"+readyHead {
t.Fatalf("the merge tool was not handed the approved heads: %v", got)
}
}
// Issue 1022: an Approve whose merge the tool refused is asked again at once on the verdict that decides it.
func TestAPassingVerdictAsksTheMergeAtOnceEvenJustAfterARefusal(t *testing.T) {
w := announced(t)
id := IDOf("novox/app", readyHead)
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errAskedAgain
approve(w)
if len(w.forge.merges) != 1 {
t.Fatalf("the merge tool was asked %v; wanted once", w.forge.merges)
}
w.forge.refuse = nil
w.h.PullMerged(merged("novox/app", 9, "9999999999999999", "t2t2t2t2t2t2"))
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t2t2t2t2t2t2"))
if len(w.forge.merges) != 2 || w.delivery(id).Ready.Merged == nil {
t.Fatalf("the passing verdict did not ask the merge at once: %v %+v", w.forge.merges, w.delivery(id).Ready)
}
}
var errAskedAgain = &askedAgainError{}
type askedAgainError struct{}
func (*askedAgainError) Error() string {
return "the forge refused gitea_merge_pull_request: novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. The check of head cccccccccccc merged into trunk bbbbbbbbbbbb was asked; took 3s. Ask again at cccccccccccc once both statuses pass. Nothing was merged"
}
// The gate reads nothing from a pull request's description (novox/hq ADR 0493 §6, ADR 0659, which ends ADR 0496): its form
// of a pull request and its delivery hold no description, no review line, no pushed-by and no after: line, so no wait can
// read one; a pull request is asked once its other waits are met, and the ask names no review.
func TestAPullRequestIsAskedWhateverItsDescriptionSays(t *testing.T) {
for _, form := range []any{PullEvent{}, Delivery{}} {
ty := reflect.TypeOf(form)
for i := 0; i < ty.NumField(); i++ {
name := strings.ToLower(ty.Field(i).Name)
for _, never := range []string{"body", "description", "pushed", "after", "review"} {
if strings.Contains(name, never) {
t.Errorf("%s holds the field %s, a description's", ty.Name(), ty.Field(i).Name)
}
}
}
}
w := newWorld(t)
w.op.auto = false
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
open := w.op.open()
if len(open) != 1 {
t.Fatalf("an unreviewed pull request with its other waits met was not asked: %d", len(open))
}
if e := w.op.ask(open[0]).Explanation; strings.Contains(strings.ToLower(e), "review") {
t.Fatalf("the ask names a review:\n%s", e)
}
}
// Issue 1022 (ADR 0421 §6): a push ends the Approve and says so once on the pull request.
func TestAPushAfterTheAskEndsItAndSaysSoOnceOnThePullRequest(t *testing.T) {
w := announced(t)
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
old := w.op.open()[0]
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
w.h.React()
w.h.React()
if len(w.op.cancelled) != 1 || w.op.cancelled[0] != old || len(w.op.open()) != 0 {
t.Fatalf("the ask was not cancelled: %v, open %v", w.op.cancelled, w.op.open())
}
if len(w.forge.comments) != 1 || !strings.Contains(w.forge.comments[0], "Approve") ||
!strings.Contains(w.forge.comments[0], "ended") {
t.Fatalf("the end of the Approve is not said once on the pull request: %v", w.forge.comments)
}
// The new head is asked on its own when its checks pass.
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, pushed, "pass", "t1t1t1t1t1t1"))
if open := w.op.open(); len(open) != 1 || open[0] == old {
t.Fatalf("the new head was not asked afresh: %v", open)
}
if len(w.forge.comments) != 1 {
t.Fatalf("said more than once: %v", w.forge.comments)
}
}
@@ -1,445 +0,0 @@
package main
import (
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"testing"
"time"
)
// What a test stands the holder on: a store in memory that can be made to fail, a controller that records
// what it was asked and answers what it is told, a forge that records every note, view and status.
type memStore struct {
mu sync.Mutex
deliveries map[string][]byte
groups map[string][]byte
failing bool
// puts counts the deliveries written, failed or not.
puts int
}
func newMemStore() *memStore {
return &memStore{deliveries: map[string][]byte{}, groups: map[string][]byte{}}
}
func (s *memStore) PutDelivery(d *Delivery) error {
s.mu.Lock()
defer s.mu.Unlock()
s.puts++
if s.failing {
return errors.New("the bus is away")
}
s.deliveries[d.ID] = mustJSON(d)
return nil
}
func (s *memStore) DeleteDelivery(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.deliveries, id)
return nil
}
func (s *memStore) PutGroup(g *Group) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.failing {
return errors.New("the bus is away")
}
s.groups[g.ID] = mustJSON(g)
return nil
}
func (s *memStore) DeleteGroup(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.groups, id)
return nil
}
func (s *memStore) Deliveries() ([]*Delivery, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []*Delivery
for key, raw := range s.deliveries {
if key == carriedKey { // the mark an earlier owner's carry-over wrote, as the real store skips it
continue
}
var d Delivery
if err := json.Unmarshal(raw, &d); err != nil {
return nil, err
}
out = append(out, &d)
}
return out, nil
}
func (s *memStore) Groups() ([]*Group, error) {
s.mu.Lock()
defer s.mu.Unlock()
var out []*Group
for _, raw := range s.groups {
var g Group
if err := json.Unmarshal(raw, &g); err != nil {
return nil, err
}
out = append(out, &g)
}
return out, nil
}
type fakeForge struct {
mu sync.Mutex
notes map[string][]string
views map[string]string
statuses map[string]string
// statusWrites counts the statuses set.
statusWrites int
down bool
// viewErr is what View answers instead, when set: a pull request the forge no longer holds.
viewErr error
viewCalls int
// What Statuses reads: the pull requests by owner/repo#number, the base's required statuses, whether the
// protection cannot be read, and what it was asked.
pulls map[string]ForgePull
// unwritable are the repositories (owner/repo) the forge's merging account cannot write (issue 1945).
unwritable map[string]bool
required []string
protectionDown bool
asked []string
// contains answers gitea_contains by "<commit> in <in>"; a pair it does not hold is a forge that cannot say.
contains map[string]bool
containsDown bool
containsAsks []string
// open is what OpenPulls answers; openDown a forge that cannot say.
open []OpenPull
openDown bool
openCalls int
// merges and comments are what was asked of the forge for an approved merge (novox/hq issue 722); approvedHeads the
// heads each merge was handed as approved (ADR 0421 §9).
merges, comments []string
approvedHeads [][]string
refuse error
// refuseRepo refuses the merges of one repository only ("owner/name"), beside refuse (issue 1049).
refuseRepo map[string]error
// calls are the comments in the order asked.
calls []string
}
func (f *fakeForge) OpenPulls() ([]OpenPull, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.openCalls++
if f.openDown {
return nil, errors.New("the forge is away")
}
return append([]OpenPull(nil), f.open...), nil
}
func newFakeForge() *fakeForge {
return &fakeForge{notes: map[string][]string{}, views: map[string]string{}, statuses: map[string]string{},
pulls: map[string]ForgePull{}, contains: map[string]bool{}}
}
func (f *fakeForge) Note(owner, repo, commit, line string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.down {
return errors.New("the forge is away")
}
key := owner + "/" + repo + "@" + commit
for _, l := range f.notes[key] {
if l == line {
return nil // appended once
}
}
f.notes[key] = append(f.notes[key], line)
return nil
}
func (f *fakeForge) View(owner, repo string, number int, body string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.viewCalls++
if f.viewErr != nil {
return f.viewErr
}
if f.down {
return errors.New("the forge is away")
}
f.views[fmt.Sprintf("%s/%s#%d", owner, repo, number)] = body
return nil
}
func (f *fakeForge) Status(owner, repo, commit, context, state, description, target string) error {
f.mu.Lock()
defer f.mu.Unlock()
if f.down {
return errors.New("the forge is away")
}
f.statusWrites++
f.statuses[owner+"/"+repo+"@"+commit+" "+context] = state + " " + description + " → " + target
return nil
}
// Statuses answers what the fake was told: a pull request's head from pulls, the statuses set on the commit
// (those Status set too, as the forge would keep them), and the base's required statuses from required.
func (f *fakeForge) Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.down {
return nil, errors.New("the forge is away")
}
f.asked = append(f.asked, fmt.Sprintf("%s/%s#%d@%s base=%s", owner, repo, number, commit, base))
out := &ForgeChecks{}
if number > 0 {
if p, ok := f.pulls[fmt.Sprintf("%s/%s#%d", owner, repo, number)]; ok {
out.Pull = &p
if commit == "" {
commit = p.HeadSHA
}
if base == "" {
base = p.Base
}
} else if commit == "" {
return nil, fmt.Errorf("no pull request %d", number)
}
}
for key, said := range f.statuses {
at, context, _ := strings.Cut(key, " ")
if !strings.HasPrefix(at, owner+"/"+repo+"@") || !sameCommit(strings.TrimPrefix(at, owner+"/"+repo+"@"), commit) {
continue
}
if len(strings.TrimPrefix(at, owner+"/"+repo+"@")) > len(commit) {
commit = strings.TrimPrefix(at, owner+"/"+repo+"@")
}
state, rest, _ := strings.Cut(said, " ")
description, link, _ := strings.Cut(rest, " → ")
out.Statuses = append(out.Statuses, ForgeStatus{Context: context, State: state, Description: description,
TargetURL: link, UpdatedAt: "2026-10-06T12:00:00Z", Creator: "mesh-admin"})
}
sort.Slice(out.Statuses, func(i, j int) bool { return out.Statuses[i].Context < out.Statuses[j].Context })
out.Commit = commit
if base == "" {
base = "main"
}
out.Merge = ForgeMerge{Branch: base, Protected: f.required != nil, Required: f.required}
if f.unwritable[owner+"/"+repo] {
no := false
out.Merge.Writes, out.Merge.CannotWrite = &no, "the forge account cannot write "+owner+"/"+repo+": gitea_grant_write"
}
if f.protectionDown {
out.Merge.Says = "the protection of " + base + " could not be read"
return out, nil
}
ok := true
for _, r := range f.required {
state := "missing"
for _, st := range out.Statuses {
if st.Context == r {
state = st.State
}
}
if state != "success" {
ok = false
out.Merge.Blocking = append(out.Merge.Blocking, struct {
Context string `json:"context"`
State string `json:"state"`
}{r, state})
}
}
out.Merge.Mergeable = &ok
out.Merge.Says = fmt.Sprintf("%s requires %v", base, f.required)
return out, nil
}
// Contains answers what the fake was told of two commits.
func (f *fakeForge) Contains(owner, repo, commit, in string) (bool, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.containsAsks = append(f.containsAsks, commit+" in "+in)
if f.containsDown || f.down {
return false, errors.New("the forge is away")
}
yes, said := f.contains[commit+" in "+in]
if !said {
return false, fmt.Errorf("the forge has no commit %s or %s", commit, in)
}
return yes, nil
}
func (f *fakeForge) notesOn(commit string) []string {
f.mu.Lock()
defer f.mu.Unlock()
for key, lines := range f.notes {
if strings.HasSuffix(key, "@"+commit) {
return append([]string(nil), lines...)
}
}
return nil
}
// world is a holder over fakes, with a clock a test moves.
type world struct {
t *testing.T
h *Holder
store *memStore
forge *fakeForge
op *fakeOperator
merges *fakeMerges
old *fakeOld
source *fakeSource
owed *fakeOwed
// asked is the key of the ask a test answered.
asked string
now time.Time
logbuf strings.Builder
said []map[string]any
mu sync.Mutex
}
func newWorld(t *testing.T) *world {
return newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC))
}
func newWorldOver(t *testing.T, store *memStore, forge *fakeForge, now time.Time) *world {
w := &world{t: t, store: store, forge: forge, now: now}
w.h = &Holder{Store: store, Forge: forge,
Now: func() time.Time { w.mu.Lock(); defer w.mu.Unlock(); return w.now },
Logf: func(format string, a ...any) { t.Logf(format, a...); fmt.Fprintf(&w.logbuf, format+"\n", a...) },
Emit: func(event string, body any) error {
w.mu.Lock()
defer w.mu.Unlock()
var m map[string]any
_ = json.Unmarshal(mustJSON(body), &m)
m["event-kind"] = event
w.said = append(w.said, m)
return nil
}}
// The operator approves at once unless a test says otherwise (w.op.auto = false): most tests are not about the Approve.
w.op = newFakeOperator(w.h.Now)
w.op.auto = true
w.h.Operator = w.op
w.merges, w.source = newFakeMerges(forge), &fakeSource{}
w.old = &fakeOld{states: map[string]map[string]oldEntry{}}
w.h.Merges, w.h.Old, w.h.Source = w.merges, w.old, w.source
w.merges.h = w.h
w.owed = newFakeOwed(forge)
w.owed.h = w.h
w.h.Owed = w.owed
w.h.init()
if err := w.h.Load(); err != nil {
t.Fatal(err)
}
return w
}
func (w *world) later(d time.Duration) {
w.mu.Lock()
w.now = w.now.Add(d)
w.mu.Unlock()
}
// settleAll takes a look a few times: the answers the teller wrote are read at each, and each member of a group handed to the
// forge bridge is merged by the fake bridge, whose word is heard by the next look.
func (w *world) settleAll() {
for range 4 {
w.h.React()
}
}
func (w *world) delivery(id string) *Delivery {
w.t.Helper()
w.h.mu.Lock()
defer w.h.mu.Unlock()
d := w.h.deliveries[id]
if d == nil {
w.t.Fatalf("no delivery %s; there are %v", id, w.ids())
}
c := *d
return &c
}
func (w *world) ids() []string {
var out []string
for id := range w.h.deliveries {
out = append(out, id)
}
sort.Strings(out)
return out
}
func (w *world) state(id string) State { return w.delivery(id).State }
// pr is a pull request's head as the forge announces it.
func pr(repo string, number int, sha, branch string, paths ...string) PullEvent {
owner, name, _ := strings.Cut(repo, "/")
return PullEvent{Owner: owner, Repo: name, Number: number, Base: "main", Head: branch, HeadSHA: sha,
HTMLURL: fmt.Sprintf("https://forge.invalid/%s/pulls/%d", repo, number), Paths: paths}
}
// seatUpdated is a pull request's new head as the seat git says it (pull.updated).
func seatUpdated(p PullEvent) gitseat.PullUpdatedEvent {
e := gitseat.PullUpdatedEvent{Repository: p.Owner + "/" + p.Repo, Number: int64(p.Number), Base: p.Base, Branch: p.Head,
Head: p.HeadSHA, URL: p.HTMLURL, Title: p.Title, BaseHead: p.BaseSHA, Author: p.Author}
if p.Paths != nil {
e.Change = &gitseat.Change{Paths: p.Paths, PathsTruncated: p.PathsTruncated, Removed: p.Removed, ModuleDirs: p.ModuleDirs}
}
return e
}
// seatMerged is a pull request merged as the seat git says it (pull.merged).
func seatMerged(p PullEvent) gitseat.PullMergedEvent {
e := gitseat.PullMergedEvent{Repository: p.Owner + "/" + p.Repo, Number: int64(p.Number), Base: p.Base, Branch: p.Head,
Head: p.HeadSHA, MergeCommit: p.MergeCommit, URL: p.HTMLURL, HeadChecks: p.HeadChecks}
if at, err := time.Parse(time.RFC3339, p.MergedAt); err == nil {
e.MergedAt = &at
}
if p.Paths != nil {
e.Change = &gitseat.Change{Paths: p.Paths}
}
return e
}
// seatOpenPull is an open pull request as the seat git's open-pulls answers it.
func seatOpenPull(p OpenPull) gitseat.OpenPull {
o := gitseat.OpenPull{Pull: gitseat.Pull{Repository: p.Owner + "/" + p.Repo, Number: int64(p.Number), Base: p.Base,
Head: p.HeadSHA, URL: p.HTMLURL, State: gitseat.PullStateOpen}, Required: p.Required, Checks: map[string]gitseat.Status{}}
for context, c := range p.Checks {
o.Checks[context] = gitseat.Status{Context: context, State: c.State, Description: c.Description}
}
return o
}
// seatClosed is a pull request closed as the seat git says it (pull.closed).
func seatClosed(p PullEvent) gitseat.PullClosedEvent {
return gitseat.PullClosedEvent{Repository: p.Owner + "/" + p.Repo, Number: int64(p.Number), Base: p.Base, Branch: p.Head,
Head: p.HeadSHA}
}
// verdict is the controller's `checked` for a head.
func verdict(repo string, number int, sha, gate string) CheckedEvent {
owner, name, _ := strings.Cut(repo, "/")
c := CheckedEvent{Owner: owner, Repo: name, Number: number, Commit: sha, Verdict: gate, Summary: "the gate said " + gate,
ID: "check-of-" + sha}
return c
}
// merged is the forge's `pull.merged`.
func merged(repo string, number int, head, merge string) PullEvent {
p := pr(repo, number, head, "feature")
p.MergeCommit = merge
p.MergedAt = "2026-10-06T12:30:00Z"
return p
}
// Comment records what was asked (novox/hq issue 722).
func (f *fakeForge) Comment(owner, repo string, number int, body string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.comments = append(f.comments, fmt.Sprintf("%s/%s#%d: %s", owner, repo, number, body))
f.calls = append(f.calls, "comment "+body)
return nil
}
func (w *world) logs() string { return w.logbuf.String() }
@@ -0,0 +1,88 @@
package main
import (
"sync"
"time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
)
// The holder of the seat mesh-delivery (novox/hq ADR 0239, to-be 47): from milestone G's delivery it keeps no merge states
// and asks no Approve per merge (design 47 G §172, §431, §592, §593, §613). What it keeps is the delivery records
// (record.go), the source revision per watched branch (merges.go) and what it owes the forge. It merges nothing.
// Seat is the seat the gate holds.
const Seat = "mesh-delivery"
// Holder is the seat's holder.
type Holder struct {
// Settings reads the operator's values (settings.go); nil keeps every default.
Settings func() Settings
Now func() time.Time
Logf func(format string, a ...any)
// Merges and Old are the old sections the switch reads (switch.go); Source is the gate's source (merges.go).
Merges MergeStore
Old OldState
Source SourceStore
// Owed says what the gate owes the forge as the seat's event owed (owed.go).
Owed OwedSayer
// Records keeps the delivery records on the gate's ledger (record.go, ADR 0656 §7); nil keeps none.
Records *Records
// Consumer applies the step events to the records (consumer.go); nil applies none.
Consumer *StepConsumer
// SaySeat says an event of the seat mesh-delivery (recordevents.go); nil says none.
SaySeat SeatSay
// Say says a condition to the operator, and Conditions keeps which the gate raised (conditions.go); nil says none.
Say func(c oc.Condition) error
Conditions ConditionStore
// said is what Conditions keeps, read at the first look; sayFailures the looks in a row a condition could not be said.
// Touched by React alone.
said map[string]string
sayFailures int
mu sync.Mutex
started time.Time
refusals []string
// tried counts the owed effects tried since this owner started, by how each ended (novox/hq issue 407).
tried owedTries
// reactMu keeps one React at a time; poke asks for a look (react.go); wanted is when the next is wanted, and its timer.
reactMu sync.Mutex
poke chan struct{}
wanted lookWanted
}
// owedTries is how many owed effects were tried since this owner started, and when the last was.
type owedTries struct {
done, failed, givenUp int
last time.Time
}
// The word this owner signs its acts with.
const byOwner = "mesh-delivery"
func (h *Holder) init() {
if h.Now == nil {
h.Now = func() time.Time { return time.Now().UTC() }
}
if h.Logf == nil {
h.Logf = func(string, ...any) {}
}
h.poke = make(chan struct{}, 1)
h.started = h.Now()
}
// Refuse keeps a refusal from outside the lock.
func (h *Holder) Refuse(err error) {
h.mu.Lock()
defer h.mu.Unlock()
h.refuse(err)
}
// refuse keeps a refusal, said, for the status; under the lock.
func (h *Holder) refuse(err error) {
h.Logf("[mesh-delivery] %v", err)
h.refusals = append(h.refusals, h.Now().Format(time.RFC3339)+" "+err.Error())
if len(h.refusals) > 50 {
h.refusals = h.refusals[len(h.refusals)-50:]
}
}
@@ -1,153 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"slices"
"strings"
"time"
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
)
// The forge as the seat `git` says it (design 47 *The seat git*): its events heard as the seat's, by the key the runtime
// hands them under (`git.<event>`), each read into the gate's own forms here and nowhere else.
// gitEvent is the key a seat `git` event arrives under.
func gitEvent(event string) string { return gitseat.Seat + "." + event }
// gitEvents are the seat's events the gate hears.
var gitEvents = []string{
gitEvent(gitseat.EventPullUpdated), gitEvent(gitseat.EventPullMerged), gitEvent(gitseat.EventPullClosed),
gitEvent(gitseat.EventForgePull), gitEvent(gitseat.EventForgeStatus), gitEvent(gitseat.EventRepoWritable),
}
// splitRepository is a repository's owner and name.
func splitRepository(repository string) (owner, repo string) {
owner, repo, _ = strings.Cut(repository, "/")
return owner, repo
}
// timeText is a time as the gate keeps it, RFC 3339; empty when not said.
func timeText(t *time.Time) string {
if t == nil || t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC3339)
}
// withChange carries a pull request's changed paths into the gate's form; a list of module directories absent is one the
// forge could not read.
func withChange(p *PullEvent, c *gitseat.Change) {
if c == nil {
return
}
p.Paths, p.PathsTruncated, p.Removed = c.Paths, c.PathsTruncated, c.Removed
p.ModuleDirs, p.ModuleDirsSaid = c.ModuleDirs, c.ModuleDirs != nil
}
// gitPullEvent reads a seat `git` event of a pull request into the gate's PullEvent. The event's head is its commit and its
// branch the head branch, as the gate's Head and HeadSHA name them.
func gitPullEvent(key string, body json.RawMessage) (PullEvent, error) {
var p PullEvent
switch key {
case gitEvent(gitseat.EventPullUpdated):
var e gitseat.PullUpdatedEvent
if err := json.Unmarshal(body, &e); err != nil {
return p, err
}
p = PullEvent{Number: int(e.Number), Title: e.Title, Base: e.Base, Head: e.Branch, HeadSHA: e.Head, HTMLURL: e.URL,
BaseSHA: e.BaseHead, Author: e.Author}
p.Owner, p.Repo = splitRepository(e.Repository)
withChange(&p, e.Change)
case gitEvent(gitseat.EventPullMerged):
var e gitseat.PullMergedEvent
if err := json.Unmarshal(body, &e); err != nil {
return p, err
}
p = PullEvent{Number: int(e.Number), Title: e.Title, Base: e.Base, Head: e.Branch, HeadSHA: e.Head, HTMLURL: e.URL,
MergeCommit: e.MergeCommit, MergedAt: timeText(e.MergedAt), HeadChecks: e.HeadChecks, Merged: true, Author: e.Author}
p.Owner, p.Repo = splitRepository(e.Repository)
withChange(&p, e.Change)
case gitEvent(gitseat.EventPullClosed):
var e gitseat.PullClosedEvent
if err := json.Unmarshal(body, &e); err != nil {
return p, err
}
p = PullEvent{Number: int(e.Number), Title: e.Title, Base: e.Base, Head: e.Branch, HeadSHA: e.Head, HTMLURL: e.URL,
Author: e.Author}
p.Owner, p.Repo = splitRepository(e.Repository)
case gitEvent(gitseat.EventForgePull):
var e gitseat.ForgePullEvent
if err := json.Unmarshal(body, &e); err != nil {
return p, err
}
p = PullEvent{Number: int(e.Number), Base: e.Base, Head: e.Branch, HeadSHA: e.Head, MergeCommit: e.MergeCommit,
Merged: e.Merged, Author: e.Author}
p.Owner, p.Repo = splitRepository(e.Repository)
default:
return p, fmt.Errorf("%s is no pull request event of the seat git", key)
}
if p.Owner == "" || p.Repo == "" {
return p, fmt.Errorf("names no repository <owner>/<name>")
}
return p, nil
}
// forgeAction is the forge's own word in a `forge.pull` event: what it did to the pull request.
func forgeAction(body json.RawMessage) string {
var e struct {
Action string `json:"action"`
}
_ = json.Unmarshal(body, &e)
return e.Action
}
// gitStatusEvent reads the seat's `forge.status` into the gate's ForgeStatusEvent.
func gitStatusEvent(body json.RawMessage) (ForgeStatusEvent, error) {
var e gitseat.ForgeStatusEvent
if err := json.Unmarshal(body, &e); err != nil {
return ForgeStatusEvent{}, err
}
s := ForgeStatusEvent{SHA: e.Commit, Context: e.Context, State: e.State, Description: e.Description}
s.Owner, s.Repo = splitRepository(e.Repository)
if s.Owner == "" || s.Repo == "" || s.SHA == "" {
return s, fmt.Errorf("names no repository <owner>/<name> or no commit")
}
return s, nil
}
// forgePullOf is the seat's pull request in the gate's form: a merged one is closed and merged, as the forge said it before.
func forgePullOf(p gitseat.Pull) *ForgePull {
out := &ForgePull{Number: int(p.Number), Title: p.Title, State: p.State, Base: p.Base, Head: p.Branch, HeadSHA: p.Head,
MergeCommit: p.MergeCommit, MergedAt: timeText(p.MergedAt), HTMLURL: p.URL}
if p.State == gitseat.PullStateMerged {
out.State, out.Merged = gitseat.PullStateClosed, true
}
return out
}
// forgeStatusOf is one status of the seat in the gate's form.
func forgeStatusOf(s gitseat.Status) ForgeStatus {
return ForgeStatus{Context: s.Context, State: s.State, Description: s.Description, TargetURL: s.Target,
UpdatedAt: timeText(s.At), Creator: s.By}
}
// openPullOf is one of the seat's open pull requests in the gate's form.
func openPullOf(p gitseat.OpenPull) OpenPull {
out := OpenPull{Number: int(p.Pull.Number), Base: p.Pull.Base, HeadSHA: p.Pull.Head, HTMLURL: p.Pull.URL,
Required: p.Required, Statuses: []string{}}
out.Owner, out.Repo = splitRepository(p.Pull.Repository)
if out.Required == nil {
out.Required = []string{}
}
if len(p.Checks) > 0 {
out.Checks = map[string]HeadCheck{}
}
for context, s := range p.Checks {
out.Statuses = append(out.Statuses, context)
out.Checks[context] = HeadCheck{State: s.State, Description: s.Description, UpdatedAt: timeText(s.At)}
}
slices.Sort(out.Statuses)
return out
}
@@ -1,117 +0,0 @@
package main
import (
"encoding/json"
"errors"
"reflect"
"strings"
"testing"
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
)
// W11.18 (design 47 *The seat git*): every forge read of the gate is a verb of the seat git, its answer typed in the seat's
// contract, and a failed read is an error, never "none"; every forge event is heard as the seat's.
func TestTheGateReadsAndHearsTheForgeThroughTheSeatGit(t *testing.T) {
head := strings.Repeat("c", 40)
pull := gitseat.Pull{Repository: "novox/app", Number: 7, Base: "main", Branch: "feat/x", Head: head, State: "merged",
MergeCommit: strings.Repeat("d", 40), URL: "https://forge.invalid/novox/app/pulls/7"}
answers := map[string]any{
"seat:git.open-pulls": gitseat.OpenPullsAnswer{Pulls: []gitseat.OpenPull{{Pull: pull, Required: []string{"mesh/check"},
Checks: map[string]gitseat.Status{"mesh/check": {Context: "mesh/check", State: "pending"}}}}},
"seat:git.contains": gitseat.ContainsAnswer{Contains: true},
"seat:git.pull": gitseat.PullAnswer{Pull: &pull, Merging: &gitseat.Merging{Mergeable: true, Protected: true,
Required: []string{"mesh/check"}, Writes: true}},
"seat:git.statuses": gitseat.StatusesAnswer{Commit: head, Combined: "pending",
Statuses: []gitseat.Status{{Context: "mesh/check", State: "pending", By: "mesh"}}},
}
for _, c := range []struct {
name string
failed string // the verb whose answer is an error
read func(seatForge) (any, error)
want any
asked []string
}{
{name: "open pull requests", read: func(f seatForge) (any, error) { return f.OpenPulls() },
want: []OpenPull{{Owner: "novox", Repo: "app", Number: 7, Base: "main", HeadSHA: head, HTMLURL: pull.URL,
Required: []string{"mesh/check"}, Statuses: []string{"mesh/check"},
Checks: map[string]HeadCheck{"mesh/check": {State: "pending"}}}},
asked: []string{"seat:git.open-pulls"}},
{name: "open pull requests not read", failed: "seat:git.open-pulls",
read: func(f seatForge) (any, error) { return f.OpenPulls() }, asked: []string{"seat:git.open-pulls"}},
{name: "contains", read: func(f seatForge) (any, error) { return f.Contains("novox", "app", "a", "b") }, want: true,
asked: []string{"seat:git.contains"}},
{name: "contains not read", failed: "seat:git.contains",
read: func(f seatForge) (any, error) { return f.Contains("novox", "app", "a", "b") }, asked: []string{"seat:git.contains"}},
{name: "a pull request by its number", read: func(f seatForge) (any, error) {
c, err := f.Statuses("novox", "app", 7, "", "")
if err != nil {
return nil, err
}
p := *c.Pull
conflict := *p.Mergeable
p.Mergeable = nil
return []any{c.Commit, p, c.Statuses, conflict, *c.Merge.Mergeable, c.Merge.Blocking[0].Context + " " +
c.Merge.Blocking[0].State, c.Merge.Says, *c.Merge.Writes}, nil
}, want: []any{head, ForgePull{Number: 7, State: "closed", Merged: true, Base: "main", Head: "feat/x", HeadSHA: head,
MergeCommit: pull.MergeCommit, HTMLURL: pull.URL}, []ForgeStatus{{Context: "mesh/check", State: "pending", Creator: "mesh"}},
true, false, "mesh/check pending", "main requires mesh/check", true},
asked: []string{"seat:git.pull", "seat:git.statuses"}},
// What the forge would merge now comes from the pull answer's merging alone: none for a commit no pull request names.
{name: "no merge state for a commit alone", read: func(f seatForge) (any, error) {
c, err := f.Statuses("novox", "app", 0, head, "main")
if err != nil {
return nil, err
}
return []any{c.Merge.Mergeable == nil, c.Merge.Writes == nil}, nil
}, want: []any{true, true}, asked: []string{"seat:git.statuses"}},
{name: "a pull request not read", failed: "seat:git.pull",
read: func(f seatForge) (any, error) { return f.Statuses("novox", "app", 7, "", "") },
asked: []string{"seat:git.pull"}},
{name: "statuses not read", failed: "seat:git.statuses",
read: func(f seatForge) (any, error) { return f.Statuses("novox", "app", 0, head, "main") },
asked: []string{"seat:git.statuses"}},
} {
t.Run(c.name, func(t *testing.T) {
var asked []string
f := seatForge{ask: func(key string, body any) (json.RawMessage, error) {
asked = append(asked, key)
if key == c.failed {
return json.Marshal(map[string]string{"error": "the forge answered 502"})
}
a, ok := answers[key]
if !ok {
return nil, errors.New("no such verb: " + key)
}
return json.Marshal(a)
}}
got, err := c.read(f)
if !reflect.DeepEqual(asked, c.asked) {
t.Fatalf("asked %v, wanted %v", asked, c.asked)
}
if c.failed != "" {
if err == nil || !strings.Contains(err.Error(), "the forge answered 502") {
t.Fatalf("a failed read answered %v, %v; wanted its error", got, err)
}
return
}
if err != nil || !reflect.DeepEqual(got, c.want) {
t.Fatalf("read %#v, %v; wanted %#v", got, err, c.want)
}
})
}
t.Run("a merge heard", func(t *testing.T) {
body, _ := json.Marshal(gitseat.PullMergedEvent{Repository: "novox/app", Number: 7, Base: "main", Branch: "feat/x",
Head: head, MergeCommit: pull.MergeCommit, Author: "jo", Change: &gitseat.Change{Paths: []string{"a.go"}}})
p, err := gitPullEvent(gitEvent(gitseat.EventPullMerged), body)
want := PullEvent{Owner: "novox", Repo: "app", Number: 7, Base: "main", Head: "feat/x", HeadSHA: head,
MergeCommit: pull.MergeCommit, Merged: true, Author: "jo", Paths: []string{"a.go"}}
if err != nil || !reflect.DeepEqual(p, want) {
t.Fatalf("heard %+v, %v; wanted %+v", p, err, want)
}
if _, err := gitPullEvent(gitEvent(gitseat.EventPullMerged), []byte(`{"number":7}`)); err == nil {
t.Fatal("a merge naming no repository was taken")
}
})
}
@@ -1,396 +0,0 @@
package main
import (
"fmt"
"sort"
"strings"
"time"
)
// A group is merged whole or not at all, in its order (issue 1275, novox/hq ADR 0418).
//
// - Before the operator is asked, and again before any member is merged, every member's required statuses are read at
// its head on the forge; one that is not a success holds the group, and nothing is merged.
// - The members are merged one after the other in the group's order, and the pass stops at the first refusal: no later
// member is merged.
// - A group held or half merged is said as a stalled line, urgent, until every member is merged or a person decides.
// memberRead is what a status read of one member needs, taken under the lock.
type memberRead struct {
id, owner, repo, base, head string
number int
}
// readOf is a delivery as a status read needs it, at the head the Approve or the ask covers.
func readOf(d *Delivery, head string) memberRead {
if head == "" {
head = d.Commit
}
return memberRead{id: d.ID, owner: d.Owner(), repo: d.Repo(), base: d.Base, head: head, number: d.Number}
}
// forgeMergeGrace is how long the forge may say a member does not merge now before the group is held for it (issue 1275):
// the forge says so for a few seconds after a base moved, while it works out whether the pull request still merges; within
// the grace the merge is asked again every forgeMergeAgain. Past it, the member is in conflict with its trunk: held, said
// urgent, and asked again on the next event (a new head, a status).
const (
forgeMergeGrace = 2 * time.Minute
forgeMergeAgain = 5 * time.Second
)
// notASuccess reads a member's required statuses at its head and says, in the forge's words, which are not a success,
// and whether the forge says the pull request merges into its base now (its head still the one approved, and not in
// conflict); empty when all hold. forge says the reason is the forge's merge answer rather than a status. An error is a
// read that could not be made: the member then counts as not mergeable.
func (h *Holder) notASuccess(m memberRead) (says string, forge bool, err error) {
return h.memberRead(m, true)
}
// notMergeableNow is notASuccess at the merge: the head the Approve covers and the forge's merge answer, and no required
// status read again, for the merge judges the operator's Approve alone (novox/hq ADR 0661 §4).
func (h *Holder) notMergeableNow(m memberRead) (says string, forge bool, err error) {
return h.memberRead(m, false)
}
func (h *Holder) memberRead(m memberRead, statuses bool) (says string, forge bool, err error) {
fc, err := h.Forge.Statuses(m.owner, m.repo, m.number, m.head, m.base)
if err != nil {
return "", false, err
}
if statuses && fc.Merge.Mergeable == nil {
return "", false, fmt.Errorf("the forge could not say whether %s may merge: %s", m.id, fc.Merge.Says)
}
if statuses && !*fc.Merge.Mergeable {
var parts []string
for _, b := range fc.Merge.Blocking {
parts = append(parts, fmt.Sprintf("%s (%s)", b.Context, b.State))
}
return fmt.Sprintf("%s requires %s; not a success: %s", fc.Merge.Branch, strings.Join(fc.Merge.Required, ", "),
strings.Join(parts, ", ")), false, nil
}
if p := fc.Pull; p != nil && m.number > 0 {
if p.HeadSHA != "" && !sameCommit(p.HeadSHA, m.head) {
return fmt.Sprintf("its head is now %s, not the %s approved", shortOf(p.HeadSHA, 12), shortOf(m.head, 12)), false, nil
}
if p.Mergeable != nil && !*p.Mergeable {
base := firstOf(p.Base, m.base)
return fmt.Sprintf("the forge says it does not merge into %s now (in conflict with it, a work in progress, or still "+
"being worked out after %s moved)", base, base), true, nil
}
}
return "", false, nil
}
// unmergedLive are a group's members not yet merged and not over, in the group's order. Under the lock.
func (h *Holder) unmergedLive(g *Group) []*Delivery {
var out []*Delivery
for _, d := range h.membersOf(g) {
if d.MergedAs == "" && !d.State.Final() && (d.Ready == nil || d.Ready.Merged == nil) {
out = append(out, d)
}
}
return out
}
// groupMergeable reads every member's head and the forge's merge answer at the head the Approve covers, no required status
// (novox/hq ADR 0661 §4), and says whether all hold: no member is merged unless every member merges now (issue 1275, 2026-10-26: two of
// three merged and the third refused). When one does not, or cannot be read, nothing is handed to the bridge: the group is
// held, said urgent (ADR 0441 §2, §4), and the target's turn is given back so the next event that decides whether it may
// merge asks it again. A read that could not be made is a failed ask, tried again after askAgainAfter; the forge saying a
// member does not merge now is asked again every forgeMergeAgain for forgeMergeGrace before the group is held. Outside
// the lock.
func (h *Holder) groupMergeable(gid, target string, reads []memberRead) bool {
release := func() {
if d := h.deliveries[target]; d != nil && d.Ready != nil {
d.Ready.merging = false
d.Ready.LastTry = time.Time{}
}
}
for _, m := range reads {
says, forge, err := h.notMergeableNow(m)
if err == nil && says == "" {
continue
}
h.mu.Lock()
if err != nil {
h.Logf("[mesh-delivery] group %s: the required statuses of %s cannot be read, so nothing is merged yet: %v", gid, m.id, err)
release()
h.again("a group's merge whose required statuses could not be read")
h.mu.Unlock()
return false
}
if forge {
if h.unmergeableSince == nil {
h.unmergeableSince = map[string]time.Time{}
}
key, now := gid+" "+m.id, h.Now()
first, seen := h.unmergeableSince[key]
if !seen {
first = now
h.unmergeableSince[key] = now
h.Logf("[mesh-delivery] group %s: nothing is merged yet: %s at %s: %s; asked again every %s for %s", gid, m.id,
shortOf(m.head, 12), says, forgeMergeAgain, forgeMergeGrace)
}
if now.Sub(first) < forgeMergeGrace {
release()
h.lookAt(now.Add(forgeMergeAgain), "a group's member the forge was still weighing")
h.mu.Unlock()
return false
}
}
if g := h.groups[gid]; g != nil {
if g.Hold == nil || g.Hold.Kind != "blocked" || g.Hold.Says != says || g.Hold.Member != m.id {
h.Logf("[mesh-delivery] group %s is approved and NOT merged: %s at %s: %s", gid, m.id, shortOf(m.head, 12), says)
}
since := time.Time{}
if g.Hold != nil && g.Hold.Kind == "blocked" {
since = g.Hold.Since
}
if since.IsZero() {
since = h.Now()
}
g.Hold = &GroupHold{Kind: "blocked", Member: m.id, Says: says, Since: since}
h.keepGroup(g)
}
// No timer: the status turning success, or a new head, is an event on the forge (CommitStatus, mergeAgainNow), and
// it asks the merge again at once (issue 1275: a pass that stops part way resumes on an event).
release()
h.mu.Unlock()
return false
}
h.mu.Lock()
for key := range h.unmergeableSince {
if strings.HasPrefix(key, gid+" ") {
delete(h.unmergeableSince, key)
}
}
if g := h.groups[gid]; g != nil && g.Hold != nil {
g.Hold = nil
h.keepGroup(g)
}
h.mu.Unlock()
return true
}
// passWaits is why an approved merge is not handed yet because another approved merge's pass is under way into a
// repository it goes into; empty when none is (issue 1275, 2026-10-26 13:40: group feat/890-assignments merged
// mesh-delivery#72 between the members of fix/734-lost-ask, whose mesh-delivery#73 then conflicted). A pass is under way
// from its first member handed until every member is merged; one half merged stays under way while it is refused or held,
// until it is whole, declined, or its remaining members are gone or asked anew (a new head is a new Approve). The first
// merge handed has the repositories; the other is handed once that pass ends, on the event that ends it. This holds
// whether or not mesh/check gates the merge: it waits for a merge, never for a check (ADR 0551). Under the lock.
func (h *Holder) passWaits(d *Delivery) string {
c := h.covering(d)
if c == nil || c.Approved == nil || c.AskID == "" {
return ""
}
mine := h.unmergedRepos(c)
for _, p := range h.passes() {
if p.run.AskID == c.AskID || !h.passUnderWay(p.run) {
continue
}
for repo := range h.unmergedRepos(p.run) {
if mine[repo] {
return fmt.Sprintf("approved; waits for the merge of %s into %s to end, so no other merge lands there between "+
"its members (issue 1275)", p.name, repo)
}
}
}
return ""
}
// pass is an approved merge not merged whole: the run holding its Approve and the name of its group (or pull request).
type pass struct {
run *ReadyRun
name string
}
// passes are the approved merges not merged whole and not declined, each ask once. Unlike approvedMerges it keeps one whose
// members merged or were refused: a half-merged pass still holds its repositories (issue 1275). Under the lock.
func (h *Holder) passes() []pass {
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids)
seen := map[string]bool{}
var out []pass
for _, id := range ids {
d := h.deliveries[id]
r := d.Ready
if r == nil || r.Approved == nil || r.declined() || r.AskID == "" || len(r.Heads) == 0 || seen[r.AskID] ||
d.Base != MergeBranch || h.allMerged(r.Heads) {
continue
}
seen[r.AskID] = true
out = append(out, pass{run: r, name: askGroup(d)})
}
return out
}
// unmergedRepos are the repositories (lowercase) of the deliveries an Approve covers that are not merged and may still be
// merged on it: not over (closed, held), at the head approved, and covered by this Approve, not a newer one. Under the lock.
func (h *Holder) unmergedRepos(r *ReadyRun) map[string]bool {
out := map[string]bool{}
for id, sha := range r.Heads {
m := h.deliveries[id]
if m == nil || m.MergedAs != "" || (m.Ready != nil && m.Ready.Merged != nil) || m.State.Final() || !sameCommit(m.Commit, sha) {
continue
}
if c := h.covering(m); c == nil || c.AskID != r.AskID {
continue // asked anew (its new head, or a new Approve): that ask is the pass now
}
out[strings.ToLower(m.Repository)] = true
}
return out
}
// passUnderWay is whether an approved merge's pass has started and not ended: a member is handed to the bridge, or some
// are merged and others may still merge on this Approve, refused or held or not (issue 1275, review: a held half-merged
// group that let another group start, then asked again, waited on that group while it waited on it). Under the lock.
func (h *Holder) passUnderWay(r *ReadyRun) bool {
if len(h.unmergedRepos(r)) == 0 {
return false
}
for id := range r.Heads {
m := h.deliveries[id]
if m == nil {
continue
}
if m.MergedAs != "" || (m.Ready != nil && m.Ready.Merged != nil) || (m.Ready != nil && m.Ready.merging) {
return true
}
}
return false
}
// groupLines are the groups that are held or half merged, as urgent stalled lines the gate says as conditions (conditions.go):
// one for each, on its first member not merged, until every member is merged or a person decides (issue 1275). A member
// the merge tool refused is said by readyLines, with its group; this adds a group held for a required status, and a
// group half merged with no refusal. Under the lock.
func (h *Holder) groupLines(now time.Time) []StalledLine {
var out []StalledLine
for _, g := range h.groups {
live := h.unmergedLive(g)
if len(live) == 0 {
continue
}
first, refused, running, merged := live[0], false, false, 0
for _, d := range live {
if d.Ready != nil && (d.Ready.RefusedSince != nil || d.Ready.Failed != "" || d.Ready.Refused != "") {
refused = true
}
if d.Ready != nil && d.Ready.merging {
running = true
}
}
for _, d := range h.membersOf(g) {
if d.MergedAs != "" || (d.Ready != nil && d.Ready.Merged != nil) {
merged++
}
}
heldOn := h.deliveries[holdMember(g)]
switch {
// A held member the merge tool refused is said by readyLines already: one line for an id, not two.
case g.Hold != nil && g.Hold.Kind == "blocked" && !running && !(heldOn != nil && heldOn.Ready != nil && heldOn.Ready.RefusedSince != nil):
stands := "is approved and nothing is merged"
if merged > 0 {
stands = "is half merged" // a member merged before the pass stopped (issue 1275)
}
out = append(out, StalledLine{ID: g.Hold.Member, Number: numberOf(h.deliveries[g.Hold.Member]), State: first.State,
For: now.Sub(g.Hold.Since).Round(time.Second).String(), Bound: "0s", Severity: "urgent",
H2: "none: the required status turns success or a new head merges, which asks the merge again — or the operator's decision",
Says: fmt.Sprintf("group %s %s (merged %d of %d): %s does not merge now "+
"at its head: %s. Needed: that status turns success (its check asked again), a new head that merges "+
"(rebased on its trunk), or the operator decides.",
g.ID, stands, merged, len(h.membersOf(g)), g.Hold.Member, g.Hold.Says)})
case merged > 0 && !refused && !running:
out = append(out, StalledLine{ID: first.ID, Number: first.Number, State: first.State,
For: now.Sub(g.Updated).Round(time.Second).String(), Bound: "0s", Severity: "urgent",
H2: "none: every member merged, or the operator decides",
Says: fmt.Sprintf("group %s is half merged (%d of %d members merged): %s is not merged. Needed: it merges "+
"(its Approve and checks hold at its head), or the operator decides.", g.ID, merged, len(h.membersOf(g)), first.ID)})
}
}
return out
}
func numberOf(d *Delivery) int {
if d == nil {
return 0
}
return d.Number
}
func holdMember(g *Group) string {
if g.Hold == nil {
return ""
}
return g.Hold.Member
}
// MergeAgain is the tool delivery_merge_again: on the operator's word, the merge of an approved delivery the forge refused,
// or of a group held part way, is asked again at once at the heads the Approve named (issue 1275): a refusal the forge
// meant as "later", a conflict cleared on the trunk, or a refusal repeated until the gate stopped asking. A delivery whose
// head moved is asked again by its new head, never by this. Refused, naming why, for one that is not an approved merge
// waiting after a refusal or a hold.
func (h *Holder) MergeAgain(id, why string) (string, error) {
h.mu.Lock()
defer h.mu.Unlock()
d := h.deliveries[id]
if d == nil {
return "", fmt.Errorf("no delivery %q", id)
}
if strings.TrimSpace(why) == "" {
return "", fmt.Errorf("say why: the merge asked again is the operator's word, kept in the log")
}
c := h.covering(d)
if c == nil || c.Approved == nil || c.declined() || c.Heads[d.ID] != d.Commit {
return "", fmt.Errorf("%s is not approved at its head %s: nothing to ask again (a new head is asked on its own)", id, shortOf(d.Commit, 12))
}
if d.MergedAs != "" || (d.Ready != nil && d.Ready.Merged != nil) {
return "", fmt.Errorf("%s is merged already", id)
}
g := h.groups[d.Group]
if d.Group == "" {
g = nil
}
held := g != nil && g.Hold != nil && g.Hold.Kind == "blocked"
refused := false
for hid := range c.Heads {
if m := h.deliveries[hid]; m != nil && m.Ready != nil && m.Ready.Merged == nil && m.Ready.RefusedSince != nil {
refused = true
}
}
if !held && !refused {
return "", fmt.Errorf("%s was neither refused nor held: %s", id, firstOf(d.readyWords(), "it is merged at its turn"))
}
if waits := h.passWaits(d); waits != "" {
return "", fmt.Errorf("%s is not asked again yet: it %s; it is asked at that pass's end", id, strings.TrimPrefix(waits, "approved; "))
}
for hid := range c.Heads {
if m := h.deliveries[hid]; m != nil && m.Ready != nil && m.Ready.Merged == nil {
m.Ready.Repeats, m.Ready.LastTry, m.Ready.merging = 0, time.Time{}, false
h.keep(m)
}
}
if held {
g.Hold = nil
h.keepGroup(g)
}
for key := range h.unmergeableSince {
if g != nil && strings.HasPrefix(key, g.ID+" ") {
delete(h.unmergeableSince, key)
}
}
h.Logf("[mesh-delivery] %s: its merge is asked again on the operator's word, at the heads approved: %s", id, why)
return fmt.Sprintf("%s's merge is asked again at the next look, every member read first; nothing merges unless every member merges now", id), nil
}
// readyWords is what an approved delivery's run says it waits for, if anything.
func (d *Delivery) readyWords() string {
if d.Ready == nil {
return ""
}
return d.Ready.Waits
}
@@ -1,157 +0,0 @@
package main
import (
"encoding/json"
"errors"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"strings"
"testing"
"time"
)
// **A group is merged in one pass on its one Approve, and delivered by one plan** (novox/hq ADR 0418, ADR 0419, ADR
// 0421, issues 978 and 1047): the members are not ordered, none waits for another's merge or delivery, and a trunk
// move does not ask the operator a second time.
const (
gCtl, gCat = "novox/mesh-controller", "novox/mesh-catalog"
gHc, gHk = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
gTrunk0, gT1 = "a2c2c35000000000000000000000000000000000", "d28ca95000000000000000000000000000000000"
)
// approvedGroup is a group of two pull requests on one branch, checks green, composed green and the operator's Approve
// asked and not yet answered (w.op.auto is off).
func approvedGroup(t *testing.T) *world {
return approvedGroupOf(t, member{gCtl, 1, gHc, "internal/a.go"}, member{gCat, 2, gHk, "modules/app/module.json"})
}
// member is a pull request of a group in feat/g: its repository, number, head and the paths it changes.
type member struct {
repo string
number int
head string
paths string
}
// approvedGroupOf is a group feat/g of these members, each checked, composed together and asked once.
func approvedGroupOf(t *testing.T, ms ...member) *world {
t.Helper()
w := newWorld(t)
w.op.auto = false
for _, m := range ms {
w.h.PullUpdated(pr(m.repo, m.number, m.head, "feat/g", strings.Split(m.paths, ",")...))
}
for _, m := range ms {
w.h.Checked(judged(m.repo, m.number, m.head, "pass", gTrunk0))
}
w.settleAll()
g := w.h.groups["feat/g"]
if g == nil || g.Check == nil {
t.Fatalf("no composed check was asked: %+v", g)
}
c := CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed"}
for _, m := range ms {
owner, repo, _ := strings.Cut(m.repo, "/")
c.Members = append(c.Members, struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Commit string `json:"commit"`
Trunk string `json:"trunk,omitempty"`
}{Owner: owner, Repo: repo, Commit: m.head, Trunk: gTrunk0[:12]})
}
w.h.Checked(c)
w.settleAll()
if w.op.count() != 1 || len(w.op.open()) != 1 {
t.Fatalf("the green group is asked once: %d asks, open %v", w.op.count(), w.op.open())
}
return w
}
// Issue 978 (the pin): a feature's single pull request in one repository is a lone delivery, asked alone.
func TestAFeaturesSinglePullRequestIsALoneDeliveryAskedAlone(t *testing.T) {
w := newWorld(t)
w.op.auto = false
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.settleAll()
d := w.delivery(IDOf("novox/app", readyHead))
if d.Group != "" || len(w.h.groups) != 0 {
t.Fatalf("a single pull request made a group: %q %v", d.Group, w.h.groups)
}
if w.op.count() != 1 {
t.Fatalf("a lone delivery is asked once, alone: %d", w.op.count())
}
}
// W11.10 (design 47; F-forge-7, F-forge-8): groups answers the seat's groups: the members in merge order (the SDK first,
// whatever its id), the acts said and not answered with their ids, the acts a record holds with their outcomes, and for a
// group it does not know no groups and no error.
func TestTheGroupsAnswerIsTheSeatsGroupInMergeOrderWithItsActs(t *testing.T) {
w := approvedGroupOf(t, member{"novox/app", 1, gHc, "go.mod"}, member{SDKRepository, 2, gHk, "go/x.go"})
app := w.h.deliveries[IDOf("novox/app", gHc)]
said := w.now
app.Owed = append(app.Owed, Effect{Kind: EffectStatus, Context: "mesh/merge-gate", State: "pending", Line: "waits",
Commit: gHc, Since: said, ID: "status.x", Said: &said})
a := w.h.Groups(false, "")
if a.Error != "" || len(a.Groups) != 1 || len(a.Groups[0].Members) != 2 {
t.Fatalf("groups answers %+v", a)
}
g := a.Groups[0]
if m := g.Members[0]; m.Repository != SDKRepository || m.Number != 2 || m.Head != gHk || m.Base != "main" || !m.Open {
t.Fatalf("the first member in merge order is %+v, not the SDK", m)
}
var act *seat.Act
for i := range g.Acts {
if g.Acts[i].Owed.ID == "status.x" {
act = &g.Acts[i]
}
}
if act == nil || act.Outcome != "" || act.Owed.Act != seat.OwedActStatus || act.Owed.Repository != "novox/app" ||
act.Owed.Number != 1 || act.Owed.Head != gHc || act.Owed.Context != "mesh/merge-gate" || act.Owed.Said == nil {
t.Fatalf("the act said and not answered is %+v in %+v", act, g.Acts)
}
if one := w.h.Groups(false, "feat/g"); len(one.Groups) != 1 || one.Groups[0].ID != "feat/g" {
t.Fatalf("groups of feat/g answers %+v", one)
}
none := w.h.Groups(true, "feat/nothing")
if raw, _ := json.Marshal(none); none.Error != "" || len(none.Groups) != 0 || strings.Contains(string(raw), "error") {
t.Fatalf("a group it does not know is answered %s", raw)
}
at := w.now
recorded := recordedActs([]*Record{{Acts: map[string]RecordAct{
"note.y": {ID: "note.y", Act: EffectNote, Repository: "novox/App", Number: 1, Head: gHc, Outcome: seat.ActOutcomeRefused, Words: "no", At: at},
"other": {ID: "other", Act: EffectNote, Repository: "novox/other", Number: 9, Head: gHc, Outcome: seat.ActOutcomePerformed, At: at},
}}}, map[string]bool{"novox/app@" + gHc: true})
if len(recorded) != 1 || recorded[0].Owed.ID != "note.y" || recorded[0].Outcome != seat.ActOutcomeRefused ||
recorded[0].Words != "no" || recorded[0].Owed.Act != seat.OwedActNote || recorded[0].At == nil {
t.Fatalf("the recorded acts are %+v", recorded)
}
}
// Issue 1082: a refusal loop is not silent. A merge refused past ten minutes in a closed group (ADR 0418 §3) is a stalled
// line naming the member and the merge tool's last answer, which the controller raises as a condition.
func TestARefusalLoopInAClosedGroupIsSaidAtOnce(t *testing.T) {
w := approvedGroup(t)
w.forge.refuseRepo = map[string]error{gCtl: errAskedAgain}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
w.settleAll()
// Each composed pass is followed by a refusal again: the loop.
for range 12 {
w.later(time.Minute)
g := w.h.groups["feat/g"]
w.h.Checked(CheckedEvent{Group: "feat/g", ID: g.Check.Asked, Verdict: "pass", Summary: "composed again"})
w.settleAll()
}
lines := w.h.Stalled()
if len(lines) != 1 || lines[0].ID != IDOf(gCtl, gHc) || lines[0].Bound != "0s" ||
!strings.Contains(lines[0].Says, errAskedAgain.Error()) || !strings.Contains(lines[0].Says, "group feat/g") {
t.Fatalf("the loop was not said, naming the member, its group and the tool's answer: %+v", lines)
}
}
// ownPending is the merge tool's refusal of a member whose own mesh/repo-check stands pending after the group's composed
// check set its gate (issue 1049: the forge's "not a success: mesh/repo-check (pending)").
var ownPending = errors.New("the forge refused gitea_merge_pull_request: novox/mesh-controller#1 is not merged: main requires " +
"mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check (pending). A pull request merges once its checks succeed")
@@ -1,35 +0,0 @@
package main
import (
"strings"
"testing"
)
// A member that joins a group whose state stays checking is said on mesh/delivery-group, and the members already there
// read the new count: on 2026-10-30 mesh-catalog #571 joined a group of three at 21:59, and the forge kept saying
// "3 of 3 member(s) ready" of the group of four, since the line was written again only when the state changed.
func TestAGroupSaysAJoiningMember(t *testing.T) {
w := newWorld(t)
const ctlHead, catHead, hostHead = "c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2", "e3e3e3e3e3e3e3e3"
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
w.h.Checked(verdict("novox/mesh-controller", 1, ctlHead, "pass"))
w.settleAll()
first := w.forge.statuses["novox/mesh-controller@"+ctlHead+" mesh/delivery-group"]
if !strings.Contains(first, "1 of 2") {
t.Fatalf("before the join the group says %q, want 1 of 2", first)
}
w.h.PullUpdated(pr("novox/mesh-host", 3, hostHead, "feat/x", "cmd/a.go"))
w.settleAll()
g := w.h.groups["feat/x"]
st, why := GroupState(g, w.h.membersOf(g))
for _, c := range []struct{ name, key, want string }{
{"a member already there", "novox/mesh-controller@" + ctlHead, "1 of 3"},
{"the member that joined", "novox/mesh-host@" + hostHead, "1 of 3"},
} {
s := w.forge.statuses[c.key+" mesh/delivery-group"]
if !strings.Contains(s, c.want) {
t.Errorf("%s: mesh/delivery-group says %q while the group is %s (%s); want %q", c.name, s, st, why, c.want)
}
}
}
@@ -1,49 +0,0 @@
package main
import (
"encoding/json"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
var t0 = time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
// mergedAt is the forge's `pull.merged` at a moment.
func mergedAt(p PullEvent, merge string, at time.Time) PullEvent {
p.MergeCommit = merge
p.MergedAt = at.UTC().Format(time.RFC3339)
return p
}
// verdictMoving is a passing verdict. The gate plans no delivery any more (ADR 0539 §17), so it names no module.
func verdictMoving(repo string, number int, sha string, _ ...string) CheckedEvent {
return verdict(repo, number, sha, "pass")
}
func statusOn(w *world, repo, commit string) string {
w.forge.mu.Lock()
defer w.forge.mu.Unlock()
return w.forge.statuses[repo+"@"+commit+" mesh/delivery"]
}
// checkerSays puts the checker's verdict at these heads together into its verdicts bucket, as captured on the bus
// (mesh-delivery-checker, key `<group>.<heads hash>`).
func checkerSays(w *world, group, state, failed string, heads ...mdc.Head) {
checkerSaysWith(w, group, state, failed, nil, heads...)
}
// checkerSaysWith is checkerSays with more of the verdict's fields (its composition); the state "gone" deletes the key.
func checkerSaysWith(w *world, group, state, failed string, more map[string]any, heads ...mdc.Head) {
hash, _ := mdc.HeadsHash(heads)
if state == "gone" {
w.h.CheckerVerdictChanged(group+"."+hash, nil)
return
}
v := map[string]any{"v": 1, "group": group, "heads": hash, "state": state, "failed": map[string]string{"words": failed}}
for k, x := range more {
v[k] = x
}
value, _ := json.Marshal(v)
w.h.CheckerVerdictChanged(group+"."+hash, value)
}
File diff suppressed because it is too large Load Diff
@@ -1,399 +0,0 @@
package main
import (
"encoding/json"
"errors"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"sort"
"strconv"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// The holder, end to end over fakes (novox/hq ADR 0239 *How it is checked*, cut by ADR 0539 §17): a pull request's head
// proposed and ready; merged; a newer head closing the older; a restart resuming; the trunk rule; and every transition
// kept before it is said, noted on the commit and shown on the pull request.
const (
head = "aaaaaaaaaaaa1111"
merge = "mmmmmmmmmmmm9999"
)
func TestAPullRequestsHeadIsProposedAndReady(t *testing.T) {
w := newWorld(t)
p := pr("novox/app", 7, head, "feat/x", "src/a.go")
w.h.PullUpdated(p)
id := IDOf("novox/app", head)
if w.state(id) != Proposed {
t.Fatalf("a head is %s", w.state(id))
}
w.h.Checked(verdict("novox/app", 7, head, "pass"))
d := w.delivery(id)
if d.State != Ready {
t.Fatalf("a passing verdict left it %s", d.State)
}
var path []State
for _, tr := range d.Transitions {
path = append(path, tr.To)
}
if strings.Join(statesOf(path), ",") != "proposed,ready" {
t.Fatalf("its transitions are %v", path)
}
w.settleAll()
notes := w.forge.notesOn(head)
if len(notes) != 2 || !strings.Contains(notes[1], "proposed -> ready") {
t.Fatalf("the head's note is %q", notes)
}
if !strings.Contains(w.forge.statuses["novox/app@"+head+" mesh/delivery"], "success ready") {
t.Fatalf("the head's status is %q", w.forge.statuses)
}
if v := w.forge.views["novox/app#7"]; !strings.Contains(v, viewMarker) || !strings.Contains(v, "**ready**") {
t.Fatalf("the pull request's view is %q", v)
}
// A failing verdict for another head rejects it, and a newer head closes the older.
w.h.PullUpdated(pr("novox/app", 7, "bbbbbbbbbbbb2222", "feat/x"))
if w.state(id) != Closed {
t.Fatalf("a newer head left the older %s", w.state(id))
}
w.h.Checked(verdict("novox/app", 7, "bbbbbbbbbbbb2222", "fail"))
if s := w.state(IDOf("novox/app", "bbbbbbbbbbbb2222")); s != Rejected {
t.Fatalf("a failing verdict left it %s", s)
}
}
func statesOf(ss []State) []string {
var out []string
for _, s := range ss {
out = append(out, string(s))
}
return out
}
// A merge puts the delivery on the trunk and ends it there: merged, with the merge commit's note and the pull request's
// status naming the source revision it moved. Nothing after the merge is derived (ADR 0539 §17).
func TestAMergeIsMergedAndTheGateDerivesNothingAfterIt(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.h.PullMerged(merged("novox/app", 7, head, merge))
d := w.delivery(id)
if d.State != Merged || d.MergedAs != merge {
t.Fatalf("a merge left it %s as %q", d.State, d.MergedAs)
}
w.settleAll()
w.later(72 * time.Hour)
w.settleAll()
if s := w.state(id); s != Merged {
t.Fatalf("time moved a merged delivery to %s", s)
}
if st := w.forge.statuses["novox/app@"+head+" mesh/delivery"]; !strings.HasPrefix(st, "success merged: source.main moved to novox/app@"+merge[:12]) {
t.Fatalf("the pull request's mesh/delivery status is %q", st)
}
view := w.forge.views["novox/app#7"]
if !strings.Contains(view, "**Source**") || !strings.Contains(view, "source.main moved to novox/app@"+merge[:12]) ||
!strings.Contains(view, "mesh-delivery-view") || strings.Contains(view, "**Delivery plan**") || strings.Contains(view, "**Machines**") {
t.Fatalf("the pull request's view is %q", view)
}
n := 0
for _, s := range w.said {
if s["id"] == id {
n++
}
}
if n != len(d.Transitions) {
t.Fatalf("%d transitions, %d said", len(d.Transitions), n)
}
}
// The trunk rule: a ready delivery whose commit never reached the trunk is never merged.
func TestACommitOffTheTrunkIsNeverMerged(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.settleAll()
if s := w.state(id); s != Ready {
t.Fatalf("an off-trunk head is %s", s)
}
}
// A restart resumes: a new holder over the same state stands every delivery where it was.
func TestARestartResumesFromTheState(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.h.PullMerged(merged("novox/app", 7, head, merge))
w.settleAll()
if s := w.state(id); s != Merged {
t.Fatalf("before the restart it is %s", s)
}
again := newWorldOver(t, w.store, w.forge, w.now.Add(time.Hour))
if s := again.state(id); s != Merged {
t.Fatalf("read back, it is %s", s)
}
}
// A delivery kept in a state an earlier owner had and this one does not is read as the state it came to and kept so.
func TestADeliveryKeptInAGoneStateIsReadAsTheMergeStateItCameTo(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.h.PullMerged(merged("novox/app", 7, head, merge))
w.settleAll()
raw := w.store.deliveries[id]
w.store.deliveries[id] = []byte(strings.Replace(string(raw), `"state":"merged"`, `"state":"delivering"`, 1))
again := newWorldOver(t, w.store, w.forge, w.now.Add(time.Hour))
if s := again.state(id); s != Merged {
t.Fatalf("a delivering delivery was read as %s", s)
}
again.settleAll()
if !strings.Contains(string(w.store.deliveries[id]), `"state":"merged"`) {
t.Fatalf("the read state was not kept: %s", w.store.deliveries[id])
}
}
// Kept before it is said: with the state unreachable, nothing is emitted or noted; once it is kept, all of it.
func TestATransitionIsKeptBeforeItIsSaid(t *testing.T) {
w := newWorld(t)
w.store.failing = true
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
if len(w.said) != 0 || len(w.forge.notesOn(head)) != 0 {
t.Fatalf("said before it was kept: %v %v", w.said, w.forge.notesOn(head))
}
w.store.failing = false
w.settleAll()
if len(w.said) != 1 || len(w.forge.notesOn(head)) != 1 {
t.Fatalf("once kept, said %d and noted %d", len(w.said), len(w.forge.notesOn(head)))
}
// A bucket the bus cannot take is written again; a note is appended once however often it is asked.
w.owed.down = true
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.settleAll()
if len(w.forge.notesOn(head)) != 1 {
t.Fatal("noted while the bus was away")
}
w.owed.down = false
w.later(time.Hour) // each failure put its next try later: past every one of them
w.settleAll()
w.settleAll()
if notes := w.forge.notesOn(head); len(notes) != 2 {
t.Fatalf("after the bus came back the note has %d line(s): %q", len(notes), notes)
}
if d := w.delivery(IDOf("novox/app", head)); len(d.Owed) != 0 {
t.Fatalf("still owed: %+v", d.Owed)
}
}
// A merge before the check passed is held for a person; a merge this owner never heard the head of is made
// from the forge's word and merged when the forge says its gate passed.
func TestAMergeWithoutAPassingCheckIsHeld(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.Checked(verdict("novox/app", 7, head, "fail"))
w.h.PullMerged(merged("novox/app", 7, head, merge))
if s := w.state(IDOf("novox/app", head)); s != Held {
t.Fatalf("a rejected head merged is %s", s)
}
// Never heard: the forge's statuses on its head decide.
m := merged("novox/lab", 3, "eeeeeeeeeeee5555", "ffffffffffff6666")
m.HeadChecks = map[string]string{"mesh/check": "success"}
m.Paths = []string{"README.md"}
w.h.PullMerged(m)
w.settleAll()
d := w.delivery(IDOf("novox/lab", "eeeeeeeeeeee5555"))
if d.State != Merged {
t.Fatalf("a merge whose head passed by the forge's word is %s", d.State)
}
}
// A group merged whole ends merged, member by member and as a group, and source.main holds its heads (ADR 0539 §17, R6-C2).
func TestAMergedGroupEndsMergedWithSourceMainMoved(t *testing.T) {
w := newWorld(t)
w.op.auto = false // the merges here are the forge's, not the gate's on an Approve
heads := map[string]string{"novox/a": "a1a1a1a1a1a1a1", "novox/b": "b2b2b2b2b2b2b2"}
ids := map[string]string{}
for i, repo := range []string{"novox/a", "novox/b"} {
ids[repo] = IDOf(repo, heads[repo])
w.h.PullUpdated(pr(repo, i+1, heads[repo], "feat/y", "x"))
w.h.Checked(verdict(repo, i+1, heads[repo], "pass"))
}
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/y", ID: "check-1", Verdict: "pass", Summary: "compose"})
for i, repo := range []string{"novox/a", "novox/b"} {
w.h.PullMerged(merged(repo, i+1, heads[repo], strings.Repeat(string(rune('p'+i)), 12)))
}
w.settleAll()
for repo, id := range ids {
if s := w.state(id); s != Merged {
t.Errorf("%s is %s, wanted merged", repo, s)
}
}
g := w.h.groups["feat/y"]
if state, why := GroupState(g, w.h.membersOf(g)); state != "merged" {
t.Fatalf("the group is %s (%s), wanted merged", state, why)
}
if w.source.main == nil || w.source.main.Heads["novox/a"] != strings.Repeat("p", 12) || w.source.main.Heads["novox/b"] != strings.Repeat("q", 12) {
t.Fatalf("source.main is %+v, wanted both merges", w.source.main)
}
// A merge that leaves no module.json carries no trees (issue 1367): the gate reads it, and refuses nothing.
msg := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/a", Number: 9, Head: strings.Repeat("e", 40),
Merge: strings.Repeat("f", 40), Root: "r"}
body, err := forgemsg.EncodeForge(msg)
if err != nil {
t.Fatalf("a merged message with no trees is refused: %v", err)
}
w.h.Forged("mesh-delivery-forge-bridge.novox.a.merged", body)
if len(w.h.refusals) != 0 {
t.Errorf("the gate refused a merged message with no trees: %v", w.h.refusals)
}
// A group's end is the indexer's: the gate ignores it before decoding, and says no refusal (1585/T25).
w.h.Forged("mesh-delivery-forge-bridge.forge.group.merged", []byte(`{"v":1,"group":"feat/y"}`))
if len(w.h.refusals) != 0 {
t.Errorf("the gate refused a group's end: %v", w.h.refusals)
}
// The repository forge/group's merged message ends the same way: it is decoded as a forge message, and this one,
// which is none, is refused.
w.h.Forged("mesh-delivery-forge-bridge.forge.forge.group.merged", []byte(`{"v":1,"group":"feat/y"}`))
if len(w.h.refusals) != 1 {
t.Errorf("forge/group's merged message was passed by as a group's end: refusals %v", w.h.refusals)
}
}
// W11.5 (novox/hq ADR 0659 §4, ADR 0493 §4): the issues an ask names are the issue tracker's links of each member's pull
// request, read through the seat issue-tracker, never the title; a tracker that does not answer is said, and holds nothing;
// the author is the forge's pull request record's. ADR 0419 §2's after: notice is gone with the description.
func TestTheAskNamesTheIssuesLinkedInTheIssueTracker(t *testing.T) {
w := newWorld(t)
w.op.auto = false
links := &fakeLinks{linked: map[string][]int{"novox/a#1": {948, 12}}, failing: map[string]bool{"novox/b#2": true}}
w.h.Links = links
pa := pr("novox/a", 1, "a1a1a1a1a1a1a1", "feat/z")
pa.Title, pa.Author = "Fix issue 77", "quiet-deliveries"
w.h.PullUpdated(pa)
w.h.PullUpdated(pr("novox/b", 2, "b2b2b2b2b2b2b2", "feat/z"))
w.h.Checked(verdict("novox/a", 1, "a1a1a1a1a1a1a1", "pass"))
w.h.Checked(verdict("novox/b", 2, "b2b2b2b2b2b2b2", "pass"))
w.settleAll()
w.later(time.Minute)
w.settleAll()
open := w.op.open()
if len(open) != 1 {
t.Fatalf("the group with an unread tracker is not asked: %v", open)
}
e := w.op.ask(open[0]).Explanation
if !strings.Contains(e, "Issues: issue 12, issue 948, b#2's unread (the issue tracker did not answer)") || strings.Contains(e, "77") {
t.Fatalf("the ask's issues are not the links:\n%s", e)
}
if d := w.delivery(IDOf("novox/a", "a1a1a1a1a1a1a1")); d.Author != "quiet-deliveries" || authorOf(d) != "the author of feat/z (quiet-deliveries)" {
t.Fatalf("the author is %q, said %q", d.Author, authorOf(d))
}
if links.asked["novox/a#1"] != 1 {
t.Fatalf("the links of novox/a#1 were read %d times at one head, want once", links.asked["novox/a#1"])
}
for _, c := range w.forge.comments {
if strings.Contains(c, "after:") {
t.Fatalf("an after: notice is said: %q", c)
}
}
}
// fakeLinks is the issue tracker's links of pull requests, by `<repository>#<number>`.
type fakeLinks struct {
mu sync.Mutex
linked map[string][]int
failing map[string]bool
asked map[string]int
}
func (f *fakeLinks) Linked(repository string, number int) ([]int, error) {
f.mu.Lock()
defer f.mu.Unlock()
key := repository + "#" + strconv.Itoa(number)
if f.asked == nil {
f.asked = map[string]int{}
}
f.asked[key]++
if f.failing[key] {
return nil, errors.New("no responders")
}
out := append([]int(nil), f.linked[key]...)
sort.Ints(out)
return out, nil
}
// Stalled: a delivery past its state's bound is listed with what H2 may do. Only a proposed
// delivery has a bound now: its check was asked and not answered.
func TestStalledWorksFromTheTable(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.later(3 * time.Hour)
stalled := w.h.Stalled()
if len(stalled) != 1 || stalled[0].ID != id || stalled[0].State != Proposed {
t.Fatalf("stalled %+v", stalled)
}
// A merge is the gate's last word: it is never stalled, however old.
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.h.PullMerged(merged("novox/app", 7, head, merge))
w.later(72 * time.Hour)
for _, s := range w.h.Stalled() {
if s.ID == id {
t.Fatalf("a merged delivery is stalled: %+v", s)
}
}
// A merge without a passing check is held: a person's, with no transition for H2.
w.h.PullUpdated(pr("novox/app", 8, "ffffffffffff0000", "feat/q"))
w.h.PullMerged(merged("novox/app", 8, "ffffffffffff0000", "eeeeeeeeeeee0000"))
w.later(48 * time.Hour)
}
// novox/hq issue 867, ADR 0382 §1 (contract A): each member of a group in `groups` says its pull request number, full
// head, base branch and whether it is open, under the seat's keys.
func TestGroupsNamesEachMembersPullRequestHeadBaseAndOpen(t *testing.T) {
w := newWorld(t)
headA := "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
headB := "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"
pb := pr("novox/b", 2, headB, "feat/z")
pb.Base = "release/1"
w.h.PullUpdated(pr("novox/a", 1, headA, "feat/z"))
w.h.PullUpdated(pb)
lines := w.h.Groups(false, "").Groups
if len(lines) != 1 || len(lines[0].Members) != 2 {
t.Fatalf("groups: %+v", lines)
}
got := map[string]seat.Member{}
for _, m := range lines[0].Members {
got[m.Repository] = m
}
a, b := got["novox/a"], got["novox/b"]
if a.Number != 1 || a.Head != headA || a.Base != "main" || !a.Open || a.ID == "" || a.State == "" {
t.Fatalf("member a: %+v", a)
}
if b.Number != 2 || b.Head != headB || b.Base != "release/1" || !b.Open {
t.Fatalf("member b: %+v", b)
}
// Closed unmerged: still listed while the group is, but not open.
w.h.PullClosed(pr("novox/b", 2, headB, "feat/z"))
for _, l := range w.h.Groups(true, "").Groups {
for _, m := range l.Members {
if m.Repository == "novox/b" && m.Open {
t.Fatalf("a closed pull request is open: %+v", m)
}
}
}
// JSON keys are the contract's.
raw, _ := json.Marshal(a)
for _, k := range []string{`"number":1`, `"head":"` + headA + `"`, `"base":"main"`, `"open":true`} {
if !strings.Contains(string(raw), k) {
t.Fatalf("%s lacks %s", raw, k)
}
}
}
@@ -1,52 +0,0 @@
package main
import (
"testing"
)
// novox/hq issue 1083: mesh-controller #375's delivery was stopped by a person, then #375 was closed and the same commit
// opened as #376 from another branch. The delivery is named repository@commit; it was Stopped before its pull request
// closed, so it was not ClosedUnmerged, and #376 got no delivery of its own: its Approve was never asked. A new pull
// request at a commit whose delivery is Stopped and unmerged, under another number, takes that record over (the key stays
// repository@commit); the take-over is said in its transitions, with who stopped it and why.
const (
head1083 = "e375e375e375e375"
branch1083 = "feat/backup-before-send"
)
// A closed record that was merged is not taken over, and the same pull request announced again stays as it was.
func TestAStoppedRecordThatWasMergedIsNotTakenOverByAnotherPullRequest(t *testing.T) {
w := newWorld(t)
id := IDOf(catalog783, head783c+"1")
w.h.PullUpdated(pr(catalog783, 400, head783c+"1", branch1083, "src/a.go"))
w.h.PullMerged(merged(catalog783, 400, head783c+"1", "f400f400f400f400"))
w.h.PullUpdated(pr(catalog783, 401, head783c+"1", branch1083+"-2", "src/a.go"))
if d := w.delivery(id); d.Number != 400 || d.MergedAs == "" {
t.Fatalf("a merged record was taken over: Number %d, merged as %q", d.Number, d.MergedAs)
}
}
// The take-over leaves the stopped record's group: the new record joins its own group by its branch.
func TestATakenOverRecordLeavesTheGroupItWasStoppedIn(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr(gCtl, 1, gHc, "feat/g", "internal/a.go"))
w.h.PullUpdated(pr(gCat, 2, gHk, "feat/g", "modules/app/module.json"))
id := IDOf(gCtl, gHc)
w.h.PullClosed(pr(gCtl, 1, gHc, "feat/g"))
w.h.PullUpdated(pr(gCtl, 9, gHc, "feat/other", "internal/a.go"))
w.h.mu.Lock()
defer w.h.mu.Unlock()
for _, g := range w.h.groups {
if g.ID == "feat/g" {
for _, m := range g.Members {
if m == id {
t.Fatalf("group feat/g still lists %s, now #9 on feat/other: %v", id, g.Members)
}
}
}
}
if d := w.h.deliveries[id]; d.Number != 9 || d.State != Proposed || d.Group == "feat/g" {
t.Fatalf("taken over: Number %d, %s, group %q", d.Number, d.State, d.Group)
}
}
@@ -1,63 +0,0 @@
package main
import (
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"encoding/json"
"testing"
)
// Issue 1107: a pull request closed unmerged in a quiet repository is heard from the forge's own event
// (gitea.forge.pull-request.closed), not only from the poll, which skips a repository whose update time did not move.
// Its delivery leaves its group and stops; a merged one is ignored (the webhook says merged for it); a close heard
// twice, from the event and the poll, changes nothing the second time.
func forgeClosed(owner, repo string, number int, merged bool) gitseat.ForgePullEvent {
return gitseat.ForgePullEvent{Action: "closed", Repository: owner + "/" + repo, Number: int64(number), Merged: merged}
}
func TestAForgeClosedEventTakesAGroupMembersDeliveryOutOfItsGroupAndStopsIt(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr(gCtl, 1, gHc, "feat/g", "internal/a.go"))
w.h.PullUpdated(pr(gCat, 2, gHk, "feat/g", "modules/app/module.json"))
id := IDOf(gCtl, gHc)
hear(t, w, gitEvent(gitseat.EventForgePull), forgeClosed("novox", "mesh-controller", 1, false))
d := w.delivery(id)
if !d.ClosedUnmerged || !d.State.Final() || inGroup(w, "feat/g", id) {
t.Fatalf("closed %v, state %s, in its group %v; wanted closed, stopped, out of its group", d.ClosedUnmerged, d.State,
inGroup(w, "feat/g", id))
}
// Heard again, from the event and from the poll: nothing changes.
before, _ := json.Marshal(d)
hear(t, w, gitEvent(gitseat.EventForgePull), forgeClosed("novox", "mesh-controller", 1, false))
hear(t, w, gitEvent(gitseat.EventPullClosed), gitseat.PullClosedEvent{Repository: "novox/mesh-controller", Number: 1})
if after, _ := json.Marshal(w.delivery(id)); string(after) != string(before) {
t.Fatalf("a second close changed it:\n%s\n%s", before, after)
}
}
func TestAForgeClosedEventOfAMergedPullRequestIsIgnored(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr(gCtl, 1, gHc, "feat/g", "internal/a.go"))
w.h.PullUpdated(pr(gCat, 2, gHk, "feat/g", "modules/app/module.json"))
id := IDOf(gCtl, gHc)
hear(t, w, gitEvent(gitseat.EventForgePull), forgeClosed("novox", "mesh-controller", 1, true))
if d := w.delivery(id); d.ClosedUnmerged || !inGroup(w, "feat/g", id) || d.State.Final() {
t.Fatalf("a merged pull request's close stopped it: closed %v, %s", d.ClosedUnmerged, d.State)
}
}
// inGroup is whether the group still lists the delivery as a member.
func inGroup(w *world, group, id string) bool {
w.h.mu.Lock()
defer w.h.mu.Unlock()
g := w.h.groups[group]
if g == nil {
return false
}
for _, m := range g.Members {
if m == id {
return true
}
}
return false
}
@@ -1,82 +0,0 @@
package main
import (
"testing"
)
// novox/hq issue 1114: mesh-controller #379 was opened at the head of the still-open #378. Its announcement read as the
// same delivery announced again, so when #378 closed unmerged the shared record went Stopped and #379 had no delivery.
// The other open pull request at the head is kept on the record; a close of the record's own pull request starts the
// record over for it at once (as issue 1083's take-over does for a later announcement), and an edit of it does so too,
// so a case stuck before the fix frees itself. A merged record is never taken over.
const head1114 = "0149a3d70149a3d70149a3d70149a3d70149a3d7"
func TestClosingThePullRequestOfASharedHeadStartsTheOtherOnesDeliveryOver(t *testing.T) {
w := newWorld(t)
w.op.auto = false
id := IDOf(gCtl, head1114)
w.h.PullUpdated(pr(gCtl, 378, head1114, "feat/chain", "internal/a.go"))
b := pr(gCtl, 379, head1114, "feat/chain-2", "internal/a.go")
w.h.PullUpdated(b) // announced while #378 is live: the same delivery heard again
if d := w.delivery(id); d.Number != 378 {
t.Fatalf("setup: the record is #%d, wanted #378 still", d.Number)
}
w.h.PullClosed(pr(gCtl, 378, head1114, "feat/chain"))
d := w.delivery(id)
if d.Number != 379 || d.State.Final() || d.ClosedUnmerged {
t.Fatalf("after #378 closed the record is #%d %s (closed %v); wanted #379, proposed", d.Number, d.State, d.ClosedUnmerged)
}
w.h.Checked(verdict(gCtl, 379, head1114, "pass"))
w.settleAll()
if open := w.op.open(); w.state(id) != Ready || len(open) != 1 {
t.Fatalf("#379's delivery is %s with asks %v; wanted ready and one Approve ask", w.state(id), w.op.open())
}
}
func TestClosingTheOtherPullRequestOfASharedHeadLeavesTheRecordAlone(t *testing.T) {
w := newWorld(t)
id := IDOf(gCtl, head1114)
w.h.PullUpdated(pr(gCtl, 378, head1114, "feat/chain", "internal/a.go"))
w.h.PullUpdated(pr(gCtl, 379, head1114, "feat/chain-2", "internal/a.go"))
w.h.PullClosed(pr(gCtl, 379, head1114, "feat/chain-2"))
w.h.PullClosed(pr(gCtl, 378, head1114, "feat/chain"))
if d := w.delivery(id); d.Number != 378 || !d.ClosedUnmerged || !d.State.Final() {
t.Fatalf("with #379 closed first, #378's close left %d %s; wanted it stopped under #378", d.Number, d.State)
}
}
func TestAnEditOfAPullRequestWhoseSharedHeadsRecordIsStoppedUnderAnotherTakesItOver(t *testing.T) {
w := newWorld(t)
id := IDOf(gCtl, head1114)
w.h.PullUpdated(pr(gCtl, 378, head1114, "feat/chain", "internal/a.go"))
w.h.PullClosed(pr(gCtl, 378, head1114, "feat/chain")) // #379 was never heard: the stuck case
if d := w.delivery(id); !d.State.Final() {
t.Fatalf("setup: %s", d.State)
}
e := pr(gCtl, 379, head1114, "feat/chain-2")
w.h.PullEdited(e)
d := w.delivery(id)
if d.Number != 379 || d.State != Proposed || len(d.Paths) == 0 {
t.Fatalf("after #379's edit the record is #%d %s paths %v; wanted #379 proposed, read, with the paths", d.Number,
d.State, d.Paths)
}
w.h.PullEdited(e) // an edit heard again changes nothing
if again := w.delivery(id); again.Number != 379 || again.State != Proposed {
t.Fatalf("a second edit moved it: #%d %s", again.Number, again.State)
}
}
func TestAMergedRecordIsNeverTakenOverByTheOtherPullRequestOfItsHead(t *testing.T) {
w := newWorld(t)
id := IDOf(gCtl, head1114)
w.h.PullUpdated(pr(gCtl, 378, head1114, "feat/chain", "internal/a.go"))
w.h.PullUpdated(pr(gCtl, 379, head1114, "feat/chain-2", "internal/a.go"))
w.h.PullMerged(merged(gCtl, 378, head1114, "f378f378f378f378"))
w.h.PullClosed(pr(gCtl, 378, head1114, "feat/chain"))
e := pr(gCtl, 379, head1114, "feat/chain-2")
w.h.PullEdited(e)
if d := w.delivery(id); d.Number != 378 || d.MergedAs == "" {
t.Fatalf("a merged record was taken over: #%d merged as %q", d.Number, d.MergedAs)
}
}
@@ -1,742 +0,0 @@
package main
import (
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
const time30s = 31 * time.Second
// Issue 1275 (the 2026-10-19 18:05 group): a group is merged whole or not at all, in its order, and a group that is held or
// half merged is said loudly. Issue 1276/AC1: a refusal naming the member's own repository check as missing asks that check.
const (
gate, repoCheck = "mesh/merge-gate", "mesh/repo-check"
)
// statusOf sets a member head's two merge checks on the forge, and makes both required.
func setChecks(w *world, repository, sha, gateState, repoState string) {
w.forge.required = []string{gate, repoCheck}
for ctx, state := range map[string]string{gate: gateState, repoCheck: repoState} {
key := repository + "@" + sha + " " + ctx
if state == "" {
delete(w.forge.statuses, key) // no status at all: the forge says it is missing
continue
}
w.forge.statuses[key] = state + " said → x"
}
}
func allGreen(w *world) {
setChecks(w, gCat, gHk, "success", "success")
setChecks(w, gCtl, gHc, "success", "success")
}
func refusedWords(repository string) error {
return errors.New("the forge refused gitea_merge_pull_request: " + repository + " is not merged: main requires " +
"mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check (missing). A pull request merges once its checks succeed")
}
func urgentLines(w *world) []StalledLine {
var out []StalledLine
for _, l := range w.h.Stalled() {
if l.Severity == "urgent" {
out = append(out, l)
}
}
return out
}
// novox/hq ADR 0661 §4 (changing 1275/AC2 and ADR 0441 §2): an approved group whose member's required status turned
// failed after the Approve merges whole, with no read of its statuses at the merge, and raises no line.
func TestAnApprovedGroupWhoseMemberStatusTurnedAfterTheApproveMergesWhole(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing after the ask
w.settleAll()
if len(w.forge.merges) != 2 || len(urgentLines(w)) != 0 {
t.Fatalf("merges %v, lines %+v; wanted both merged on the Approve and no line", w.forge.merges, urgentLines(w))
}
}
// 1275/AC4: a group half merged (the second member refused after the first merged) stays raised, pass after pass, until
// the second is merged; then it goes.
func TestAHalfMergedGroupStaysRaisedUntilEveryMemberIsMerged(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCtl: refusedWords("novox/mesh-controller#1")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
for range 3 {
w.later(time30s)
w.settleAll()
lines := urgentLines(w)
if len(lines) != 1 || lines[0].ID != IDOf(gCtl, gHc) || !strings.Contains(lines[0].Says, "feat/g") {
t.Fatalf("the half merged group is not raised: %+v", lines)
}
}
// A half merged group with no refusal on record (the pass died between members) is raised too.
d := w.delivery(IDOf(gCtl, gHc))
d.Ready.RefusedSince = nil
if lines := urgentLines(w); len(lines) != 1 || !strings.Contains(lines[0].Says, "half merged") {
t.Fatalf("a half merged group without a refusal is not raised: %+v", lines)
}
w.forge.refuseRepo = nil
setChecks(w, gCtl, gHc, "success", "success")
w.h.PullMerged(merged(gCtl, 1, gHc, gT1))
w.settleAll()
if lines := urgentLines(w); len(lines) != 0 {
t.Fatalf("the group is whole and still raised: %+v", lines)
}
}
// 1275/AC5: a member's required status turning not-success while the ask waits withdraws the ask at the router; the
// answer to the withdrawn ask merges nothing; the group is asked again once every member may merge.
func TestAStatusThatTurnsWhileTheAskWaitsWithdrawsItAndItsAnswerMergesNothing(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
ask := w.op.open()[0]
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-catalog", SHA: gHk, Context: repoCheck, State: "pending"})
setChecks(w, gCat, gHk, "success", "pending")
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.cancelled[0] != ask {
t.Fatalf("the ask was not withdrawn at the router: %v", w.op.cancelled)
}
w.op.answer(ask, OptionApprove) // an answer that arrives late
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("the answer to a withdrawn ask merged: %v", w.forge.merges)
}
if w.op.count() != 1 {
t.Fatalf("asked again while a status was still pending: %d", w.op.count())
}
setChecks(w, gCat, gHk, "success", "success")
w.later(time30s)
w.settleAll()
if w.op.count() != 2 {
t.Fatalf("not asked again once every member may merge: %d", w.op.count())
}
}
// 1275/AC6, as novox/hq ADR 0505 §4 changed it: a member refused because its verdict is out of date (the trunk moved; its
// fresh check runs) is no stalled line while the gate answers it, and urgent once ten minutes passed with the group not
// merged; the pass stops at the refusal all the same.
func TestAnOutOfDateRefusalIsNoLineWhileAnsweredAndUrgentAfterTenMinutes(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errAskedAgain
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("the pass did not stop at the refusal: %v", w.forge.merges)
}
sev := func() []string {
var out []string
for _, l := range w.h.Stalled() {
out = append(out, l.Severity)
}
return out
}
if got := sev(); len(got) != 0 {
t.Fatalf("an out of date refusal is %v at once; wanted no line while it is answered", got)
}
w.later(9 * time.Minute)
if got := sev(); len(got) != 0 {
t.Fatalf("at nine minutes it is %v; wanted still no line", got)
}
w.later(2 * time.Minute)
if got := sev(); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("after ten minutes it is %v; wanted urgent", got)
}
}
// 1275/AC6: every other refusal (here a missing status) is urgent at once.
func TestAMissingStatusRefusalIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if lines := w.h.Stalled(); len(lines) != 1 || lines[0].Severity != "urgent" {
t.Fatalf("a missing status refusal is not urgent at once: %+v", lines)
}
}
func severities(w *world) []string {
var out []string
for _, l := range w.h.Stalled() {
out = append(out, l.Severity)
}
return out
}
// 1275/AC6 (b), as ADR 0505 §4 changed it: the same out of date refusal on a LATER member, after the earlier one merged,
// is no line at once and urgent after ten minutes: the half merged group does not say it at once.
func TestAnOutOfDateRefusalOnALaterMemberIsNoLineThenUrgent(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCtl: errAskedAgain}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
w.settleAll()
if got := severities(w); len(got) != 0 {
t.Fatalf("a later member's out of date refusal is %v at once; wanted no line while it is answered", got)
}
w.later(11 * time.Minute)
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("after ten minutes it is %v; wanted urgent", got)
}
}
// 1275/AC6 (c): words naming an out of date check and also a missing status are urgent at once.
func TestARefusalNamingOutOfDateAndAMissingStatusIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: novox/mesh-catalog#2 is not merged: its verdict was " +
"judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. The check of head cccc merged into trunk bbbb was " +
"asked. main requires mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check (missing)")
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("a refusal with two causes is %v; wanted urgent at once", got)
}
}
// 1275/AC6 (d): words mesh-delivery cannot classify are urgent at once.
func TestARefusalInUnknownWordsIsUrgentAtOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: something nobody foresaw")
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("an unclassified refusal is %v; wanted urgent at once", got)
}
}
// The merge tool's refusals, copied from gitea merge.ts with their line, `${...}` filled in.
var mergeToolRefusals = []struct {
line string
words string
outOfDated bool
}{
{"merge.ts:121", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. " +
"Its check is already asked and not yet answered (building), so it is not asked again; took 3s. Ask again at cccccccccccc once " +
"both statuses pass: this tool never merges by itself (novox/hq ADR 0323). A check that never answers is asked again with " +
"mesh-delivery.recheck. Nothing was merged", true},
{"merge.ts:147", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb. " +
"The check of head cccccccccccc merged into trunk bbbbbbbbbbbb was asked; took 3s. Ask again at cccccccccccc once both " +
"statuses pass: this tool never merges by itself (novox/hq ADR 0323). In a busy repository another merge may move the trunk " +
"again first; this is then refused and asked once more. A check that never answers is asked again with " +
"mesh-delivery.recheck. Nothing was merged", true},
{"merge.ts:144", "novox/app#7 is not merged: its verdict was judged against trunk aaaaaaaaaaaa, and main is now bbbbbbbbbbbb, " +
"and its check could not be asked: the forge is away. Nothing was merged", false},
{"merge.ts:156", "novox/app#7 is not merged: main requires mesh/merge-gate, mesh/repo-check; not a success: mesh/repo-check " +
"(pending). A pull request merges once its checks succeed", false},
{"merge.ts:165", "novox/app#7 is not merged: its verdict read siblings, and which ones is not held here, so the group cannot " +
"be weighed. Ask its check again at cccccccccccc. Nothing was merged", false},
{"merge.ts:172", "novox/app#7 is not merged: its verdict was judged beside 1 sibling(s), and a sibling is not ready. A member " +
"judged beside its siblings merges once its group passed at the heads that verdict read (novox/hq ADR 0382, 0421). " +
"Nothing was merged", false},
}
// 1275/AC6 (e), as ADR 0505 §4 changed it: only the merge tool's two out of date refusals (:121, :147) are answered by
// the gate and said as no line at once; the one saying its check could not be asked (:144) and every other is urgent at
// once.
func TestOnlyTheMergeToolsTwoOutOfDateRefusalsWaitWhileAnswered(t *testing.T) {
for _, c := range mergeToolRefusals {
if got := outOfDateRefusal(c.words); got != c.outOfDated {
t.Errorf("%s: outOfDateRefusal = %v, wanted %v", c.line, got, c.outOfDated)
}
w := approvedGroup(t)
allGreen(w)
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: " + c.words)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
got := severities(w)
if c.outOfDated && len(got) != 0 {
t.Errorf("%s: said %v at once, wanted no line while it is answered", c.line, got)
}
if !c.outOfDated && (len(got) != 1 || got[0] != "urgent") {
t.Errorf("%s: severity %v at once, wanted urgent", c.line, got)
}
}
}
// 1275/AC6 (f): the severity never steps down: a missing status refusal, then an out of date one, stays urgent.
func TestASeverityOnceUrgentNeverStepsDownToAWarning(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.forge.refuseRepo = map[string]error{gCat: errAskedAgain}
w.h.mergeAgainNow(w.delivery(IDOf(gCat, gHk)))
w.later(time30s)
w.settleAll()
if d := w.delivery(IDOf(gCat, gHk)); !outOfDateRefusal(d.Ready.Answer) {
t.Fatalf("the second refusal was not the out of date one: %q", d.Ready.Answer)
}
if got := severities(w); len(got) != 1 || got[0] != "urgent" {
t.Fatalf("severity %v after an out of date refusal that followed an urgent one; wanted urgent", got)
}
}
// fakeConditions is the gate's bucket conditions and the operator-channel seat's subject for conditions: what it keeps,
// and every condition said, in order.
type fakeConditions struct {
mu sync.Mutex
kept map[string]string
told []oc.Condition
}
func (f *fakeConditions) Said() (map[string]string, error) {
f.mu.Lock()
defer f.mu.Unlock()
out := map[string]string{}
for k, v := range f.kept {
out[k] = v
}
return out, nil
}
func (f *fakeConditions) PutSaid(said map[string]string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.kept = map[string]string{}
for k, v := range said {
f.kept[k] = v
}
return nil
}
func (f *fakeConditions) say(c oc.Condition) error {
if _, err := occ.EncodeCondition(c); err != nil {
return err // what the router would refuse
}
f.mu.Lock()
defer f.mu.Unlock()
f.told = append(f.told, c)
return nil
}
// last is the newest condition said for a delivery id, or nil.
func (f *fakeConditions) last(id string) *oc.Condition {
f.mu.Lock()
defer f.mu.Unlock()
for i := len(f.told) - 1; i >= 0; i-- {
if f.told[i].Subject.ID == id {
c := f.told[i]
return &c
}
}
return nil
}
// hearConditions has the gate say its conditions into a fake seat and bucket.
func hearConditions(w *world) *fakeConditions {
f := &fakeConditions{kept: map[string]string{}}
w.h.Say, w.h.Conditions = f.say, f
return f
}
// handedInOrder answers each member the gate hands the forge bridge as the forge does on 2026-10-25: the forge's word that
// it merged (gitea.pull.merged) first, the bridge's word after; or the bridge's first, as forgeFirst says. It returns the
// repositories in the order they were handed.
func handedInOrder(w *world, forgeFirst bool) []string {
var handed []string
numbers := map[string]int{}
for i := 0; i < 4; i++ {
var next *MergePull
for _, p := range w.merges.last("feat/g").Pulls {
if p.State == mdc.MergeMerging {
p := p
next = &p
}
}
if next == nil || numbers[next.Repository] > 0 {
break
}
handed = append(handed, next.Repository)
numbers[next.Repository] = next.Number
mergeCommit := strings.Repeat(string(rune('a'+i)), 40)
forge := func() { w.h.PullMerged(merged(next.Repository, next.Number, next.Head.Head, mergeCommit)) }
bridge := func() { bridgeMerged(w, next.Repository, next.Number, next.Head.Head, mergeCommit) }
if forgeFirst {
forge()
w.settleAll() // ten seconds pass between the two words on 2026-10-25: the gate looks in between
bridge()
} else {
bridge()
w.settleAll()
forge()
}
w.settleAll()
}
return handed
}
// Issue 1275, met again 2026-10-25 (group fix/puller-wakes-on-capacity): every member of an approved group is handed to
// the forge bridge, in the group's order, the next on the event that the one before merged; a group whose pass stops is
// said to the operator as a condition, which is cleared once it is whole; and every word a group's state is said in is
// one the merge decides on.
func TestAnApprovedGroupIsHandedWholeInItsOrderAndAStopIsSaid(t *testing.T) {
const (
sdkRepo, hS = "novox/mesh-sdk", "a8fbabbfc37369f9d1f6977c72e5523aff3d494c"
catRepo, hK = "novox/mesh-catalog", "23c827e7910c233608af4e4a8056cbc362dcb755"
ctlRepo, hC = "novox/mesh-controller", "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0"
)
sdkMember := member{sdkRepo, 80, hS, "go/stdio.go,go/buildstep/pull.go"}
catMember := member{catRepo, 479, hK, "modules/mesh-build-go/asks.go,modules/mesh-build-go/go.mod,modules/mesh-build-go/go.sum"}
ctlMember := member{ctlRepo, 7, hC, "cmd/mesh-controller/main.go"}
inOrder := func(members []member, forgeFirst bool, want ...string) func(t *testing.T) {
return func(t *testing.T) {
w := approvedGroupOf(t, members...)
w.merges.manual = true
allGreenFor(w, members...)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := handedInOrder(w, forgeFirst); strings.Join(got, " ") != strings.Join(want, " ") {
t.Fatalf("handed %v, wanted %v, every member in the group's order", got, want)
}
if g := w.h.groups["feat/g"]; g == nil {
t.Fatal("the group is gone")
} else if state, why := GroupState(g, w.h.membersOf(g)); state != GroupMerged {
t.Fatalf("the group is %s (%s), not merged whole", state, why)
}
if w.source.main == nil || len(w.source.main.Heads) != len(want) {
t.Fatalf("source.main does not hold the group as one revision: %+v", w.source.main)
}
}
}
cases := []struct {
name string
run func(t *testing.T)
}{
// 1275/AC3 (2026-10-25): the case of the day. The catalogue member pins the sdk member's commit; the forge's word
// of a merge arrives before the bridge's. Before the fix the catalogue member went first, and the sdk member was
// never handed: its group read merging, a word the turn to merge did not know.
{"sdk first, then the member that pins it, the forge's word first", inOrder([]member{catMember, sdkMember}, true, sdkRepo, catRepo)},
{"sdk first, then the member that pins it, the bridge's word first", inOrder([]member{sdkMember, catMember}, false, sdkRepo, catRepo)},
{"a member that pins nothing before one that changes a go.mod", inOrder([]member{catMember, ctlMember}, true, ctlRepo, catRepo)},
{"three members", inOrder([]member{ctlMember, catMember, sdkMember}, true, sdkRepo, ctlRepo, catRepo)},
// 1275/AC3: the merge tool refuses the first member in order; the second is not merged, and the group is said
// urgent, naming the group, the refused member and the forge's words, as a stalled line and to the operator.
{"a refused first member stops the group and is said urgent", func(t *testing.T) {
w := approvedGroup(t)
told := hearConditions(w)
allGreen(w)
w.forge.refuseRepo = map[string]error{gCat: refusedWords("novox/mesh-catalog#2")}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 || !strings.Contains(w.forge.merges[0], "mesh-catalog#2") {
t.Fatalf("wanted only the first member asked: %v", w.forge.merges)
}
lines := urgentLines(w)
if len(lines) != 1 || lines[0].ID != IDOf(gCat, gHk) || !strings.Contains(lines[0].Says, "feat/g") ||
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") || !strings.Contains(lines[0].Says, "Needed") {
t.Fatalf("not said as one urgent line: %+v", lines)
}
if c := told.last(IDOf(gCat, gHk)); c == nil || c.State != occ.StateRaised || c.Severity != occ.SeverityUrgent ||
!strings.Contains(c.Summary, "feat/g") {
t.Fatalf("the operator was not told, urgent: %+v", c)
}
}},
// 1275/AC4: a group half merged (its pass stopped after the first member) raises one urgent condition, said once
// however many looks follow, and cleared once the group is whole.
{"a half-merged group is raised once and cleared once whole", func(t *testing.T) {
w := approvedGroup(t)
told := hearConditions(w)
allGreen(w)
w.merges.manual = true
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.h.PullMerged(merged(gCat, 2, gHk, gT1))
bridgeMerged(w, gCat, 2, gHk, gT1)
moveForgeHead(w, gCtl, 1, otherHead) // the second member's head moved at the forge: the pass stops at it
w.settleAll()
for range 3 {
w.later(time30s)
w.settleAll()
}
id := IDOf(gCtl, gHc)
raised := 0
for _, c := range told.told {
if c.Subject.ID == id && c.State == occ.StateRaised {
raised++
}
}
if c := told.last(id); raised != 1 || c == nil || c.Severity != occ.SeverityUrgent || !strings.Contains(c.Summary, "feat/g") ||
!strings.Contains(c.Summary, "half merged (merged 1 of 2)") {
t.Fatalf("raised %d times, last %+v; wanted once, urgent, naming the group half merged", raised, c)
}
moveForgeHead(w, gCtl, 1, gHc)
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "mesh-controller", SHA: gHc, Context: repoCheck, State: "success"})
w.settleAll() // the status is an event: the pass goes on with no timer
handed := false
for _, p := range w.merges.last("feat/g").Pulls {
handed = handed || (p.Repository == gCtl && p.State == mdc.MergeMerging)
}
if !handed {
t.Fatalf("the status turned success and the second member was not handed: %+v", w.merges.last("feat/g"))
}
w.h.PullMerged(merged(gCtl, 1, gHc, gT1))
bridgeMerged(w, gCtl, 1, gHc, gT1)
w.settleAll()
if c := told.last(id); c == nil || c.State != occ.StateCleared {
t.Fatalf("the group is whole and its condition is not cleared: %+v", c)
}
}},
// A condition the gate raised before it restarted, whose delivery stalls no more, is cleared at the first look.
{"a condition kept from before a restart is cleared", func(t *testing.T) {
w := newWorld(t)
told := hearConditions(w)
told.kept[conditionKeyOf("novox/app@0123456789ab")] = "urgent||"
w.settleAll()
if c := told.last("novox/app@0123456789ab"); c != nil {
t.Fatalf("cleared before the forge's open pull requests were read: %+v", c)
}
w.h.wantForge()
w.settleAll()
if c := told.last("novox/app@0123456789ab"); c == nil || c.State != occ.StateCleared || len(told.kept) != 0 {
t.Fatalf("not cleared: %+v, kept %v", c, told.kept)
}
}},
// Review round 1, blocker 1: a pull request closed or merged on the forge with no event heard (de-spiegel #4,
// closed 2026-10-17, ready here eight days later) settles at the next complete read of the open list, and no
// condition is raised for it; a merge learnt so does not move source.main.
{"a pull request gone from the open list settles and raises nothing", func(t *testing.T) {
for _, merged := range []bool{false, true} {
w := newWorld(t)
w.op.auto = false
const repo, head = "novox/de-spiegel", "fdcbaefb3123fdcbaefb3123fdcbaefb3123fdcb"
w.h.PullUpdated(pr(repo, 4, head, "feat/x", "src/a.go"))
w.h.Checked(judgedAt(repo, 4, head, "pass", "t1t1t1t1t1t1"))
w.settleAll()
w.forge.refuse = errors.New("novox/de-spiegel#4 merges into main, which no protection rule requiring the checks guards")
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
id := IDOf(repo, head)
if len(w.h.Stalled()) == 0 {
t.Fatalf("the refused head is not stalled to begin with")
}
p := ForgePull{Number: 4, State: "closed", HeadSHA: head, Base: "main"}
if merged {
p.Merged, p.MergeCommit = true, strings.Repeat("e", 40)
}
w.forge.pulls[repo+"#4"] = p
w.forge.open = []OpenPull{{Owner: "novox", Repo: "other", Number: 1, Base: "main", HeadSHA: "abab"}}
told := hearConditions(w)
moves := len(w.source.moves)
w.h.wantForge()
w.settleAll()
want := Closed
if merged {
want = Merged
}
if st := w.state(id); st != want {
t.Fatalf("merged %v: the delivery is %s, wanted %s", merged, st, want)
}
if c := told.last(id); c != nil {
t.Fatalf("merged %v: a condition was said for it: %+v", merged, c)
}
if len(w.source.moves) != moves {
t.Fatalf("merged %v: source.main moved for a merge learnt by its absence: %+v", merged, w.source.moves)
}
}
}},
// Review round 2: a merge learnt after the fact moves no other delivery's trunk (an approved group of the same
// repository keeps its trunk, its state and its composed check), takes the forge's time, and a catch-up after it
// does not move source.main.
{"a merge learnt late moves no trunk and no source", func(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.merges.manual = true
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
const lateHead, lateMerge = "9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a9a", "8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b8b"
w.h.PullUpdated(pr(gCat, 9, lateHead, "fix/old", "modules/app/x.go"))
w.h.Checked(judged(gCat, 9, lateHead, "pass", gTrunk0))
w.settleAll()
late := IDOf(gCat, lateHead)
trunkOf := func() map[string]string {
out := map[string]string{}
for _, id := range []string{IDOf(gCat, gHk), IDOf(gCtl, gHc)} {
out[id] = w.delivery(id).TrunkNow + " " + string(w.delivery(id).State)
}
return out
}
before, moves := trunkOf(), len(w.source.moves)
w.forge.pulls[gCat+"#9"] = ForgePull{Number: 9, State: "closed", Merged: true, MergeCommit: lateMerge,
MergedAt: "2026-10-01T09:00:00Z", HeadSHA: lateHead, Base: "main"}
w.forge.open = []OpenPull{{Owner: "novox", Repo: "mesh-catalog", Number: 2, Base: "main", HeadSHA: gHk},
{Owner: "novox", Repo: "mesh-controller", Number: 1, Base: "main", HeadSHA: gHc}}
w.h.wantForge()
w.settleAll()
d := w.delivery(late)
if d.State != Merged || !d.MergedAt.Equal(time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)) || !d.LearntLate {
t.Fatalf("the late merge is %s at %v (learnt late %v)", d.State, d.MergedAt, d.LearntLate)
}
if after := trunkOf(); fmt.Sprint(after) != fmt.Sprint(before) {
t.Fatalf("the group's trunk or state moved: %v, was %v", after, before)
}
if g := w.h.groups["feat/g"]; g.Check == nil || g.Check.OutOfDate != "" {
t.Fatalf("the group's composed check went out of date: %+v", g.Check)
}
w.h.SourceCatchUp()
w.settleAll()
if len(w.source.moves) != moves {
t.Fatalf("source.main moved for a merge learnt late: %+v", w.source.moves[moves:])
}
}},
// mesh-delivery#51 (2026-10-25 21:43): the open list was read seconds after the bridge merged it and before the forge's
// merged event, so it was learnt late, moved no trunk, and its group's finish left it out of source.main. A merge
// learnt within replayLate of its landing is taken as heard as it happened; one a gate before the fix took late is
// written at the catch-up of the start.
{"a merge learnt from the open list seconds after it landed moves source.main", func(t *testing.T) {
const repo, head, merge = "novox/mesh-delivery", "4628ba26b41a4628ba26b41a4628ba26b41a4628", "db53ec975f01db53ec975f01db53ec975f01db53"
for _, oldGate := range []bool{false, true} {
w := newWorld(t)
w.merges.manual = true
w.h.PullUpdated(pr(repo, 51, head, "fix/1945", "modules/a/a.go"))
w.h.Checked(judgedAt(repo, 51, head, "pass", "t1t1t1t1t1t1"))
w.settleAll()
w.source.main = &Source{V: mdc.Version, Group: "before", Heads: map[string]string{repo: "73be3df28cd4"}}
w.forge.pulls[repo+"#51"] = ForgePull{Number: 51, State: "closed", Merged: true, MergeCommit: merge,
MergedAt: w.h.Now().Add(-3 * time.Second).UTC().Format(time.RFC3339), HeadSHA: head, Base: "main"}
w.forge.open = []OpenPull{{Owner: "novox", Repo: "other", Number: 1, Base: "main", HeadSHA: "abab"}}
w.h.wantForge()
w.settleAll()
if oldGate { // what the gate before the fix left
w.h.mu.Lock()
kept := w.h.deliveries[IDOf(repo, head)]
kept.LearntLate, kept.InSource, kept.Ready.SourceMoved = true, true, true
w.h.mu.Unlock()
w.source.main.Heads[repo] = "73be3df28cd4"
w.h.SourceCatchUp()
w.settleAll()
}
d := w.delivery(IDOf(repo, head))
if d.State != Merged || (!oldGate && d.LearntLate) || w.source.main.Heads[repo] != merge {
t.Fatalf("old gate %v: %s, learnt late %v, source.main %v", oldGate, d.State, d.LearntLate, w.source.main.Heads)
}
if heads, _ := w.h.SourceMain()["heads"].(map[string]string); heads[repo] != merge {
t.Fatalf("delivery_status's source-main says %v", w.h.SourceMain())
}
}
}},
// Review round 1, blocker 2 (1275/AC6): an out-of-date refusal at t0 with no other event is said urgent at t0+10
// minutes: the gate asks itself for a look then.
{"an out-of-date refusal turns urgent on the clock alone", func(t *testing.T) {
w := approvedGroup(t)
told := hearConditions(w)
allGreen(w)
w.forge.refuse = errAskedAgain
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
t0 := w.h.Now()
w.h.wanted.mu.Lock()
at := w.h.wanted.at
w.h.wanted.mu.Unlock()
if at.IsZero() || at.Sub(t0) > outOfDateFor {
t.Fatalf("no look is wanted by t0+%s: %v", outOfDateFor, at)
}
for i := 0; i < 5 && w.h.Now().Before(t0.Add(outOfDateFor)); i++ { // the timer fires a look, and nothing else happens
w.h.wanted.mu.Lock()
at = w.h.wanted.at
w.h.wanted.mu.Unlock()
if at.IsZero() {
break
}
w.later(at.Sub(w.h.Now()))
w.h.React()
}
c := told.last(IDOf(gCat, gHk))
if c == nil || c.State != occ.StateRaised || c.Severity != occ.SeverityUrgent {
t.Fatalf("at t0+%s with no event: %+v", w.h.Now().Sub(t0), c)
}
}},
// Review round 1, blocker 3: a gate that restarts while a head still waits on the forge does not clear its
// condition (the head's first sight is in memory alone, so its bound runs again), and does not say it again.
{"a restart keeps the condition of a head still waiting", func(t *testing.T) {
w := newWorld(t)
told := hearConditions(w)
const head = "bfe82315f42c8f4509d0598c32569a202d5eada6"
id := IDOf("novox/hq", head)
w.forge.open = []OpenPull{{Owner: "novox", Repo: "hq", Number: 243, Base: "main", HeadSHA: head, Required: []string{"mesh/repo-check"}}}
w.h.wantForge()
w.settleAll()
w.later(UncheckedAfter + time.Minute)
w.settleAll()
if c := told.last(id); c == nil || c.State != occ.StateRaised {
t.Fatalf("the unchecked head is not raised: %+v", c)
}
before := len(told.told)
again := newWorldOver(t, w.store, w.forge, w.h.Now()) // the gate restarts
again.h.Say, again.h.Conditions = told.say, told
again.h.wantForge()
again.settleAll()
again.later(time.Minute)
again.settleAll()
if len(told.told) != before {
t.Fatalf("the restart said something of a head still waiting: %+v", told.told[before:])
}
w.forge.open = nil // it closes
again.h.wantForge()
again.settleAll()
if c := told.last(id); c == nil || c.State != occ.StateCleared {
t.Fatalf("closed, and its condition is not cleared: %+v", c)
}
}},
// The kind behind the day's fault: a decision on a group's state named a word GroupState no longer says. Every word
// it says is one groupStatusState knows, and the merge goes on exactly in ready and merging.
{"every word of a group's state is decided on", func(t *testing.T) {
for _, word := range groupWords {
if groupStatusState(word) == "pending" && word != GroupChecking {
t.Errorf("groupStatusState does not know %q", word)
}
if may := groupMayMerge(word); may != (word == GroupReady || word == GroupMerging) {
t.Errorf("groupMayMerge(%q) = %v", word, may)
}
}
}},
}
for _, c := range cases {
t.Run(c.name, c.run)
}
}
// allGreenFor makes both merge checks a success at every member's head.
func allGreenFor(w *world, ms ...member) {
for _, m := range ms {
setChecks(w, m.repo, m.head, "success", "success")
}
}
// bridgeMerged is the forge bridge's word that it merged a member.
func bridgeMerged(w *world, repository string, number int, head, merge string) {
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository, Number: number, Head: head, Merge: merge, Root: "root",
Trees: map[string]string{"app": "tree"}})
}
// moveForgeHead sets the head the forge says a pull request has, as a push the gate has not heard yet does.
func moveForgeHead(w *world, repo string, number int, head string) {
w.forge.mu.Lock()
defer w.forge.mu.Unlock()
key := fmt.Sprintf("%s#%d", repo, number)
p := w.forge.pulls[key]
p.HeadSHA = head
w.forge.pulls[key] = p
}
@@ -1,332 +0,0 @@
package main
import (
"errors"
"fmt"
"sort"
"strings"
"testing"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// Issue 1275, met a third time (2026-10-26 13:40, group fix/734-lost-ask): the operator approved mesh-sdk#92,
// mesh-controller#472 and mesh-delivery#73; the first two merged, the forge answered #73's merge 405 "Please try again
// later", and in the seconds between, group feat/890-assignments merged mesh-delivery#72, so #73 then conflicted. A group is
// merged only when every member merges now, and no other approved merge lands in a repository between its members.
const (
wSDK, wCtl, wDel = "novox/mesh-sdk", "novox/mesh-controller", "novox/mesh-delivery"
wH92, wH472, wH73 = "3203358c4cb5fa5bed223f7ce85577bbb70a346e", "ccc7ba334c82554d809b5b38d8eb0eb5f36be7be", "bf574172154a9f6ed7d4e790643e0dcab2a10a91"
wH72, wH73b = "72727272727272727272727272727272727272aa", "b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0"
forgeLater = "Gitea API /repos/novox/mesh-delivery/pulls/73/merge: 405 {\"message\":\"Please try again later\"}"
conflictWords = "does not merge into main now"
headMovedWords = "its head is now"
)
var lostAsk = []member{
{wSDK, 92, wH92, "go/buildstep/ask.go"},
{wCtl, 472, wH472, "internal/grants.go"},
{wDel, 73, wH73, "modules/mesh-delivery-checker/internal/check/lostask.go,modules/mesh-delivery-checker/go.mod"},
}
// forgePull says what the forge answers of a pull request: its head and whether it merges now.
func forgePull(w *world, repository string, number int, head string, mergeable bool) {
w.forge.mu.Lock()
defer w.forge.mu.Unlock()
m := mergeable
w.forge.pulls[fmt.Sprintf("%s#%d", repository, number)] = ForgePull{Number: number, State: "open", Base: "main", HeadSHA: head, Mergeable: &m}
}
func TestAnApprovedGroupMergesNoMemberUnlessEveryMemberMergesNow(t *testing.T) {
approve := func(t *testing.T) *world {
w := approvedGroupOf(t, lostAsk...)
allGreenFor(w, lostAsk...)
for _, m := range lostAsk {
forgePull(w, m.repo, m.number, m.head, true)
}
return w
}
cases := []struct {
name string
// before is the forge as the Approve lands; after, once the grace has passed (nil: as before).
before, after func(w *world)
// merged is how many members merge in the end; line what the urgent line says, when one stands.
merged int
line string
}{
{name: "a member in conflict with its trunk: none merged, the group held and said urgent", merged: 0, line: conflictWords,
before: func(w *world) {
forgePull(w, wDel, 73, wH73, false)
w.forge.refuseRepo = map[string]error{wDel: errors.New(forgeLater)} // what the merge tool would answer
}},
{name: "the forge still weighing a member: none merged, then every member once it says yes", merged: 3,
before: func(w *world) { forgePull(w, wDel, 73, wH73, false) },
after: func(w *world) { forgePull(w, wDel, 73, wH73, true) }},
{name: "a member's head moved after the Approve: none merged, said urgent", merged: 0, line: headMovedWords,
before: func(w *world) { forgePull(w, wDel, 73, wH73b, true) }},
{name: "every member merges now: all merged in one pass", merged: 3, before: func(*world) {}},
}
// The recovery of a group that ended half merged: its remaining member's new head, or the operator's word.
t.Run("a half-merged group is made whole by its remaining member's new head", halfMergedMadeWholeByANewHead)
t.Run("a member refused part way is asked again on the operator's word", refusedPartWayAskedAgainOnTheOperatorsWord)
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
w := approve(t)
c.before(w)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if c.after == nil && c.merged == 3 {
if len(w.forge.merges) != 3 {
t.Fatalf("merged %v, wanted all three", w.forge.merges)
}
return
}
if len(w.forge.merges) != 0 {
t.Fatalf("a member was merged while another does not merge now: %v", w.forge.merges)
}
if c.after != nil {
c.after(w)
}
w.later(forgeMergeGrace + time30s)
w.settleAll()
if len(w.forge.merges) != c.merged {
t.Fatalf("merged %v, wanted %d", w.forge.merges, c.merged)
}
lines := urgentLines(w)
if c.line == "" {
if len(lines) != 0 {
t.Fatalf("a group merged whole still says %+v", lines)
}
return
}
if len(lines) != 1 || !strings.Contains(lines[0].Says, "feat/g") ||
!strings.Contains(lines[0].Says, IDOf(wDel, wH73)) || !strings.Contains(lines[0].Says, c.line) ||
!strings.Contains(lines[0].Says, "nothing is merged") {
t.Fatalf("the held group is not one urgent line naming the member and why: %+v", lines)
}
})
}
}
// handing are the pull requests the gate has handed the forge bridge and the bridge has not answered, in every merges key.
func handing(w *world) []MergePull {
w.merges.mu.Lock()
defer w.merges.mu.Unlock()
var out []MergePull
for _, v := range w.merges.values {
for _, p := range v.Pulls {
if p.State == mdc.MergeMerging {
out = append(out, p)
}
}
}
sort.Slice(out, func(i, j int) bool { return out[i].Repository < out[j].Repository })
return out
}
func handedWords(ps []MergePull) string {
var out []string
for _, p := range ps {
out = append(out, fmt.Sprintf("%s#%d", p.Repository, p.Number))
}
return strings.Join(out, " ")
}
// Two approved merges into one repository: the one whose pass started has the repository until every member merged, and
// the other is handed only then (2026-10-26: mesh-delivery#72 of feat/890-assignments merged between fix/734-lost-ask's
// members). It waits for a merge, never for a check: once the group's pass ended #72 is handed on its Approve, its verdict
// gone out of date with #73's merge or not (novox/hq ADR 0661 §3).
func TestTwoApprovedMergesIntoOneRepositoryDoNotInterleave(t *testing.T) {
w := approvedGroupOf(t, lostAsk...)
w.merges.manual = true
allGreenFor(w, lostAsk...)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if got := handedWords(handing(w)); got != "novox/mesh-sdk#92" {
t.Fatalf("handed %q, wanted the group's first member", got)
}
// feat/890-assignments: a lone pull request into mesh-delivery, approved while the group's pass is under way.
w.h.PullUpdated(pr(wDel, 72, wH72, "feat/890-assignments", "modules/mesh-delivery-checker/go.mod"))
w.h.Checked(judged(wDel, 72, wH72, "pass", gTrunk0))
setChecks(w, wDel, wH72, "success", "success")
w.settleAll()
if open := w.op.open(); len(open) != 1 {
t.Fatalf("the lone pull request is asked once: %v", open)
} else {
w.op.answer(open[0], OptionApprove)
}
w.settleAll()
var order []string
steps := []struct {
repo string
number int
head string
}{{wSDK, 92, wH92}, {wCtl, 472, wH472}, {wDel, 73, wH73}, {wDel, 72, wH72}}
for _, next := range steps {
handed := handing(w)
if len(handed) != 1 || handed[0].Repository != next.repo || handed[0].Number != next.number {
t.Fatalf("after %v: handed %q, wanted only %s#%d", order, handedWords(handed), next.repo, next.number)
}
merge := strings.Repeat(string(rune('a'+next.number%20)), 40)
bridgeMerged(w, next.repo, next.number, next.head, merge)
w.h.PullMerged(merged(next.repo, next.number, next.head, merge))
w.settleAll()
order = append(order, fmt.Sprintf("%s#%d", next.repo, next.number))
}
if left := handing(w); len(left) != 0 {
t.Fatalf("still handed: %q", handedWords(left))
}
}
// A group that ended half merged (its last member refused after the others merged) is made whole by its remaining member's
// new head: rebased on its trunk, checked, composed with the merged members, asked once and merged, and the urgent line
// is cleared (issue 1275, the recovery of 2026-10-26 13:40).
func halfMergedMadeWholeByANewHead(t *testing.T) {
w := approvedGroupOf(t, lostAsk...)
allGreenFor(w, lostAsk...)
w.forge.refuseRepo = map[string]error{wDel: errors.New(forgeLater)}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 3 || len(urgentLines(w)) != 1 {
t.Fatalf("the case: two merged and the third refused, said urgent: %v %+v", w.forge.merges, urgentLines(w))
}
w.forge.refuseRepo = nil
// #73 rebased on its trunk: a new head, checked, composed with the members merged.
w.h.PullUpdated(pr(wDel, 73, wH73b, "feat/g", "modules/mesh-delivery-checker/go.mod"))
w.h.Checked(judged(wDel, 73, wH73b, "pass", gT1))
setChecks(w, wDel, wH73b, "success", "success")
w.settleAll()
open := w.op.open()
if len(open) != 1 {
t.Fatalf("the remaining member's new head is asked once: %v", open)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
if last := w.forge.merges[len(w.forge.merges)-1]; len(w.forge.merges) != 4 || !strings.Contains(last, "mesh-delivery#73@"+wH73b) {
t.Fatalf("the new head is not merged alone: %v", w.forge.merges)
}
if lines := urgentLines(w); len(lines) != 0 {
t.Fatalf("the group is whole and still said: %+v", lines)
}
}
// delivery_merge_again (issue 1275, the tools rule): a member refused after its siblings merged, with no new head and no
// trunk move to ask it again, is asked again on the operator's word, at the head approved; a delivery neither refused nor
// held is refused, naming why.
func refusedPartWayAskedAgainOnTheOperatorsWord(t *testing.T) {
w := approvedGroupOf(t, lostAsk...)
allGreenFor(w, lostAsk...)
if _, err := w.h.MergeAgain(IDOf(wDel, wH73), "too early"); err == nil {
t.Fatal("a delivery not approved is asked again")
}
w.forge.refuseRepo = map[string]error{wDel: errors.New(forgeLater)}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.forge.refuseRepo = nil
w.settleAll()
if len(w.forge.merges) != 3 {
t.Fatalf("the case: the third member refused and not asked again by itself: %v", w.forge.merges)
}
if _, err := w.h.MergeAgain(IDOf(wDel, wH73), ""); err == nil {
t.Fatal("asked again with no why")
}
if _, err := w.h.MergeAgain(IDOf(wDel, wH73), "the forge said try again later"); err != nil {
t.Fatal(err)
}
w.settleAll()
if len(w.forge.merges) != 4 || len(urgentLines(w)) != 0 {
t.Fatalf("not merged on the operator's word: %v %+v", w.forge.merges, urgentLines(w))
}
if _, err := w.h.MergeAgain(IDOf(wDel, wH73), "again"); err == nil || !strings.Contains(err.Error(), "merged already") {
t.Fatalf("a merged delivery is asked again: %v", err)
}
}
// approveSecondGroup makes a second group of these members on branch, checked, composed together and approved; the world
// already holds group feat/g.
func approveSecondGroup(t *testing.T, w *world, branch string, ms ...member) {
t.Helper()
for _, m := range ms {
w.h.PullUpdated(pr(m.repo, m.number, m.head, branch, strings.Split(m.paths, ",")...))
w.h.Checked(judged(m.repo, m.number, m.head, "pass", gTrunk0))
}
allGreenFor(w, ms...)
w.settleAll()
g := w.h.groups[branch]
if g == nil || g.Check == nil {
t.Fatalf("no composed check was asked for %s: %+v", branch, g)
}
c := CheckedEvent{Group: branch, ID: g.Check.Asked, Verdict: "pass", Summary: "composed"}
for _, m := range ms {
owner, repo, _ := strings.Cut(m.repo, "/")
c.Members = append(c.Members, struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Commit string `json:"commit"`
Trunk string `json:"trunk,omitempty"`
}{Owner: owner, Repo: repo, Commit: m.head, Trunk: gTrunk0[:12]})
}
w.h.Checked(c)
w.settleAll()
for _, key := range w.op.open() {
if strings.Contains(key, strings.ReplaceAll(branch, "/", "_")) || strings.Contains(key, branch) {
w.op.answer(key, OptionApprove)
}
}
}
// A group held half merged keeps its repositories until it is whole or declined: another group sharing one waits, and the
// operator's word that asks the held group again never leaves the two waiting on each other (issue 1275, review round 1:
// a2 waited for group B, b2 waited for group A, for ever). And the operator's word is refused while another pass it would
// wait for is under way, naming it.
func TestAHeldHalfMergedGroupKeepsItsRepositoriesAndNoTwoPassesWaitOnEachOther(t *testing.T) {
groupA := []member{lostAsk[0], lostAsk[2]} // mesh-sdk#92, then mesh-delivery#73
groupB := []member{lostAsk[1], {wDel, 72, wH72, "modules/mesh-delivery-checker/go.mod"}}
t.Run("half merged and held: the other group waits, and both merge once the held one is asked again", func(t *testing.T) {
w := approvedGroupOf(t, groupA...)
allGreenFor(w, groupA...)
w.forge.refuseRepo = map[string]error{wDel: errors.New(forgeLater)}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("the case: mesh-sdk#92 merged, mesh-delivery#73 refused: %v", w.forge.merges)
}
approveSecondGroup(t, w, "feat/890-assignments", groupB...)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("group feat/890-assignments merged while feat/g is half merged into a repository both go into: %v", w.forge.merges)
}
w.forge.refuseRepo = nil
if _, err := w.h.MergeAgain(IDOf(wDel, wH73), "the forge said try again later"); err != nil {
t.Fatal(err)
}
w.settleAll()
want := []string{"mesh-sdk#92", "mesh-delivery#73", "mesh-delivery#73", "mesh-controller#472", "mesh-delivery#72"}
if len(w.forge.merges) != len(want) {
t.Fatalf("merged %v, wanted %v: the held group first, then the other", w.forge.merges, want)
}
for i, m := range want {
if !strings.Contains(w.forge.merges[i], m+"@") {
t.Fatalf("merged %v, wanted %v in that order", w.forge.merges, want)
}
}
})
t.Run("the operator's word is refused while another pass it would wait for is under way", func(t *testing.T) {
w := approvedGroupOf(t, groupA...)
allGreenFor(w, groupA...)
w.forge.refuseRepo = map[string]error{wSDK: errors.New(forgeLater)} // refused first: nothing of feat/g merged
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.merges.manual = true
approveSecondGroup(t, w, "feat/890-assignments", groupB...)
w.settleAll()
if got := handedWords(handing(w)); got != "novox/mesh-controller#472" {
t.Fatalf("the other group's pass did not start: handed %q", got)
}
_, err := w.h.MergeAgain(IDOf(wSDK, wH92), "asked again")
if err == nil || !strings.Contains(err.Error(), "feat/890-assignments") {
t.Fatalf("asked again while a pass it waits for is under way: %v", err)
}
})
}
@@ -1,246 +0,0 @@
package main
import (
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// **The gate asks through its bucket asks and merges through the bridge** (novox/hq ADR 0437 §6, §8, ADR 0441 §1 to §5,
// ADR 0450 §3, issue 1348). Fixture sources: the key and the id of an ask and the layout of a merge are meshdeliveryclient's
// (MergeAskKey, AskID, Merge); the act an ask binds is spelled as mesh-controller cmd/mesh-controller/moved.go:98-106 verifies
// it and as the teller checks it (mesh-delivery modules/mesh-delivery-teller teller.go mergeAct, headsOf); the bridge's
// messages are the ones mesh-delivery modules/mesh-delivery-forge-bridge internal/bridge acts.go writes (RefusedMessage) and
// bridge.go merged writes, and the fake bridge here (bridge_test.go) writes them in that layout.
// AC1: a ready group writes one ask key, with today's words, and the gate calls no router.
func TestAReadyGroupWritesOneAskKeyWithTodaysWordsAndCallsNoRouter(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
if w.op.count() != 1 {
t.Fatalf("%d asks written, wanted one", w.op.count())
}
key := w.op.open()[0]
kind, group, hash, err := mdc.ParseAskKey(key)
if err != nil || kind != "merge" || group != "feat/g" {
t.Fatalf("the key %q is not merge.<group>.<heads hash>: %q %q %v", key, kind, group, err)
}
ask := w.op.byKey[key]
routed := w.op.ask(key)
if ask.Group != "feat/g" || len(ask.Heads) != 2 {
t.Fatalf("the ask value %+v does not name the group and both heads", ask)
}
if want, err := mdc.MergeAskKey("feat/g", ask.Heads); err != nil || want != key || !strings.HasSuffix(key, hash) {
t.Fatalf("the heads %v give the key %q, the ask is under %q (%v)", ask.Heads, want, key, err)
}
if routed.ID != mdc.AskID(key) {
t.Fatalf("the router's id of the ask is %q, wanted %q", routed.ID, mdc.AskID(key))
}
// Today's words (approval.go composeMergeAsk): the headline names the group, the details its members.
if !strings.Contains(routed.Headline, "group feat") || !strings.Contains(routed.Details, "mesh-controller#1") ||
!strings.Contains(routed.Details, "mesh-catalog#2") || len(routed.Options) != 2 || routed.Who != occ.Operator {
t.Fatalf("the ask is not today's merge ask: %+v", routed)
}
// A group's headline says a failed check first (issue 1166); a passed or a running one keeps the plain words.
members := []*Delivery{{Repository: "novox/mesh-controller", Number: 1}, {Repository: "novox/mesh-catalog", Number: 2}}
for _, c := range []struct{ key, head string }{
{checkSaidFail, "Check failed \u2014 merge into main: group feat"},
{checkSaidPass, "Merge into main: group feat"},
{"running", "Merge into main: group feat"},
} {
if h := composeMergeAsk("id", members[0], "feat/g", members, nil, checkSaid{Key: c.key, Words: "w"}, time.Time{}).Headline; !strings.HasPrefix(h, c.head) {
t.Fatalf("a %s check gives the headline %q, wanted it to start %q", c.key, h, c.head)
}
}
// The act the Approve binds is the one the teller checks and the controller verifies: this ask's router id, the option, and
// the heads as `owner/repo@<first 12 characters>` to the whole commit.
approve, _ := occ.OptionOf(routed, OptionApprove)
heads := map[string]string{IDOf(gCtl, gHc): gHc, IDOf(gCat, gHk): gHk}
want, _ := occ.ActDigest(occ.Act{"verb": "mesh-delivery.merge", "ask": mdc.AskID(key), "option": "approve",
"heads": "novox/mesh-catalog@d2d2d2d2d2d2=" + gHk + ",novox/mesh-controller@c1c1c1c1c1c1=" + gHc})
if approve.Binds != want {
t.Fatalf("the Approve binds %s, wanted the act %v of %v", approve.Binds, mergeAct(mdc.AskID(key), "approve", heads), heads)
}
if len(w.forge.calls) != 0 || len(w.merges.puts) != 0 || len(w.forge.merges) != 0 {
t.Fatalf("something was written beside the ask: forge %v, merges %d", w.forge.calls, len(w.merges.puts))
}
}
// answeredFirst is a group approved by the teller's answer, with the bridge saying nothing yet.
func answeredFirst(t *testing.T) *world {
w := approvedGroup(t)
allGreen(w)
w.merges.manual = true
w.asked = w.op.open()[0]
w.op.answer(w.asked, OptionApprove)
w.settleAll()
return w
}
// AC2: an answer naming the heads asked now hands the first member only; the bridge's merged hands the next; its refused
// hands none.
func TestAnAnswerHandsTheFirstMemberOnlyAndTheBridgesMergedHandsTheNext(t *testing.T) {
w := answeredFirst(t)
if len(w.merges.puts) != 1 {
t.Fatalf("the gate wrote %d merges, wanted the first member handed once", len(w.merges.puts))
}
first := w.merges.last("feat/g")
if len(first.Pulls) != 1 || first.Pulls[0].Repository != gCat || first.Pulls[0].State != mdc.MergeMerging || first.Pulls[0].Head.Head != gHk {
t.Fatalf("the first member is not handed alone, merging, at its head: %+v", first.Pulls)
}
if first.Approved[mdc.ApprovedKey(gCat, 2)] != gHk || first.Approved[mdc.ApprovedKey(gCtl, 1)] != gHc || len(first.Approved) != 2 {
t.Fatalf("the merge does not carry the Approve's heads of both members: %v", first.Approved)
}
if first.Ask != w.asked {
t.Fatalf("the merge names no ask: %+v", first)
}
w.settleAll()
if len(w.merges.puts) != 1 {
t.Fatalf("the next member was handed before the bridge said the first merged: %d merges written", len(w.merges.puts))
}
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
w.settleAll()
if len(w.merges.puts) != 2 {
t.Fatalf("the second member was not handed after the first merged: %d merges written", len(w.merges.puts))
}
second := w.merges.last("feat/g")
if len(second.Pulls) != 2 || second.Pulls[0].State != mdc.MergeMerged || second.Pulls[0].Commit != strings.Repeat("a", 40) ||
second.Pulls[1].Repository != gCtl || second.Pulls[1].State != mdc.MergeMerging {
t.Fatalf("the next merge is not the first merged and the second merging: %+v", second.Pulls)
}
}
// AC2: the bridge's refused hands no member, and the group is said urgent, naming the member and the forge's words.
func TestTheBridgesRefusalHandsNoMemberAndIsSaidUrgent(t *testing.T) {
w := answeredFirst(t)
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeRefused, Repository: gCat, Number: 2, Head: gHk, Act: "merge",
Words: "the forge refused gitea_merge_pull_request: mesh/repo-check (missing)"})
w.later(time30s)
w.settleAll()
if len(w.merges.puts) != 2 {
t.Fatalf("%d merges written; wanted the first handed and then written refused, and none handed after", len(w.merges.puts))
}
refused := w.merges.last("feat/g")
if len(refused.Pulls) != 1 || refused.Pulls[0].State != mdc.MergeRefused || !strings.Contains(refused.Pulls[0].Words, "mesh/repo-check (missing)") {
t.Fatalf("the refusal is not written into the merge: %+v", refused.Pulls)
}
lines := urgentLines(w)
if len(lines) != 1 || lines[0].ID != IDOf(gCat, gHk) || !strings.Contains(lines[0].Says, "feat/g") ||
!strings.Contains(lines[0].Says, "mesh/repo-check (missing)") {
t.Fatalf("the refusal is not said as one urgent line: %+v", lines)
}
}
// novox/hq ADR 0661 §4 (changing ADR 0441 §2): the next member is handed on the Approve covering its head, with no read of
// the group's required statuses, after the first member merged.
func TestTheNextMemberIsHandedWithNoReadOfTheGroupsRequiredStatuses(t *testing.T) {
w := answeredFirst(t)
w.merges.say(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: gCat, Number: 2, Head: gHk, Merge: strings.Repeat("a", 40), Root: "r", Trees: map[string]string{"x": "t"}})
setChecks(w, gCtl, gHc, "success", "") // the second member's repository check went missing meanwhile
w.settleAll()
if len(w.merges.puts) != 2 || len(urgentLines(w)) != 0 {
t.Fatalf("%d merges written, lines %+v: wanted the second member handed on its Approve", len(w.merges.puts), urgentLines(w))
}
}
// AC4: once every member is merged the gate writes source.main once, and each merge is announced with the ask, its asker the
// teller, and the heads.
func TestAGroupMergedWholeMovesSourceMainOnceAndAnnouncesWithTheTeller(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("merges %v", w.forge.merges)
}
if len(w.source.moves) != 1 {
t.Fatalf("source.main moved %d times, wanted once", len(w.source.moves))
}
main := w.source.moves[0]
commit := strings.Repeat("c", 40)
if main.Group != "feat/g" || main.Heads[gCat] != commit || main.Heads[gCtl] != commit || len(main.Heads) != 2 {
t.Fatalf("source.main is %+v; wanted both repositories at their merge commits, moved by the group", main)
}
final := w.merges.last("feat/g")
if len(final.Pulls) != 2 || final.Pulls[0].State != mdc.MergeMerged || final.Pulls[1].State != mdc.MergeMerged {
t.Fatalf("the merge is not written as all merged: %+v", final.Pulls)
}
say := mergedSaid(w)
if len(say) != 2 {
t.Fatalf("announced %d merges, wanted each member's once: %v", len(say), say)
}
for _, m := range say {
if m["asker"] != tellerModule || !strings.HasPrefix(m["ask_id"].(string), "merge-feat_g-") || m["option"] != OptionApprove {
t.Fatalf("the announcement does not name the ask and its asker the teller: %v", m)
}
}
w.settleAll()
if len(w.source.moves) != 1 {
t.Fatalf("source.main moved again: %d", len(w.source.moves))
}
}
// 1106: the same refusal again and again with the trunk not moved is not asked on without end. Three refusals in a row with
// the same cause end the asking by a verdict, a status or a composed check, and say it urgent; a trunk's move, or the
// operator's Approve, starts it again.
func TestARefusalThatRepeatsWithNoTrunkMoveIsNotAskedAgainWithoutEnd(t *testing.T) {
w := answeredFirst(t)
d := w.h.deliveries[IDOf(gCat, gHk)]
refuse := func(took string) {
w.h.ForgeRefused(forgemsg.Forge{Kind: forgemsg.ForgeRefused, Repository: gCat, Number: 2, Head: gHk, Act: "merge",
Words: "novox/mesh-catalog#2 is not merged: its verdict names no trunk commit it was judged against, and main is now f70a99dbf485. " +
"The check of head d2d2d2d2d2d2 merged into trunk f70a99dbf485 was asked; the last check took " + took + ". Ask again once both statuses pass"})
}
tried := w.now
for i, took := range []string{"4m1s", "4m9s"} {
refuse(took)
d.Ready.LastTry = tried
w.h.mergeAgainNow(d)
if !d.Ready.LastTry.IsZero() {
t.Fatalf("refusal %d: the merge was not asked again at once on a verdict, as after a first refusal", i+1)
}
}
refuse("4m5s") // the third in a row, with only the check's duration changed
if d.Ready.Repeats != 3 {
t.Fatalf("%d refusals counted for the same cause, wanted 3", d.Ready.Repeats)
}
d.Ready.LastTry = tried
w.h.mergeAgainNow(d) // the composed check or a verdict answers: nothing is asked
if d.Ready.LastTry.IsZero() {
t.Fatal("the merge was asked again after the same refusal three times with the trunk not moved")
}
if lines := urgentLines(w); len(lines) != 1 || !strings.Contains(lines[0].Says, "names no trunk commit") {
t.Fatalf("the repeated refusal is not said urgent: %+v", lines)
}
// The trunk moves: the merge is asked again.
d.TrunkNow = "a1a1a1a1a1a1a1a1"
w.h.mergeAgainNow(d)
if !d.Ready.LastTry.IsZero() {
t.Fatal("a trunk's move did not let the merge be asked again")
}
}
// 1785: a verdict, a composed check or a status of the same heads that changes what the refusal waited on starts the count of
// the same words again, so an approved merge whose cause cleared is asked again without a second Approve; a status that is no
// success changes nothing.
func TestANewCauseOfTheSameHeadsStartsTheCountOfARepeatedRefusalAgain(t *testing.T) {
w := answeredFirst(t)
d := w.h.deliveries[IDOf(gCat, gHk)]
d.Ready.Repeats, d.Ready.RepeatTrunk = maxRepeatedRefusals, d.TrunkNow
d.Ready.LastTry = w.now
owner, repo, _ := strings.Cut(gCat, "/")
w.h.CommitStatus(ForgeStatusEvent{Owner: owner, Repo: repo, SHA: gHk, Context: "mesh/check", State: "pending"})
if d.Ready.Repeats != maxRepeatedRefusals || d.Ready.LastTry.IsZero() {
t.Fatalf("a status that is no success changed the refusal: %d repeats", d.Ready.Repeats)
}
w.h.CommitStatus(ForgeStatusEvent{Owner: owner, Repo: repo, SHA: gHk, Context: "mesh/check", State: "success"})
if d.Ready.Repeats != 0 || !d.Ready.LastTry.IsZero() {
t.Fatalf("the check turned success on the same head: %d repeats, last try %v; the merge is asked again and the count starts anew",
d.Ready.Repeats, d.Ready.LastTry)
}
}
@@ -1,116 +0,0 @@
package main
import (
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"strings"
"testing"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// **The gate writes nothing to the forge: its statuses, views, notes and comments are owed keys, and its plan is read from
// `plans`** (novox/hq ADR 0437 §2, §8, ADR 0450 §2, §4, §7, issue 1349; the empty check answer is the mesh-delivery half of
// issue 1311). Fixture sources: the key and the value of an owed act are meshdeliveryclient's (OwedKey, Owed); the arguments
// of `plans` are mesh-controller cmd/mesh-controller/seatverbs.go's (repository, paths, module-dirs, head, base, removed, data)
// and its answer the document of deliveryPlanDocument (plan, reach, mesh, gated).
// AC1: a transition that today sets a status, a view, a note or a comment writes the matching owed key.
func TestATransitionWritesItsStatusViewAndNoteAsOwedKeys(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
page := "https://forge.invalid/novox/app/pulls/7"
statusKey, _ := mdc.OwedKey("novox/app", 7, head, "status=mesh/delivery")
viewKey, _ := mdc.OwedKey("novox/app", 7, head, "view")
noteKey, _ := mdc.OwedKey("novox/app", 7, head, "note")
for _, key := range []string{statusKey, viewKey, noteKey} {
if _, there := w.owed.keys[key]; !there {
t.Fatalf("the key %s was not written; the keys are %v", key, w.owed.writes)
}
}
if s := w.owed.keys[statusKey]; s.Act != mdc.OwedStatus || s.Context != "mesh/delivery" || s.State != "pending" || s.Target != page || s.Text == "" {
t.Fatalf("the owed status is %+v; wanted mesh/delivery pending, pointing at %s", s, page)
}
if v := w.owed.keys[viewKey]; v.Act != mdc.OwedView || !strings.Contains(v.Text, id) {
t.Fatalf("the owed view is %+v", v)
}
if n := w.owed.keys[noteKey]; n.Act != mdc.OwedNote || !strings.Contains(n.Text, "proposed") {
t.Fatalf("the owed note is %+v", n)
}
// What the bridge would perform is what the forge holds.
if len(w.forge.notesOn(head)) == 0 || !strings.HasPrefix(statusOn(w, "novox/app", head), "pending") {
t.Fatalf("the fake bridge did not perform what was owed: %v", w.forge.notesOn(head))
}
}
// AC1: a note on the merge commit, which no pull request names, is keyed with the number 0.
func TestANoteOnTheMergeCommitIsOwedWithNumberZero(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.h.PullMerged(merged("novox/app", 7, head, merge))
w.settleAll()
key, _ := mdc.OwedKey("novox/app", 0, merge, "note")
if n, there := w.owed.keys[key]; !there || n.Act != mdc.OwedNote {
t.Fatalf("no note was owed on the merge commit under %s; the keys are %v", key, w.owed.writes)
}
}
// AC1: two comments on one head are two acts: the sequence number is in the act.
func TestTwoCommentsOnOneHeadAreTwoOwedKeys(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.mu.Lock()
d := w.h.deliveries[id]
w.h.sayOn(d, "the first thing said")
w.h.sayOn(d, "the second thing said")
w.h.mu.Unlock()
w.settleAll()
one, _ := mdc.OwedKey("novox/app", 7, head, "comment.1")
two, _ := mdc.OwedKey("novox/app", 7, head, "comment.2")
if w.owed.keys[one].Text != "the first thing said" || w.owed.keys[two].Text != "the second thing said" {
t.Fatalf("the comments are %+v and %+v", w.owed.keys[one], w.owed.keys[two])
}
if len(w.forge.comments) != 2 {
t.Fatalf("the bridge would say %d comments: %v", len(w.forge.comments), w.forge.comments)
}
}
// AC2 (R6-C1): the gate's code calls no gitea write tool and no controller verb at all (ADR 0539 §17: the gate asks the
// controller nothing, and holds no port to it). Tests are left out: they name the words to prove them gone.
func TestTheGatesCodeCallsNoForgeWriteToolAndNoControllerVerb(t *testing.T) {
files, err := filepath.Glob("*.go")
if err != nil {
t.Fatal(err)
}
forbidden := []string{"delivery-plan", "gitea_commit_status\"", "gitea_delivery_view", "gitea_note_append", "gitea_add_comment",
"gitea_merge_pull_request\"", "gitea_open_pulls_write", "seat:mesh-controller", "plan-moved"}
fset := token.NewFileSet()
for _, f := range files {
if strings.HasSuffix(f, "_test.go") {
continue
}
file, err := parser.ParseFile(fset, f, nil, 0)
if err != nil {
t.Fatal(err)
}
ast.Inspect(file, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.BasicLit:
if x.Kind == token.STRING {
for _, word := range forbidden {
if strings.Contains(x.Value, word) {
t.Errorf("%s holds the string %s", f, x.Value)
}
}
}
}
return true
})
}
}
@@ -1,123 +0,0 @@
package main
import (
"strings"
"testing"
"time"
)
// novox/hq issue 1726: a delivery made from a check's verdict that arrived before its pull request's head was heard
// (transition "its verdict arrived before its head was heard", as mesh-controller#419 at a47476252772 was) holds no base,
// branch, title, URLs or review record. The ready pass skipped it in silence, so no Approve was ever asked, and the
// announcement of the same head, heard later, filled none of it.
// verdictFirst is the live shape: the verdict heard, the head not, and the delivery ready.
func verdictFirst(t *testing.T) (*world, string) {
w := newWorld(t)
w.op.auto = false
id := IDOf("novox/app", readyHead)
w.h.Checked(judgedAt("novox/app", 419, readyHead, "pass", "t1t1t1t1t1t1"))
w.settleAll()
d := w.delivery(id)
if d.State != Ready || d.Base != "" || d.Branch != "" || d.Title != "" || d.HTMLURL != "" {
t.Fatalf("not the live shape: %s base %q branch %q title %q", d.State, d.Base, d.Branch, d.Title)
}
return w, id
}
func TestADeliveryWithoutItsHeadSaysWhatItWaitsForAndAsksNoOne(t *testing.T) {
w, id := verdictFirst(t)
if n := len(w.op.open()); n != 0 {
t.Fatalf("an ask was made for a delivery with no base: %d", n)
}
d := w.delivery(id)
if d.Ready == nil || !strings.Contains(d.Ready.Waits, "announcement") {
t.Fatalf("it does not say what it waits for: %+v", d.Ready)
}
shown, err := w.h.Show(id)
if err != nil {
t.Fatal(err)
}
if waits, _ := shown.(map[string]any)["waits"].(string); !strings.Contains(waits, "announcement") {
t.Fatalf("show waits %q", waits)
}
}
func TestAnAnnouncementFillsADeliveryMadeFromItsVerdictAndTheOperatorIsAsked(t *testing.T) {
w, id := verdictFirst(t)
p := pr("novox/app", 419, readyHead, "fix/x", "src/a.go")
p.Title, p.CloneURL = "Fix x", "https://forge.invalid/novox/app.git"
w.h.PullUpdated(p)
w.settleAll()
d := w.delivery(id)
if d.Base != "main" || d.Branch != "fix/x" || d.Title != "Fix x" || d.HTMLURL == "" || d.CloneURL == "" || len(d.Paths) != 1 {
t.Fatalf("the announcement did not fill the delivery: %+v", d)
}
if n := len(w.op.open()); n != 1 {
t.Fatalf("the operator was asked %d times; wanted once", n)
}
if d.Ready.Waits != "" && !strings.Contains(d.Ready.Waits, "Approve") {
t.Fatalf("still waits: %q", d.Ready.Waits)
}
}
func TestADeliveryWithoutItsHeadPastItsBoundIsStalled(t *testing.T) {
w, id := verdictFirst(t)
if s := w.h.Stalled(); len(s) != 0 {
t.Fatalf("stalled before its bound: %+v", s)
}
w.later(headHeardBound + time.Minute)
w.settleAll()
var found bool
for _, l := range w.h.Stalled() {
if l.ID == id && strings.Contains(l.Says, "announcement") {
found = true
}
}
if !found {
t.Fatalf("not stalled past its bound: %+v", w.h.Stalled())
}
}
// A head asked, then a push whose verdict is heard before its head: the announcement of the new head supersedes the old
// one exactly as a first announcement does, so one Approve stands, for the new head.
func TestAnAnnouncementFillingAVerdictFirstHeadSupersedesTheOlderHeadAndItsAsk(t *testing.T) {
w := newWorld(t)
w.op.auto = false
a, b := IDOf("novox/app", readyHead), IDOf("novox/app", pushed)
w.h.PullUpdated(pr("novox/app", 419, readyHead, "fix/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 419, readyHead, "pass", "t1t1t1t1t1t1"))
w.settleAll()
if n := len(w.op.open()); n != 1 {
t.Fatalf("head A: %d asks open; wanted 1", n)
}
w.h.Checked(judgedAt("novox/app", 419, pushed, "pass", "t1t1t1t1t1t1"))
w.settleAll()
w.h.PullUpdated(pr("novox/app", 419, pushed, "fix/x", "src/a.go"))
w.settleAll()
if got := w.delivery(a); got.NewerHead != pushed || got.State != Closed {
t.Fatalf("head A is %s with newer head %q; wanted closed by %s", got.State, got.NewerHead, pushed)
}
if got := w.delivery(b); got.Base != "main" || got.Branch != "fix/x" {
t.Fatalf("head B not filled: %+v", got)
}
if n := len(w.op.open()); n != 1 {
t.Fatalf("%d asks open; wanted one, for the new head", n)
}
}
// A description edit is a natural trigger too: it fills a delivery made from its verdict.
func TestAnEditOfThePullRequestFillsADeliveryMadeFromItsVerdict(t *testing.T) {
w, id := verdictFirst(t)
p := pr("novox/app", 419, readyHead, "fix/x")
p.Title = "Fix x"
w.h.PullEdited(p)
w.settleAll()
d := w.delivery(id)
if d.Base != "main" || d.Branch != "fix/x" || d.Title != "Fix x" {
t.Fatalf("the edit did not fill the delivery: %+v", d)
}
if n := len(w.op.open()); n != 1 {
t.Fatalf("the operator was asked %d times; wanted once", n)
}
}
@@ -1,156 +0,0 @@
package main
import (
"strings"
"testing"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// Issue 1726 (2026-10-24, mesh-catalog #471 and #472), kept in release 6 where no seat holder arrives to run a catch-up: the
// checker takes a repository's trunk head only from source.main, and the gate moves it only on a merge it hears. A merge marked
// in the source and never written (as a gate before the fix left #471) reaches source.main when the gate starts, once; one older
// than the head source.main holds never moves it back.
const (
i1726Repo = "novox/mesh-catalog"
i1726Head1 = "aaaa000000000000000000000000000000000001"
i1726Merge1 = "dddd000000000000000000000000000000000002"
i1726Merge2 = "bbbb000000000000000000000000000000000003"
)
func TestStartingWritesTheNewestMergeOfEachRepositoryNotYetInSourceMain(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr(i1726Repo, 470, i1726Head1, "fix/a", "modules/app/a.go"))
w.h.PullMerged(i888MergedAt(i1726Repo, 470, i1726Head1, i1726Merge1, w.now.Add(-time.Hour)))
const head2 = "cccc000000000000000000000000000000000001"
w.h.PullUpdated(pr(i1726Repo, 471, head2, "fix/b", "modules/app/b.go"))
w.h.PullMerged(i888MergedAt(i1726Repo, 471, head2, i1726Merge2, w.now.Add(-time.Minute)))
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != i1726Merge2 {
t.Fatalf("the heard merges left source.main at %s", got)
}
// What a gate before the fix left: the merge marked in the source, source.main still at the merge before.
w.source.main = &Source{V: mdc.Version, Group: "before", Heads: map[string]string{i1726Repo: i1726Merge1, "novox/other": "b0"}}
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != i1726Merge1 {
t.Fatalf("source.main moved to %s with no catch-up asked", got)
}
w.h.SourceCatchUp()
w.settleAll()
if got := w.source.main.Heads[i1726Repo]; got != i1726Merge2 || w.source.main.Heads["novox/other"] != "b0" {
t.Fatalf("source.main is %+v after the catch-up; wanted %s at %s and the other kept", w.source.main.Heads, i1726Repo, i1726Merge2)
}
n := len(w.source.moves)
w.h.SourceCatchUp()
w.settleAll()
if len(w.source.moves) != n {
t.Fatalf("a second catch-up moved source.main again: %d moves, then %d", n, len(w.source.moves))
}
// Issue 1422: a repository never merged through the gate gets main's head as the forge announced its open pull request
// with, once, beside the others; a pull request into another base, or one announced with no base head (the forge could
// not read it), seeds nothing; a head source.main holds is never replaced.
const never, trunkN, trunkM = "novox/never-merged", "eeee000000000000000000000000000000000001", "eeee000000000000000000000000000000000002"
s := newWorld(t)
s.source.main = &Source{V: mdc.Version, Group: "before", Heads: map[string]string{i1726Repo: i1726Merge1}}
other := pr("novox/other-base", 6, "f1f1000000000000000000000000000000000001", "fix/c", "a.go")
other.Base, other.BaseSHA = "nox-mesh", trunkM
s.h.PullUpdated(other)
unread := pr("novox/unread", 2, "f2f2000000000000000000000000000000000002", "fix/d", "a.go")
s.h.PullUpdated(unread)
seed := pr(never, 4, "f3f3000000000000000000000000000000000003", "fix/e", "a.go")
seed.BaseSHA = trunkN
s.h.PullUpdated(seed)
s.settleAll()
if h := s.source.main.Heads; h[never] != trunkN || h[i1726Repo] != i1726Merge1 || h["novox/other-base"] != "" || h["novox/unread"] != "" || len(s.source.moves) != 1 {
t.Fatalf("source.main is %v after %d moves; wanted %s seeded at %s, the catalogue kept and nothing else, in one move", h, len(s.source.moves), never, trunkN)
}
if !strings.Contains(s.logs(), "seeded with "+trunkN[:12]) {
t.Errorf("the seed was not said: %s", s.logs())
}
again := pr(never, 5, "f4f4000000000000000000000000000000000004", "fix/f", "a.go")
again.BaseSHA = trunkM
s.h.PullUpdated(again)
s.h.SourceCatchUp()
s.settleAll()
if got := s.source.main.Heads[never]; got != trunkN || len(s.source.moves) != 1 {
t.Fatalf("a seeded head was replaced by %s (%d moves)", got, len(s.source.moves))
}
// The freshest announcement's base head is the seed, not the newest delivery's: #4, made first, is announced again after #5
// with main's head moved on.
const trunkF = "eeee000000000000000000000000000000000003"
f := newWorld(t)
first := pr(never, 4, "f5f5000000000000000000000000000000000005", "fix/g", "a.go")
first.BaseSHA = trunkN
f.h.PullUpdated(first)
f.later(time.Minute)
second := pr(never, 5, "f6f6000000000000000000000000000000000006", "fix/h", "a.go")
second.BaseSHA = trunkM
f.h.PullUpdated(second)
f.later(time.Minute)
first.BaseSHA = trunkF
f.h.PullUpdated(first)
f.settleAll()
if got := f.source.main.Heads[never]; got != trunkF {
t.Fatalf("seeded with %s; wanted %s, the base head of the latest announcement", got, trunkF)
}
// A green pull request retargeted from nox-mesh to main is seeded and asked; one retargeted from main to nox-mesh has
// its ask taken back and is not merged (review round 2).
retarget := func(from, to string) (*world, int) {
w := newWorld(t)
w.op.auto = false
p := pr(never, 7, readyHead, "feat/x", "src/a.go")
p.Base, p.BaseSHA = from, trunkN
w.h.PullUpdated(p)
w.h.Checked(judgedAt(never, 7, readyHead, "pass", "t1t1t1t1t1t1"))
open := openPull(never, 7, readyHead)
open.Base = from
w.forge.open = []OpenPull{open}
w.h.SeeOpenPulls()
w.settleAll()
before := len(w.op.open())
p.Base, open.Base = to, to
w.forge.open = []OpenPull{open}
w.h.PullUpdated(p)
w.h.SeeOpenPulls()
w.settleAll()
w.later(2 * time.Minute)
w.settleAll()
return w, before
}
if r, before := retarget("nox-mesh", "main"); before != 0 || r.source.main == nil || r.source.main.Heads[never] != trunkN || len(r.op.open()) != 1 {
t.Fatalf("retargeted to main: %d asks before, source.main %+v, open asks %v; wanted none before, then seeded at %s and asked", before, r.source.main, r.op.open(), trunkN)
}
if r, before := retarget("main", "nox-mesh"); before != 1 || len(r.op.open()) != 0 || len(r.forge.merges) != 0 || !strings.Contains(r.logs(), "retargeted") {
t.Fatalf("retargeted to nox-mesh: %d asks before, open asks %v, merges %v; wanted one before, then none and the retarget said", before, r.op.open(), r.forge.merges)
}
o := newWorld(t)
o.source.main = &Source{V: mdc.Version, Group: "newer", Heads: map[string]string{i1726Repo: i1726Merge2}}
o.h.PullUpdated(pr(i1726Repo, 471, head2, "fix/b", "modules/app/b.go"))
o.h.PullMerged(i888MergedAt(i1726Repo, 471, head2, i1726Merge2, o.now.Add(-time.Minute)))
o.h.PullMerged(i888MergedAt(i1726Repo, 470, i1726Head1, i1726Merge1, o.now.Add(-time.Hour)))
o.settleAll()
o.h.SourceCatchUp()
o.settleAll()
if got := o.source.main.Heads[i1726Repo]; got != i1726Merge2 {
t.Fatalf("the catch-up moved source.main back to %s", got)
}
}
// A delivery kept with no merge time (adopted from a walk by an earlier owner) is dated by when the gate made it, so it does not
// make every older merge look newer, and the catch-up at the start weighs it.
func TestAMergeKeptWithNoMergeTimeIsDatedByWhenTheGateMadeIt(t *testing.T) {
d := &Delivery{MergedAs: i1726Merge2, Created: time.Date(2026, 10, 24, 12, 0, 0, 0, time.UTC)}
if got := mergedWhen(d); !got.Equal(d.Created) {
t.Fatalf("dated %s", got)
}
d.MergedAt = d.Created.Add(-time.Hour)
if got := mergedWhen(d); !got.Equal(d.MergedAt) {
t.Fatalf("dated %s with a merge time", got)
}
}
@@ -1,70 +0,0 @@
package main
import (
"testing"
"time"
)
// novox/hq issue 326: a delivery whose state did not change is not written again — a pass over deliveries that did not
// move writes nothing — and one that changed is written, and said.
func TestADeliveryIsWrittenOnlyWhenItChanged(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.settleAll()
if w.state(id) != Ready {
t.Fatalf("it is %s", w.state(id))
}
// Passes over the same deliveries, as every event asks for one: nothing changed, nothing written.
before, said := w.store.puts, len(w.said)
for range 5 {
w.later(30 * time.Second)
w.settleAll()
}
if w.store.puts != before {
t.Fatalf("five passes over deliveries that did not move wrote %d delivery(ies)", w.store.puts-before)
}
if len(w.said) != said {
t.Fatalf("five passes over deliveries that did not move said %d event(s)", len(w.said)-said)
}
// The merge is heard: the delivery changes, is written, and is said.
w.h.PullMerged(merged("novox/app", 7, head, merge))
w.settleAll()
if w.state(id) != Merged {
t.Fatalf("a merge left it %s", w.state(id))
}
if w.store.puts == before {
t.Fatal("a delivery that changed was not written")
}
back, err := w.store.Deliveries()
if err != nil {
t.Fatal(err)
}
for _, d := range back {
if d.ID == id && d.State != Merged {
t.Fatalf("the store holds it %s, the holder %s", d.State, Merged)
}
}
if len(w.said) == said {
t.Fatal("a delivery that changed was not said")
}
}
// A delivery that could not be written is written again, even though it did not change since.
func TestADeliveryNotWrittenIsWrittenAgainUnchanged(t *testing.T) {
w := newWorld(t)
id := IDOf("novox/app", head)
w.store.failing = true
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
if _, there := w.store.deliveries[id]; there {
t.Fatal("a failing store kept it")
}
w.store.failing = false
w.h.Flush()
if _, there := w.store.deliveries[id]; !there {
t.Fatal("a delivery that could not be written was not written again")
}
}
@@ -1,226 +0,0 @@
package main
import (
"strings"
"testing"
"time"
)
// **A verdict holds for the trunk commit it judged** (novox/hq ADR 0323, issue 341). The check judges the head
// merged into a named trunk commit; when another merge moves that trunk, a ready delivery goes back to proposed,
// a rejected one stays rejected, and nothing is asked. A verdict for an old trunk is kept and shown and does not
// count, so the table reaches a fixed point. A group's composed check goes out of date on a merge that is not one of
// its members, and runs again for new heads instead of holding the group.
const (
trunk0 = "a2c2c35000000000000000000000000000000000"
merge1 = "d28ca95000000000000000000000000000000000"
)
// judged is a verdict of a head merged into a trunk commit.
func judged(repo string, number int, sha, gate, trunk string) CheckedEvent {
c := verdict(repo, number, sha, gate)
c.Trunk = shortOf(trunk, 12)
c.ID = "check-of-" + sha + "-at-" + shortOf(trunk, 8)
c.Took = "4m12s"
c.Beside = map[string]string{"mesh-catalog": "0123456789ab"}
return c
}
func TestATrunkMoveSendsAReadyDeliveryBackToProposed(t *testing.T) {
const host = "novox/mesh-host"
const h64, h66, h67 = "6464646464646464", "dfb1415dfb1415df", "6767676767676767"
w := newWorld(t)
for _, p := range []PullEvent{pr(host, 64, h64, "a"), pr(host, 66, h66, "b"), pr(host, 67, h67, "c")} {
w.h.PullUpdated(p)
}
w.h.Checked(judged(host, 64, h64, "pass", trunk0))
w.h.Checked(judged(host, 66, h66, "pass", trunk0))
w.h.Checked(judged(host, 67, h67, "fail", trunk0))
id66, id67 := IDOf(host, h66), IDOf(host, h67)
if s := w.state(id66); s != Ready {
t.Fatalf("#66 checked green against the trunk is %s", s)
}
// #64 merges: the trunk moves to its merge commit.
w.h.PullMerged(merged(host, 64, h64, merge1))
w.settleAll()
d := w.delivery(id66)
if d.State != Proposed {
t.Fatalf("#66's verdict names trunk %s, the trunk is now %s, and it is %s", trunk0[:12], merge1[:12], d.State)
}
last := d.Transitions[len(d.Transitions)-1]
if last.Event != EvTrunkMoved || !strings.Contains(last.Why, trunk0[:12]) || !strings.Contains(last.Why, merge1[:12]) {
t.Fatalf("its transition was %s, %q: not trunk moved with both commits", last.Event, last.Why)
}
if d.Check == nil || d.Check.Trunk != trunk0[:12] {
t.Fatalf("its out-of-date verdict was not kept to be shown: %+v", d.Check)
}
if s := w.state(id67); s != Rejected {
t.Fatalf("a rejected delivery went to %s on the trunk's move", s)
}
// Out of date and not asked: no bound, so stalled does not list it, however long it waits.
w.later(5 * time.Hour)
w.settleAll()
for _, l := range w.h.Stalled() {
if l.ID == id66 {
t.Fatalf("stalled lists a proposed delivery whose check is out of date and not asked: %+v", l)
}
}
// A late verdict for the old trunk is kept, does not count, and moves nothing: the table's fixed point.
before := len(w.delivery(id66).Transitions)
late := judged(host, 66, h66, "pass", trunk0)
late.ID = "check-late"
w.h.Checked(late)
w.settleAll()
d = w.delivery(id66)
if d.State != Proposed || len(d.Transitions) != before || d.Check == nil {
t.Fatalf("a late verdict for an old trunk left it %s after %d transition(s), verdict %+v", d.State,
len(d.Transitions)-before, d.Check)
}
if ts := settle(d, Facts{Now: w.now}); len(ts) != 0 {
t.Fatalf("the table took %d more transition(s) on a verdict that does not count", len(ts))
}
// The check asked again judges the head merged into the trunk's head: it counts.
w.h.Checked(judged(host, 66, h66, "pass", merge1))
w.settleAll()
if s := w.state(id66); s != Ready {
t.Fatalf("a verdict judged against the trunk's head left it %s", s)
}
}
// A rejected delivery judged again against another trunk — the merge tool's ask — is taken as its recheck.
func TestARejectedDeliveryJudgedAgainstANewTrunkIsRecheckedByTheVerdict(t *testing.T) {
const repo = "novox/mesh-lab"
const head = "1a2b3c4d5e6f7a8b"
w := newWorld(t)
w.h.PullUpdated(pr(repo, 9, head, "x"))
w.h.Checked(judged(repo, 9, head, "fail", trunk0))
id := IDOf(repo, head)
if s := w.state(id); s != Rejected {
t.Fatalf("it is %s", s)
}
w.h.PullMerged(merged(repo, 8, "8888888888888888", merge1))
w.h.Checked(judged(repo, 9, head, "pass", merge1))
w.settleAll()
d := w.delivery(id)
if d.State != Ready || d.Check.Trunk != merge1[:12] {
t.Fatalf("judged green against the trunk's head it is %s with %+v", d.State, d.Check)
}
}
// **A verdict names the trunk commit it judged** in `checks` and `show`, and the kept plan records it (ADR 0323 §3).
func TestChecksAndShowNameTheHeadAndTheTrunkCommit(t *testing.T) {
const repo = "novox/mesh-lab"
const head = "1a2b3c4d5e6f7a8b"
w := newWorld(t)
w.h.PullUpdated(pr(repo, 9, head, "x"))
w.h.Checked(judged(repo, 9, head, "pass", trunk0))
id := IDOf(repo, head)
a, err := w.h.Checks(repo, 0, head)
if err != nil {
t.Fatal(err)
}
want := "head " + head[:12] + " merged into trunk " + trunk0[:12]
if a.Judged != want || a.Verdict == nil || a.Verdict.Head != head[:12] || a.Verdict.Trunk != trunk0[:12] {
t.Fatalf("checks says %q, verdict %+v; want %q", a.Judged, a.Verdict, want)
}
if !strings.Contains(a.Says, want) {
t.Fatalf("checks' words do not say what was judged: %s", a.Says)
}
if a.Verdict.Beside["mesh-catalog"] != "0123456789ab" || a.Verdict.Took != "4m12s" {
t.Fatalf("checks does not name the other repositories' commits or how long it took: %+v", a.Verdict)
}
shown, err := w.h.Show(id)
if err != nil {
t.Fatal(err)
}
d := shown.(map[string]any)["delivery"].(*Delivery)
if d.Check.Head != head[:12] || d.Check.Trunk != trunk0[:12] {
t.Fatalf("show gives the verdict %+v", d.Check)
}
w.h.PullMerged(merged(repo, 8, "8888888888888888", merge1))
if a, _ = w.h.Checks(repo, 0, head); !strings.Contains(a.OutOfDate, merge1[:12]) {
t.Fatalf("checks does not say the verdict is out of date: %+v", a)
}
}
// A head whose merge check the forge's holder put out of date is not a check asked and not answered: `stalled` does
// not list it, however long it waits — rejected, or with no delivery here — until its check is asked (ADR 0323 §7).
func TestAnOutOfDateCheckOnTheForgeIsNotSaidUnanswered(t *testing.T) {
const repo = "novox/mesh-lab"
const head = "1a2b3c4d5e6f7a8b9c0d"
w := newWorld(t)
w.h.PullUpdated(pr(repo, 9, head, "x"))
w.h.Checked(judged(repo, 9, head, "fail", trunk0))
words := outOfDateWords(trunk0, merge1)
pending := func(description, at string) map[string]HeadCheck {
return map[string]HeadCheck{ctxGate: {State: "pending", Description: description, UpdatedAt: at},
ctxRepo: {State: "pending", Description: description, UpdatedAt: at}}
}
w.forge.open = []OpenPull{{Owner: "novox", Repo: "mesh-lab", Number: 9, Base: "main", HeadSHA: head,
Required: []string{ctxGate, ctxRepo}, Statuses: []string{ctxGate, ctxRepo}, Checks: pending(words, "2026-10-06T12:01:00Z")}}
w.h.SeeOpenPulls()
w.later(3 * time.Hour)
w.h.SeeOpenPulls()
for _, l := range w.h.Stalled() {
if strings.HasPrefix(l.ID, repo) {
t.Fatalf("an out-of-date check nobody asked is said stalled: %+v", l)
}
}
// Asked by the merge tool: pending anew, and its bound runs from then.
w.forge.open[0].Checks = pending("waiting in the build queue since 15:01: asked by the merge tool", "2026-10-06T15:01:00Z")
w.h.SeeOpenPulls()
w.later(2 * time.Hour)
w.h.SeeOpenPulls()
said := false
for _, l := range w.h.Stalled() {
said = said || l.State == Unanswered
}
if !said {
t.Fatal("a check the merge tool asked and that never answered is not said")
}
}
// The merge tool's ask announces an out-of-date head again: its one-hour bound runs from then, and a check that never
// answers is said stalled (ADR 0323 §7).
func TestAnOutOfDateDeliveryAskedAgainIsBoundFromTheAsk(t *testing.T) {
const repo = "novox/mesh-lab"
const head = "1a2b3c4d5e6f7a8b"
w := newWorld(t)
w.h.PullUpdated(pr(repo, 9, head, "x"))
w.h.Checked(judged(repo, 9, head, "pass", trunk0))
w.h.PullMerged(merged(repo, 8, "8888888888888888", merge1))
id := IDOf(repo, head)
w.later(3 * time.Hour)
w.h.PullUpdated(pr(repo, 9, head, "x")) // the merge tool asked its check
w.later(30 * time.Minute)
for _, l := range w.h.Stalled() {
if l.ID == id {
t.Fatalf("listed half an hour after its check was asked again: %+v", l)
}
}
w.later(time.Hour)
said := false
for _, l := range w.h.Stalled() {
if l.ID == id {
said = true
if !strings.Contains(l.Says, "asked again") || !strings.Contains(l.H2, "operator") {
t.Fatalf("its stalled line says %+v", l)
}
}
}
if !said {
t.Fatal("a check asked again that never answered is not said")
}
// Its verdict judged against the trunk's head: ready, and nothing bounds it as proposed.
w.h.Checked(judged(repo, 9, head, "pass", merge1))
if d := w.delivery(id); d.State != Ready || d.AskedAgain != nil {
t.Fatalf("judged again it is %s, asked again %v", d.State, d.AskedAgain)
}
}
@@ -1,69 +0,0 @@
package main
import (
"testing"
"time"
)
// novox/hq issue 403, ADR 0313: the issue tracker measures a fix's time from the move to fixing to delivered and
// splits it at the pull request's opening and its merge. A transition says what it reads them by: when the pull
// request was first heard (its first head, not a later one), when it merged, and the class of what it moves —
// core, leaf, or none for a change that moves no module. `checks` says the same for a fix the tracker hears of late.
// transitionsTo are the transition events said for a delivery that moved to a state.
func transitionsTo(w *world, id string, to State) []map[string]any {
var out []map[string]any
for _, e := range w.said {
if e["event-kind"] == "transition" && e["id"] == id && e["to"] == string(to) {
out = append(out, e)
}
}
return out
}
func TestATransitionSaysWhenItsPullRequestOpenedAndMerged(t *testing.T) {
w := newWorld(t)
repo := "novox/mesh-catalog"
opened := w.now
first := pr(repo, 9, "head9first", "fix/403-x", "modules/app/a.go")
w.h.PullUpdated(first)
w.later(3 * time.Minute)
second := pr(repo, 9, "head9second", "fix/403-x", "modules/app/a.go")
w.h.PullUpdated(second)
w.h.Checked(verdictMoving(repo, 9, "head9second", "app"))
w.later(2 * time.Minute)
mergedWhen := w.now
w.h.PullMerged(mergedAt(second, "merge9aaaaaa", mergedWhen))
w.settleAll()
id := IDOf(repo, "head9second")
if s := w.state(id); s != Merged {
t.Fatalf("the merge is %s, not merged", s)
}
said := transitionsTo(w, id, Merged)
if len(said) == 0 {
t.Fatalf("no transition to merged was said: %v", w.said)
}
e := said[len(said)-1]
if e["opened"] != opened.UTC().Format(time.RFC3339) {
t.Errorf("opened %v, want the first head's %s (not the second head's)", e["opened"], opened.Format(time.RFC3339))
}
if e["merged_at"] != mergedWhen.UTC().Format(time.RFC3339) {
t.Errorf("merged_at %v, want %s", e["merged_at"], mergedWhen.Format(time.RFC3339))
}
if _, has := e["class"]; has {
t.Errorf("the transition names a class %v: the gate plans nothing", e["class"])
}
if _, has := e["plan"]; has {
t.Errorf("the transition names a plan %v", e["plan"])
}
a, err := w.h.Checks(repo, 9, "")
if err != nil {
t.Fatal(err)
}
if a.Delivery == nil || a.Delivery.Opened != opened.UTC().Format(time.RFC3339) ||
a.Delivery.MergedAt != mergedWhen.UTC().Format(time.RFC3339) || a.Delivery.MergedAs != "merge9aaaaaa" {
t.Errorf("checks says %+v", a.Delivery)
}
}
@@ -1,105 +0,0 @@
package main
import (
"testing"
"time"
)
// triedSaid is delivery_status's count of owed effects tried (novox/hq issue 407), read as a test reads it.
func triedSaid(t *testing.T, w *world) map[string]any {
t.Helper()
got, ok := w.h.Status("listening")["owed-tried"].(map[string]any)
if !ok {
t.Fatalf("delivery_status says no owed-tried: %v", w.h.Status("listening"))
}
return got
}
func countOf(t *testing.T, m map[string]any, key string) int {
t.Helper()
n, ok := m[key].(int)
if !ok {
t.Fatalf("owed-tried has no count %q: %v", key, m)
}
return n
}
// novox/hq issue 407: delivery_status says how many owed effects were tried since this owner started, how
// many were done, failed to be tried again, or were given up, and when the last was tried — so a window's
// writes can be checked against the tries made in it (issue 326's rule) by reading it at both ends.
func TestDeliveryStatusCountsTheOwedEffectsTried(t *testing.T) {
w := newWorld(t)
start := w.now
tried := triedSaid(t, w)
if countOf(t, tried, "tried") != 0 || countOf(t, tried, "done") != 0 || countOf(t, tried, "failed") != 0 ||
countOf(t, tried, "given-up") != 0 {
t.Fatalf("a holder that tried nothing says %v", tried)
}
if tried["since"] != start.Format(time.RFC3339) {
t.Fatalf("its count is since %v, the holder started %s", tried["since"], start.Format(time.RFC3339))
}
if _, there := tried["last"]; there {
t.Fatalf("nothing tried, yet a last try is said: %v", tried)
}
// A head seen: its event and its note are owed and done.
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
tried = triedSaid(t, w)
done := countOf(t, tried, "done")
if done < len(w.said)+len(w.forge.notesOn(head)) || done == 0 {
t.Fatalf("said %d and noted %d, yet done counts %d", len(w.said), len(w.forge.notesOn(head)), done)
}
if countOf(t, tried, "tried") != done || countOf(t, tried, "failed") != 0 {
t.Fatalf("every try succeeded, yet %v", tried)
}
if tried["last"] != w.now.Format(time.RFC3339) {
t.Fatalf("last try said %v, tried at %s", tried["last"], w.now.Format(time.RFC3339))
}
// The bus away: its effects fail and are counted failed; quiet passes try nothing more.
w.owed.down = true
w.later(time.Minute)
w.h.Checked(verdict("novox/app", 7, head, "pass"))
w.h.Flush()
tried = triedSaid(t, w)
failed := countOf(t, tried, "failed")
if failed == 0 {
t.Fatalf("the bus away failed nothing: %v", tried)
}
if countOf(t, tried, "tried") != countOf(t, tried, "done")+failed+countOf(t, tried, "given-up") {
t.Fatalf("tries do not add up: %v", tried)
}
if tried["last"] != w.now.Format(time.RFC3339) {
t.Fatalf("last try said %v, tried at %s", tried["last"], w.now.Format(time.RFC3339))
}
before := countOf(t, tried, "tried")
w.h.Flush() // before each failure's next try: nothing is tried
if n := countOf(t, triedSaid(t, w), "tried"); n != before {
t.Fatalf("a pass before any next try tried %d", n-before)
}
// The forge back, past every next try: the failed effects are done.
w.owed.down = false
w.later(time.Hour)
w.h.Flush()
tried = triedSaid(t, w)
if countOf(t, tried, "tried") != before+failed || countOf(t, tried, "done") <= done {
t.Fatalf("the forge back: tried %d (was %d, %d failed), %v", countOf(t, tried, "tried"), before, failed, tried)
}
}
// The count is this run's: a restarted holder says it counts since its own start, from nothing.
func TestOwedTriedCountsSinceTheStart(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
if countOf(t, triedSaid(t, w), "tried") == 0 {
t.Fatal("nothing counted before the restart")
}
again := newWorldOver(t, w.store, w.forge, w.now.Add(time.Minute))
tried := triedSaid(t, again)
if countOf(t, tried, "tried") != 0 || tried["since"] != again.now.Format(time.RFC3339) {
t.Fatalf("a restarted holder says %v", tried)
}
}
@@ -1,183 +0,0 @@
package main
import (
"encoding/json"
"slices"
"strings"
"testing"
"time"
)
// openPullsOf reads gitea_open_pulls' answer as the forge module gives it, so the tests read the real shape: the
// pull requests below are mesh-controller #212's and #211's heads and mesh-catalog #194's, as the forge said them on
// 2026-10-11 (novox/hq issue 438), with only their numbers and shas as they were.
func openPullsOf(t *testing.T, answer string) []OpenPull {
t.Helper()
var open struct {
Pulls []OpenPull `json:"pulls"`
}
if err := json.Unmarshal([]byte(answer), &open); err != nil {
t.Fatal(err)
}
return open.Pulls
}
// A head the mesh's check started on and never answered: mesh/merge-gate pending since the controller began mapping
// it, mesh/repo-check never set. Its base requires both.
const startedNeverAnswered = `{"pulls": [{
"base": "main", "head_sha": "c11222026a3b26a0cc4b277119e1ea059b82a36b",
"html_url": "http://127.0.0.1:20000/novox/mesh-controller/pulls/212", "number": 212,
"owner": "novox", "repo": "mesh-controller",
"required": ["mesh/merge-gate", "mesh/repo-check"],
"statuses": ["mesh/merge-gate", "mesh/delivery"],
"checks": {
"mesh/delivery": {"state": "pending", "description": "checking", "updated_at": "2026-10-11T00:11:46Z"},
"mesh/merge-gate": {"state": "pending", "description": "the mesh is mapping this head onto its module graph",
"updated_at": "2026-10-11T00:11:39Z"}
}
}]}`
// **A head whose merge check started and never answered is said** (novox/hq issue 438): pending, or missing a check
// its base requires, for as long as the delivery table gives a check asked and not answered (Bounds[Proposed]) since
// this owner first saw it so, it is a stalled line naming each check that did not answer and since when. Before,
// any merge-check status counted as checked, pending included, and such a head was never said.
func TestAHeadWhoseCheckStartedAndNeverAnsweredIsSaid(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 12, 5, 0, time.UTC))
w.forge.open = openPullsOf(t, startedNeverAnswered)
w.h.SeeOpenPulls()
w.later(Bounds[Proposed].For - time.Minute)
w.h.SeeOpenPulls()
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a head still within the bound of a check asked and not answered is said: %+v", lines)
}
w.later(2 * time.Minute)
lines := w.h.Stalled()
if len(lines) != 1 {
t.Fatalf("a head whose check started and never answered is not said: %+v", lines)
}
l := lines[0]
if l.ID != IDOf("novox/mesh-controller", "c11222026a3b26a0cc4b277119e1ea059b82a36b") || l.Number != 212 ||
l.State != Unanswered || l.Bound != Bounds[Proposed].For.String() || l.For != "1h1m0s" {
t.Fatalf("the line is not this head's, past the table's bound: %+v", l)
}
for _, want := range []string{"mesh/merge-gate", "pending since 2026-10-11T00:11:39Z", "mesh/repo-check", "never set",
"never answered"} {
if !strings.Contains(l.Says, want) {
t.Fatalf("the line does not say %q: %s", want, l.Says)
}
}
if want := []string{"mesh/merge-gate pending since 2026-10-11T00:11:39Z", "mesh/repo-check never set"}; !slices.Equal(l.Waiting, want) {
t.Fatalf("the line's checks waited on are %q, not %q", l.Waiting, want)
}
raw, _ := json.Marshal(l)
t.Logf("the line as the controller reads it: %s", raw) // mesh-controller's test of its words reads this line
if strings.Contains(l.Says, "mesh/delivery") {
t.Fatalf("the line names a status that is not the merge check: %s", l.Says)
}
// Announced at last: a delivery of its own, with its own bound — no longer this line.
w.h.PullUpdated(pr("novox/mesh-controller", 212, "c11222026a3b26a0cc4b277119e1ea059b82a36b", "fix/402"))
w.h.SeeOpenPulls()
for _, l := range w.h.Stalled() {
if l.State == Unannounced || l.State == Unanswered {
t.Fatalf("an announced head is still said unannounced: %+v", l)
}
}
}
// **A check that answers does not start the bound again**: merge-gate answers at fifty minutes, repo-check is never
// set, and the head is said on the bound from this owner's first sight of it, not fifty minutes later.
func TestACheckThatAnswersDoesNotStartTheBoundAgain(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 12, 5, 0, time.UTC))
w.forge.open = openPullsOf(t, startedNeverAnswered)
w.h.SeeOpenPulls()
w.later(50 * time.Minute)
w.forge.open = openPullsOf(t, strings.Replace(startedNeverAnswered, `"state": "pending", "description": "the mesh is mapping`,
`"state": "success", "description": "pass: the mesh is mapping`, 1))
w.h.SeeOpenPulls()
w.later(11 * time.Minute)
w.h.SeeOpenPulls()
lines := w.h.Stalled()
if len(lines) != 1 || lines[0].For != "1h1m0s" || !strings.Contains(lines[0].Says, "mesh/repo-check never set") ||
strings.Contains(lines[0].Says, "mesh/merge-gate") {
t.Fatalf("a head whose merge-gate answered at fifty minutes is not said at sixty-one on what is left: %+v", lines)
}
}
// A forge time with an offset is said in UTC, as the module's other lines say their times.
func TestAPendingTimeIsSaidInUTC(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 12, 5, 0, time.UTC))
w.forge.open = openPullsOf(t, strings.Replace(startedNeverAnswered, "2026-10-11T00:11:39Z", "2026-10-11T02:11:39+02:00", 1))
w.h.SeeOpenPulls()
w.later(Bounds[Proposed].For + time.Minute)
if lines := w.h.Stalled(); len(lines) != 1 || !strings.Contains(lines[0].Says, "pending since 2026-10-11T00:11:39Z") {
t.Fatalf("the pending time is not said in UTC: %+v", lines)
}
}
// A check asked again — its pending status set anew, as a recheck or a new announcement does — starts the bound again:
// the head is said only once that ask too has gone unanswered past it.
func TestACheckAskedAgainStartsItsBoundAgain(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 12, 5, 0, time.UTC))
w.forge.open = openPullsOf(t, startedNeverAnswered)
w.h.SeeOpenPulls()
w.later(50 * time.Minute)
again := openPullsOf(t, strings.Replace(startedNeverAnswered, "2026-10-11T00:11:39Z", "2026-10-11T01:02:00Z", 1))
w.forge.open = again
w.h.SeeOpenPulls()
w.later(20 * time.Minute)
w.h.SeeOpenPulls()
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a check asked again twenty minutes ago is said on the first ask's time: %+v", lines)
}
w.later(41 * time.Minute)
if lines := w.h.Stalled(); len(lines) != 1 || !strings.Contains(lines[0].Says, "pending since 2026-10-11T01:02:00Z") {
t.Fatalf("a check asked again and not answered past the bound is not said: %+v", lines)
}
}
// **A head whose required checks all answered is not said**, whatever they answered: a failure is a verdict, and the
// pull request says it itself. Nor is one whose only pending check is one its base does not require: hq requires
// mesh/repo-check alone.
func TestAHeadWhoseRequiredChecksAnsweredIsNotSaid(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 12, 5, 0, time.UTC))
w.forge.open = openPullsOf(t, `{"pulls": [{
"base": "main", "head_sha": "99df6b999bad77c51dfdd13470cf57b1f38e8c24", "number": 194, "owner": "novox", "repo": "mesh-catalog",
"required": ["mesh/merge-gate", "mesh/repo-check"],
"statuses": ["mesh/merge-gate", "mesh/repo-check", "mesh/delivery"],
"checks": {
"mesh/delivery": {"state": "failure", "description": "rejected", "updated_at": "2026-10-11T00:05:37Z"},
"mesh/merge-gate": {"state": "success", "description": "pass: builds mounts → shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)", "updated_at": "2026-10-11T00:05:28Z"},
"mesh/repo-check": {"state": "failure", "description": "fail: its merge-check.sh failed", "updated_at": "2026-10-11T00:05:28Z"}
}
}, {
"base": "main", "head_sha": "11b719fd5cdec43970e513a10a44a558042599e6", "number": 308, "owner": "novox", "repo": "hq",
"required": ["mesh/repo-check"],
"statuses": ["mesh/merge-gate", "mesh/repo-check", "mesh/delivery"],
"checks": {
"mesh/merge-gate": {"state": "pending", "description": "the mesh is mapping this head onto its module graph", "updated_at": "2026-10-11T00:01:00Z"},
"mesh/repo-check": {"state": "success", "description": "pass: its merge-check.sh passed", "updated_at": "2026-10-11T00:02:00Z"}
}
}]}`)
w.h.SeeOpenPulls()
w.later(3 * Bounds[Proposed].For)
w.h.SeeOpenPulls()
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a head whose required checks answered is said: %+v", lines)
}
}
// A forge module that says only which contexts a head has, not their states — the gitea module before issue 438 —
// is read as before: a head with any merge check is not said, and nothing is guessed of what it does not say.
func TestAForgeThatSaysNoStatesIsReadAsBefore(t *testing.T) {
w := newWorld(t)
w.forge.open = openPullsOf(t, `{"pulls": [{"base": "main", "head_sha": "c11222026a3b26a0cc4b277119e1ea059b82a36b",
"number": 212, "owner": "novox", "repo": "mesh-controller", "required": ["mesh/merge-gate", "mesh/repo-check"],
"statuses": ["mesh/merge-gate", "mesh/delivery"]}]}`)
w.h.SeeOpenPulls()
w.later(3 * Bounds[Proposed].For)
w.h.SeeOpenPulls()
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a head is said on states the forge did not give: %+v", lines)
}
}
@@ -1,234 +0,0 @@
package main
import (
"slices"
"strings"
"testing"
"time"
)
// A delivery held in ready or rejected whose required merge check reads pending or missing on the forge
// (novox/hq issue 441). The pull requests below are mesh-controller #215's head, ready, and mesh-lab #76's, rejected,
// as gitea_open_pulls said them on 2026-10-11, with only the one status changed that the case is about: a status
// write that lagged or was lost (issue 423), or a check asked again after the verdict.
const readyHeadSHA = "d2e9dc61590f950b74ae2e93282c354646705188"
// readyRepoCheckPending is #215 with mesh/repo-check pending since its delivery was ready.
const readyRepoCheckPending = `{"pulls": [{
"base": "main", "head_sha": "d2e9dc61590f950b74ae2e93282c354646705188",
"html_url": "http://127.0.0.1:20000/novox/mesh-controller/pulls/215", "number": 215,
"owner": "novox", "repo": "mesh-controller",
"required": ["mesh/merge-gate", "mesh/repo-check"],
"statuses": ["mesh/merge-gate", "mesh/repo-check", "mesh/delivery"],
"checks": {
"mesh/delivery": {"state": "success", "description": "ready: it delivers once merged", "updated_at": "2026-10-11T00:42:06Z"},
"mesh/merge-gate": {"state": "success", "description": "pass: builds build-agent → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)", "updated_at": "2026-10-11T00:42:01Z"},
"mesh/repo-check": {"state": "pending", "description": "the repository's own check runs", "updated_at": "2026-10-11T00:42:01Z"}
}
}]}`
// readyRepoCheckMissing is #215 whose mesh/repo-check was never written.
const readyRepoCheckMissing = `{"pulls": [{
"base": "main", "head_sha": "d2e9dc61590f950b74ae2e93282c354646705188", "number": 215,
"owner": "novox", "repo": "mesh-controller",
"required": ["mesh/merge-gate", "mesh/repo-check"],
"statuses": ["mesh/merge-gate", "mesh/delivery"],
"checks": {
"mesh/delivery": {"state": "success", "description": "ready: it delivers once merged", "updated_at": "2026-10-11T00:42:06Z"},
"mesh/merge-gate": {"state": "success", "description": "pass", "updated_at": "2026-10-11T00:42:01Z"}
}
}]}`
// readyAnswered is #215 as the forge said it: every required check answered.
const readyAnswered = `{"pulls": [{
"base": "main", "head_sha": "d2e9dc61590f950b74ae2e93282c354646705188", "number": 215,
"owner": "novox", "repo": "mesh-controller",
"required": ["mesh/merge-gate", "mesh/repo-check"],
"statuses": ["mesh/merge-gate", "mesh/repo-check", "mesh/delivery"],
"checks": {
"mesh/merge-gate": {"state": "success", "description": "pass", "updated_at": "2026-10-11T00:42:01Z"},
"mesh/repo-check": {"state": "success", "description": "pass: its merge-check.sh passed", "updated_at": "2026-10-11T00:42:01Z"}
}
}]}`
// heldIn is a delivery of #215's head in ready (a passing verdict) or rejected (a failing one).
func heldIn(t *testing.T, w *world, gate string, want State) string {
t.Helper()
w.h.PullUpdated(pr("novox/mesh-controller", 215, readyHeadSHA, "fix/215"))
w.h.Checked(verdict("novox/mesh-controller", 215, readyHeadSHA, gate))
id := IDOf("novox/mesh-controller", readyHeadSHA)
if s := w.state(id); s != want {
t.Fatalf("a %s verdict left it %s", gate, s)
}
return id
}
// **A delivery in ready whose required check reads pending on the forge is said** (novox/hq issue 441) on the same
// rule as a head with no delivery (issue 438): past the table's bound for a check asked and not answered, since this
// owner first saw it so, as an unanswered line naming the check and since when. Before, any head with a delivery was
// skipped, and ready has no bound of its own, so it waited for ever — unable to merge — with nothing said.
func TestADeliveryInReadyWhoseCheckIsPendingIsSaid(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 43, 0, 0, time.UTC))
id := heldIn(t, w, "pass", Ready)
w.forge.open = openPullsOf(t, readyRepoCheckPending)
w.h.SeeOpenPulls()
w.later(Bounds[Proposed].For - time.Minute)
w.h.SeeOpenPulls()
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a ready delivery still within the bound is said: %+v", lines)
}
w.later(2 * time.Minute)
lines := w.h.Stalled()
if len(lines) != 1 {
t.Fatalf("a ready delivery whose required check is pending past the bound is not said: %+v", lines)
}
l := lines[0]
if l.ID != id || l.Number != 215 || l.State != Unanswered || l.Bound != Bounds[Proposed].For.String() || l.For != "1h1m0s" {
t.Fatalf("the line is not this delivery's, past the table's bound: %+v", l)
}
if want := []string{"mesh/repo-check pending since 2026-10-11T00:42:01Z"}; !slices.Equal(l.Waiting, want) {
t.Fatalf("the line's checks waited on are %q, not %q", l.Waiting, want)
}
for _, want := range []string{"novox/mesh-controller#215", "ready", "mesh/repo-check pending since 2026-10-11T00:42:01Z", "recheck"} {
if !strings.Contains(l.Says, want) {
t.Fatalf("the line does not say %q: %s", want, l.Says)
}
}
if strings.Contains(l.Says, "No delivery of it is held") {
t.Fatalf("the line says no delivery is held of a head whose delivery is ready: %s", l.Says)
}
// The forge's status catches up: the check answered, and the line is gone.
w.forge.open = openPullsOf(t, readyAnswered)
w.h.SeeOpenPulls()
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a ready delivery whose check answered is still said: %+v", lines)
}
}
// **A delivery in rejected whose required check was never set is said** too: a lost status write leaves the pull
// request with no verdict to show, and nothing here asks again. It was announced — a delivery exists — so it is
// unanswered, never unannounced, though its head has no merge check at all.
func TestADeliveryInRejectedWhoseCheckIsMissingIsSaid(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 43, 0, 0, time.UTC))
id := heldIn(t, w, "fail", Rejected)
w.forge.open = openPullsOf(t, strings.Replace(readyRepoCheckMissing,
`"mesh/merge-gate": {"state": "success", "description": "pass", "updated_at": "2026-10-11T00:42:01Z"}`,
`"mesh/merge-gate": {"state": "failure", "description": "fail", "updated_at": "2026-10-11T00:42:01Z"}`, 1))
w.h.SeeOpenPulls()
w.later(Bounds[Proposed].For + time.Minute)
w.h.SeeOpenPulls()
lines := w.h.Stalled()
if len(lines) != 1 || lines[0].ID != id || lines[0].State != Unanswered ||
!slices.Equal(lines[0].Waiting, []string{"mesh/repo-check never set"}) || !strings.Contains(lines[0].Says, "rejected") {
t.Fatalf("a rejected delivery whose required check was never set is not said: %+v", lines)
}
// No merge check on its head at all — both writes lost — is still a delivery's: unanswered, never unannounced.
w2 := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 43, 0, 0, time.UTC))
heldIn(t, w2, "fail", Rejected)
w2.forge.open = openPullsOf(t, `{"pulls": [{"base": "main", "head_sha": "`+readyHeadSHA+`", "number": 215,
"owner": "novox", "repo": "mesh-controller", "required": ["mesh/merge-gate", "mesh/repo-check"],
"statuses": ["mesh/delivery"], "checks": {"mesh/delivery": {"state": "failure", "description": "rejected"}}}]}`)
w2.h.SeeOpenPulls()
if lines := w2.h.Stalled(); len(lines) != 0 {
t.Fatalf("a delivery's head without a check is said at once, before the table's bound: %+v", lines)
}
w2.later(Bounds[Proposed].For + time.Minute)
w2.h.SeeOpenPulls()
lines = w2.h.Stalled()
if len(lines) != 1 || lines[0].State != Unanswered ||
!slices.Equal(lines[0].Waiting, []string{"mesh/merge-gate never set", "mesh/repo-check never set"}) {
t.Fatalf("a rejected delivery whose head has no merge check is not said as unanswered: %+v", lines)
}
}
// **A delivery in a state with a bound of its own is not said here**: a proposed one waits on its check under
// Bounds[Proposed] and H2's re-ask, and a second line for it would say the same thing twice.
func TestAProposedDeliveryIsSaidByItsOwnBoundOnly(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 43, 0, 0, time.UTC))
w.h.PullUpdated(pr("novox/mesh-controller", 215, readyHeadSHA, "fix/215"))
id := IDOf("novox/mesh-controller", readyHeadSHA)
w.forge.open = openPullsOf(t, readyRepoCheckPending)
w.h.SeeOpenPulls()
w.later(3 * Bounds[Proposed].For)
w.h.SeeOpenPulls()
for _, l := range w.h.Stalled() {
if l.State == Unanswered || l.State == Unannounced {
t.Fatalf("a proposed delivery %s is said as a head without a delivery: %+v", id, l)
}
}
}
// **A ready delivery whose required checks answered is not said**, as the forge said every one on 2026-10-11.
func TestAReadyDeliveryWhoseChecksAnsweredIsNotSaid(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 43, 0, 0, time.UTC))
heldIn(t, w, "pass", Ready)
w.forge.open = openPullsOf(t, readyAnswered)
w.h.SeeOpenPulls()
w.later(3 * Bounds[Proposed].For)
w.h.SeeOpenPulls()
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a ready delivery whose checks answered is said: %+v", lines)
}
}
// saidReady is a ready delivery of #215's head whose mesh/repo-check reads pending on the forge, said past the bound.
func saidReady(t *testing.T) (*world, string) {
t.Helper()
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 43, 0, 0, time.UTC))
id := heldIn(t, w, "pass", Ready)
w.forge.open = openPullsOf(t, readyRepoCheckPending)
w.h.SeeOpenPulls()
w.later(Bounds[Proposed].For + time.Minute)
if lines := w.h.Stalled(); len(lines) != 1 || lines[0].ID != id || lines[0].State != Unanswered {
t.Fatalf("a ready delivery whose check is pending past the bound is not said: %+v", lines)
}
return w, id
}
// unansweredOf are the unanswered lines among what `stalled` says.
func unansweredOf(w *world) []StalledLine {
var out []StalledLine
for _, l := range w.h.Stalled() {
if l.State == Unanswered {
out = append(out, l)
}
}
return out
}
// **A said line clears when its delivery's state changes** (novox/hq issue 441): a merge ends it, past the pull
// request. It clears the line at once —
// before the forge is read again, which may lag — and stay cleared once it is read.
func TestASaidLineClearsWhenItsDeliveryMovesOn(t *testing.T) {
t.Run("a merge ends it", func(t *testing.T) {
w, id := saidReady(t)
w.h.PullMerged(merged("novox/mesh-controller", 215, readyHeadSHA, merge))
if s := w.state(id); s != Merged {
t.Fatalf("a merge left it %s", s)
}
if lines := unansweredOf(w); len(lines) != 0 {
t.Fatalf("a merged delivery is still said as unanswered: %+v", lines)
}
w.h.SeeOpenPulls() // the forge's open list lags the merge: the head is still on it
if lines := unansweredOf(w); len(lines) != 0 {
t.Fatalf("a merged delivery still on a lagging open list is said: %+v", lines)
}
})
}
// **A said line clears when its pull request leaves the open list** — closed, its event to this owner lost, so the
// delivery is still ready: the forge no longer lists it, and nothing is said of it.
func TestASaidLineClearsWhenItsPullRequestIsNoLongerOpen(t *testing.T) {
w, id := saidReady(t)
w.forge.open = nil
w.h.SeeOpenPulls()
if s := w.state(id); s != Ready {
t.Fatalf("the delivery moved to %s with no event: the case is not the one meant", s)
}
if lines := unansweredOf(w); len(lines) != 0 {
t.Fatalf("a pull request no longer open is still said: %+v", lines)
}
}
@@ -1,66 +0,0 @@
package main
import (
"encoding/json"
"slices"
"strings"
"testing"
"time"
)
// **An unanswered line gives the checks it waits on as data** (novox/hq issue 443): each check's context, its state
// and, for a pending one, since when in RFC 3339 — so the controller says the time in the operator's own zone. A time
// inside a finished sentence cannot be said again in another zone, and the operator read "since
// 2026-10-11T00:11:39Z". The finished words stay beside the data, for a controller that does not read it yet.
func TestAnUnansweredLineGivesItsChecksAsData(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeForge(), time.Date(2026, 10, 11, 0, 12, 5, 0, time.UTC))
w.forge.open = openPullsOf(t, strings.Replace(startedNeverAnswered, "2026-10-11T00:11:39Z", "2026-10-11T02:11:39+02:00", 1))
w.h.SeeOpenPulls()
w.later(Bounds[Proposed].For + time.Minute)
lines := w.h.Stalled()
if len(lines) != 1 {
t.Fatalf("the head is not said: %+v", lines)
}
want := []WaitingCheck{{Context: "mesh/merge-gate", State: CheckPending, Since: "2026-10-11T00:11:39Z"},
{Context: "mesh/repo-check", State: CheckNeverSet}}
if !slices.Equal(lines[0].Checks, want) {
t.Fatalf("the line's checks are %+v, not %+v", lines[0].Checks, want)
}
if words := []string{"mesh/merge-gate pending since 2026-10-11T00:11:39Z", "mesh/repo-check never set"}; !slices.Equal(lines[0].Waiting, words) {
t.Fatalf("the finished words a controller of before reads are gone: %q", lines[0].Waiting)
}
raw, _ := json.Marshal(lines[0])
t.Logf("the line as the controller reads it: %s", raw)
if !strings.Contains(string(raw), `"checks":[{"context":"mesh/merge-gate","state":"pending","since":"2026-10-11T00:11:39Z"},{"context":"mesh/repo-check","state":"never-set"}]`) {
t.Fatalf("the line's checks are not in the shape the controller reads: %s", raw)
}
}
// The data and the words never disagree: for every head the words are made from, the checks give the same contexts
// in the same order, and a pending time the forge did not say, or said unreadably, is given as no time at all.
func TestAnUnansweredLinesChecksAgreeWithItsWords(t *testing.T) {
for _, tc := range []struct {
name, at string
since string
}{{"a time in UTC", "2026-10-11T00:11:39Z", "2026-10-11T00:11:39Z"},
{"a time with an offset", "2026-10-11T02:11:39+02:00", "2026-10-11T00:11:39Z"},
{"no time", "", ""},
{"an unreadable time", "yesterday", ""}} {
t.Run(tc.name, func(t *testing.T) {
p := openPullsOf(t, strings.Replace(startedNeverAnswered, "2026-10-11T00:11:39Z", tc.at, 1))[0]
words, _, _ := p.unanswered()
checks := p.waitingChecks()
if len(words) != len(checks) {
t.Fatalf("words %q, checks %+v", words, checks)
}
for i, c := range checks {
if !strings.HasPrefix(words[i], c.Context+" ") {
t.Fatalf("word %q is not of check %+v", words[i], c)
}
}
if checks[0].State != CheckPending || checks[0].Since != tc.since {
t.Fatalf("the pending check is %+v, not since %q", checks[0], tc.since)
}
})
}
}
@@ -1,48 +0,0 @@
package main
import (
"encoding/json"
"strings"
"testing"
"time"
)
// **An unanswered line says the state of the delivery it holds, as data** (novox/hq issue 451): for a delivery in
// ready or rejected, whose check answered once, the controller advises a recheck rather than the build queue, and
// it reads that state from the line, never from its words. A head with no delivery held says none.
func TestAnUnansweredLineNamesTheStateOfItsDelivery(t *testing.T) {
at := time.Date(2026, 10, 11, 0, 43, 0, 0, time.UTC)
for _, c := range []struct {
gate string
want State
}{{"pass", Ready}, {"fail", Rejected}} {
w := newWorldOver(t, newMemStore(), newFakeForge(), at)
id := heldIn(t, w, c.gate, c.want)
w.forge.open = openPullsOf(t, readyRepoCheckPending)
w.h.SeeOpenPulls()
w.later(Bounds[Proposed].For + time.Minute)
w.h.SeeOpenPulls()
lines := w.h.Stalled()
if len(lines) != 1 || lines[0].ID != id || lines[0].State != Unanswered || lines[0].Held != c.want {
t.Fatalf("a %s delivery's unanswered line does not say its state as data: %+v", c.want, lines)
}
raw, _ := json.Marshal(lines[0])
if !strings.Contains(string(raw), `"held":"`+string(c.want)+`"`) {
t.Fatalf("the line the controller reads carries no held state: %s", raw)
}
}
// The same head with no delivery held here: unanswered, and no state to say.
w := newWorldOver(t, newMemStore(), newFakeForge(), at)
w.forge.open = openPullsOf(t, readyRepoCheckPending)
w.h.SeeOpenPulls()
w.later(Bounds[Proposed].For + time.Minute)
w.h.SeeOpenPulls()
lines := w.h.Stalled()
if len(lines) != 1 || lines[0].State != Unanswered || lines[0].Held != "" {
t.Fatalf("a head with no delivery held says a state: %+v", lines)
}
if raw, _ := json.Marshal(lines[0]); strings.Contains(string(raw), `"held"`) {
t.Fatalf("a line with no delivery held carries a held field: %s", raw)
}
}
@@ -1,130 +0,0 @@
package main
import (
"strings"
"testing"
"time"
)
// novox/hq issue 783: mesh-catalog commit b5417c28 was pushed while #305 (from branch adr-359-mesh-mail-733) was
// open, so this owner recorded its delivery as #305's. #305 then closed unmerged (its base branch had been
// deleted), and #315, opened from the same branch and head, merged it as 0914a33f. The delivery stayed closed
// ("the pull request closed unmerged"), though it recorded merged_as 0914a33f: a rule did not hold — a delivery
// keyed by commit follows the pull request that merges that commit, and a closed duplicate does not end it — and
// the walk that carried the merge waited 64 minutes for this owner's word until a person's `plans go` released it.
//
// A delivery closed only because its pull request closed unmerged, with no merge recorded, does not end its
// commit's delivery for another pull request with the same head: it is started over, under the same id, for the
// pull request that announces or merges it next.
const (
head783, merge783 = "d305d305d305d305", "d315d315d315d315"
head783b, merge783b = "d306d306d306d306", "d316d316d316d316"
head783c = "d307d307d307d307"
head783d, merge783d = "d308d308d308d308", "d318d318d318d318"
catalog783 = "novox/mesh-catalog"
branch783 = "adr-359-mesh-mail-733"
)
// TestADeliveryClosedByAClosedDuplicateFollowsThePullRequestThatMergesItsCommit is the issue's own shape: #305
// announced, closed unmerged (closed); #315 announced from the same head, checked and readied; #315 merges it,
// and the walk that answers that merge is let go — the delivery follows #315, never stuck on #305's stop.
func TestADeliveryClosedByAClosedDuplicateFollowsThePullRequestThatMergesItsCommit(t *testing.T) {
w := newWorld(t)
id := IDOf(catalog783, head783)
w.h.PullUpdated(pr(catalog783, 305, head783, branch783, "src/a.go"))
w.h.PullClosed(pr(catalog783, 305, head783, branch783))
if w.state(id) != Closed {
t.Fatalf("#305 closed unmerged left it %s; want closed", w.state(id))
}
w.h.PullUpdated(pr(catalog783, 315, head783, branch783, "src/a.go"))
if d := w.delivery(id); d.State == Closed || d.Number != 315 {
t.Fatalf("#315, the same head, left it %s, Number %d; want started over for #315, not closed", d.State, d.Number)
}
if d := w.delivery(id); !strings.Contains(d.Transitions[len(d.Transitions)-1].Why, "#305 with the same head closed unmerged") {
t.Fatalf("the start-over's transition names what it replaces: %q", d.Transitions[len(d.Transitions)-1].Why)
}
w.h.Checked(verdict(catalog783, 315, head783, "pass"))
if d := w.delivery(id); d.State != Ready {
t.Fatalf("a passing verdict for #315 left it %s", d.State)
}
w.h.PullMerged(merged(catalog783, 315, head783, merge783))
if d := w.delivery(id); d.State != Merged || d.Number != 315 {
t.Fatalf("#315's merge left it %s, Number %d; want merged under #315", d.State, d.Number)
}
}
// TestAMergeWithoutAnAnnouncementStartsOverAClosedDuplicateToo is decision 4: the merging pull request is never
// announced on its own (heard only as `pull.merged`) — the start-over happens in PullMerged itself, not only in
// PullUpdated, so a merge heard without the open announcement is not lost either.
func TestAMergeWithoutAnAnnouncementStartsOverAClosedDuplicateToo(t *testing.T) {
w := newWorld(t)
id := IDOf(catalog783, head783b)
w.h.PullUpdated(pr(catalog783, 306, head783b, branch783+"-b", "src/b.go"))
w.h.PullClosed(pr(catalog783, 306, head783b, branch783+"-b"))
if w.state(id) != Closed {
t.Fatalf("#306 closed unmerged left it %s; want closed", w.state(id))
}
m := merged(catalog783, 316, head783b, merge783b)
m.HeadChecks = map[string]string{"mesh/check": "success"}
w.h.PullMerged(m)
if d := w.delivery(id); d.State == Closed || d.Number != 316 {
t.Fatalf("#316's merge, heard with no announcement of its own, left it %s, Number %d; want started over for #316",
d.State, d.Number)
}
if d := w.delivery(id); d.State != Merged {
t.Fatalf("#316's merge left it %s; want merged", d.State)
}
}
// TestAClosedDeliveryAnnouncedAgainByItsOwnPullRequestStaysClosed guards decision 3's "any other closed
// delivery ... is untouched": the same pull request announced again after its own close is the ordinary
// 'announced again' path, never a start-over, and stays closed.
func TestAClosedDeliveryAnnouncedAgainByItsOwnPullRequestStaysClosed(t *testing.T) {
w := newWorld(t)
id := IDOf(catalog783, head783c)
w.h.PullUpdated(pr(catalog783, 307, head783c, branch783+"-c", "src/c.go"))
w.h.PullClosed(pr(catalog783, 307, head783c, branch783+"-c"))
if w.state(id) != Closed {
t.Fatalf("#307 closed unmerged left it %s; want closed", w.state(id))
}
w.h.PullUpdated(pr(catalog783, 307, head783c, branch783+"-c", "src/c.go"))
if d := w.delivery(id); d.State != Closed || d.Number != 307 {
t.Fatalf("#307 announced again by itself left it %s, Number %d; want it to stay closed", d.State, d.Number)
}
}
// TestAMergeHeardWithoutAnAnnouncementStartsOverAClosedDuplicateAsMadeFromTheForgesWord: a delivery started over from a
// merge heard without its open announcement takes FromForge exactly as a first-heard merge does. History is gone with the
// states after the merge (ADR 0539 §17): a merge heard long after it happened is merged, or held when no check passed.
func TestAMergeHeardWithoutAnAnnouncementStartsOverAClosedDuplicateAsMadeFromTheForgesWord(t *testing.T) {
w := newWorld(t)
id := IDOf(catalog783, head783d)
w.h.PullUpdated(pr(catalog783, 308, head783d, branch783+"-d", "src/d.go"))
w.h.PullClosed(pr(catalog783, 308, head783d, branch783+"-d"))
if w.state(id) != Closed {
t.Fatalf("#308 closed unmerged left it %s; want closed", w.state(id))
}
m := merged(catalog783, 318, head783d, merge783d)
m.MergedAt = w.now.Add(-48 * time.Hour).UTC().Format(time.RFC3339)
m.HeadChecks = map[string]string{"mesh/check": "success"}
w.h.PullMerged(m)
d := w.delivery(id)
if d.State != Merged || d.Number != 318 {
t.Fatalf("#318's merge, heard with no announcement of its own, left it %s, Number %d; want merged under #318", d.State, d.Number)
}
if !d.FromForge {
t.Fatal("a merge heard without its own announcement is made from the forge's word: FromForge is false")
}
}
@@ -1,165 +0,0 @@
package main
import (
"reflect"
"testing"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
// Issue 888: the checker judges on the trunk source.main names (mesh-delivery modules/mesh-delivery-checker main.go), and the
// gate moved it only for a merge it handed to the forge bridge (finish). A merge made at the forge directly moved every other
// delivery's trunk (trunkMoved) and left source.main, so the checker kept judging on the trunk before it.
const (
i888Other = "novox/other"
i888OtherH = "0000000000000000000000000000000000000001"
i888App = "novox/app"
i888Head1 = "1111111111111111111111111111111111111111"
i888Head2 = "2222222222222222222222222222222222222222"
i888Merge1 = "3333333333333333333333333333333333333333"
i888Merge2 = "4444444444444444444444444444444444444444"
)
func i888MergedAt(repo string, number int, head, merge string, at time.Time) PullEvent {
p := merged(repo, number, head, merge)
p.MergedAt = at.UTC().Format(time.RFC3339)
return p
}
// 888/AC1: a merge into main the gate did not hand is heard from the forge: source.main holds it, and keeps the others.
func TestAMergeHeardOnlyFromTheForgeMovesSourceMain(t *testing.T) {
w := newWorld(t)
w.h.Records = &Records{Ledger: newLedgerFake(t), Now: w.h.Now}
w.source.main = &Source{V: mdc.Version, Group: "earlier", Heads: map[string]string{i888Other: i888OtherH}}
w.h.PullUpdated(pr(i888App, 3, i888Head1, "fix/x", "internal/a.go"))
w.settleAll()
w.h.PullMerged(i888MergedAt(i888App, 3, i888Head1, i888Merge1, w.now.Add(-time.Minute)))
w.settleAll()
if w.source.main == nil || w.source.main.Heads[i888App] != i888Merge1 || w.source.main.Heads[i888Other] != i888OtherH {
t.Fatalf("source.main is %+v; wanted %s at %s and %s kept", w.source.main, i888App, i888Merge1, i888Other)
}
// The seat's verb source answers each repository's trunk branch and head, and the revision, from the same key.
if a := w.h.SourceRevision(MergeBranch); a.Error != "" || a.Revision != 1 || len(a.Repositories) != 2 ||
a.Repositories[0] != (seat.SourceHead{Repository: i888App, Trunk: MergeBranch, Head: i888Merge1}) ||
a.Repositories[1].Repository != i888Other || a.Repositories[1].Head != i888OtherH {
t.Fatalf("source answers %+v", a)
}
// §690: given no branch it answers the branches of the keys it keeps, and no repository; a branch it keeps no key of has none.
if a := w.h.SourceRevision(""); a.Error != "" || !reflect.DeepEqual(a.Branches, []string{MergeBranch}) || len(a.Repositories) != 0 || a.Revision != 0 {
t.Fatalf("source with no branch answers %+v, want the branches %v alone", a, []string{MergeBranch})
}
if a := w.h.SourceRevision("beta"); a.Error != "" || a.Branch != "beta" || a.Revision != 0 || len(a.Repositories) != 0 {
t.Fatalf("source of a branch with no key answers %+v", a)
}
n := len(w.source.moves)
w.settleAll()
w.h.PullMerged(i888MergedAt(i888App, 3, i888Head1, i888Merge1, w.now.Add(-time.Minute))) // heard again
w.settleAll()
if len(w.source.moves) != n || n != 1 {
t.Fatalf("source.main moved %d then %d times; wanted once", n, len(w.source.moves))
}
// W11.17e: a merge with no record's merge-pull opens a forge-made record at its head, said once as a transition
// with why forge-made.
records, err := w.h.Records.Current()
if err != nil || len(records) != 1 || !records[0].ForgeMade || records[0].Heads[mdc.ApprovedKey(i888App, 3)] != i888Head1 {
t.Fatalf("the forge-made record is %+v (%v)", records, err)
}
var said []map[string]any
for _, e := range w.said {
if e["event-kind"] == "transition" && e["why"] == "forge-made" {
said = append(said, e)
}
}
if len(said) != 1 || said[0]["record"] != records[0].Instance {
t.Fatalf("the forge-made record is said %d times: %v", len(said), said)
}
}
// 888/AC2: a group the gate merged through the bridge moves source.main once, in finish, though the forge's word of each
// member's merge is heard too.
func TestAGroupTheGateMergedMovesSourceMainOnceThoughTheForgeSaysItsMerges(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.settleAll()
if len(w.forge.merges) != 2 || len(w.source.moves) != 1 {
t.Fatalf("merges %v, source.main moved %d times", w.forge.merges, len(w.source.moves))
}
commit := w.source.moves[0].Heads[gCat]
w.h.PullMerged(i888MergedAt(gCtl, 1, gHc, w.source.moves[0].Heads[gCtl], w.now))
w.h.PullMerged(i888MergedAt(gCat, 2, gHk, commit, w.now))
w.settleAll()
if len(w.source.moves) != 1 {
t.Fatalf("source.main moved %d times; wanted once, by the group: %+v", len(w.source.moves), w.source.moves)
}
}
// 888/AC3: an older merge heard after a newer one, or replayed as history, never moves source.main backwards.
func TestAnOlderMergeHeardLaterNeverMovesSourceMainBack(t *testing.T) {
w := newWorld(t)
w.h.PullMerged(i888MergedAt(i888App, 4, i888Head2, i888Merge2, w.now.Add(-time.Minute)))
w.settleAll()
w.h.PullMerged(i888MergedAt(i888App, 3, i888Head1, i888Merge1, w.now.Add(-2*time.Minute)))
w.settleAll()
if w.source.main == nil || w.source.main.Heads[i888App] != i888Merge2 || len(w.source.moves) != 1 {
t.Fatalf("source.main is %+v after %d moves; wanted the newer merge %s, moved once", w.source.main, len(w.source.moves), i888Merge2)
}
h := newWorld(t)
h.source.main = &Source{V: mdc.Version, Group: "now", Heads: map[string]string{i888App: i888Merge2}}
h.h.PullMerged(i888MergedAt(i888App, 3, i888Head1, i888Merge1, h.now.Add(-4*24*time.Hour)))
h.settleAll()
if h.source.main.Heads[i888App] != i888Merge2 || len(h.source.moves) != 0 {
t.Fatalf("a merge replayed as history moved source.main to %+v", h.source.main)
}
}
// 888/AC4: an approved group whose members were merged at the forge before the gate handed them moves source.main once.
func TestAnApprovedGroupMergedAtTheForgeMovesSourceMainOnce(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
mc, mk := "5555555555555555555555555555555555555555", "6666666666666666666666666666666666666666"
w.h.PullMerged(i888MergedAt(gCtl, 1, gHc, mc, w.now))
w.h.PullMerged(i888MergedAt(gCat, 2, gHk, mk, w.now))
w.settleAll()
w.settleAll()
if len(w.source.moves) != 1 || w.source.main.Heads[gCtl] != mc || w.source.main.Heads[gCat] != mk {
t.Fatalf("source.main moved %d times to %+v; wanted once, both members at their merge commits", len(w.source.moves), w.source.main)
}
}
// 888/AC5 (review round 1): an older merge heard from the forge after the bridge merged a newer one, whose own pull.merged is
// not heard yet, never moves source.main back; nor does one older than the gate keeps records for, over a head it does not know.
func TestAnOlderForgeMergeNeverOverwritesANewerBridgeMergeOrAnUnknownHead(t *testing.T) {
w := approvedGroup(t)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.settleAll()
if len(w.source.moves) != 1 {
t.Fatalf("the group did not move source.main once: %d", len(w.source.moves))
}
bridged := w.source.main.Heads[gCat]
w.h.PullUpdated(pr(gCat, 7, "7777777777777777777777777777777777777777", "fix/old", "modules/app/module.json"))
w.h.PullMerged(i888MergedAt(gCat, 7, "7777777777777777777777777777777777777777", "8888888888888888888888888888888888888888", w.now.Add(-10*time.Minute)))
w.settleAll()
if w.source.main.Heads[gCat] != bridged || len(w.source.moves) != 1 {
t.Fatalf("an older forge merge moved source.main from the bridge's %s to %s", bridged, w.source.main.Heads[gCat])
}
o := newWorld(t)
o.source.main = &Source{V: mdc.Version, Group: "pruned", Heads: map[string]string{i888App: i888Merge2}}
at := o.now
o.h.PullUpdated(pr(i888App, 3, i888Head1, "fix/x", "internal/a.go"))
o.later(keptFinal + 24*time.Hour)
o.h.PullMerged(i888MergedAt(i888App, 3, i888Head1, i888Merge1, at.Add(time.Minute)))
o.settleAll()
if o.source.main.Heads[i888App] != i888Merge2 || len(o.source.moves) != 0 {
t.Fatalf("a merge older than the records kept overwrote a head the gate does not know: %+v", o.source.main)
}
}
@@ -1,65 +0,0 @@
package main
import (
"strings"
"testing"
)
// novox/hq issue 902: a group that is delivering passed its composed check for its members' heads, so
// mesh/delivery-group says success there; pending would keep its later members from ever merging.
func TestReplay902(t *testing.T) {
for state, want := range map[string]string{
"ready": "success", "merging": "success", "merged": "success",
"checking": "pending", "rejected": "failure", "held": "error", "closed": "error",
} {
if got := groupStatusState(state); got != want {
t.Errorf("a group %s says %s on mesh/delivery-group, want %s", state, got, want)
}
}
}
// group902 is a two-member group whose first member in its order merged.
func group902(t *testing.T) (*world, string, string, string, string) {
w := newWorld(t)
const ctlHead, catHead = "c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2"
ctl, cat := IDOf("novox/mesh-controller", ctlHead), IDOf("novox/mesh-catalog", catHead)
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
w.h.Checked(verdict("novox/mesh-controller", 1, ctlHead, "pass"))
w.h.Checked(verdict("novox/mesh-catalog", 2, catHead, "pass"))
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
w.h.PullMerged(merged("novox/mesh-catalog", 2, catHead, "m2m2m2m2m2m2"))
w.settleAll()
return w, ctl, cat, ctlHead, catHead
}
func TestReplay902TheNextMemberOfADeliveringGroupReadsSuccess(t *testing.T) {
w, _, _, ctlHead, _ := group902(t)
g := w.h.groups["feat/x"]
st, _ := GroupState(g, w.h.membersOf(g))
s := w.forge.statuses["novox/mesh-controller@"+ctlHead+" mesh/delivery-group"]
t.Logf("state=%s status=%q", st, s)
if st != "merging" || !strings.HasPrefix(s, "success") {
t.Fatalf("want merging/success, got %s %q", st, s)
}
}
func TestReplay902AClosedGroupComposedAgainAndFailingSaysNoSuccess(t *testing.T) {
w, ctl, _, _, _ := group902(t)
const newHead = "e3e3e3e3e3e3e3e3"
_ = ctl
w.h.PullUpdated(pr("novox/mesh-controller", 1, newHead, "feat/x", "internal/a.go"))
w.h.Checked(verdict("novox/mesh-controller", 1, newHead, "pass"))
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-2", Verdict: "fail", Summary: "no longer compose"})
w.settleAll()
g := w.h.groups["feat/x"]
st, why := GroupState(g, w.h.membersOf(g))
s := w.forge.statuses["novox/mesh-controller@"+newHead+" mesh/delivery-group"]
t.Logf("closed=%v check=%+v state=%s (%s) status=%q", g.Closed, g.Check, st, why, s)
if strings.HasPrefix(s, "success") {
t.Fatalf("a closed group whose composed check failed for its heads says success: %q", s)
}
}
@@ -1,106 +0,0 @@
package main
// The issues an ask names, from the issue tracker's links (novox/hq ADR 0493 §4, ADR 0659 §4): a pull request is linked to
// the issue it fixes by a tool call of the seat issue-tracker, never matched from its title or description, and the gate
// reads those links through the seat's verb `list`. Words only: an ask is never held for them; a read that fails is said in
// the ask as the tracker unread, and read again at the pull request's next head.
import (
"encoding/json"
"fmt"
"sort"
"strconv"
)
// TrackerListVerb is the seat issue-tracker's verb the gate reads a pull request's issues with (the manifest invokes it).
const TrackerListVerb = "seat:issue-tracker.list"
// IssueLinks reads the issues linked to a pull request in the issue tracker.
type IssueLinks interface {
// Linked is the issues linked to the pull request number of repository (owner/name), ascending.
Linked(repository string, number int) ([]int, error)
}
// seatIssues is the issue tracker through its seat.
type seatIssues struct{ ask asking }
func (s seatIssues) Linked(repository string, number int) ([]int, error) {
// A pull request is linked as `<repository's name>#<number>`, lower-case, as the tracker keeps it.
target := "pull-request:" + repoName(repository) + "#" + strconv.Itoa(number)
raw, err := s.ask(TrackerListVerb, map[string]any{"linked": target, "limit": 200})
if err != nil {
return nil, err
}
answer, output, ok := answerOf(raw)
if !ok {
return nil, fmt.Errorf("the seat issue-tracker refused list: %s", lastLine(output))
}
var listed struct {
Rows []struct {
Number int `json:"number"`
} `json:"rows"`
Error string `json:"error"`
}
if err := json.Unmarshal(answer, &listed); err != nil {
return nil, fmt.Errorf("the seat issue-tracker's list is not readable: %s", clip(string(answer), 200))
}
if listed.Error != "" {
return nil, fmt.Errorf("the seat issue-tracker could not list %s: %s", target, listed.Error)
}
out := make([]int, 0, len(listed.Rows))
for _, r := range listed.Rows {
out = append(out, r.Number)
}
sort.Ints(out)
return out, nil
}
// issuesToRead is the read of the linked issues of every member not read at its head yet, done outside the lock; nil when
// all are read or no issue tracker is reached. Under the lock.
func (h *Holder) issuesToRead(members []*Delivery) func() {
if h.Links == nil {
return nil
}
type read struct {
id, repository, head, why string
number int
issues []int
}
var reads []read
for _, m := range members {
if m.Number != 0 && m.IssuesRead != m.Commit {
reads = append(reads, read{id: m.ID, repository: m.Repository, head: m.Commit, number: m.Number})
}
}
if len(reads) == 0 {
return nil
}
for _, m := range members {
if m.issuesReading == m.Commit && m.IssuesRead != m.Commit {
return func() {} // read already: its end looks again
}
}
for _, m := range members {
m.issuesReading = m.Commit
}
return func() {
for i := range reads {
r := &reads[i]
issues, err := h.Links.Linked(r.repository, r.number)
r.issues = issues
if err != nil {
r.why = err.Error()
h.Logf("[mesh-delivery] %s: the issues linked to its pull request cannot be read: %v; its ask says so", r.id, err)
}
}
h.mu.Lock()
for _, r := range reads {
if d := h.deliveries[r.id]; d != nil && d.Commit == r.head {
d.Issues, d.IssuesRead, d.IssuesUnread = r.issues, r.head, r.why
h.keep(d)
}
}
h.mu.Unlock()
h.lookAt(h.Now(), "an ask whose issues were read from the issue tracker")
}
}
@@ -6,7 +6,7 @@ import "git.novox.be/novox/mesh-sdk/go/announce"
// issue 1790): the module says it itself, from its code, before it serves, and a reader is granted what it said
// (clause 3). A state is readable when another module's manifest reads it (keeps_test.go names the readers). keeps_test.go holds the list to the
// manifest's `state`, and keeps_integration_test.go that the module announces it on a bus.
var Kept = []announce.State{{Name: "asks", Readable: true}, {Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "pulls", Readable: true}, {Name: "source", Readable: true}}
var Kept = []announce.State{{Name: "conditions"}, {Name: "deliveries"}, {Name: "groups"}, {Name: "merges", Readable: true}, {Name: "owed", Readable: true}, {Name: "source", Readable: true}}
// announceKept says Kept through keeps, which is stdio.Keeps.
func announceKept(keeps func(...announce.State) error) error { return keeps(Kept...) }
@@ -1,15 +1,11 @@
// mesh-delivery-gate: the holder of the mesh-delivery seat (novox/hq ADR 0239, to-be 47). A Go bundle the node's
// runtime launches. It owns the delivery — one commit in one repository, from its pull request's head to
// every machine — and the delivery group above it: their states in one compiled table, kept in its own state
// on the bus, every transition said as an event, noted on the commit and shown on the pull request through
// the forge's holder. It asks the Approve of a merge and merges once; it asks the controller nothing and records nothing
// after the merge (ADR 0539 §17): whether a merge is on the machines it derives when asked from the delivery view's reach
// (ADR 0556 clause 4), and a merge held without a passing check is settled on the operator's answer (clause 5). stdout
// is the MCP channel; what this module says, it says on stderr.
// runtime launches. From milestone G's delivery it keeps no merge states and asks no Approve per merge (design 47 G §172,
// §431, §592): it keeps the delivery records, one per source revision of a watched branch the indexer takes in, and the
// source revision per watched branch, and says what it owes the forge as events the forge bridge performs. It merges
// nothing and asks the controller nothing. stdout is the MCP channel; what this module says, it says on stderr.
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
@@ -19,7 +15,6 @@ import (
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
stdio "git.novox.be/novox/mesh-sdk/go"
)
@@ -34,12 +29,9 @@ type listening struct {
func (l *listening) set(s string) { l.mu.Lock(); l.now = s; l.mu.Unlock() }
func (l *listening) get() string { l.mu.Lock(); defer l.mu.Unlock(); return l.now }
// The events this owner follows: the forge's pull requests, as the seat git says them (gitseat.go). The forge's status events
// say that a check answered (events.go). The step events the delivery record is built from follow (consumer.go).
var followed = append(append(append([]string{}, gitEvents...), forgePattern, owedEndedEvent, answeredEvent, reachedEvent, ConfigChangedEvent), stepPatterns()...)
// forgePattern is the forge bridge's messages on FORGE (novox/hq ADR 0437 §8): the gate follows a merge and the refusal of one.
const forgePattern = "mesh-delivery-forge-bridge.forge.**"
// The events this owner follows: the forge bridge's word on an act owed, and the step events the delivery records are built
// from (consumer.go).
var followed = append([]string{owedEndedEvent}, stepPatterns()...)
func main() {
settingsFile := os.Getenv("MESH_DELIVERY_GATE_SETTINGS")
@@ -60,16 +52,10 @@ func main() {
}
h := &Holder{
Settings: settings,
Store: kvStore{},
Forge: seatForge{ask: stdio.Ask},
Links: seatIssues{ask: stdio.Ask},
Emit: func(event string, body any) error { return stdio.Emit(event, body) },
Logf: logf,
Operator: kvOperator{},
Merges: kvMerges{},
Source: kvSource{},
Owed: seatOwed{say: stdio.SeatPublish},
Pulls: kvPulls{},
// The gate says what stalled to the operator itself (issue 1275, ADR 0437 §9).
Say: sayOnSeat,
Conditions: kvConditions{},
@@ -77,7 +63,6 @@ func main() {
h.init()
h.SaySeat = stdio.SeatPublish
h.Records = &Records{Ledger: sdkLedger{}, Now: h.Now, Retention: h.recordRetention, Say: stdio.SeatPublish, Logf: logf}
h.Merges = recordedMerges{MergeStore: kvMerges{}, records: h.Records, members: h.groupMembers}
h.Old = kvOld{}
h.Consumer = &StepConsumer{Records: h.Records, Families: stepFamilies, Now: h.Now, Logf: logf}
l := &listening{now: "not yet: starting"}
@@ -92,13 +77,10 @@ func main() {
}
}
// run takes the events and the operator's answers. The state is read back at the start (start), and what the gate may have
// missed meanwhile is read once then (readOnStart): the forge's open pull requests, the operator's asks — and it acts on
// events from then on (novox/hq ADR 0406 §1, issue 975): no tick, no forge poll, no reconcile loop. Each step is retried,
// each failure said, never given up on quietly.
// run switches to the records, then takes the events, and acts on events from then on (novox/hq ADR 0406 §1, issue 975): no
// tick, no forge poll, no reconcile loop. Each step is retried, each failure said, never given up on quietly.
func run(h *Holder, l *listening) {
<-stdio.Served() // Serve first: the state is reached through it, and an event is heard from as soon as it serves
loadKept(h, l)
switchToRecords(h, l)
go h.Work()
for _, pattern := range followed {
@@ -113,41 +95,8 @@ func run(h *Holder, l *listening) {
time.Sleep(wait)
}
}
// The checker's verdicts (issue 1302, ADR 0447 §2): the Approve on mesh/check is asked only on its verdict at exactly the
// heads an ask covers, and a group's verdict is that one; each change asks for a look.
go func() {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
err := stdio.State(checkerVerdicts).Watch("", func(c stdio.StateChange) error {
if c.Op == "delete" {
h.CheckerVerdictChanged(c.Key, nil)
} else {
h.CheckerVerdictChanged(c.Key, c.Value)
}
return nil
})
if err == nil {
return
}
logf("[mesh-delivery] not hearing the checker's verdicts yet (%v); asking again in %s", err, wait)
time.Sleep(wait)
}
}()
l.set("listening")
logf("[mesh-delivery] listening: %s", strings.Join(followed, ", "))
readOnStart(h)
}
// loadKept reads the kept deliveries and groups back, retrying until it can: no event is taken before.
func loadKept(h *Holder, l *listening) {
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
err := h.Load()
if err == nil {
break
}
l.set("not yet: " + err.Error())
logf("[mesh-delivery] the kept state cannot be read yet (%v); asking again in %s", err, wait)
time.Sleep(wait)
}
}
// switchToRecords carries the groups in flight into their delivery records (switch.go), retrying until it can: no event is
@@ -165,18 +114,6 @@ func switchToRecords(h *Holder, l *listening) {
}
}
// readOnStart reads once what happened while the gate was away: the forge's open pull requests (heads nobody checks) are
// read by the first look, and the asks the router closed meanwhile are read here.
func readOnStart(h *Holder) {
h.wantForge()
// The delivery view's reach, said while the gate was away, is read once through its tool; from then on its events carry it.
if err := h.ReachRead(stdio.Ask); err != nil {
logf("[mesh-delivery] the delivery view's reach cannot be read yet, so merges read as not known until it says one (%v)", err)
}
h.hearAsks()
h.SourceCatchUp() // a merge missed or marked unwritten while the gate was away reaches source.main, and so the checker (issue 1726)
}
// handle takes one event, then asks for a look at every step the event makes possible (Work, React). An event that cannot be read is
// said and acknowledged: read again it would fail again.
func handle(h *Holder, e stdio.Envelope) error {
@@ -192,71 +129,6 @@ func take(h *Holder, e stdio.Envelope) error {
if e.Key == owedEndedEvent {
return h.OwedEnded(e.Body)
}
if strings.HasPrefix(e.Key, "mesh-delivery-forge-bridge.forge.") {
h.Forged(e.Key, e.Body)
return nil
}
switch e.Key {
case answeredEvent:
h.AnswersChanged() // the teller wrote an answer: the look that follows reads the answers to the asks pending
return nil
case reachedEvent:
return h.ReachHeard(e.Body)
}
switch e.Key {
case gitEvent(gitseat.EventPullUpdated), gitEvent(gitseat.EventPullMerged), gitEvent(gitseat.EventPullClosed):
p, err := gitPullEvent(e.Key, e.Body)
if err != nil {
h.Refuse(fmt.Errorf("%s is not readable: %v", e.Key, err))
return nil
}
switch e.Key {
case gitEvent(gitseat.EventPullUpdated):
if h.PullUpdated(p) {
h.wantForge() // a head not seen before: the forge shows whether anyone checks it
}
h.KeepPull(p)
case gitEvent(gitseat.EventPullMerged):
h.PullMerged(p)
default:
h.PullClosed(p)
}
case gitEvent(gitseat.EventForgePull):
action := forgeAction(e.Body)
if action != "edited" && action != "closed" {
return nil // a head and a merge are the seat's pull.updated and pull.merged
}
p, err := gitPullEvent(e.Key, e.Body)
if err != nil {
h.Refuse(fmt.Errorf("%s is not readable: %v", e.Key, err))
return nil
}
if action == "edited" {
h.PullEdited(p) // an edit after a head was announced takes over a stopped record (issue 1114)
} else if !p.Merged {
// The forge's own close (issue 1107): a merged pull request is the merged event, never this one.
h.PullClosed(p)
}
case gitEvent(gitseat.EventForgeStatus):
s, err := gitStatusEvent(e.Body)
if err != nil {
h.Refuse(fmt.Errorf("a commit status is not readable: %v", err))
return nil
}
h.CommitStatus(s)
h.KeepCheckVerdict(s)
case writableEvent:
if err := h.WriteGranted(e.Body); err != nil {
h.Refuse(err)
}
case ConfigChangedEvent:
var c ConfigChange
if err := json.Unmarshal(e.Body, &c); err != nil {
h.Refuse(fmt.Errorf("a config change is not readable: %v", err))
return nil
}
h.ConfigChanged(c)
}
return nil
}
@@ -312,29 +184,26 @@ func tools(h *Holder, l *listening) []stdio.Tool {
seat := Seat + "."
list := []stdio.Tool{
{Name: seat + "deliveries",
Description: "Every delivery not final, those that ended in the last day, and any stop `stalled` still lists, one line each: its id " +
"(owner/repository@commit), its state, its group, what it waits for and since when, and for a merged one whether it is on " +
"the machines (machines, derived from the delivery view's reach, novox/hq ADR 0556). Narrowed by state, " +
"repository or group.",
Input: map[string]any{"type": "object", "properties": map[string]any{"state": str("one state"),
Description: "Every delivery record the gate keeps, as it is kept (design 47 G A-24): the open ones, and those that ended in " +
"the last thirty days (all: every one kept), each with its milestone or group, its pull requests, its entries' outcomes " +
"per node, its acts owed the forge and why it is not ended. Narrowed by state (open or ended), repository or group.",
Input: map[string]any{"type": "object", "properties": map[string]any{"state": str("open or ended"),
"repository": str("owner/repository"), "group": str("a group's id (its branch name)"),
"all": map[string]any{"type": "string", "enum": []string{"true", "false"}}}},
Run: func(a map[string]any) (any, error) {
return map[string]any{
"deliveries": h.Deliveries(strArg(a, "state"), strArg(a, "repository"), strArg(a, "group"), boolArg(a, "all"))}, nil
return h.Deliveries(strArg(a, "state"), strArg(a, "repository"), strArg(a, "group"), boolArg(a, "all"))
}},
{Name: seat + "show",
Description: "One delivery or group whole: every transition with when and why, and its group. " +
"For a merged delivery, whether its merge is on the machines (machines), derived from the delivery view's reach of its " +
"repository (novox/hq ADR 0556), and else what holds it.",
Description: "One delivery record whole, as it is kept: by its instance, its milestone, its group's id or the id of one of " +
"its pull requests (owner/repository@commit).",
Input: map[string]any{"type": "object", "properties": map[string]any{"id": str("a delivery's id, or a group's")},
"required": []string{"id"}},
Run: func(a map[string]any) (any, error) { return h.Show(strArg(a, "id")) }},
{Name: seat + "groups",
Description: "Every delivery group: its members in merge order, its check and its state, derived from its members, " +
"and the acts the gate said it owes the forge for it, each with its id and its recorded outcome (design 47 *What the " +
"gate owes the forge*). With a group, that group alone, ended or not, and no groups for a group it does not know. " +
"The group moves the source revision once (novox/hq ADR 0418); what reached the machines is the delivery view's.",
Description: "Every delivery group, from the delivery records: its members, its state (open or ended), why, and the acts the " +
"gate said it owes the forge for it, each with its id and its recorded outcome (design 47 *What the gate owes the " +
"forge*). With a group, that group alone, ended or not, and no groups for a group it does not know. What reached the " +
"machines is the delivery view's.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"all": map[string]any{"type": "string", "enum": []string{"true", "false"}, "description": "\"true\": the groups that ended too"},
"group": str("one group's id: that group alone, ended or not")}},
@@ -362,28 +231,13 @@ func tools(h *Holder, l *listening) []stdio.Tool {
return h.Records.Recovery(strArg(a, "repository"), strArg(a, "commit"), strArg(a, "why"), stdio.CallerOf(a))
}},
{Name: seat + "stalled",
Description: "Every delivery held past its state's bound, with the transition the table lets healer H2 take. " +
"Also a merge refused or not made past its bound, a group half merged, and a head with no merge check.",
Description: "Every delivery record whose open wait is past its bound, with that wait (a record open a day after it opened), " +
"and every act owed the forge unanswered past fifteen minutes (design 47 G A-24, §165).",
Run: func(a map[string]any) (any, error) { return h.Stalled(), nil }},
{Name: "delivery_merge_again",
Description: "On the operator's word, ask again at once the merge of an approved delivery the forge refused, or of an approved " +
"group held or half merged, at the heads the Approve named (novox/hq issue 1275): every member is read first and " +
"nothing merges unless every member merges now. For a refusal that was the forge's \"try again later\", a conflict " +
"cleared on the trunk, or a refusal the gate stopped asking after it repeated. A member whose head moved is asked by its " +
"new head, never by this. Refused, naming why, for a delivery that is not an approved merge waiting after a refusal or a hold.",
Input: map[string]any{"type": "object", "properties": map[string]any{"id": str("the delivery's id (owner/repository@commit), any member of the group"),
"why": str("the operator's word, kept in the log")}, "required": []string{"id", "why"}},
Run: func(a map[string]any) (any, error) {
if err := need(a, "id", "why"); err != nil {
return nil, err
}
return h.MergeAgain(strArg(a, "id"), strArg(a, "why"))
}},
{Name: "delivery_status",
Description: "Whether this owner works: how many deliveries in each state, the groups, what is owed to " +
"the forge and the bus and not yet done, how many owed effects were tried since it started (done, failed " +
"and tried again, given up) and when the last was, whether its events arrive, the last refusals, and " +
"source.main as the gate's bucket source holds it (each repository's trunk head the checker judges on).",
Description: "Whether this owner works: how many delivery records are open and ended, what is owed to the forge and not yet " +
"answered, how many owed effects were tried since it started (done, failed and tried again, given up) and when the last " +
"was, whether its events arrive, the last refusals, and source.main as the gate's bucket source holds it.",
Run: func(map[string]any) (any, error) {
out := h.Status(l.get())
out["source-main"] = h.SourceMain()
@@ -9,7 +9,6 @@ import (
"sort"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/catalogue"
@@ -116,16 +115,26 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
}
}
}
if !reflect.DeepEqual(m.Consumes, followed) || !slices.Contains(m.Consumes, "mesh-delivery-forge-bridge.forge.**") {
t.Fatalf("consumes %v, follows %v (the bridge's forge messages among them)", m.Consumes, followed)
// From G's delivery the gate hears neither the bridge's merged and pushed messages nor the teller's answers nor the view's
// reach (design 47 G §592, §593): it consumes the indexer's take-in.taken and the bridge's owed.ended, and the git events that clear
// an approval carried.
if !reflect.DeepEqual(m.Consumes, followed) {
t.Fatalf("consumes %v, follows %v", m.Consumes, followed)
}
if !reflect.DeepEqual(m.Emits, []string{"transition", "group", "merged"}) {
for _, c := range m.Consumes {
for _, gone := range []string{".forge.", "teller", "view", "forge-pull", "forge-status", "pull-updated"} {
if strings.Contains(c, gone) {
t.Errorf("the manifest still consumes %s, which G's gate does not hear (§431, §592, §593)", c)
}
}
}
if !reflect.DeepEqual(m.Emits, []string{"transition"}) { // `group` and `merged` go with the merge states (§431)
t.Fatalf("emits %v", m.Emits)
}
// Four buckets still in the old `state`, the four of the template `state` moved to `requires` (design 47 G §613), and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3 and the conditions it raised (issue 1275). No
// Two buckets still in the old `state` (merges, source), `pulls` and `asks` gone (§613), the four of the template `state` moved to `requires` (design 47 G §613), and no `asked` (the teller asks, ADR 0437 §6): ADR 0450 §3 and the conditions it raised (issue 1275). No
// `turns`, no turn tool and no `check-gates-merge`: no check gates a merge (novox/hq ADR 0661 §1, §2, §6).
buckets := m.buckets()
if !reflect.DeepEqual(buckets, []string{"pulls", "asks", "merges", "source"}) {
if !reflect.DeepEqual(buckets, []string{"merges", "source"}) {
t.Fatalf("state %v", buckets)
}
for _, tool := range m.Tools {
@@ -141,26 +150,14 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
if !reflect.DeepEqual(m.Capabilities, []string{buscalls.LedgerCalls, buscalls.LedgerUpkeepCalls}) {
t.Fatalf("capabilities %v, want %s and %s", m.Capabilities, buscalls.LedgerCalls, buscalls.LedgerUpkeepCalls)
}
// What it reads: the checker's verdicts alone (issue 1302; they go with the checker). The teller's answers and the delivery
// view's reach, which `deliveries` and `show` derive whether a merge is on the machines from (ADR 0556 clause 4), are no
// state it reads (ADR 0596 §2-§4): it hears their owners' events answered and reached and asks their tools.
if !reflect.DeepEqual(m.Reads, []string{checkerVerdicts}) {
// It reads no other module's state (ADR 0596 §2): the checker's verdicts went with the checks, and the teller's answers and the
// view's reach are neither state it reads nor events it hears (§593).
if len(m.Reads) != 0 {
t.Fatalf("reads %v", m.Reads)
}
for _, c := range []string{"mesh-delivery-teller.answered", "mesh-delivery-view.reached"} {
if !slices.Contains(m.Consumes, c) {
t.Errorf("the manifest does not consume %s", c)
}
}
if answeredEvent != "mesh-delivery-teller.answered" || reachedEvent != "mesh-delivery-view.reached" {
t.Errorf("the events heard are %q and %q", answeredEvent, reachedEvent)
}
// It calls no controller verb and reads the forge through the seat git's verbs (design 47 *The seat git*), and the forge's
// merge state through the one statuses tool the seat does not carry yet (seatForge.mergeState); the forge's writes are the
// bridge's (ADR 0437 §8). It uses operator-channel only to say its conditions (ADR 0437 §9, issue 1275): the teller asks
// (ADR 0437 §6).
want := []string{"seat:git.pull", "seat:git.statuses", "seat:git.open-pulls", "seat:git.contains", "seat:mesh-hand-acts.hand-act",
TrackerListVerb, WantsEntryVerb, EngineCompositionVerb, tellerAnswersTool, viewReachTool}
// It calls no controller verb and no forge verb: the forge's reads and writes are the bridge's (ADR 0437 §8; design 47 §592).
// It uses operator-channel only to say its conditions (ADR 0437 §9, issue 1275): the teller asks no more (§431).
want := []string{"seat:mesh-hand-acts.hand-act", WantsEntryVerb, EngineCompositionVerb}
if !reflect.DeepEqual(m.Uses, []string{"operator-channel"}) {
t.Fatalf("uses %v: operator-channel, for its conditions alone", m.Uses)
}
@@ -246,22 +243,6 @@ func TestNoReadyMarkIsLeftInTheModule(t *testing.T) {
}
}
// A row written while the ready mark stood still reads: its mark's fields are ignored, its ask and merge kept (ADR 0421
// §4 consequences, issue 1022).
func TestARowWrittenWithTheReadyMarkStillReads(t *testing.T) {
store := newMemStore()
row := `{"id":"novox/app@111111111111","repository":"novox/app","commit":"` + readyHead + `","number":7,"base":"main",
"state":"ready","since":"2026-10-17T10:00:00Z","created":"2026-10-17T10:00:00Z","let_go":"0001-01-01T00:00:00Z",
"ready":{"head":"` + readyHead + `","by":"lead@shanks","marked_at":"2026-10-17T10:01:00Z","seen":"2026-10-17T10:02:00Z",
"last_try":"0001-01-01T00:00:00Z","asked_at":"2026-10-17T10:03:00Z","ask_id":"merge-7-111111111111-1","heads":{"novox/app@111111111111":"` + readyHead + `"}}}`
store.deliveries["novox/app@111111111111"] = []byte(row)
w := newWorldOver(t, store, newFakeForge(), time.Date(2026, 10, 17, 11, 0, 0, 0, time.UTC))
d := w.delivery("novox/app@111111111111")
if d.Ready == nil || d.Ready.AskID != "merge-7-111111111111-1" || d.Ready.Head != readyHead {
t.Fatalf("the old row is not read: %+v", d.Ready)
}
}
// R6-C3 (ADR 0539 §17): the gate holds the seat as the SDK's table of release 6 has it. Every verb of the table that is not
// optional is served; the verbs release 6 removed are optional rows the SDK answers by name, and the gate serves none of them;
// and the SDK's own test of whether a module can hold a seat (CanHold) finds nothing missing or unpromised.
@@ -285,7 +266,7 @@ func TestTheGateHoldsTheSeatAsTheSDKsTableOfRelease6HasIt(t *testing.T) {
served[verb] = true
}
}
for _, removed := range []string{"settle", "checks"} { // design 47 G §431
for _, removed := range []string{"settle", "checks", "table", "recheck", "close"} { // design 47 G §431
if served[removed] {
t.Errorf("the gate serves %s, which G removed with the merge states", removed)
}
@@ -4,29 +4,27 @@ import (
"encoding/json"
"errors"
"fmt"
"reflect"
"regexp"
"sort"
"strings"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The gate merges through the forge bridge, never itself (novox/hq ADR 0437 §8, ADR 0441 §5, ADR 0450 §3). It writes into
// `merges.<group>` the Approve's heads and ONE member at a time, the next only after the bridge's message on FORGE says the
// one before it merged, and none after a refusal. It re-reads every member's required statuses before it hands a member
// (ADR 0441 §2). Once every member is merged it moves `source.main` in one compare-and-set write and the mesh's source has
// the group as one revision. What it announces for each merge (`merged`) names the ask, its asker the teller, and the heads.
// The gate merges nothing and hands nothing to the forge (design 47 G §592): `merges` stays an old section the switch reads
// (switch.go, §613), never written; `source` is the gate's source revision per watched branch (§15, §291), served by the verb
// `source` below.
// MergeBranch is the trunk branch each repository's merge branch names in a source key.
const MergeBranch = "main"
// changeAttempts is how often a lost compare-and-set is read and applied again before it is said.
const changeAttempts = 10
// MergeStore is the gate's bucket merges.
type MergeStore interface {
Get(group string) (*Merge, error)
Put(group string, m Merge) error
}
// OldState reads the gate's old state owed, which the switch says again as switched acts (switch.go): each key, and one
@@ -79,19 +77,6 @@ func (kvMerges) Get(group string) (*Merge, error) {
return &m, nil
}
func (kvMerges) Put(group string, m Merge) error {
key, err := mdc.MergesKey(group)
if err != nil {
return err
}
raw, err := EncodeMerge(m)
if err != nil {
return err
}
_, err = stdio.State("merges").Put(key, json.RawMessage(raw))
return err
}
// kvSource is source over the runtime.
type kvSource struct{}
@@ -217,674 +202,3 @@ func (h *Holder) SourceMain() map[string]any {
}
return map[string]any{"group": held.Group, "heads": held.Heads}
}
// approvedOf are the heads an Approve covers as a merge carries them: `<repository>@<number>` to the full head.
func (h *Holder) approvedOf(heads map[string]string) map[string]string {
out := map[string]string{}
for id, sha := range heads {
if m := h.deliveries[id]; m != nil {
out[mdc.ApprovedKey(m.Repository, m.Number)] = sha
}
}
return out
}
// handTurn is d's turn to be handed to the bridge, when it is: the first member of its group not merged (a lone pull request
// is a group of one), every member approved at its head and ready to merge. Nil when it is not. Under the lock; the hand
// runs outside it.
func (h *Holder) handTurn(g *Group, d *Delivery, now time.Time) func() {
var reads []memberRead
c := h.covering(d)
if g != nil {
order := h.unmergedLive(g)
if len(order) == 0 || order[0] != d {
return nil
}
for _, m := range order {
if h.notHandable(m) != "" {
return nil // the others are not approved at their heads yet: nothing is merged before they are (said by notHanded)
}
reads = append(reads, readOf(m, h.covering(m).Heads[m.ID]))
}
}
if c == nil || c.Approved == nil || c.Heads[d.ID] == "" {
return nil
}
d.Ready.Tries++
d.Ready.LastTry, d.Ready.merging = now, true
h.keep(d)
gid, name, key, target, approved := "", askGroup(d), c.AskID, d.ID, h.approvedOf(c.Heads)
if g != nil {
gid = g.ID
}
return func() { h.hand(gid, name, key, target, reads, approved) }
}
// notHandable is why a group member cannot be handed in its group's pass, empty when it can: handTurn's own test, in words.
// Under the lock.
func (h *Holder) notHandable(m *Delivery) string {
mc := h.covering(m)
switch {
case m.Ready == nil:
return m.ID + " has no ready run (" + string(m.State) + ")"
case m.Ready.declined():
return m.ID + " stands declined since " + m.Ready.Declined.UTC().Format(time.RFC3339)
case mc == nil || mc.Approved == nil:
return m.ID + " is not approved at its head " + shortOf(m.Commit, 12)
case mc.Heads[m.ID] != m.Commit:
return m.ID + "'s head " + shortOf(m.Commit, 12) + " is not the head approved"
}
if w := h.readyWaits(m); w != "" {
return m.ID + " waits: " + w
}
return ""
}
// notHanded is why the approved merge of d, which waits for nothing, is not handed: its group's pass cannot start, and
// says which member holds it and why. Empty when nothing holds it, or when the pass is under way or its refusal is said
// already. Under the lock.
func (h *Holder) notHanded(g *Group, d *Delivery) string {
if g == nil {
return ""
}
order := h.unmergedLive(g)
if len(order) == 0 {
return ""
}
if first := order[0].Ready; first != nil && (first.merging || !first.LastTry.IsZero() || first.RefusedSince != nil) {
return "" // the pass is under way, or its refusal is said on its own line
}
for _, m := range order {
if why := h.notHandable(m); why != "" {
return fmt.Sprintf("its group %s is not handed to the forge bridge: %s", g.ID, why)
}
}
return ""
}
// noteNotHanded keeps on d's run why its approved merge is not handed, said once in the log when it changes, and as a
// stalled line meanwhile (readyLines). Under the lock.
func (h *Holder) noteNotHanded(d *Delivery, why string, now time.Time) {
r := d.Ready
if r == nil || r.NotHanded == why {
return
}
r.NotHanded = why
if why == "" {
r.NotHandedSince = nil
} else {
at := now
r.NotHandedSince = &at
h.Logf("[mesh-delivery] %s is approved, waits for nothing, and is NOT handed to merge: %s", d.ID, why)
}
h.keep(d)
}
// hand reads a group's required statuses and, when all are a success, writes the target as the member being merged.
func (h *Holder) hand(gid, name, key, target string, reads []memberRead, approved map[string]string) {
if gid != "" && !h.groupMergeable(gid, target, reads) {
return
}
if h.waitForRoom(target) {
return
}
h.mu.Lock()
value, ok := h.mergeValue(name, key, approved, target, mdc.MergeMerging)
h.mu.Unlock()
if !ok {
h.giveBack(target, nil)
return
}
cur, err := h.Merges.Get(name)
if err == nil && cur != nil && SameMerge(*cur, value) {
return // handed already, the bridge has the revision
}
if err == nil {
err = h.Merges.Put(name, value)
}
if err != nil {
if errors.Is(err, ErrRecordsFull) {
h.mu.Lock()
h.holdFull(target, h.Now())
h.mu.Unlock()
}
h.giveBack(target, err)
return
}
h.Logf("[mesh-delivery] %s is handed to the forge bridge to merge, at the heads the operator approved", target)
}
// giveBack gives a target its turn back after a hand that could not be made, to be asked again after a while.
func (h *Holder) giveBack(target string, err error) {
h.mu.Lock()
defer h.mu.Unlock()
if d := h.deliveries[target]; d != nil && d.Ready != nil {
d.Ready.merging = false
d.Ready.LastTry = time.Time{}
}
if err != nil {
h.Logf("[mesh-delivery] %s could not be handed to the forge bridge: %v; asked again in %s", target, err, askAgainAfter)
}
h.again("a merge that could not be handed to the forge bridge")
}
// mergeValue is the value of a group's merges key with these members merged, and the target in the state asked (merging, or
// refused with the forge's words): the group's members in order, those the Approve covers. Under the lock. False when it
// cannot be made (the target is not among the heads the Approve covers).
func (h *Holder) mergeValue(name, key string, approved map[string]string, target string, state mdc.MergeState) (Merge, bool) {
t := h.deliveries[target]
if t == nil {
return Merge{}, false
}
members := []*Delivery{t}
if g := h.groups[t.Group]; t.Group != "" && g != nil {
members = h.membersOf(g)
}
m := Merge{Group: name, Ask: key, Approved: approved}
for _, o := range members {
head := approved[mdc.ApprovedKey(o.Repository, o.Number)]
if head == "" {
continue
}
pull := MergePull{Head: mdc.Head{Repository: o.Repository, Number: o.Number, Head: head}}
switch {
case o.MergedAs != "" || (o.Ready != nil && o.Ready.Merged != nil):
commit := o.MergedAs
if o.Ready != nil && o.Ready.MergeCommit != "" {
commit = o.Ready.MergeCommit
}
if commit == "" {
continue
}
pull.State, pull.Commit = mdc.MergeMerged, commit
case o == t:
pull.State = state
if state == mdc.MergeRefused {
pull.Words = o.Ready.Answer
}
m.Pulls = append(m.Pulls, pull)
return m, true
default:
return m, false
}
m.Pulls = append(m.Pulls, pull)
}
return m, len(m.Pulls) > 0 && state == mdc.MergeMerged
}
// forgeGroupEnd is the whole event of a group's end on FORGE, the forge bridge's event name under its module: compared
// exactly, so the merged message of a repository named forge/group, which ends the same way, is read as one.
const forgeGroupEnd = forgeBridgeModule + "." + forgemsg.ForgeGroupEndEvent
// Forged takes a message of the forge bridge on FORGE: the gate follows a merge and the refusal of a merge, and nothing else.
//
// The end of a group's merges (`forge.group.merged`, a body this gate has no use for: the gate wrote the merges itself) is
// the indexer's, and is ignored before it is decoded, so no refusal is said for it (issue 1585, ADR 0633 clause 4).
func (h *Holder) Forged(event string, body []byte) {
if event == forgeGroupEnd {
return
}
f, err := forgemsg.DecodeForge(body)
if err != nil {
h.Refuse(fmt.Errorf("a message of the forge bridge is not readable: %v", err))
return
}
switch {
case f.Kind == forgemsg.ForgeMerged:
h.ForgeMerged(f)
case f.Kind == forgemsg.ForgeRefused && f.Act == "merge":
h.ForgeRefused(f)
}
}
// memberOfForge is the delivery a forge message is about: at the head it names.
func (h *Holder) memberOfForge(f forgemsg.Forge) *Delivery {
if d := h.deliveries[IDOf(f.Repository, f.Head)]; d != nil && d.Number == f.Number {
return d
}
return nil
}
// ForgeMerged takes the bridge's word that a pull request the gate handed to it merged: the merge is announced once,
// with the ask, its asker the teller and the heads (issue 1025), and the next member's turn comes at the next look.
func (h *Holder) ForgeMerged(f forgemsg.Forge) {
h.mu.Lock()
defer h.mu.Unlock()
d := h.memberOfForge(f)
if d == nil || d.Ready == nil || d.Ready.Approved == nil || d.Ready.Merged != nil {
return
}
r, now := d.Ready, h.Now()
d.Owed = append(d.Owed, Effect{Kind: EffectEmit, Event: "merged", Line: string(mustJSON(mergedEvent(d, f.Head))), Since: now})
r.Merged, r.Answer, r.RefusedSince, r.merging, r.MergeCommit = &now, "merged", nil, false, f.Merge
h.Logf("[mesh-delivery] %s merged on the operator's Approve at %s (%s)", d.ID, shortOf(f.Head, 12), h.checkAt(h.askMembers(d)).Words)
h.keep(d)
}
// ForgeRefused takes the bridge's word that the forge refused a merge: nothing after it is merged, and the refusal is said
// as a stalled line (ADR 0441 §3, §4). The merge is handed again only at the next event that decides whether it may
// merge (mergeAgainNow).
func (h *Holder) ForgeRefused(f forgemsg.Forge) {
h.mu.Lock()
defer h.mu.Unlock()
d := h.memberOfForge(f)
if d == nil || d.Ready == nil || d.Ready.Merged != nil {
return
}
r, now := d.Ready, h.Now()
r.merging, r.refusedNoted = false, false
answer := oneLine(f.Words)
var on time.Time
if d.Check != nil {
on = d.Check.At
}
if r.RefusedSince == nil {
r.RefusedSince, r.FirstRefusedOn = &now, on
}
r.LastRefusedOn = on
r.noteRefusal(d, answer)
if !outOfDateRefusal(answer) {
r.Urgent = true // never a warning again for this head
}
if r.Repeats >= maxRepeatedRefusals {
// Loud, not endless (novox/hq issue 1106): the same refusal again with the trunk not moved is no trunk's move, so
// the merge is not asked again by a verdict or a status; the operator is told, urgent, and decides.
r.Urgent = true
if r.Repeats == maxRepeatedRefusals {
h.Logf("[mesh-delivery] %s: the merge tool refused it %d times in a row with the same words and the trunk not moved, so "+
"the merge is asked no more until the trunk moves, a new head is announced or the operator approves again: %s",
d.ID, r.Repeats, answer)
}
}
if answer != r.Answer {
h.Logf("[mesh-delivery] %s is approved, and the merge tool did not merge it (asked again at the next verdict, Approve or composed check): %s", d.ID,
answer)
}
r.Answer = answer
h.keep(d)
}
// settleMerges works out, under the lock, what a merge owes its key and the source once the bridge has spoken: a refused
// member written refused, and a group whose members are all merged written merged and moved onto source.main in one write.
// The writing happens outside the lock.
func (h *Holder) settleMerges() []func() {
var out []func()
done := map[string]bool{}
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
d := h.deliveries[id]
r := d.Ready
if r == nil || r.Approved == nil || r.AskID == "" || len(r.Heads) == 0 {
continue
}
name, key, approved := askGroup(d), r.AskID, h.approvedOf(r.Heads)
switch {
case r.Merged == nil && r.RefusedSince != nil && !r.refusedNoted && r.Answer != "" && r.Answer != "merged":
r.refusedNoted = true
target := d.ID
out = append(out, func() {
h.mu.Lock()
value, ok := h.mergeValue(name, key, approved, target, mdc.MergeRefused)
h.mu.Unlock()
if ok {
if err := h.Merges.Put(name, value); err != nil {
h.Logf("[mesh-delivery] the refusal of %s could not be written to its merge: %v", target, err)
}
}
})
case (r.Merged != nil || d.MergedAs != "") && !r.SourceMoved && !done[key]:
// Merged through the bridge, or at the forge directly after the Approve (issue 888): the group is moved once.
if !h.allMerged(r.Heads) {
continue
}
done[key] = true
heads := map[string]string{}
var members []string
for hid := range r.Heads {
m := h.deliveries[hid]
if m.LearntLate {
continue // learnt after the fact: never written to source.main (settleGone)
}
commit := m.MergedAs
if m.Ready != nil && m.Ready.MergeCommit != "" {
commit = m.Ready.MergeCommit
}
heads[m.Repository] = commit
members = append(members, hid)
}
if len(members) == 0 {
continue // every member learnt after the fact: nothing to write
}
sort.Strings(members)
last := h.deliveries[members[len(members)-1]]
out = append(out, func() { h.finish(name, key, approved, last.ID, heads, members) })
}
}
return out
}
// allMerged is whether every delivery an Approve covers is merged.
func (h *Holder) allMerged(heads map[string]string) bool {
for id := range heads {
m := h.deliveries[id]
if m == nil || (m.MergedAs == "" && (m.Ready == nil || m.Ready.Merged == nil)) {
return false
}
}
return true
}
// finish writes a group's merge as all merged and moves source.main to the merge commits in one compare-and-set write: the
// group is one revision of the mesh's source (ADR 0437 §1, §8).
func (h *Holder) finish(name, key string, approved map[string]string, last string, heads map[string]string, members []string) {
h.mu.Lock()
value, ok := h.mergeValue(name, key, approved, last, mdc.MergeMerged)
h.mu.Unlock()
var err error
if ok {
err = h.Merges.Put(name, value)
}
if err == nil {
err = h.Source.Move(func(prev *Source) (Source, bool) {
next := Source{Group: name, Heads: map[string]string{}}
if prev != nil {
for repository, head := range prev.Heads {
next.Heads[repository] = head
}
}
for repository, commit := range heads {
next.Heads[repository] = commit
}
return next, prev == nil || !reflect.DeepEqual(prev.Heads, next.Heads)
})
}
h.mu.Lock()
defer h.mu.Unlock()
if err != nil {
h.Logf("[mesh-delivery] group %s merged, and its place in the source cannot be written yet: %v; asked again in %s", name, err, askAgainAfter)
h.again("source.main, moved once every member is merged")
return
}
for _, id := range members {
if m := h.deliveries[id]; m != nil && m.Ready != nil {
m.Ready.SourceMoved = true
h.keep(m)
}
}
h.Logf("[mesh-delivery] group %s is merged whole: source.main holds %s", name, strings.Join(members, ", "))
}
// gateMerges says whether finish moves source.main for a delivery: one the operator approved through an ask, whose merge the
// gate hands to the forge bridge.
func gateMerges(d *Delivery) bool {
return d.Ready != nil && d.Ready.Approved != nil && d.Ready.AskID != "" && len(d.Ready.Heads) > 0
}
// leftOutOfForgeSource is whether settleForgeMerges never writes a merge to source.main: one the gate handed on an Approve
// (finish writes it), and one learnt after the fact, long past or with no time the forge said. One learnt late within
// replayLate of its merge, as a gate before the fix took mesh-delivery#51 (2026-10-25, its group's finish left it out and
// source.main kept the trunk before it), is weighed like any merge heard from the forge at a catch-up, and never moves
// source.main back (olderHead).
func (d *Delivery) leftOutOfForgeSource() bool {
if d.LearntLate {
return d.MergedAt.IsZero() || d.Since.Sub(d.MergedAt) >= replayLate
}
return gateMerges(d)
}
// settleForgeMerges moves source.main for each merge into the trunk heard only from the forge (issue 888): a pull request
// merged at the forge directly, not handed by the gate on an Approve. Every other delivery's trunk moved with it (trunkMoved),
// and the checker judges on the trunk source.main names, so source.main moves too. A merge replayed as history, or older than
// a merge of its repository already heard, is never written: source.main never moves backwards. Under the lock; the writing
// happens outside it.
func (h *Holder) settleForgeMerges() []func() {
var out []func()
again := map[*Delivery]bool{}
if h.catchUp {
h.catchUp = false
again = h.newestInSource()
}
for _, d := range h.deliveries {
if d.MergedAs == "" || (d.InSource && !again[d]) || d.leftOutOfForgeSource() {
continue
}
if d.Base != MergeBranch || d.replayed() || h.newerMergeOf(d) {
d.InSource = true // not the trunk, or not its head: nothing to write
h.keep(d)
continue
}
id, repository, base, commit, at, group := d.ID, d.Repository, d.Base, d.MergedAs, mergedWhen(d), firstOf(d.Branch, d.Repository)
out = append(out, func() {
err := h.Source.Move(func(prev *Source) (Source, bool) {
next := Source{Group: group, Heads: map[string]string{repository: commit}}
if prev != nil {
held := prev.Heads[repository]
if sameCommit(held, commit) {
return next, false // there already: the bridge's word and the forge's are one merge
}
if held != "" && !h.olderHead(repository, base, held, at) {
return next, false // source.main holds a newer merge, or one the gate cannot date: never moved back
}
for r, head := range prev.Heads {
if r != repository {
next.Heads[r] = head
}
}
}
return next, true
})
h.mu.Lock()
defer h.mu.Unlock()
if err != nil {
h.Logf("[mesh-delivery] %s merged at the forge, and its place in the source cannot be written yet: %v; asked again in %s", id, err, askAgainAfter)
h.again("source.main, moved for a merge heard from the forge")
return
}
if m := h.deliveries[id]; m != nil {
m.InSource = true
h.keep(m)
}
h.Logf("[mesh-delivery] %s merged at the forge: source.main holds %s at %s", id, repository, shortOf(commit, 12))
})
}
return out
}
// recordForgeMade opens a forge-made delivery record for every merge into the trunk the forge made with no record's
// merge-pull (design 47 *What the gate owes the forge*): one the gate did not say the merge of, heard as it happened. It
// holds the pull request at its head, has no Approve, and is said as the delivery's transition with why forge-made, once.
// Under the lock; the record is opened outside it.
func (h *Holder) recordForgeMade() []func() {
if h.Records == nil {
return nil
}
var out []func()
for _, d := range h.deliveries {
if d.MergedAs == "" || d.ForgeRecorded || d.LearntLate || gateMerges(d) || d.Number == 0 || d.Base != MergeBranch {
continue
}
id, milestone, heads := d.ID, milestoneOf(d), map[string]string{mdc.ApprovedKey(d.Repository, d.Number): d.Commit}
out = append(out, func() {
r, _, err := h.Records.Open(milestone, heads, true)
h.mu.Lock()
defer h.mu.Unlock()
m := h.deliveries[id]
if err != nil || m == nil || r == nil {
if err != nil {
h.Logf("[mesh-delivery] %s merged at the forge with no record's merge-pull, and its forge-made record cannot be opened yet: %v; asked again in %s", id, err, askAgainAfter)
h.again("a forge-made record, opened for a merge the forge made")
}
return
}
now := h.Now()
body := transitionEvent(m, Transition{At: now, From: m.State, To: m.State, Event: "forge-made", Why: "forge-made"}, time.Time{})
body["record"] = r.Instance
m.Owed = append(m.Owed, Effect{Kind: EffectEmit, Event: "transition", Line: string(mustJSON(body)), Since: now})
m.ForgeRecorded = true
h.keep(m)
h.Logf("[mesh-delivery] %s merged at the forge with no record's merge-pull: its forge-made record is %s", id, r.Instance)
})
}
return out
}
// trunkHead is a full commit id as the forge names a branch's head.
var trunkHead = regexp.MustCompile(`^[0-9a-f]{40}$`)
// seedTrunks gives source.main the trunk head of a repository it names none of (issue 1422): one never merged through the
// gate, whose open pull requests the checker could otherwise never judge. The head is the base_sha of the latest announcement of
// any open pull request into main (the seat git's pull.updated; ADR 0323; lastAnnounced), the trunk's freshest head the gate heard. It is
// added in one compare-and-set write that never replaces a head source.main holds, so a merge always wins; the first merge
// through the gate moves it as any other. source.main is per repository and names main's head only (ADR 0484 §11, MergeBranch):
// a pull request into another base seeds nothing. A repository is weighed once per start after it was found or seeded.
// Under the lock; the writing happens outside it.
func (h *Holder) seedTrunks() []func() {
newest := map[string]*Delivery{}
for _, d := range h.deliveries {
if h.seeded[d.Repository] || d.Base != MergeBranch || !trunkHead.MatchString(d.BaseSHA) || d.State.Final() || d.MergedAs != "" {
continue
}
if o := newest[d.Repository]; o == nil || lastAnnounced(d).After(lastAnnounced(o)) {
newest[d.Repository] = d
}
}
var out []func()
for repository, d := range newest {
id, head, group := d.ID, d.BaseSHA, firstOf(d.Branch, d.Repository)
out = append(out, func() {
seeded := false
err := h.Source.Move(func(prev *Source) (Source, bool) {
seeded = false
next := Source{Group: group, Heads: map[string]string{}}
if prev != nil {
if prev.Heads[repository] != "" {
return *prev, false // named already: never replaced
}
next.Group = prev.Group
for r, held := range prev.Heads {
next.Heads[r] = held
}
}
next.Heads[repository] = head
seeded = true
return next, true
})
h.mu.Lock()
defer h.mu.Unlock()
if err != nil {
h.Logf("[mesh-delivery] %s: source.main names no trunk head of %s, and it cannot be seeded yet: %v; asked again in %s", id, repository, err, askAgainAfter)
h.again("source.main, seeded with the trunk head of a repository it names none of")
return
}
if h.seeded == nil {
h.seeded = map[string]bool{}
}
h.seeded[repository] = true
if seeded {
h.Logf("[mesh-delivery] source.main named no trunk head of %s: seeded with %s, main's head as the forge announced %s with, so its pull requests are judged (issue 1422)", repository, shortOf(head, 12), id)
}
})
}
return out
}
// lastAnnounced is when the forge last announced a delivery's head with its base head: BaseSHAAt, or when the gate made it
// for one kept before that was written.
func lastAnnounced(d *Delivery) time.Time {
if !d.BaseSHAAt.IsZero() {
return d.BaseSHAAt
}
return d.Created
}
// replayLate is how long after its merge a merge heard only from the forge's word is a replay: the forge announces a merge
// within seconds, so one heard this late was replayed from the bus's history, and the source already holds what came after.
const replayLate = 15 * time.Minute
// replayed is whether the merge was heard from the forge's word replayLate or more after it happened (novox/hq issue 313,
// kept for source.main's sake after the history state went, ADR 0539 §17): never written to source.main.
func (d *Delivery) replayed() bool {
return d.FromForgesWord() && !d.MergedAt.IsZero() && d.Created.Sub(d.MergedAt) >= replayLate
}
// mergeOf is a delivery's merge into its trunk as the gate knows it: the forge's word (MergedAs), or else the bridge's
// (Ready.MergeCommit, Ready.Merged), which comes first for a merge the gate handed. Empty when it is not merged.
func mergeOf(d *Delivery) (string, time.Time) {
if d.MergedAs != "" {
return d.MergedAs, mergedWhen(d)
}
if d.Ready != nil && d.Ready.Merged != nil && d.Ready.MergeCommit != "" {
return d.Ready.MergeCommit, *d.Ready.Merged
}
return "", time.Time{}
}
// newerMergeOf says whether a merge into the same trunk of d's repository was heard that landed after d's.
func (h *Holder) newerMergeOf(d *Delivery) bool {
for _, o := range h.deliveries {
if commit, at := mergeOf(o); o != d && o.Repository == d.Repository && o.Base == d.Base && commit != "" &&
!sameCommit(commit, d.MergedAs) && at.After(mergedWhen(d)) {
return true
}
}
return false
}
// olderHead says whether the head source.main holds for a repository is a merge the gate knows landed before at, so a merge
// at that time may take its place. A head it does not know (written before its records, or pruned with its delivery) is
// older only when at is within the time final records are kept: a pruned merge ended before then (issue 888, review).
// Takes the lock.
func (h *Holder) olderHead(repository, base, head string, at time.Time) bool {
h.mu.Lock()
defer h.mu.Unlock()
for _, o := range h.deliveries {
if commit, when := mergeOf(o); o.Repository == repository && o.Base == base && commit != "" && sameCommit(commit, head) {
return !when.After(at)
}
}
return at.After(h.Now().Add(-keptFinal))
}
// mergedWhen is when a merged delivery's merge landed: its merge time (the forge's), else when the gate made the delivery. A
// zero time made every other merge of the repository look newer, so such a merge never moved source.main (issue 1726).
func mergedWhen(d *Delivery) time.Time {
if !d.MergedAt.IsZero() {
return d.MergedAt
}
return d.Created
}
// SourceCatchUp asks the next look to weigh again, once, the newest merge into the trunk of each repository though it is
// marked in the source (issue 1726, mesh-catalog #474, kept here because no seat holder arrives in release 6 to run it): what
// the gate missed while it was not running, or marked without writing, reaches source.main. The write never moves a repository
// back (olderHead), and one already there is not written again.
func (h *Holder) SourceCatchUp() {
h.mu.Lock()
h.catchUp = true
h.mu.Unlock()
h.Poke()
}
// newestInSource is, for each repository, its newest merge into the trunk heard from the forge, marked in the source. Under the lock.
func (h *Holder) newestInSource() map[*Delivery]bool {
newest := map[string]*Delivery{}
for _, d := range h.deliveries {
if d.MergedAs == "" || !d.InSource || d.leftOutOfForgeSource() || d.Base != MergeBranch || d.replayed() {
continue
}
if o := newest[d.Repository]; o == nil || mergedWhen(d).After(mergedWhen(o)) {
newest[d.Repository] = d
}
}
out := map[*Delivery]bool{}
for _, d := range newest {
if !h.newerMergeOf(d) {
out[d] = true
}
}
return out
}
@@ -2,262 +2,10 @@ package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
// What mesh-delivery owns (novox/hq ADR 0239): the delivery — one commit in one repository — and the
// delivery group above it. Each is kept whole, one key of the module's own state per delivery and per
// group, written only by this holder.
// Delivery is one commit in one repository, from its pull request's head to every machine.
type Delivery struct {
// ID is owner/repository@<twelve characters of its commit>.
ID string `json:"id"`
Repository string `json:"repository"`
Commit string `json:"commit"`
// The pull request, when there is one: its number, title, base, head branch and page.
Number int `json:"number,omitempty"`
Title string `json:"title,omitempty"`
Base string `json:"base,omitempty"`
Branch string `json:"branch,omitempty"`
HTMLURL string `json:"html_url,omitempty"`
CloneURL string `json:"clone_url,omitempty"`
// The diffset, as the forge said it: what the planner maps onto modules.
Paths []string `json:"paths,omitempty"`
PathsTruncated bool `json:"paths_truncated,omitempty"`
Removed []string `json:"removed,omitempty"`
ModuleDirs []string `json:"module_dirs,omitempty"`
ModuleDirsSaid bool `json:"module_dirs_said,omitempty"`
// Comments is how many comments this owner owed on the pull request at this head, which numbers the next.
Comments int `json:"comments,omitempty"`
// Issues are the issues linked to its pull request in the issue tracker (novox/hq ADR 0493 §4, ADR 0659 §4), shown to the
// operator who is asked to approve its merge; IssuesRead is the head they were read at, IssuesUnread why the issue
// tracker could not say them. Words only: nothing waits on them.
Issues []int `json:"issues,omitempty"`
IssuesRead string `json:"issues_read,omitempty"`
IssuesUnread string `json:"issues_unread,omitempty"`
// issuesReading is a read of them in flight, so a look meanwhile does not ask it again; not kept.
issuesReading string
// Group is the delivery group it is in: its head branch name. Empty for a lone delivery.
Group string `json:"group,omitempty"`
State State `json:"state"`
Since time.Time `json:"since"`
Created time.Time `json:"created"`
// Check is its own check's verdict.
Check *Verdict `json:"check,omitempty"`
// Required are the statuses its base requires, as the forge last said them (the seat git's open-pulls): mesh/check makes it
// the checker's (issue 1302). Empty until the forge said them.
Required []string `json:"required,omitempty"`
// CheckState is its head's mesh/check as the forge last said it (pending, success, failure, error): a pending one
// after a pass is the checker judging it again, and no Approve is asked meanwhile (issue 1302).
CheckState string `json:"check_state,omitempty"`
// CheckAsk is this owner's own ask of its check, when the forge's announcement did not bring one: a head
// adopted while it already waited, or one whose announcement the controller missed.
CheckAsk *CheckAsk `json:"check_ask,omitempty"`
// TrunkNow is its trunk's head as this owner last heard it move while it was open: the commit the last merge
// into its trunk landed as (novox/hq ADR 0323 §7). A verdict that names another trunk commit is out of date.
// Empty until a merge into its trunk is heard after it was made.
TrunkNow string `json:"trunk_now,omitempty"`
// BaseSHA is the base branch's head the forge last announced its head with (ADR 0323): sent with a check this
// owner asks, for a verdict no build judges to name its trunk commit.
BaseSHA string `json:"base_sha,omitempty"`
// BaseSHAAt is when the forge last announced its head with BaseSHA: the freshest of a repository's announcements gives
// source.main its trunk head when it names none (seedTrunks, issue 1422). Zero for one kept before it was written.
BaseSHAAt time.Time `json:"base_sha_at,omitzero"`
// AskedAgain is when its head was announced again while its verdict was out of date — the merge tool's ask of
// its check against the trunk's head (ADR 0323 §5) — and no verdict has come since: its one-hour bound runs from
// then. Nil while nothing is asked, and cleared by a verdict or another move of its trunk.
AskedAgain *time.Time `json:"asked_again,omitempty"`
// The facts the table's guards read. NewerHead is the head that superseded it; ClosedUnmerged the pull
// request closed; MergedAs the commit it landed on the trunk as; HeldWhy what holds it for a person.
NewerHead string `json:"newer_head,omitempty"`
ClosedUnmerged bool `json:"closed_unmerged,omitempty"`
// Others are the other open pull requests announced at this head while this one was live (issue 1114), as they were
// announced: the record is theirs to take over when this one's pull request closes unmerged.
Others []PullEvent `json:"others,omitempty"`
MergedAs string `json:"merged_as,omitempty"`
MergedAt time.Time `json:"merged_at"`
// InSource says the merge was looked at for source.main: written there, or left as older than the trunk's head (issue 888).
// A merge the gate handed to the bridge on an Approve is moved there by finish instead (merges.go).
InSource bool `json:"in_source,omitempty"`
// ForgeRecorded is a merge the forge made with no record's merge-pull whose forge-made record is opened and said
// (merges.go recordForgeMade).
ForgeRecorded bool `json:"forge_recorded,omitempty"`
// LearntLate says its merge was learnt after the fact, from its absence among the forge's open pull requests
// (settleGone, issue 1275): it moved no trunk here, and it never moves source.main, not even at a catch-up.
LearntLate bool `json:"learnt_late,omitempty"`
HeldWhy string `json:"held_why,omitempty"`
// FromForge is a delivery made from the forge's word at its merge: its head was never heard by this owner.
FromForge bool `json:"from_forge,omitempty"`
// Ready is what this owner did with the operator's Approve of its merge into main, and with the merge (novox/hq ADR
// 0421, ready.go). The json key is the one the ready mark's record had, so rows written before still read.
Ready *ReadyRun `json:"ready,omitempty"`
// Author is who opened its pull request, as the forge's pull request record says (the seat git's events): named with
// its branch when a new head is needed (novox/hq ADR 0505 §5, ADR 0659 §4).
Author string `json:"author,omitempty"`
// FreshWanted is whether a passing verdict that arrived out of date is to have its check asked against the
// trunk's head, before the operator is asked (novox/hq ADR 0421 §3, issue 1022). Cleared when it is asked.
FreshWanted bool `json:"fresh_wanted,omitempty"`
Transitions []Transition `json:"transitions,omitempty"`
// Owed are the forge's and the bus's effects of its transitions not yet done: said only after the
// delivery is kept, retried until done.
Owed []Effect `json:"owed,omitempty"`
}
// Transition is one move of the table, kept.
type Transition struct {
At time.Time `json:"at"`
From State `json:"from"`
To State `json:"to"`
Event Event `json:"event"`
Why string `json:"why"`
By string `json:"by,omitempty"`
}
// Verdict is a check's verdict: the gate's and the repository's own. ID is the build seat's ask that ran
// it, On the machine it ran on, At when this owner heard it; Report is the check's own account, as bounded
// as the controller says it. All of it is what `checks` answers, whole, beside the forge's short statuses.
type Verdict struct {
ID string `json:"id,omitempty"`
On string `json:"on,omitempty"`
At time.Time `json:"at"`
Gate string `json:"gate"`
Summary string `json:"summary"`
Modules []string `json:"modules,omitempty"`
Dependents []string `json:"dependents,omitempty"`
Repo string `json:"repo,omitempty"`
RepoSaid string `json:"repo_summary,omitempty"`
Report string `json:"report,omitempty"`
// Head and Trunk are what it judged (novox/hq ADR 0323 §3): the head merged into the trunk commit, twelve
// characters each. Trunk is empty for a verdict of a build seat that judged the head alone. Beside are the
// commits of the other repositories the check read, by directory; Took how long it ran.
Head string `json:"head,omitempty"`
Trunk string `json:"trunk,omitempty"`
Beside map[string]string `json:"beside,omitempty"`
Took string `json:"took,omitempty"`
// Status is the forge status it was read from when it is not the controller's verdict: mesh/check, the checker's
// (novox/hq ADR 0447, issue 1302). Empty for the controller's `checked`.
Status string `json:"status,omitempty"`
}
// Judged is what a verdict judged, in words: "head 1a2b3c4d5e6f merged into trunk 7a8b9c0d1e2f" (ADR 0323 §3);
// empty for a verdict that names no trunk commit.
func (v *Verdict) Judged() string {
if v == nil || v.Trunk == "" {
return ""
}
return fmt.Sprintf("head %s merged into trunk %s", shortOf(v.Head, 12), shortOf(v.Trunk, 12))
}
// outOfDate is why a delivery's verdict no longer counts: it names a trunk commit that is not its trunk's head
// as this owner heard it (novox/hq ADR 0323 §7). Empty when it counts — or names no trunk commit, as a verdict
// of a build seat from before the rule does, which counts as it did.
// approvedAtHead is whether the operator's Approve covers this delivery at its head: once it does, the merge judges that
// Approve alone, not the check (novox/hq ADR 0661 §3, §4).
func (d *Delivery) approvedAtHead() bool {
r := d.Ready
return r != nil && r.Approved != nil && !r.declined() && r.Heads[d.ID] == d.Commit
}
func (d *Delivery) outOfDate() string {
if d.Check == nil || d.Check.Trunk == "" || d.TrunkNow == "" || sameCommit(d.Check.Trunk, d.TrunkNow) {
return ""
}
return outOfDateWords(d.Check.Trunk, d.TrunkNow)
}
// outOfDateWords is a verdict out of date, as the forge's holder says it on the pull request (ADR 0323 §4).
func outOfDateWords(judged, now string) string {
return fmt.Sprintf("out of date: judged against trunk %s, the trunk is now %s; judged again on a recheck or when "+
"it is merged", shortOf(judged, 12), shortOf(now, 12))
}
// CheckAsk is a head's own check, asked of the controller by this owner (or re-asked by healer H2).
type CheckAsk struct {
// ID is the ask's id as the controller answered it.
ID string `json:"id"`
At time.Time `json:"at"`
By string `json:"by"`
Why string `json:"why"`
// Reasked is how many times H2 asked it again: once at most, then the operator's.
Reasked int `json:"reasked,omitempty"`
}
// GateComposed is the gate layer of a group member's own verdict when the group's composed check judges it instead
// (novox/hq ADR 0403 §1): no gate was run for the member alone.
const GateComposed = "composed"
// Passes is whether the verdict lets the delivery be ready: the gate passed or warned, and the
// repository's own check did not fail or error.
func (v *Verdict) Passes() bool {
if v == nil {
return false
}
// A group member's own gate is the group's composed check's (novox/hq ADR 0403 §1): the controller says the layer
// "composed", and the member counts as passed here once its repository's check did not fail, as the composed
// check, asked after every member is ready, then judges them together.
gate := v.Gate == "pass" || v.Gate == "warning" || v.Gate == GateComposed
repo := v.Repo == "" || v.Repo == "pass" || v.Repo == "warning"
return gate && repo
}
// Effect is one thing owed outside the state: an event said, a line appended to a commit's note, the view
// on the pull request refreshed, a status set.
type Effect struct {
Kind string `json:"kind"`
Event string `json:"event,omitempty"` // an emit's module event, when not transition or group (issue 1025)
Commit string `json:"commit,omitempty"`
Line string `json:"line,omitempty"`
Context string `json:"context,omitempty"`
State string `json:"state,omitempty"`
Tries int `json:"tries,omitempty"`
Last string `json:"last,omitempty"`
Since time.Time `json:"since"`
// Next is when a failed effect is tried again: later after every failure, up to retryCap.
Next *time.Time `json:"next,omitempty"`
// ID and Said are the act's id and when it was first said as the event owed: it stays until its owed.ended is heard.
ID string `json:"id,omitempty"`
Said *time.Time `json:"said,omitempty"`
}
// The kinds of effect.
const (
EffectEmit = "emit"
EffectNote = "note"
EffectView = "view"
EffectStatus = "status"
// EffectComment is a comment on the pull request; its Context is the sequence number of the comment on the head, which
// keeps two comments on one head two acts (meshdeliveryclient OwedCommentAct).
EffectComment = "comment"
)
// FromForgesWord is whether the delivery was made from the forge's word at its merge: said by FromForge, or —
// for one made before FromForge was kept — by its first transition.
func (d *Delivery) FromForgesWord() bool {
if d.FromForge {
return true
}
return len(d.Transitions) > 0 && d.Transitions[0].Event == EvAnnounced &&
strings.Contains(d.Transitions[0].Why, "so it is made from the forge's word")
}
func (d *Delivery) shownOnItsPullRequest() bool {
return d.Number > 0
}
// Owner and Repo split the repository.
func (d *Delivery) Owner() string { o, _, _ := strings.Cut(d.Repository, "/"); return o }
func (d *Delivery) Repo() string { _, r, _ := strings.Cut(d.Repository, "/"); return r }
// IDOf is a delivery's id: the repository and twelve characters of its commit.
// IDOf is a pull request's id as a record names its members: the repository and twelve characters of its commit.
func IDOf(repository, commit string) string {
return strings.ToLower(repository) + "@" + shortOf(commit, 12)
}
@@ -280,108 +28,14 @@ func sameCommit(a, b string) bool {
return len(a) >= 7 && strings.HasPrefix(strings.ToLower(b), strings.ToLower(a))
}
// authorOf is a delivery's author as the operator reads it: "the author of <branch>", and who opened its pull request as
// the forge's record says (the router refuses an @ in its words).
func authorOf(d *Delivery) string {
who := "the author of " + d.Branch
if d.Branch == "" {
who = "its author"
}
if d.Author != "" {
who += " (" + strings.Replace(d.Author, "@", " on ", 1) + ")"
}
return who
}
// Group is a delivery group: two or more deliveries sharing a head branch name, one level only.
type Group struct {
ID string `json:"id"`
Members []string `json:"members"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
// Check is its composed check: asked of the controller for these member commits, and its verdict.
Check *GroupCheck `json:"check,omitempty"`
// SaidGate is, by member id, why the member's mesh/merge-gate was last said pending (a group the controller judges;
// a group on mesh/check is owed none, issue 1302): written again only when it
// changes (novox/hq ADR 0403 §1, §6, issue 978). Cleared once a composed verdict stands at the heads, which the
// forge module then sets as every member's gate.
SaidGate map[string]string `json:"said_gate,omitempty"`
// Closed is set once a member reached the trunk: a group delivering takes no new member.
Closed bool `json:"closed,omitempty"`
// Said is the derived state last said, so a change of it is said once.
Said string `json:"said,omitempty"`
// Hold is why the group's merge or its Approve's ask is held back by a member's required status that is not a
// success (issue 1275); nil when nothing is.
Hold *GroupHold `json:"hold,omitempty"`
// SaidStatus is the mesh/delivery-group status last written for it: a change of the status a state takes is
// written too, though the state did not change (novox/hq issue 902).
SaidStatus string `json:"said_status,omitempty"`
// SaidWhy and SaidMembers are the why and the member ids (each names its head) last said: a member joining, leaving or
// pushing, or a count in the why moving, is said too, though the state did not change (2026-10-30: a group of four
// said "3 of 3 member(s) ready" for ten minutes, and the member that joined had no mesh/delivery-group at all).
SaidWhy string `json:"said_why,omitempty"`
SaidMembers string `json:"said_members,omitempty"`
Owed []Effect `json:"owed,omitempty"`
}
// GroupHold is a group held back from its Approve or its merge because a member's required status is not a success.
type GroupHold struct {
// Kind is "unasked" (the Approve is not asked) or "blocked" (the approved merge is not started: nothing is merged).
Kind string `json:"kind"`
Member string `json:"member"`
Says string `json:"says"`
Since time.Time `json:"since"`
}
// GroupCheck is a group's composed check.
type GroupCheck struct {
Asked string `json:"asked"`
At time.Time `json:"at"`
Commits map[string]string `json:"commits"`
Verdict string `json:"verdict,omitempty"`
Summary string `json:"summary,omitempty"`
// Trunks are the trunk commit each member's head was merged into, by member id (novox/hq ADR 0323 §6).
Trunks map[string]string `json:"trunks,omitempty"`
// OutOfDate is why its verdict no longer counts: a merge that is not one of the group's members moved a
// member's trunk, at OutOfDateAt. It counts as outstanding (ADR 0239 §18), and runs again once a member is
// judged again after it.
OutOfDate string `json:"out_of_date,omitempty"`
OutOfDateAt time.Time `json:"out_of_date_at"`
}
// Commits is each member's commit, by id.
func commitsOf(ds []*Delivery) map[string]string {
out := map[string]string{}
for _, d := range ds {
out[d.ID] = d.Commit
}
return out
}
// sortedIDs are the members' ids (repository@head), sorted.
func sortedIDs(ds []*Delivery) []string {
out := make([]string, 0, len(ds))
for _, d := range ds {
out = append(out, d.ID)
}
sort.Strings(out)
return out
}
// sameMembers is whether two member→commit maps are one.
func sameMembers(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if b[k] != v {
return false
}
}
return true
}
func mustJSON(v any) json.RawMessage {
raw, _ := json.Marshal(v)
return raw
}
func clip(s string, n int) string {
if len(s) > n {
return s[:n-1] + "…"
}
return s
}
@@ -1,403 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"regexp"
"slices"
"sort"
"strings"
"time"
_ "time/tzdata" // the operator's zone is known whatever this machine has installed
checker "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// **One required status** (novox/hq ADR 0447, issue 1302; ADR 0503 §3, §5).
//
// Main requires `mesh/check` everywhere but hq: the checker's verdict on a pull request's head merged onto its trunk's
// head, written by its forge bridge. On such a base this owner reads that one status, and nothing of the controller:
//
// - the verdict of a head is its mesh/check, heard as the forge's status event (its words name the head and the trunk
// it judged: "pass <head12> on <trunk12> | …") or read from the forge's open pull requests; it is kept as the
// delivery's Check, marked as mesh/check's, and moves the delivery as the controller's verdict did;
// - the Approve is asked only when the checker's verdict for exactly the heads the ask covers passed (its verdicts
// bucket, keyed by the heads' hash, ADR 0447 §2): a member's pass from a check that judged it alone, before a sibling
// joined, is not the group's;
// - a group's verdict is that one verdict (no composed check is asked, and no member is owed a mesh/merge-gate); a
// member's mesh/check that is no longer a success withdraws the group's ask (ADR 0441 §1, withdrawForStatus);
// - no check is asked of the controller: the checker checks every head on every move, and the trunk's move is its to
// say on mesh/check (ADR 0447 §4). The controller is asked only for a base that requires mesh/repo-check without
// mesh/check (hq), and only what it was asked before (the lead's decision for issue 1302 in R4e).
//
// A base whose required statuses are not known yet is read as before: the forge's open pull requests say them at the next
// look, and a mesh/check heard on the head says it too.
// ctxCheck is the one status main requires (ADR 0447 §1).
const ctxCheck = "mesh/check"
// checkerVerdicts is the checker's verdicts bucket as this module reads it (its manifest's reads).
const checkerVerdicts = "mesh-delivery-checker.verdicts"
// The states of a checker's verdict, as the checker's client names them.
const (
checkerRunning = checker.VerdictRunning
checkerPass = checker.VerdictPass
)
// checkerVerdict is a verdict of the checker as its verdicts bucket keeps it (the seat's verdicts value, read here by
// the fields the gate needs): the group, the hash of the heads it judged, each repository's trunk head and its state.
type checkerVerdict struct {
Group string `json:"group"`
Heads string `json:"heads"`
Trunk map[string]string `json:"trunk,omitempty"`
State string `json:"state"`
Failed struct {
Test string `json:"test,omitempty"`
Machine string `json:"machine,omitempty"`
Words string `json:"words,omitempty"`
} `json:"failed,omitzero"`
// Composition is what the check of the machines' composition made of it (the verdict's composition): its state
// not-run says the composition was not judged.
Composition *struct {
State string `json:"state"`
Words string `json:"words,omitempty"`
} `json:"composition,omitempty"`
}
// CheckerVerdictChanged takes a key of the checker's verdicts bucket (`<group>.<heads hash>`) put or deleted (an empty
// value): the view of the checker's verdicts the Approve and a group's verdict are read from. A value that is no verdict
// is said and dropped.
func (h *Holder) CheckerVerdictChanged(key string, value []byte) {
h.mu.Lock()
defer h.mu.Unlock()
if h.checker == nil {
h.checker = map[string]checkerVerdict{}
}
hash := key
if i := strings.LastIndex(key, "."); i >= 0 {
hash = key[i+1:]
}
if h.runningSince == nil {
h.runningSince = map[string]time.Time{}
}
if len(value) == 0 || string(value) == "null" {
delete(h.checker, hash)
delete(h.runningSince, hash)
h.Poke()
return
}
var v checkerVerdict
if err := json.Unmarshal(value, &v); err != nil || v.State == "" {
h.Logf("[mesh-delivery] the checker's verdict %s cannot be read (%v); it is not taken", key, err)
return
}
if v.Heads == "" {
v.Heads = hash
}
if prev, ok := h.checker[v.Heads]; v.State == checkerRunning && (!ok || prev.State != checkerRunning) {
h.runningSince[v.Heads] = h.Now() // a check starts: its words say since when
} else if v.State != checkerRunning {
delete(h.runningSince, v.Heads)
}
h.checker[v.Heads] = v
h.Poke()
}
// checkWords are the head and the trunk mesh/check's line names: "<head12> on <trunk12>" (ADR 0447 §3).
var checkWords = regexp.MustCompile(`\b([0-9a-f]{12}) on ([0-9a-f]{12})\b`)
// checkLine is the head and the trunk commit a mesh/check line names, twelve characters each; empty when it names none.
func checkLine(desc string) (head, trunk string) {
m := checkWords.FindStringSubmatch(strings.ToLower(desc))
if m == nil {
return "", ""
}
return m[1], m[2]
}
// verdictOfCheck is a delivery's verdict from its head's mesh/check: its state as a verdict, its line as the summary, and
// the head and trunk it names.
func verdictOfCheck(state, desc string, now time.Time) *Verdict {
head, trunk := checkLine(desc)
return &Verdict{At: now, Gate: verdictOfState(state), Summary: firstOf(desc, "mesh/check "+state), Head: head, Trunk: trunk,
Status: ctxCheck}
}
// judgedByCheck is whether the checker judges the delivery: always. Every base requires mesh/check since hq's did (ADR 0527
// §2), and the controller judges no pull request any more (ADR 0524 §5), so nothing is asked of it (issue 1318).
func (d *Delivery) judgedByCheck() bool { return true }
// noteRequired keeps what the forge said the delivery's base requires. Under the lock.
func (h *Holder) noteRequired(d *Delivery, required []string) {
if d == nil || required == nil || slices.Equal(d.Required, required) {
return
}
d.Required = slices.Clone(required)
h.keep(d)
}
// takeCheckStatus takes a head's mesh/check as its delivery's verdict, as the controller's `checked` was taken: a
// proposed delivery moves on it, a rejected one judged again against another trunk or now passing is proposed again
// first. A pending mesh/check is no verdict and changes nothing here (its ask is withdrawn by withdrawForStatus). The same
// verdict heard twice changes nothing. Under the lock.
func (h *Holder) takeCheckStatus(repository, sha, state, desc string) {
d := h.deliveries[IDOf(strings.ToLower(repository), sha)]
if d == nil || state == "" {
return // its announcement makes it; the forge's open pull requests bring the verdict then
}
if d.CheckState != state {
d.CheckState = state
h.keep(d)
}
if state == "pending" {
return
}
if !slices.Contains(d.Required, ctxCheck) {
d.Required = append(d.Required, ctxCheck)
}
now := h.Now()
v := verdictOfCheck(state, desc, now)
if c := d.Check; c != nil && c.Status == ctxCheck && c.Gate == v.Gate && c.Trunk == v.Trunk && c.Summary == v.Summary {
return
}
if d.State == Rejected && d.Check != nil && (v.Passes() || v.Trunk != "" && !sameCommit(d.Check.Trunk, v.Trunk)) {
t, err := Apply(d, EvRecheck, true, Facts{Now: now, By: byOwner, Why: fmt.Sprintf("its mesh/check judged it again: %s",
oneLine(clip(desc, 120)))})
if err != nil {
h.refuse(err)
return
}
h.moved(d, []Transition{t})
}
if d.approvedAtHead() && (d.State == Ready || d.State == Merged) {
// The merge judges the operator's Approve alone (novox/hq ADR 0661 §3): a verdict after it, a carry's, is shown and
// moves nothing; a failure is said (failedAfterApprove).
d.Check = v
h.Logf("[mesh-delivery] %s: its mesh/check is %s — %s (after the operator's Approve)", d.ID, state, oneLine(clip(desc, 120)))
h.failedAfterApprove(d, v)
h.keep(d)
return
}
if d.State != Proposed {
h.keep(d)
return // past its verdict: the merge reads the forge's status itself
}
d.Check, d.AskedAgain, d.CheckAsk = v, nil, nil
h.causeChanged(d)
if why := d.outOfDate(); why != "" {
h.Logf("[mesh-delivery] %s: its mesh/check is %s; the checker checks it again", d.ID, why)
}
h.Logf("[mesh-delivery] %s: its mesh/check is %s — %s", d.ID, state, oneLine(clip(desc, 120)))
h.settleAndKeep(d)
}
// headsOf are the heads the checker judges together for an ask: the members the ask covers.
func headsOf(members []*Delivery) []mdc.Head {
out := make([]mdc.Head, 0, len(members))
for _, m := range members {
out = append(out, mdc.Head{Repository: strings.ToLower(m.Repository), Number: m.Number, Head: strings.ToLower(m.Commit)})
}
return out
}
// checkerAt is the checker's verdict for exactly these members' heads, and their hash.
func (h *Holder) checkerAt(members []*Delivery) (checkerVerdict, string, bool) {
hash, err := mdc.HeadsHash(headsOf(members))
if err != nil {
return checkerVerdict{}, "", false
}
v, ok := h.checker[hash]
return v, hash, ok
}
// checkerWaits is why the Approve of a delivery on mesh/check is not asked yet: the checker's verdict for the heads the ask
// covers is not held, or did not pass. Empty when it passed, for a delivery not judged by mesh/check, and once an ask
// covers the head (the merge then reads the forge's statuses itself). Under the lock.
func (h *Holder) checkerWaits(d *Delivery) string {
if !d.judgedByCheck() || h.covering(d) != nil {
return ""
}
members := h.askMembers(d)
for _, m := range members {
if m.CheckState != "" && m.CheckState != "success" {
return fmt.Sprintf("the mesh/check of %s is %s at its head %s", m.ID, m.CheckState, shortOf(m.Commit, 12))
}
}
v, hash, ok := h.checkerAt(members)
what := "its head"
if len(members) > 1 {
what = fmt.Sprintf("its group's %d heads", len(members))
}
switch {
case !ok:
return fmt.Sprintf("the checker's verdict at %s (heads %s) is not held yet", what, hash)
case v.State == checkerRunning:
return fmt.Sprintf("the checker's verdict at %s (heads %s) is running", what, hash)
case v.State != checkerPass:
return fmt.Sprintf("the checker's verdict at %s (heads %s) is %s", what, hash, v.State)
}
return ""
}
// oneStatusGroup is whether a group is judged by mesh/check: one of its members is.
func oneStatusGroup(members []*Delivery) bool {
return slices.ContainsFunc(members, (*Delivery).judgedByCheck)
}
// composeFromChecker makes a group's verdict on mesh/check from the checker's one verdict at its members' heads (ADR 0447
// §2): no composed check is asked. It is kept in the group's check as a composed verdict was, so the group's state, its
// Approve and its merges read it unchanged: running or not held is no verdict yet, a pass or a failure is the group's, and
// a member whose trunk moved past what it judged puts it out of date. Under the lock.
func (h *Holder) composeFromChecker(g *Group, members []*Delivery, now time.Time) {
live := make([]*Delivery, 0, len(members))
for _, d := range members {
if d.MergedAs == "" && !d.State.Final() {
live = append(live, d)
}
}
if len(live) == 0 || (g.Closed && g.Check != nil && g.Check.Verdict != "") {
return // every member on the trunk, or a closed group: the verdict its Approve was asked on stands
}
v, hash, ok := h.checkerAt(live)
c := &GroupCheck{Asked: "mesh/check " + hash, At: now, Commits: commitsOf(members)}
if g.Check != nil && g.Check.Asked == c.Asked && sameMembers(g.Check.Commits, c.Commits) {
c.At = g.Check.At
}
if ok && v.State != checkerRunning {
c.Verdict = v.State
c.Summary = "the checker's verdict at the members' heads (" + hash + ")"
if w := firstOf(v.Failed.Test, v.Failed.Machine, v.Failed.Words); w != "" {
c.Summary += ": " + w
}
c.Trunks = map[string]string{}
ids := make([]string, 0, len(live))
for _, d := range live {
ids = append(ids, d.ID)
}
sort.Strings(ids)
for _, id := range ids {
d := h.deliveries[id]
judged := v.Trunk[strings.ToLower(d.Repository)]
if judged == "" {
continue
}
c.Trunks[d.ID] = judged
if c.OutOfDate == "" && d.TrunkNow != "" && !sameCommit(judged, d.TrunkNow) {
c.OutOfDate = fmt.Sprintf("the checker's verdict is out of date: %s was judged on trunk %s, the trunk is now %s",
d.ID, shortOf(judged, 12), shortOf(d.TrunkNow, 12))
c.OutOfDateAt = now
}
}
}
if g.Check != nil && g.Check.Asked == c.Asked && g.Check.Verdict == c.Verdict && g.Check.OutOfDate == c.OutOfDate &&
sameMembers(g.Check.Commits, c.Commits) {
return
}
g.Check, g.Updated = c, now
h.keepGroup(g)
if c.Verdict != "" {
for _, d := range members {
h.causeChanged(d) // a composed check that differs from the last: what a refusal waited on changed (issue 1785)
}
h.Logf("[mesh-delivery] group %s: %s — %s", g.ID, c.Verdict, c.Summary)
}
}
// checkerRefusal is why a delivery on mesh/check is asked nothing of the controller: the checker checks it.
func checkerRefusal(id string) error {
return fmt.Errorf("%s is judged by mesh/check, the checker's: the checker checks its head again on every move of the head, "+
"its trunk or its group, and nothing is asked of it here (novox/hq ADR 0447). Read where its check stands with the "+
"checker's checker_pull tool", id)
}
// The states of the check at an ask's heads, as the ask says them (issue 1733).
const (
checkSaidPass = "pass"
checkSaidRunning = "running"
checkSaidNone = "none"
checkSaidFail = "fail"
)
// checkSaid is the state of the check at the heads an ask covers, and its words for the ask: what the checker's verdict at
// exactly those heads says, never what the gate would like it to say (issue 1733). Key is one of the checkSaid states.
type checkSaid struct {
Key string
Words string
}
// passed is whether the check at the heads passed.
func (c checkSaid) passed() bool { return c.Key == checkSaidPass }
// checkAt reads the check at these members' heads: the checker's verdict for exactly those heads (its verdicts bucket,
// keyed by their hash), and, while none is held, the forge's mesh/check at a member's head (a failure or a pending one
// there). "passed its check" is said only for a passing verdict at these heads. Under the lock.
func (h *Holder) checkAt(members []*Delivery) checkSaid {
v, hash, ok := h.checkerAt(members)
if ok {
switch v.State {
case checkerPass:
return checkSaid{Key: checkSaidPass, Words: "passed its check" + notComposed(v)}
case checkerRunning:
words := "its check has no verdict yet: it is running"
if since, held := h.runningSince[hash]; held && !since.IsZero() {
words += " (since " + clockSaid(since) + ")"
}
return checkSaid{Key: checkSaidRunning, Words: words}
default:
why := firstOf(oneLine(v.Failed.Test), oneLine(v.Failed.Words), v.State)
if v.Failed.Test != "" && v.Failed.Words != "" {
why = oneLine(v.Failed.Test) + ": " + oneLine(v.Failed.Words)
}
return checkSaid{Key: checkSaidFail, Words: "its check failed: " + clip(why, 120)}
}
}
for _, m := range members {
switch m.CheckState {
case "failure", "error":
why := "mesh/check is " + m.CheckState + " at " + shortOf(m.Commit, 12)
if m.Check != nil && m.Check.Summary != "" {
why = oneLine(m.Check.Summary)
}
return checkSaid{Key: checkSaidFail, Words: "its check failed: " + clip(why, 120)}
}
}
succeeded := 0
for _, m := range members {
switch m.CheckState {
case "pending":
return checkSaid{Key: checkSaidRunning, Words: "its check has no verdict yet: it is running"}
case "success":
succeeded++
}
}
switch {
case succeeded > 0 && len(members) == 1:
return checkSaid{Key: checkSaidPass, Words: "passed its check"} // its head's mesh/check is the checker's verdict at it
case succeeded == len(members) && succeeded > 0:
return checkSaid{Key: checkSaidNone, Words: "its members' own checks passed; the check of their heads together has not answered"}
}
return checkSaid{Key: checkSaidNone, Words: "its check has no verdict yet"}
}
// notComposed is what a passing verdict adds when its composition was not judged (checker.CompositionNotRun).
// Empty otherwise.
func notComposed(v checkerVerdict) string {
if v.Composition == nil || v.Composition.State != checker.CompositionNotRun {
return ""
}
why := firstOf(oneLine(v.Composition.Words), "it was not run")
return ", but its composition was not judged (" + why + ")"
}
// operatorZone is the zone the operator reads times in, as the messenger says them (its time-zone setting).
var operatorZone = func() *time.Location {
if loc, err := time.LoadLocation("Europe/Brussels"); err == nil {
return loc
}
return time.UTC
}()
// clockSaid is a time of day as the operator reads it: in their zone, named ("00:35 CET").
func clockSaid(t time.Time) string { return t.In(operatorZone).Format("15:04 MST") }
@@ -1,79 +0,0 @@
//go:build integration
package main
import (
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"context"
"encoding/json"
"strings"
"testing"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// **Issue 1302's acceptance over a real bus** (ADR 0447, 1302-S10): a pull request into a main that requires mesh/check goes
// ready on its mesh/check alone — the forge's status event and the checker's verdict in its verdicts bucket — has its Approve
// asked, and on the Approve its merge is handed to the bridge; nothing is asked of the controller (no delivery-check, no
// forget-checks) and no controller verdict is heard at all.
func TestADeliveryOnMeshCheckGoesReadyAskedAndMergedOverARealBus(t *testing.T) {
b := newTestBus(t)
ctx := context.Background()
rt := b.start(t)
evctx, cancel := context.WithCancel(ctx)
defer cancel()
rt.deliverEvents(evctx)
rt.until("the module to subscribe to the events it follows", 20*time.Second, func() bool {
rt.mu.Lock()
defer rt.mu.Unlock()
return len(rt.subs) >= len(followed)
})
rt.untilListening()
const head = "2222222222222222222222222222222222222222"
const trunk = "3333333333333333333333333333333333333333"
rt.mu.Lock()
rt.openPulls = []OpenPull{{Owner: "novox", Repo: "app", Number: 8, Base: "main", HeadSHA: head, Required: []string{ctxCheck},
Statuses: []string{}, Checks: map[string]HeadCheck{}}}
rt.mu.Unlock()
announced := pr("novox/app", 8, head, "fix/y", "src/b.go")
b.publish(t, "mesh.seat.git.event.pull-updated", seatUpdated(announced))
// The checker's verdict at the head, then the forge's word that its bridge set mesh/check.
hash, err := mdc.HeadsHash([]mdc.Head{{Repository: "novox/app", Number: 8, Head: head}})
if err != nil {
t.Fatal(err)
}
verdict, _ := json.Marshal(map[string]any{"v": 1, "group": "fix/y", "heads": hash, "trunk": map[string]string{"novox/app": trunk},
"state": "pass"})
if _, err := b.kvs[checkerVerdicts].Put(ctx, "fix/y."+hash, verdict); err != nil {
t.Fatal(err)
}
line := "pass " + head[:12] + " on " + trunk[:12] + " | unit 3 run, 0 kept"
rt.mu.Lock()
rt.openPulls[0].Statuses = []string{ctxCheck}
rt.openPulls[0].Checks = map[string]HeadCheck{ctxCheck: {State: "success", Description: line}}
rt.mu.Unlock()
set := b.publish(t, "mesh.seat.git.event.forge-status", gitseat.ForgeStatusEvent{Repository: "novox/app", Commit: head,
Context: ctxCheck, State: "success", Description: line})
var key string
rt.until("the ask to be written on mesh/check alone", 10*time.Second, func() bool { key = onlyAsk(b); return key != "" })
t.Logf("the ask was written %s after mesh/check was set", time.Since(set))
b.answer(t, key, OptionApprove)
var handed Merge
rt.until("the merge to be handed to the bridge", 10*time.Second, func() bool { handed = mergeAt(b, "fix/y"); return len(handed.Pulls) > 0 })
if len(handed.Pulls) != 1 || handed.Pulls[0].Head.Head != head || handed.Pulls[0].State != mdc.MergeMerging {
t.Fatalf("the merge handed is %+v", handed)
}
rt.noControllerAsk()
// No mesh/merge-gate is owed on the head.
keys, _ := b.kvs["owed"].Keys(ctx)
for _, k := range keys {
if strings.Contains(k, "merge-gate") {
t.Fatalf("a mesh/merge-gate was owed: %s", k)
}
}
}
@@ -1,186 +0,0 @@
package main
import (
"encoding/json"
"strings"
"testing"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// The tests of issue 1302 (ADR 0447 §2, §4; ADR 0441 §1; ADR 0421 §1, §3; lead decisions L6 and the gate's part of
// 1318): on a base that requires mesh/check the gate reads that one status. A delivery is ready, its Approve asked and its
// merge handed on when mesh/check passed at its head and the checker's verdict covers every member's head as they are; no
// check is asked of the controller, no composed check is asked, and no mesh/merge-gate is owed.
const (
oneRepo, oneRepo2 = "novox/mesh-sdk", "novox/mesh-delivery"
oneHead, oneHead2 = "e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5e5", "f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6"
oneTrunk = "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
oneTrunk2 = "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"
)
// requiresCheck makes the forge say each pull request's base requires mesh/check alone, as gitea_open_pulls answers.
func requiresCheck(w *world, pulls ...PullEvent) {
w.forge.mu.Lock()
defer w.forge.mu.Unlock()
w.forge.open = nil
for _, p := range pulls {
w.forge.open = append(w.forge.open, OpenPull{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Base: "main",
HeadSHA: p.HeadSHA, Required: []string{ctxCheck}, Statuses: []string{}, Checks: map[string]HeadCheck{}})
}
}
// checkStatus is the forge's word that mesh/check was set on a head, with its line as the checker writes it.
func checkStatus(repository, head, state, trunk string) ForgeStatusEvent {
owner, repo, _ := strings.Cut(repository, "/")
words := map[string]string{"success": "pass", "pending": "checking", "failure": "failed: a test"}[state]
return ForgeStatusEvent{Owner: owner, Repo: repo, SHA: head, Context: ctxCheck, State: state,
Description: words + " " + head[:12] + " on " + trunk[:12] + " | unit 4 run, 0 kept"}
}
// putCheckerVerdict puts the checker's verdict for these heads in the gate's view of the checker's verdicts bucket.
func putCheckerVerdict(w *world, state string, heads ...mdc.Head) {
w.t.Helper()
hash, err := mdc.HeadsHash(heads)
if err != nil {
w.t.Fatal(err)
}
trunk := map[string]string{}
for _, h := range heads {
trunk[h.Repository] = oneTrunk
}
value, _ := json.Marshal(map[string]any{"v": 1, "group": "g", "heads": hash, "trunk": trunk, "state": state})
w.h.CheckerVerdictChanged("g."+hash, value)
}
func headOf(repository string, number int, head string) mdc.Head {
return mdc.Head{Repository: repository, Number: number, Head: head}
}
func mergeGateOwed(w *world) []string {
w.forge.mu.Lock()
defer w.forge.mu.Unlock()
var out []string
for k := range w.forge.statuses {
if strings.HasSuffix(k, " mesh/merge-gate") {
out = append(out, k)
}
}
return out
}
// 1302 acceptance: the gate's table moves a lone delivery to ready on mesh/check alone, asks its Approve and merges it,
// with no check asked of the controller and no mesh/merge-gate written.
func TestALoneDeliveryGoesReadyAskedAndMergedOnMeshCheckAlone(t *testing.T) {
w := newWorld(t)
p := pr(oneRepo, 1, oneHead, "feat/one")
requiresCheck(w, p)
w.h.PullUpdated(p)
w.settleAll()
id := IDOf(oneRepo, oneHead)
if s := w.state(id); s != Proposed {
t.Fatalf("before mesh/check it is %s, want proposed", s)
}
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
w.settleAll()
if s := w.state(id); s == Proposed || s == Rejected {
t.Fatalf("after mesh/check passed it is %s (%s), want ready or on", s, w.logs())
}
if d := w.delivery(id); d.Check == nil || d.Check.Status != ctxCheck || d.Check.Trunk != oneTrunk[:12] || d.Check.Head != oneHead[:12] {
t.Fatalf("the verdict is not mesh/check's: %+v", d.Check)
}
if w.op.count() != 1 {
t.Fatalf("its Approve was asked %d times, want once", w.op.count())
}
if len(w.forge.merges) != 1 || !strings.Contains(w.forge.merges[0], "mesh-sdk#1") {
t.Fatalf("merges %v, want mesh-sdk#1", w.forge.merges)
}
if got := mergeGateOwed(w); len(got) != 0 {
t.Fatalf("mesh/merge-gate was written: %v", got)
}
}
// 1302 (lead: the verdict read must be the group's): mesh/check passing on a head is not enough while the checker's
// verdict for exactly the delivery's heads is not held: the Approve waits for it, and says so.
func TestTheApproveWaitsForTheCheckersVerdictAtTheDeliverysHeads(t *testing.T) {
w := newWorld(t)
p := pr(oneRepo, 1, oneHead, "feat/one")
requiresCheck(w, p)
w.h.PullUpdated(p)
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
w.settleAll()
if w.op.count() != 0 {
t.Fatalf("the Approve was asked with no checker verdict at the head")
}
if d := w.delivery(IDOf(oneRepo, oneHead)); d.Ready == nil || !strings.Contains(d.Ready.Waits, "checker") {
t.Fatalf("it does not say it waits for the checker's verdict: %+v", d.Ready)
}
putCheckerVerdict(w, "running", headOf(oneRepo, 1, oneHead))
w.settleAll()
if w.op.count() != 0 {
t.Fatalf("the Approve was asked on a running verdict")
}
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
w.settleAll()
if w.op.count() != 1 {
t.Fatalf("the Approve was asked %d times after the checker's pass, want once", w.op.count())
}
}
// 1302 acceptance (ADR 0447 §2, ADR 0441 §1): a group on mesh/check is asked once when every member's mesh/check passed
// and the checker's verdict names every member's head; a member's mesh/check that changes withdraws the ask. No composed
// check is asked of the controller and no member is owed a mesh/merge-gate.
func TestAGroupOnMeshCheckIsAskedOnTheCheckersOneVerdictAndWithdrawnWhenAMemberChanges(t *testing.T) {
w := newWorld(t)
w.op.auto = false
a, b := pr(oneRepo, 1, oneHead, "feat/two"), pr(oneRepo2, 2, oneHead2, "feat/two")
requiresCheck(w, a, b)
w.h.PullUpdated(a)
w.h.PullUpdated(b)
w.h.wantForge() // as the event's handler does for a head not seen before (main.go)
w.settleAll()
// One member's verdict alone, of a check that judged it by itself: not the group's.
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead))
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "success", oneTrunk))
w.h.CommitStatus(checkStatus(oneRepo2, oneHead2, "success", oneTrunk))
w.settleAll()
if w.op.count() != 0 {
t.Fatalf("the group was asked on a verdict that does not name every member's head")
}
putCheckerVerdict(w, "pass", headOf(oneRepo, 1, oneHead), headOf(oneRepo2, 2, oneHead2))
w.settleAll()
if w.op.count() != 1 || len(w.op.open()) != 1 {
t.Fatalf("the group was asked %d times (open %v), want once", w.op.count(), w.op.open())
}
if got := mergeGateOwed(w); len(got) != 0 {
t.Fatalf("mesh/merge-gate was owed: %v", got)
}
w.h.CommitStatus(checkStatus(oneRepo2, oneHead2, "pending", oneTrunk2))
w.settleAll()
if len(w.op.open()) != 0 {
t.Fatalf("the group's ask stands though a member's mesh/check changed: %v", w.op.open())
}
}
// 1302: a failing mesh/check rejects the delivery; nothing is asked.
func TestAFailingMeshCheckRejects(t *testing.T) {
w := newWorld(t)
p := pr(oneRepo, 1, oneHead, "feat/one")
requiresCheck(w, p)
w.h.PullUpdated(p)
w.h.CommitStatus(checkStatus(oneRepo, oneHead, "failure", oneTrunk))
w.settleAll()
if s := w.state(IDOf(oneRepo, oneHead)); s != Rejected {
t.Fatalf("it is %s, want rejected", s)
}
}
// 1318 (the gate's part): a pull request closed unmerged asks the controller to forget nothing: forget-checks is not called.
func TestAClosedPullRequestAsksNoForgetChecks(t *testing.T) {
w := newWorld(t)
p := pr(oneRepo, 1, oneHead, "feat/one")
w.h.PullUpdated(p)
w.h.PullClosed(p)
}
@@ -1,121 +0,0 @@
package main
import (
"fmt"
"sync"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
)
// fakeOperator is the gate's bucket asks and the teller behind it: it keeps the asks written, and answers as the test says
// (the teller's answer under the ask's key). In auto mode it approves every ask the moment it is read (the tests of other
// behaviour, which are not about the Approve). An ask is known here by its key (meshdeliveryclient MergeAskKey), as the
// delivery keeps it.
type fakeOperator struct {
mu sync.Mutex
now func() time.Time
auto bool
asked []oc.Ask
byKey map[string]Ask
order []string
seen map[string]bool
cancelled []string
state map[string]string
answers map[string]*Answer
refuse error
}
func newFakeOperator(now func() time.Time) *fakeOperator {
return &fakeOperator{now: now, byKey: map[string]Ask{}, seen: map[string]bool{}, state: map[string]string{}, answers: map[string]*Answer{}}
}
func (o *fakeOperator) Ask(key string, a Ask) error {
o.mu.Lock()
defer o.mu.Unlock()
if o.refuse != nil {
return o.refuse
}
ask, err := askOf(a)
if err != nil {
return err
}
if err := occ.Check(ask, o.now()); err != nil {
return err
}
if _, err := EncodeAsk(a); err != nil {
return err
}
if o.state[key] == "open" && SameAsk(o.byKey[key], a) {
return nil // the same ask is there, as kvOperator leaves it
}
o.asked = append(o.asked, ask)
o.byKey[key] = a
if _, known := o.seen[key]; !known {
o.order = append(o.order, key)
o.seen[key] = true
}
o.state[key] = "open"
delete(o.answers, key)
return nil
}
func (o *fakeOperator) Cancel(key string) error {
o.mu.Lock()
defer o.mu.Unlock()
o.cancelled = append(o.cancelled, key)
o.state[key] = "cancelled"
return nil
}
func (o *fakeOperator) Where(key string) (*Answer, error) {
o.mu.Lock()
defer o.mu.Unlock()
if o.auto && o.state[key] == "open" {
o.answerLocked(key, OptionApprove)
}
return o.answers[key], nil
}
// ask is the router's ask at a key, as the gate composed it.
func (o *fakeOperator) ask(key string) oc.Ask {
if a, ok := o.byKey[key]; ok {
ask, err := askOf(a)
if err != nil {
panic(err)
}
return ask
}
panic("no ask " + key)
}
func (o *fakeOperator) answerLocked(key, option string) {
a := o.ask(key)
opt, _ := occ.OptionOf(a, option)
o.state[key] = "answered"
o.answers[key] = &Answer{Option: option, Warrant: fmt.Sprintf("%s@%s", a.ID, o.now().UTC().Format(time.RFC3339)),
Words: "the operator, via telegram, chose " + opt.Label}
}
// answer is the operator's choice, proven to the teller by the router and written by it.
func (o *fakeOperator) answer(key, option string) {
o.mu.Lock()
defer o.mu.Unlock()
o.answerLocked(key, option)
}
// open is the keys of the asks waiting for an answer, in the order asked.
func (o *fakeOperator) open() []string {
o.mu.Lock()
defer o.mu.Unlock()
var out []string
for _, k := range o.order {
if o.state[k] == "open" {
out = append(out, k)
}
}
return out
}
func (o *fakeOperator) count() int { o.mu.Lock(); defer o.mu.Unlock(); return len(o.asked) }
@@ -2,10 +2,8 @@ package main
import (
"errors"
"fmt"
"hash/fnv"
"strconv"
"strings"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
@@ -13,16 +11,12 @@ import (
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The gate writes nothing to the forge: each act a transition owes it — a status, the view on the pull request, a line of
// the commit's note, a comment — is said as the event owed of the seat mesh-delivery, on
// The gate writes nothing to the forge: each act it owes it — the delivery's view on the pull request, a comment, a line of
// the commit's note — is said as the event owed of the seat mesh-delivery, on
// `mesh.seat.mesh-delivery.event.owed.<repository>.<number>`, under one id, which the forge bridge performs through the
// seat git and answers as owed.ended (design 47 *What the gate owes the forge*). The act stays on its delivery's or group's
// state, already on the bus, until its owed.ended is heard; at the gate's start every act still unanswered is said again
// under the same id, and the forge holds it once. The number is 0 for a commit no pull request names; a comment's act
// carries a sequence number, so two comments on one head are two acts.
// noPage is the target of a status whose delivery has no page to point at.
const noPage = "none"
// seat git and answers as owed.ended (design 47 *What the gate owes the forge*, §167). The act is kept in its delivery record's
// acts, with when it was said, before it is said, and stays there until its owed.ended is heard (A-35). A comment's act carries a
// sequence number, so two comments on one head are two acts.
// notesRef is the notes ref the commit's note is kept under.
const notesRef = "mesh-plan"
@@ -40,40 +34,33 @@ func (s seatOwed) Say(o seat.Owed) error {
}
// actID is the id of an act owed: its key, when it was first owed and a digest of what it says, so it is the same each time
// it is said again and two acts of one key (a status set twice, two lines of the note in one moment) are two ids. It holds
// an @, which a switched act's id never holds.
// it is said again and two acts of one key are two ids. It holds an @, which a switched act's id never holds.
func actID(key string, since time.Time, says string) string {
d := fnv.New32a()
_, _ = d.Write([]byte(says))
return key + "@" + strconv.FormatInt(since.UnixNano(), 10) + "." + strconv.FormatUint(uint64(d.Sum32()), 16)
}
// actSays is what an effect says, as its id's digest reads it.
func actSays(e Effect) string { return e.Kind + "\n" + e.Context + "\n" + e.State + "\n" + e.Line }
// owe says one owed act under its id. An act that has no key (a name the key refuses, a status with no page to point at)
// is said as refused and dropped (an empty id), since saying it again would fail the same.
func (h *Holder) owe(repository string, number int, head, act string, o Owed, since time.Time, says, want string) (id string, err error) {
if h.Owed == nil {
return "", errors.New("no seat to say owed on")
// owe keeps an act in the record, with when it was said, and then says it. An act the record holds already is said again under
// its id and its first said time, never kept twice.
func (h *Holder) owe(r *Record, o seat.Owed) error {
if h.Owed == nil || h.Records == nil {
return errors.New("no seat to say owed on")
}
key, err := mdc.OwedKey(repository, number, head, act)
if err == nil {
o.Act = actKind(act)
_, err = mdc.EncodeOwed(o)
now := h.Now().UTC()
if had, ok := r.Acts[o.ID]; ok && had.Owed.Said != nil {
o.Said = had.Owed.Said
} else {
o.Said = &now
}
if err != nil {
h.Refuse(fmt.Errorf("the act %q on %s#%d at %s cannot be owed and is dropped: %v", act, repository, number, shortOf(head, 12), err))
return "", nil
if err := h.Records.Act(r, RecordAct{ID: o.ID, Act: o.Act, Repository: o.Repository, Number: int(o.Number), Head: o.Head,
At: *o.Said, Owed: o}); err != nil {
return err
}
if id = actID(key, since, says); want != "" {
id = want
}
said := since.UTC()
return id, h.Owed.Say(seatOwedOf(o, id, repository, number, head, &said))
return h.Owed.Say(o)
}
// seatOwedOf is an act in the gate's working form as the seat's event owed says it.
// seatOwedOf is an act in the old form (the state owed's value) as the seat's event owed says it.
func seatOwedOf(o Owed, id, repository string, number int, head string, said *time.Time) seat.Owed {
out := seat.Owed{ID: id, Repository: repository, Number: int64(number), Head: head, Said: said, Text: o.Text}
switch o.Act {
@@ -89,73 +76,3 @@ func seatOwedOf(o Owed, id, repository string, number int, head string, said *ti
}
return out
}
// owedSaid is an effect said as owed, as do says it: the act its id names, with its repository, number and head (for a
// group's status, the member's its Last names). False for an effect that is not said as owed, or has no id yet.
func owedSaid(e Effect, repository string, number int, head, body, url string, group bool) (seat.Owed, bool) {
if e.ID == "" || e.Kind == EffectEmit || e.Kind == EffectView && number == 0 {
return seat.Owed{}, false
}
page := url
if group && e.Kind == EffectStatus {
repository, number, page = memberOfLast(e.Last)
head = e.Commit
}
act, value, num, hd := owedOf(e, repository, number, head, body, page)
if act == "" {
return seat.Owed{}, false
}
value.Act = actKind(act)
return seatOwedOf(value, e.ID, repository, num, hd, e.Said), true
}
// actKind is the kind of an owed act: `status=<context>` is a status, `comment.<seq>` a comment.
func actKind(act string) string {
kind, _, _ := mdc.ParseOwedAct(act)
return kind
}
// statusAct is the act of a status of a context.
func statusAct(context string) string { return mdc.OwedStatus + "=" + context }
// commentAct is the act of the seq-th comment on a head.
func commentAct(seq int) string {
act, _ := mdc.OwedCommentAct(seq)
return act
}
// owedOf is an effect as the act its key names and its value: the repository and number are the delivery's (or, for a
// group's status, the member's the effect names in Last), the body the view's.
func owedOf(e Effect, repository string, number int, head, body, page string) (act string, value Owed, num int, hd string) {
num, hd = number, head
switch e.Kind {
case EffectNote:
hd = e.Commit
if e.Commit != head {
num = 0 // the merge commit is named by no pull request
}
return mdc.OwedNote, Owed{Text: e.Line, Ref: notesRef}, num, hd
case EffectView:
return mdc.OwedView, Owed{Text: body}, num, hd
case EffectStatus:
// The owed status must name where it points; a delivery with no pull request page points at none, which the forge's
// tool leaves out (it links only an http or https address).
if page == "" {
page = noPage
}
return statusAct(e.Context), Owed{Context: e.Context, State: e.State, Text: e.Line, Target: page}, num, e.Commit
case EffectComment:
seq, _ := strconv.Atoi(e.Context)
return commentAct(seq), Owed{Text: e.Line}, num, hd
}
return "", Owed{}, 0, ""
}
// memberOfLast reads the repository, pull request number and page a group's status effect names in Last
// (`owner/repo#N page`).
func memberOfLast(last string) (repository string, number int, page string) {
where, page, _ := strings.Cut(last, " ")
repository, n, _ := strings.Cut(where, "#")
number, _ = strconv.Atoi(n)
return repository, number, page
}
@@ -1,129 +0,0 @@
package main
import (
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
)
// W11.17 (design 47 *What the gate owes the forge*): every act owed is said as the seat's event owed under one id and stays
// on its delivery's state until its owed.ended is heard; at the owner's start every unanswered act is said again under the
// same id; an outcome is written into the record holding the pull request before the act is answered, once; an outcome
// other than performed is said; the checker's outcomes are not the gate's.
func TestAnActOwedStaysUntilItsOutcomeIsRecorded(t *testing.T) {
w := newWorld(t)
w.owed.silent = true
l := newLedgerFake(t)
id := IDOf("novox/app", head)
w.h.Records = &Records{Ledger: l, Now: w.h.Now}
if _, _, err := w.h.Records.Open("feat/x", map[string]string{id: head}, false); err != nil {
t.Fatal(err)
}
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
w.settleAll()
w.settleAll()
held := map[string]bool{}
for _, e := range w.delivery(id).Owed {
if e.Said != nil && e.ID != "" {
held[e.ID] = true
}
}
if len(held) < 3 {
t.Fatalf("the acts said and held on the delivery: %v; its owed %+v", held, w.delivery(id).Owed)
}
saidOnce := map[string]int{}
for _, o := range w.owed.said {
saidOnce[o.ID]++
if !held[o.ID] && strings.Contains(o.ID, ".view@") {
t.Errorf("the view %s was said but is not held", o.ID)
}
}
for heldID := range held {
if saidOnce[heldID] != 1 {
t.Errorf("%s was said %d times before any answer, want once", heldID, saidOnce[heldID])
}
}
// Unanswered, it is stalled only past 15 minutes, naming the oldest act.
if lines := stalledOf(w, id); len(lines) != 0 {
t.Fatalf("stalled before its bound: %+v", lines)
}
w.later(16 * time.Minute)
if lines := stalledOf(w, id); len(lines) != 1 || !strings.Contains(lines[0].Says, "has no outcome from the forge bridge") ||
lines[0].Bound != "15m0s" {
t.Fatalf("an act unanswered past 15 minutes is not stalled: %+v", lines)
}
// The owner starts again over the same state: every unanswered act is said again, under the same id.
w2 := newWorldOver(t, w.store, w.forge, w.now)
w2.owed.silent = true
w2.h.Records = &Records{Ledger: l, Now: w2.h.Now}
w2.h.Flush()
again := map[string]bool{}
for _, o := range w2.owed.said {
again[o.ID] = true
}
for heldID := range held {
if !again[heldID] {
t.Errorf("%s was not said again at the start; said %v", heldID, again)
}
}
w2.h.Flush()
if len(w2.owed.said) != len(again) {
t.Errorf("a second flush said %d acts again, want none", len(w2.owed.said)-len(again))
}
var one string
for heldID := range held {
one = heldID
break
}
// The checker's outcome of an id is not the gate's: nothing is answered.
checkers, _ := forgemsg.EncodeOwedEnded(forgemsg.OwedEnded{Of: "mesh-delivery-checker", ID: one, Outcome: forgemsg.OwedPerformed, At: w.now})
if err := w2.h.OwedEnded(checkers); err != nil || !owedHolds(w2, id, one) {
t.Fatalf("the checker's outcome answered the gate's act: %v", err)
}
// The ledger away: the act is not answered, and the event is heard again.
l.failOn = OpAct
if err := w2.h.OwedEnded(owedEndedBody(one, forgemsg.OwedRefused, "the branch is protected")); err == nil || !owedHolds(w2, id, one) {
t.Fatalf("an outcome the record could not take answered the act: %v", err)
}
l.failOn = ""
if err := w2.h.OwedEnded(owedEndedBody(one, forgemsg.OwedRefused, "the branch is protected")); err != nil || owedHolds(w2, id, one) {
t.Fatalf("the refused outcome did not answer the act: %v", err)
}
records, _ := w2.h.Records.Current()
if a := records[0].Acts[one]; a.Outcome != forgemsg.OwedRefused || a.Words != "the branch is protected" || a.Head != head || a.Number != 7 {
t.Fatalf("the record holds the act as %+v", a)
}
if !strings.Contains(w2.logs(), "was refused: the branch is protected") {
t.Errorf("a refused act was not said: %s", w2.logs())
}
seq := records[0].Sequence
if err := w2.h.OwedEnded(owedEndedBody(one, forgemsg.OwedRefused, "the branch is protected")); err != nil || records[0].Sequence != seq {
t.Errorf("an outcome heard twice changed the record (%d, was %d): %v", records[0].Sequence, seq, err)
}
}
// stalledOf are the stalled lines naming a delivery that say an act has no outcome.
func stalledOf(w *world, id string) []StalledLine {
var out []StalledLine
for _, l := range w.h.Stalled() {
if l.ID == id && strings.Contains(l.Says, "owed to the forge") {
out = append(out, l)
}
}
return out
}
// owedHolds is whether the delivery still holds the act unanswered.
func owedHolds(w *world, id, act string) bool {
for _, e := range w.delivery(id).Owed {
if e.ID == act {
return true
}
}
return false
}
@@ -2,7 +2,6 @@ package main
import (
"fmt"
"strings"
"time"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
@@ -13,10 +12,10 @@ import (
// the forge*): the outcome of one id, the gate's own or the checker's. Its owner is the one the manifest consumes it from.
var owedEndedEvent = declared(forgemsg.OwedEndedEvent)
// OwedEnded takes the forge bridge's outcome of one act this owner said as owed. The outcome is written into the delivery
// record that holds the pull request at that head, and only then is the act answered: taken off its delivery's or group's
// state. An outcome heard twice, or of an id this owner holds no longer, changes nothing. An outcome other than performed
// is said. An error leaves the event to be heard again: the record could not take it.
// OwedEnded takes the forge bridge's outcome of one act this owner said as owed, and writes it into the delivery record that
// holds the act (A-35): an act is answered only once its outcome is recorded. An outcome heard twice, or of an id no record
// holds, changes nothing. An outcome other than performed is said. An error leaves the event to be heard again: the record
// could not take it.
func (h *Holder) OwedEnded(body []byte) error {
e, err := forgemsg.DecodeOwedEnded(body)
if err != nil {
@@ -30,104 +29,31 @@ func (h *Holder) OwedEnded(body []byte) error {
h.Refuse(fmt.Errorf("the forge bridge could not read an act this owner said: %s", e.Words))
return nil
}
h.mu.Lock()
held, ok := h.heldAct(e.ID)
h.mu.Unlock()
if !ok {
return nil
}
if e.Outcome != forgemsg.OwedPerformed {
h.Refuse(fmt.Errorf("%s: the %s owed on %s#%d at %s was %s: %s", held.owner, held.effect.Kind, held.repository,
held.number, shortOf(held.head, 12), e.Outcome, e.Words))
}
if err := h.recordAct(held, e); err != nil {
return err
}
h.mu.Lock()
defer h.mu.Unlock()
h.answer(e.ID)
return nil
}
// heldAct is an act said and not yet answered, with where it is held. Under the lock.
type heldAct struct {
owner string // the delivery's or the group's id
effect Effect
repository, head string
number int
}
func (h *Holder) heldAct(id string) (heldAct, bool) {
for did, d := range h.deliveries {
for _, o := range d.Owed {
if o.ID == id {
head := d.Commit
if o.Commit != "" {
head = o.Commit
}
return heldAct{owner: did, effect: o, repository: d.Repository, head: head, number: d.Number}, true
}
}
}
for gid, g := range h.groups {
for _, o := range g.Owed {
if o.ID == id {
repository, number, _ := memberOfLast(o.Last)
return heldAct{owner: gid, effect: o, repository: repository, head: o.Commit, number: number}, true
}
}
}
return heldAct{}, false
}
// answer takes an answered act off its delivery's or group's state. Under the lock.
func (h *Holder) answer(id string) {
drop := func(list []Effect) ([]Effect, bool) {
for i, o := range list {
if o.ID == id {
return append(list[:i:i], list[i+1:]...), true
}
}
return list, false
}
for _, d := range h.deliveries {
var dropped bool
if d.Owed, dropped = drop(d.Owed); dropped {
h.keep(d)
}
}
for _, g := range h.groups {
var dropped bool
if g.Owed, dropped = drop(g.Owed); dropped {
h.keepGroup(g)
}
}
}
// recordAct writes an act's outcome into the delivery record that holds its pull request at its head; none holds it
// before the Approve's merge is handed (switch.go), and then the outcome is on its delivery's transitions alone.
func (h *Holder) recordAct(a heldAct, e forgemsg.OwedEnded) error {
if h.Records == nil {
return nil
}
current, err := h.Records.Current()
kept, err := h.Records.Kept()
if err != nil {
return err
}
prefix := strings.ToLower(a.repository) + "@"
for _, r := range current {
for member, head := range r.Heads {
if head == a.head && strings.HasPrefix(strings.ToLower(member), prefix) {
at := e.At.UTC()
return h.Records.Act(r, RecordAct{ID: e.ID, Act: a.effect.Kind, Repository: a.repository, Number: a.number,
Head: a.head, Outcome: e.Outcome, Words: e.Words, At: at})
}
for _, r := range kept {
held, ok := r.Acts[e.ID]
if !ok {
continue
}
if e.Outcome != forgemsg.OwedPerformed {
h.Refuse(fmt.Errorf("%s: the %s owed on %s#%d at %s was %s: %s", r.Instance, held.Act, held.Repository, held.Number,
shortOf(held.Head, 12), e.Outcome, e.Words))
}
held.Outcome, held.Words, held.At = e.Outcome, e.Words, e.At.UTC()
return h.Records.Act(r, held)
}
return nil
}
// RecordAct is one act the gate owed the forge for a pull request the record holds, with the forge bridge's outcome.
// RecordAct is one act the gate owes the forge for a pull request a record holds: said, with when (At, while Outcome is empty)
// and as the seat's event says it (Owed, which saying it again at the gate's start repeats), then ended with the forge
// bridge's outcome and when (A-35, design 47 §165).
type RecordAct struct {
ID string `json:"id"`
Act string `json:"act"`
@@ -137,9 +63,10 @@ type RecordAct struct {
Outcome string `json:"outcome"`
Words string `json:"words,omitempty"`
At time.Time `json:"at"`
Owed seat.Owed `json:"owed"`
}
// Act writes one act's outcome into the record, unless it holds that outcome already.
// Act writes one act into the record: said (no outcome), or ended; nothing when the record holds it so already.
func (rs *Records) Act(r *Record, a RecordAct) error {
if had, ok := r.Acts[a.ID]; ok && had.Outcome == a.Outcome {
return nil
@@ -158,44 +85,3 @@ func (rs *Records) Act(r *Record, a RecordAct) error {
// owedBound is how long an act said may wait for its owed.ended before it is stalled: many times a forge call's time, and
// short beside the check's hour (design 47 *What the gate owes the forge*).
const owedBound = 15 * time.Minute
// owedLines are the stalled lines of the acts said and unanswered past owedBound: one per delivery or group, naming its
// oldest such act and how many more wait. Under the lock.
func (h *Holder) owedLines(now time.Time) []StalledLine {
var out []StalledLine
line := func(id string, state State, number int, owed []Effect) {
var oldest *Effect
more := 0
for i, e := range owed {
if e.Said == nil || now.Sub(*e.Said) < owedBound {
continue
}
if oldest == nil || e.Said.Before(*oldest.Said) {
if oldest != nil {
more++
}
oldest = &owed[i]
} else {
more++
}
}
if oldest == nil {
return
}
says := fmt.Sprintf("the %s owed to the forge, said at %s as %s, has no outcome from the forge bridge", oldest.Kind,
oldest.Said.UTC().Format(time.RFC3339), oldest.ID)
if more > 0 {
says += fmt.Sprintf(", nor have %d more", more)
}
out = append(out, StalledLine{ID: id, Number: number, State: state, For: now.Sub(*oldest.Said).Round(time.Second).String(),
Bound: owedBound.String(), H2: "none: the forge bridge's bridge_owed reads what it performed — the operator's",
Says: says + "; it is said again at the gate's start"})
}
for id, d := range h.deliveries {
line(id, d.State, d.Number, d.Owed)
}
for id, g := range h.groups {
line(id, "", 0, g.Owed)
}
return out
}
@@ -1,107 +0,0 @@
package main
import (
"encoding/json"
"fmt"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// What the gate reads of the teller and of the delivery view it reads through their owners' events and tools, never their
// state (ADR 0596 §2-§4): the teller says `answered` after each answer it writes and answers its answers through
// `teller_answers`; the view says `reached` after each reach it writes and answers its reach through `view_reach`. An event
// asks a look (the answers) or takes the reach it carries; the start reads what was said while the gate was away, once.
// The tools the gate asks (the manifest invokes them).
const (
tellerAnswersTool = tellerModule + ".teller_answers"
viewReachTool = viewModule + ".view_reach"
)
// The events the gate hears: `<owner>.<event>`, as the manifest consumes them.
var (
answeredEvent = declared("answered")
reachedEvent = declared("reached")
)
// answersOf reads the teller's answer to an ask through its tool: nil when the teller kept none.
func answersOf(ask asking, key string) (*Answer, error) {
raw, err := ask(tellerAnswersTool, map[string]any{"key": key})
if err != nil {
return nil, err
}
answer, output, ok := answerOf(raw)
if !ok {
return nil, fmt.Errorf("the teller refused teller_answers: %s", lastLine(output))
}
var r struct {
Found bool `json:"found"`
Answer json.RawMessage `json:"answer"`
}
if err := json.Unmarshal(answer, &r); err != nil {
return nil, fmt.Errorf("the teller's answer to %s is not readable: %s", key, clip(string(answer), 200))
}
if !r.Found {
return nil, nil
}
a, err := mdc.DecodeAnswer(r.Answer)
if err != nil {
return nil, fmt.Errorf("the answer %s is not readable: %w", key, err)
}
return &a, nil
}
// reachedBody is the body of the view's event reached: the key of the reach and the reach as the view wrote it.
type reachedBody struct {
Key string `json:"key"`
Reach json.RawMessage `json:"reach"`
}
// ReachHeard takes the view's event reached.
func (h *Holder) ReachHeard(body []byte) error {
var e reachedBody
if err := json.Unmarshal(body, &e); err != nil || e.Key == "" {
h.Refuse(fmt.Errorf("the delivery view's reach event is not readable: %v", err))
return nil
}
h.ReachChanged(e.Key, e.Reach, false)
return nil
}
// ReachRead takes every reach the view's tool answers: what was said while the gate was away.
func (h *Holder) ReachRead(ask asking) error {
raw, err := ask(viewReachTool, map[string]any{})
if err != nil {
return err
}
answer, output, ok := answerOf(raw)
if !ok {
return fmt.Errorf("the delivery view refused view_reach: %s", lastLine(output))
}
var all struct {
Reach []json.RawMessage `json:"reach"`
}
if err := json.Unmarshal(answer, &all); err != nil {
return fmt.Errorf("the delivery view's reach is not readable: %s", clip(string(answer), 200))
}
for _, r := range all.Reach {
v, err := viewstate.DecodeReach(r)
if err != nil {
h.Logf("[mesh-delivery] a reach the delivery view answers is not readable, so its merges read as not known: %v", err)
continue
}
key, err := trunk.RepositoryKey(v.Repository)
if err != nil {
continue
}
h.mu.Lock()
_, heard := h.reach[key]
h.mu.Unlock()
if !heard { // a reach the event said is as new as this read: it is kept
h.ReachChanged(key, r, false)
}
}
return nil
}
@@ -1,79 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"strings"
"testing"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// ADR 0596 §2-§4: the gate reads the teller's answer through the teller's tool, found or not, never the bucket.
func TestTheTellersAnswerIsReadThroughItsTool(t *testing.T) {
answer, err := mdc.EncodeAnswer(mdc.Answer{Option: "approve", Warrant: "w1", Words: "the operator chose Approve"})
if err != nil {
t.Fatal(err)
}
var asked []string
ask := func(key string, body any) (json.RawMessage, error) {
asked = append(asked, fmt.Sprint(key, body))
if body.(map[string]any)["key"] == "k1" {
return json.Marshal(map[string]any{"key": "k1", "found": true, "answer": json.RawMessage(answer)})
}
return json.RawMessage(`{"key":"k2","found":false}`), nil
}
a, err := answersOf(ask, "k1")
if err != nil || a == nil || a.Option != "approve" || a.Warrant != "w1" {
t.Fatalf("the answer = %+v, %v", a, err)
}
if a, err = answersOf(ask, "k2"); err != nil || a != nil {
t.Errorf("no answer = %+v, %v", a, err)
}
if len(asked) != 2 || !strings.HasPrefix(asked[0], "mesh-delivery-teller.teller_answers") {
t.Errorf("asked %v", asked)
}
if _, err = answersOf(func(string, any) (json.RawMessage, error) { return nil, fmt.Errorf("no tool runner") }, "k"); err == nil {
t.Errorf("a tool that cannot be asked was read as no answer")
}
}
// The events answered and reached are heard: the first asks a look, the second carries the reach, which a read of the view's
// tool at start fills in only where no event said one.
func TestTheViewsReachIsHeardAndReadAtStart(t *testing.T) {
w := newWorld(t)
reach := func(head string) json.RawMessage {
raw, err := viewstate.EncodeReach(viewstate.Reach{Repository: "novox/web", Reached: head, ReachedSrc: 3})
if err != nil {
t.Fatal(err)
}
return raw
}
body, _ := json.Marshal(reachedBody{Key: "novox/web", Reach: reach(strings.Repeat("a", 40))})
if err := take(w.h, stdio.Envelope{Key: reachedEvent, Body: body}); err != nil {
t.Fatal(err)
}
if got := w.h.reach["novox/web"].Reached; got != strings.Repeat("a", 40) {
t.Fatalf("the reach heard is %q", got)
}
read := func(string, any) (json.RawMessage, error) {
return json.Marshal(map[string]any{"reach": []json.RawMessage{reach(strings.Repeat("b", 40)), func() json.RawMessage {
raw, _ := viewstate.EncodeReach(viewstate.Reach{Repository: "novox/api", Reached: strings.Repeat("c", 40), ReachedSrc: 4})
return raw
}()}})
}
if err := w.h.ReachRead(read); err != nil {
t.Fatal(err)
}
if got := w.h.reach["novox/web"].Reached; got != strings.Repeat("a", 40) {
t.Errorf("the read at start overwrote the reach the event said: %q", got)
}
if got := w.h.reach["novox/api"].Reached; got != strings.Repeat("c", 40) {
t.Errorf("the reach the event never said is read at start: %q", got)
}
if err := take(w.h, stdio.Envelope{Key: reachedEvent, Body: []byte(`{"reach":`)}); err != nil {
t.Errorf("a reach event that cannot be read is said, never left to be heard again: %v", err)
}
}
@@ -2,97 +2,9 @@ package main
import (
"encoding/json"
"fmt"
"slices"
"sort"
"strings"
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// What mesh-delivery asks of others, and where it keeps what it owns. **It never sends to a machine, writes
// a declaration, a grant or a bus object, and never registers a build** (novox/hq ADR 0239 decision 6): it
// asks the controller, through the controller's seat, and the forge, through the seat `git`.
// Forge is what the forge's holder does for a delivery.
type Forge interface {
// Statuses reads a commit's statuses — a pull request's head's, given its number — and whether the branch
// it merges into would let it merge. Reads only.
Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error)
// Contains is whether a commit is contained in another — an ancestor of it, or it. Reads only.
Contains(owner, repo, commit, in string) (bool, error)
// OpenPulls is every open pull request, with what its base requires and what its head has (novox/hq issue
// 347). Reads only.
OpenPulls() ([]OpenPull, error)
}
// ForgeChecks is what the forge says of a commit's checks (the seat git's pull and statuses, and the merge state).
type ForgeChecks struct {
Commit string `json:"commit"`
Pull *ForgePull `json:"pull,omitempty"`
Combined string `json:"combined,omitempty"`
Statuses []ForgeStatus `json:"statuses"`
Merge ForgeMerge `json:"merge"`
}
// ForgePull is a pull request as the forge says it.
type ForgePull struct {
Number int `json:"number"`
Title string `json:"title,omitempty"`
State string `json:"state,omitempty"`
Merged bool `json:"merged,omitempty"`
Base string `json:"base,omitempty"`
Head string `json:"head,omitempty"`
HeadSHA string `json:"head_sha,omitempty"`
MergeCommit string `json:"merge_commit_sha,omitempty"`
// MergedAt is when it merged, as the forge says it (RFC 3339); empty when not merged or not said.
MergedAt string `json:"merged_at,omitempty"`
HTMLURL string `json:"html_url,omitempty"`
// Mergeable is the forge's own word on whether it merges into its base now (its mergeable): false while it is in
// conflict with its base, or while the forge works that out after the base moved; nil when the forge did not say.
Mergeable *bool `json:"mergeable,omitempty"`
}
// ForgeStatus is one status of a commit, the newest of its context.
type ForgeStatus struct {
Context string `json:"context"`
State string `json:"state"`
Description string `json:"description"`
TargetURL string `json:"target_url,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Creator string `json:"creator,omitempty"`
}
// ForgeMerge is whether the branch's protection lets the commit merge: every required status a success.
// Mergeable is nil when the protection could not be read.
type ForgeMerge struct {
Branch string `json:"branch"`
Protected bool `json:"protected"`
Required []string `json:"required"`
Blocking []struct {
Context string `json:"context"`
State string `json:"state"`
} `json:"blocking"`
Mergeable *bool `json:"mergeable"`
Says string `json:"says"`
// Writes is whether the forge's merging account may write the repository at all (issue 1945): false, its statuses and
// its merge are refused, and CannotWrite says so with the tool that grants it. Nil when the forge holder did not say.
Writes *bool `json:"writes,omitempty"`
CannotWrite string `json:"cannot_write,omitempty"`
}
// Store keeps deliveries and groups: one key each, in the module's own state (ADR 0201).
type Store interface {
PutDelivery(d *Delivery) error
DeleteDelivery(id string) error
Deliveries() ([]*Delivery, error)
PutGroup(g *Group) error
DeleteGroup(id string) error
Groups() ([]*Group, error)
}
// --- over the runtime -----------------------------------------------------------------------------------
// asking is how a tool is asked: stdio.Ask, or a test's.
@@ -137,202 +49,3 @@ func lastLine(s string) string {
lines := strings.Split(strings.TrimSpace(s), "\n")
return strings.TrimSpace(lines[len(lines)-1])
}
// seatForge reads the forge through the seat `git` (design 47 *The seat git*): every read a verb of the seat, its answer
// typed in the seat's contract, a failed read an error and never "none". The forge module is never named.
type seatForge struct{ ask asking }
// gitAsk calls a verb of the seat `git` and reads its typed answer; an answer carrying `error` is a failed read.
func gitAsk[A any](f seatForge, verb string, in any, errOf func(*A) string) (*A, error) {
raw, err := f.ask("seat:"+gitseat.Seat+"."+verb, in)
if err != nil {
return nil, err
}
answer, output, ok := answerOf(raw)
if !ok {
return nil, fmt.Errorf("the seat git refused %s: %s", verb, lastLine(output))
}
var a A
if err := json.Unmarshal(answer, &a); err != nil {
return nil, fmt.Errorf("the seat git's %s is not readable: %s", verb, clip(string(answer), 200))
}
if why := errOf(&a); why != "" {
return nil, fmt.Errorf("the seat git could not read %s: %s", verb, why)
}
return &a, nil
}
// Statuses reads a pull request by its number through `pull` (its head when no commit is named) and the commit's statuses
// through `statuses`. What the forge would merge now (the base's protection and its required contexts, the forge's
// conflict word, whether the merging account writes) is the pull answer's `merging` (mergeState); a commit no pull
// request names has none.
func (f seatForge) Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error) {
repository := owner + "/" + repo
c := &ForgeChecks{Commit: commit}
var merging *gitseat.Merging
if number > 0 {
a, err := gitAsk(f, gitseat.VerbPull, gitseat.PullInput{Repository: repository, Number: int64(number)},
func(a *gitseat.PullAnswer) string { return a.Error })
if err != nil {
return nil, err
}
if a.Pull == nil {
return nil, fmt.Errorf("the seat git's pull named no pull request for %s#%d", repository, number)
}
c.Pull, merging = forgePullOf(*a.Pull), a.Merging
if c.Commit == "" {
c.Commit = a.Pull.Head
}
if base == "" {
base = a.Pull.Base
}
}
if c.Commit == "" {
return nil, fmt.Errorf("the seat git named no head of %s#%d", repository, number)
}
s, err := gitAsk(f, gitseat.VerbStatuses, gitseat.StatusesInput{Repository: repository, Commit: c.Commit},
func(a *gitseat.StatusesAnswer) string { return a.Error })
if err != nil {
return nil, err
}
c.Combined = s.Combined
for _, st := range s.Statuses {
c.Statuses = append(c.Statuses, forgeStatusOf(st))
}
c.Merge = mergeState(repository, base, merging, c.Statuses)
if c.Pull != nil && merging != nil {
mergeable := merging.Mergeable
c.Pull.Mergeable = &mergeable
}
return c, nil
}
// mergeState is what the forge would merge now, from the seat git's `merging` (issues 1275, 1945; ADR 0441 §1): the base's
// protection and its required contexts, each required context not a success at the commit blocking, and whether the
// merging account writes. Without `merging` (a commit no pull request names, or a pull request merged or closed) whether
// it merges is not known.
func mergeState(repository, base string, m *gitseat.Merging, statuses []ForgeStatus) ForgeMerge {
out := ForgeMerge{Branch: base, Required: []string{}}
if m == nil {
out.Says = "the seat git says what merges only for an open pull request"
return out
}
out.Protected, out.Required = m.Protected, append([]string{}, m.Required...)
newest := map[string]string{}
for _, s := range statuses {
if _, seen := newest[s.Context]; !seen {
newest[s.Context] = s.State
}
}
for _, context := range out.Required {
if state := newest[context]; state != "success" {
if state == "" {
state = "missing"
}
out.Blocking = append(out.Blocking, struct {
Context string `json:"context"`
State string `json:"state"`
}{context, state})
}
}
mergeable := len(out.Blocking) == 0
out.Mergeable = &mergeable
switch {
case len(out.Required) == 0:
out.Says = base + " requires no status"
default:
out.Says = base + " requires " + strings.Join(out.Required, ", ")
}
writes := m.Writes
out.Writes = &writes
if !writes {
out.CannotWrite = "the forge's merging account may not write " + repository + ": its statuses and its merge are refused"
}
return out
}
// OpenPulls reads every open pull request through `open-pulls`.
func (f seatForge) OpenPulls() ([]OpenPull, error) {
a, err := gitAsk(f, gitseat.VerbOpenPulls, gitseat.OpenPullsInput{}, func(a *gitseat.OpenPullsAnswer) string { return a.Error })
if err != nil {
return nil, err
}
out := make([]OpenPull, 0, len(a.Pulls))
for _, p := range a.Pulls {
out = append(out, openPullOf(p))
}
return out, nil
}
// Contains reads through `contains` whether in contains commit.
func (f seatForge) Contains(owner, repo, commit, in string) (bool, error) {
a, err := gitAsk(f, gitseat.VerbContains, gitseat.ContainsInput{Repository: owner + "/" + repo, Commit: commit, In: in},
func(a *gitseat.ContainsAnswer) string { return a.Error })
if err != nil {
return false, err
}
return a.Contains, nil
}
// carriedKey is a key an earlier owner's carry-over wrote into the bucket `deliveries`: no delivery, never read as one.
const carriedKey = "carried"
// kvStore is the module's own state: `deliveries` and `groups`, one key each.
type kvStore struct{}
// kvKey is an id as a bucket key: only the characters a key may carry.
func kvKey(id string) string {
var b strings.Builder
for _, r := range id {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_', r == '=':
b.WriteRune(r)
default:
b.WriteString("_" + fmt.Sprintf("%02x", r) + "_")
}
}
return b.String()
}
func (kvStore) PutDelivery(d *Delivery) error {
_, err := stdio.State("deliveries").Put(kvKey(d.ID), d)
return err
}
func (kvStore) DeleteDelivery(id string) error { return stdio.State("deliveries").Delete(kvKey(id)) }
func (kvStore) PutGroup(g *Group) error {
_, err := stdio.State("groups").Put(kvKey(g.ID), g)
return err
}
func (kvStore) DeleteGroup(id string) error { return stdio.State("groups").Delete(kvKey(id)) }
// readAll reads every key of a state but the ones it is told to leave (the gate's mark `carried` is no delivery).
func readAll[T any](state string, leave ...string) ([]*T, error) {
keys, err := stdio.State(state).Keys()
if err != nil {
return nil, err
}
sort.Strings(keys)
var out []*T
for _, k := range keys {
if slices.Contains(leave, k) {
continue
}
e, err := stdio.State(state).Get(k)
if err != nil {
return nil, err
}
if e == nil {
continue
}
var v T
if err := json.Unmarshal(e.Value, &v); err != nil {
logf("[mesh-delivery] %s %s cannot be read (%v); left as it is", state, k, err)
continue
}
out = append(out, &v)
}
return out, nil
}
func (kvStore) Deliveries() ([]*Delivery, error) { return readAll[Delivery]("deliveries", carriedKey) }
func (kvStore) Groups() ([]*Group, error) { return readAll[Group]("groups") }
@@ -1,157 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// The pull request's key (novox/hq ADR 0406 §4, issue 975): mesh-delivery keeps one key per pull request on the bus,
// holding its head, the trunk commit its current check is asked against and its verdict, written by compare-and-set.
// It writes a new head when the forge says one (`pull.updated`, which carries the head and the trunk commit it was
// announced against), and a new trunk commit only when the check is asked again against one — the merge tool's ask is
// that same announcement. A merge of a group's member, and a trunk move that leaves an inactive pull request unasked,
// are announced as nothing (the forge announces `pull.updated` only for a head and for the merge tool's ask), so
// they write nothing. A verdict for that head and trunk commit writes the verdict. The build seat reads the key.
// PullKey is what the bus keeps of one pull request.
type PullKey struct {
Head string `json:"head"`
Trunk string `json:"trunk,omitempty"`
Verdict string `json:"verdict,omitempty"`
}
// PullKeys keeps the pull requests' keys: Change reads the key, applies the change to what it read and writes the result
// by compare-and-set when the change says to write; a write another writer won reads the key again and applies again.
type PullKeys interface {
Change(key string, apply func(PullKey) (PullKey, bool)) error
}
// pullKeyName is a pull request's key: owner.repo.number, with any dot in a name made an underscore.
func pullKeyName(owner, repo string, number int) string {
clean := func(s string) string { return strings.NewReplacer(".", "_", "/", "_").Replace(s) }
return fmt.Sprintf("%s.%s.%d", clean(owner), clean(repo), number)
}
// KeepPull writes the key of a pull request whose head was announced, when the head or the trunk commit it was
// announced against is not what the key holds; a new head or trunk clears the verdict.
func (h *Holder) KeepPull(p PullEvent) {
if h.Pulls == nil || p.Number == 0 || p.HeadSHA == "" {
return
}
err := h.Pulls.Change(pullKeyName(p.Owner, p.Repo, p.Number), func(k PullKey) (PullKey, bool) {
if sameCommit(k.Head, p.HeadSHA) && k.Head != "" && (p.BaseSHA == "" || sameCommit(k.Trunk, p.BaseSHA)) {
return k, false
}
return PullKey{Head: p.HeadSHA, Trunk: p.BaseSHA}, true
})
if err != nil {
h.Logf("[mesh-delivery] the key of %s/%s#%d could not be written: %v", p.Owner, p.Repo, p.Number, err)
h.again("a pull request's key that could not be written")
return
}
// The head's mesh/check may have been heard before this key held the head: the two events are taken on goroutines of
// their own, and KeepCheckVerdict then found no head to write a verdict to. The verdict the delivery holds for this head
// and trunk is written now; read after the write above landed, it cannot miss one taken before that write.
verdict := h.checkVerdictOf(p.repository(), p.HeadSHA, p.BaseSHA)
if verdict == "" {
return
}
err = h.Pulls.Change(pullKeyName(p.Owner, p.Repo, p.Number), func(k PullKey) (PullKey, bool) {
if !sameCommit(k.Head, p.HeadSHA) || !sameCommit(k.Trunk, p.BaseSHA) || k.Verdict != "" {
return k, false
}
k.Verdict = verdict
return k, true
})
if err != nil {
h.Logf("[mesh-delivery] the verdict in the key of %s/%s#%d could not be written: %v", p.Owner, p.Repo, p.Number, err)
h.again("a pull request's key that could not be written")
}
}
// checkVerdictOf is the verdict of the mesh/check the delivery of a head holds, when it judged this head on this trunk
// commit; empty otherwise. Outside the lock.
func (h *Holder) checkVerdictOf(repository, head, trunk string) string {
h.mu.Lock()
defer h.mu.Unlock()
d := h.deliveries[IDOf(strings.ToLower(repository), head)]
if d == nil || d.Check == nil || d.Check.Status != ctxCheck || !sameCommit(d.Check.Head, head) || !sameCommit(d.Check.Trunk, trunk) {
return ""
}
return d.Check.Gate
}
// KeepCheckVerdict writes the verdict of a head's mesh/check into the key of its pull request, when the key holds that head
// and the trunk commit the check line names (issue 1302; the controller's verdict wrote it before issue 1318). Outside the lock.
func (h *Holder) KeepCheckVerdict(e ForgeStatusEvent) {
if h.Pulls == nil || e.Context != ctxCheck || e.Description == "" || e.SHA == "" {
return
}
verdict := map[string]string{"success": "pass", "warning": "warning", "failure": "fail", "error": "error"}[e.State]
_, trunk := checkLine(e.Description)
if verdict == "" || trunk == "" {
return // pending is no verdict; a line naming no trunk commit is judged against none
}
h.mu.Lock()
number := 0
if d := h.deliveries[IDOf(strings.ToLower(e.Owner+"/"+e.Repo), e.SHA)]; d != nil {
number = d.Number
}
h.mu.Unlock()
if number == 0 {
return
}
err := h.Pulls.Change(pullKeyName(e.Owner, e.Repo, number), func(k PullKey) (PullKey, bool) {
if !sameCommit(k.Head, e.SHA) || !sameCommit(k.Trunk, trunk) || k.Verdict == verdict {
return k, false
}
k.Verdict = verdict
return k, true
})
if err != nil {
h.Logf("[mesh-delivery] the verdict in the key of %s/%s#%d could not be written: %v", e.Owner, e.Repo, number, err)
h.again("a pull request's key that could not be written")
}
}
// kvPulls keeps the keys in the module's own state `pulls`.
type kvPulls struct{}
// changeAttempts is how often a lost compare-and-set is read and applied again before it is said.
const changeAttempts = 10
func (kvPulls) Change(key string, apply func(PullKey) (PullKey, bool)) error {
state := stdio.State("pulls")
for range changeAttempts {
e, err := state.Get(key)
if err != nil {
return err
}
var k PullKey
if e != nil {
if err := json.Unmarshal(e.Value, &k); err != nil {
return err
}
}
next, write := apply(k)
if !write {
return nil
}
if e == nil {
_, err = state.Create(key, next)
} else {
_, err = state.Update(key, next, e.Revision)
}
if err == nil {
return nil
}
if !errors.Is(err, stdio.ErrChanged) {
return err
}
}
return fmt.Errorf("%s was written by another writer first %d times: %w", key, changeAttempts, stdio.ErrChanged)
}
@@ -1,165 +0,0 @@
package main
import (
"errors"
"sync"
"testing"
)
// **The pull request's key** (novox/hq ADR 0406 §4, issue 975): one key per pull request on the bus, holding its head,
// the trunk commit its current check is asked against and its verdict, written by compare-and-set. A head the forge
// announces writes head and trunk; a verdict for that head and trunk writes the verdict; nothing else writes.
type fakePulls struct {
mu sync.Mutex
keys map[string]PullKey
writes int
// lose makes the first n writes lose to another writer, which sets rival.
lose int
rival PullKey
}
func newFakePulls() *fakePulls { return &fakePulls{keys: map[string]PullKey{}} }
func (f *fakePulls) Change(key string, apply func(PullKey) (PullKey, bool)) error {
f.mu.Lock()
defer f.mu.Unlock()
for tries := 0; tries < 5; tries++ {
next, write := apply(f.keys[key])
if !write {
return nil
}
if f.lose > 0 { // another writer was first: read again and apply again
f.lose--
f.keys[key] = f.rival
continue
}
f.keys[key] = next
f.writes++
return nil
}
return errors.New("lost the race too often")
}
func (f *fakePulls) get(key string) PullKey {
f.mu.Lock()
defer f.mu.Unlock()
return f.keys[key]
}
const appKey = "novox.app.7"
func announcedAt(head, trunk string) PullEvent {
p := pr("novox/app", 7, head, "feat/x", "src/a.go")
p.BaseSHA = trunk
return p
}
func TestAnAnnouncedHeadWritesTheKeyAndTheSameAnnouncementAgainWritesNothing(t *testing.T) {
w := newWorld(t)
pulls := newFakePulls()
w.h.Pulls = pulls
p := announcedAt(readyHead, "t1t1t1t1t1t1")
w.h.PullUpdated(p)
w.h.KeepPull(p)
if got := pulls.get(appKey); got.Head != readyHead || got.Trunk != "t1t1t1t1t1t1" || got.Verdict != "" {
t.Fatalf("the key holds %+v", got)
}
w.h.KeepPull(p)
if pulls.writes != 1 {
t.Fatalf("the same announcement wrote %d times", pulls.writes)
}
}
func TestANewTrunkCommitClearsTheVerdictAndANewHeadReplacesTheKey(t *testing.T) {
w := newWorld(t)
pulls := newFakePulls()
w.h.Pulls = pulls
w.h.KeepPull(announcedAt(readyHead, "t1t1t1t1t1t1"))
w.h.KeepPull(announcedAt(readyHead, "t2t2t2t2t2t2"))
if got := pulls.get(appKey); got.Trunk != "t2t2t2t2t2t2" || got.Verdict != "" {
t.Fatalf("the check asked again against a new trunk left %+v", got)
}
w.h.KeepPull(announcedAt(pushed, "t2t2t2t2t2t2"))
if got := pulls.get(appKey); got.Head != pushed {
t.Fatalf("a new head left %+v", got)
}
}
func TestAMeshCheckForTheKeysHeadAndTrunkIsWrittenAndOneForAnotherIsNot(t *testing.T) {
w := newWorld(t)
pulls := newFakePulls()
w.h.Pulls = pulls
w.h.PullUpdated(announcedAt(readyHead, "a1a1a1a1a1a1"))
w.h.KeepPull(announcedAt(readyHead, "a1a1a1a1a1a1"))
status := func(head, trunk, state string) ForgeStatusEvent {
return ForgeStatusEvent{Owner: "novox", Repo: "app", SHA: head, Context: ctxCheck, State: state,
Description: "pass " + head[:12] + " on " + trunk + " | unit 3 run"}
}
w.h.KeepCheckVerdict(status(pushed, "a1a1a1a1a1a1", "success"))
if pulls.get(appKey).Verdict != "" {
t.Fatal("a verdict for another head was written")
}
w.h.KeepCheckVerdict(status(readyHead, "b2b2b2b2b2b2", "success"))
if pulls.get(appKey).Verdict != "" {
t.Fatal("a verdict for another trunk commit was written")
}
w.h.KeepCheckVerdict(status(readyHead, "a1a1a1a1a1a1", "pending"))
if pulls.get(appKey).Verdict != "" {
t.Fatal("a pending check was written as a verdict")
}
w.h.KeepCheckVerdict(status(readyHead, "a1a1a1a1a1a1", "success"))
if got := pulls.get(appKey); got.Verdict != "pass" {
t.Fatalf("the key holds %+v", got)
}
}
func TestALostRaceReadsTheKeyAgainAndAppliesTheChangeToWhatItFound(t *testing.T) {
w := newWorld(t)
pulls := newFakePulls()
pulls.lose, pulls.rival = 1, PullKey{Head: readyHead, Trunk: "t1t1t1t1t1t1", Verdict: "pass"}
w.h.Pulls = pulls
// The rival writer already holds this very head and trunk with a verdict: the change finds nothing to write.
w.h.KeepPull(announcedAt(readyHead, "t1t1t1t1t1t1"))
if got := pulls.get(appKey); got.Verdict != "pass" || pulls.writes != 0 {
t.Fatalf("after the lost race the key holds %+v after %d writes", got, pulls.writes)
}
}
// The forge's catch-up after a restart can announce a merge again past the bus's de-duplication window (issue 975):
// heard again, it changes nothing, and moves no trunk back.
func TestTheSameMergeHeardAgainAfterALaterMergeMovesNoTrunkBack(t *testing.T) {
w := newWorld(t)
a, b := "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb"
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.PullUpdated(pr("novox/app", 8, a, "feat/y", "src/a.go"))
w.h.PullUpdated(pr("novox/app", 9, b, "feat/z", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "m8m8m8m8m8m8"))
w.h.PullMerged(merged("novox/app", 8, a, "m8m8m8m8m8m8"))
w.h.PullMerged(merged("novox/app", 9, b, "m9m9m9m9m9m9"))
id := IDOf("novox/app", readyHead)
before := w.delivery(id)
w.h.PullMerged(merged("novox/app", 8, a, "m8m8m8m8m8m8")) // announced again, past the window
after := w.delivery(id)
if after.TrunkNow != before.TrunkNow || after.TrunkNow != "m9m9m9m9m9m9" {
t.Fatalf("the trunk moved from %q to %q on a merge heard again; wanted it to stay m9m9m9m9m9m9", before.TrunkNow, after.TrunkNow)
}
}
// The forge's mesh/check event handled wholly between the announcement's PullUpdated and its KeepPull (the two events are
// taken on goroutines of their own): the verdict must still reach the pull request's key.
func TestAVerdictHeardBeforeTheKeyIsWrittenReachesTheKey(t *testing.T) {
w := newWorld(t)
pulls := newFakePulls()
w.h.Pulls = pulls
p := announcedAt(readyHead, "a1a1a1a1a1a1")
w.h.PullUpdated(p)
s := ForgeStatusEvent{Owner: "novox", Repo: "app", SHA: readyHead, Context: ctxCheck, State: "success",
Description: "pass " + readyHead[:12] + " on a1a1a1a1a1a1 | unit 3 run"}
w.h.CommitStatus(s)
w.h.KeepCheckVerdict(s)
w.h.KeepPull(p)
if got := pulls.get(appKey); got.Head != readyHead || got.Verdict != "pass" {
t.Fatalf("the key holds %+v, want the head with the verdict pass", got)
}
}
@@ -1,100 +0,0 @@
package main
import (
"fmt"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// Whether a merge is on the machines (novox/hq ADR 0556 clause 4). The gate records only the merge (ADR 0539 §17, ADR 0429
// §1): it reads the delivery view's reach of each repository, which names the repository's reached head (the newest merge every
// node runs a build containing), and derives when asked, for a merged delivery, whether its merge is at or before that head.
// The order of two merges into one trunk is the gate's own: the moment each was merged, the trunk being a line of merges. A
// reached head the gate holds no merge of is said, never asked of the forge.
// ReachChanged takes one change of the view's reach.
func (h *Holder) ReachChanged(key string, value []byte, deleted bool) {
h.mu.Lock()
defer h.mu.Unlock()
if deleted {
delete(h.reach, key)
return
}
r, err := viewstate.DecodeReach(value)
if err != nil {
h.Logf("[mesh-delivery] the view's reach %s is not readable, so its merges read as not known: %v", key, err)
delete(h.reach, key)
return
}
h.reach[key] = r
}
// The words a merged delivery's machines are said in.
const (
onMachines = "on the machines"
notYetOn = "not yet on the machines: "
notKnownWhere = "not known: "
)
// machines says whether a merged delivery's merge is on the machines, derived from the view's reach; empty for a delivery
// that is not merged. Under the lock.
func (h *Holder) machines(d *Delivery) string {
if d.State != Merged || d.MergedAs == "" {
return ""
}
key, err := trunk.RepositoryKey(d.Repository)
if err != nil {
return notKnownWhere + err.Error()
}
r, ok := h.reach[key]
switch {
case !ok:
return notKnownWhere + "the delivery view says no reach of " + d.Repository
}
// A merge that changed a module a node wants and runs nothing countable of is not yet there, whatever the reached head
// (the module holds no head back, ADR 0556 clause 2).
for _, mr := range r.Modules {
if mr.Nodes == 0 || mr.Floor != "" {
continue
}
for _, c := range mr.Changes {
if sameCommit(d.MergedAs, c.Commit) {
return notYetOn + mr.Waiting
}
}
}
switch {
case r.Reached == "":
return notYetOn + holdsSaid(r)
case sameCommit(d.MergedAs, r.Reached):
return onMachines
}
head := h.mergeOf(d.Repository, r.Reached)
switch {
case head == nil:
return notKnownWhere + fmt.Sprintf("the reached head %s of %s is no merge this gate holds", shortOf(r.Reached, 12), d.Repository)
case !d.MergedAt.After(head.MergedAt):
return onMachines
}
return notYetOn + holdsSaid(r)
}
// holdsSaid is what holds a repository's next head, in words.
func holdsSaid(r viewstate.Reach) string {
if len(r.Holds) == 0 {
return "the delivery view names nothing that holds it yet"
}
return strings.Join(r.Holds, "; ")
}
// mergeOf is the delivery of a repository that merged as the commit; nil when none does. Under the lock.
func (h *Holder) mergeOf(repository, commit string) *Delivery {
for _, d := range h.deliveries {
if d.MergedAs != "" && strings.EqualFold(d.Repository, repository) && sameCommit(d.MergedAs, commit) {
return d
}
}
return nil
}
@@ -1,74 +0,0 @@
package main
import (
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// 1280 (novox/hq ADR 0556 clause 4): `deliveries` and `show` say for a merged delivery whether it is on the machines, derived
// when asked from the view's reach and the gate's own order of merges: at or before the reached head it is, after it not yet
// and why; a repository the view has no reach of, or a reached head the gate holds no merge of, is not known. Nothing records it.
func TestAMergedDeliverySaysWhetherItIsOnTheMachines(t *testing.T) {
w := newWorld(t)
at := w.now.Add(-3 * time.Hour)
merge := func(commit string, n int) *Delivery {
d := &Delivery{ID: "novox/web@" + commit[:12], Repository: "novox/web", Commit: commit[:12], Number: n, State: Merged,
Since: at, MergedAs: strings.Repeat(commit[:1], 40), MergedAt: at.Add(time.Duration(n) * time.Minute)}
w.h.mu.Lock()
w.h.keep(d)
w.h.mu.Unlock()
return d
}
first, second, third := merge("111111111111", 1), merge("222222222222", 2), merge("333333333333", 3)
reach := func(r viewstate.Reach) {
key, _ := trunk.RepositoryKey(r.Repository)
raw, err := viewstate.EncodeReach(r)
if err != nil {
t.Fatal(err)
}
w.h.ReachChanged(key, raw, false)
}
machines := func(d *Delivery) string { w.h.mu.Lock(); defer w.h.mu.Unlock(); return w.h.machines(d) }
if got := machines(first); !strings.HasPrefix(got, notKnownWhere) {
t.Errorf("with no reach: %q", got)
}
reach(viewstate.Reach{Repository: "novox/web", Reached: second.MergedAs, ReachedSrc: 7, Holds: []string{"web on g14 runs the build of 2222, not yet that of 3333"}})
for d, want := range map[*Delivery]string{first: onMachines, second: onMachines, third: notYetOn + "web on g14"} {
if got := machines(d); !strings.HasPrefix(got, want) {
t.Errorf("%s: %q, want %q", d.ID, got, want)
}
}
reach(viewstate.Reach{Repository: "novox/web", Reached: strings.Repeat("9", 40), ReachedSrc: 8})
if got := machines(first); !strings.Contains(got, "no merge this gate holds") {
t.Errorf("a reached head the gate holds no merge of: %q", got)
}
reach(viewstate.Reach{Repository: "novox/web", Holds: []string{"web on ace runs nothing yet"}})
if got := machines(first); got != notYetOn+"web on ace runs nothing yet" {
t.Errorf("no reached head: %q", got)
}
// A module a node wants and runs nothing countable of holds no head back, and a merge that changed it is not yet there.
reach(viewstate.Reach{Repository: "novox/web", Reached: third.MergedAs, ReachedSrc: 9, Modules: map[string]viewstate.ModuleReach{
"web": {FP: "src2:x", Nodes: 1, Waiting: "web on g14 runs nothing yet", Changes: []viewstate.Change{{Commit: second.MergedAs, FP: "src2:x"}}}}})
for d, want := range map[*Delivery]string{first: onMachines, second: notYetOn + "web on g14 runs nothing yet", third: onMachines} {
if got := machines(d); got != want {
t.Errorf("with a module running nowhere countable, %s: %q, want %q", d.ID, got, want)
}
}
// The verbs say it; a delivery not merged says nothing of the machines.
reach(viewstate.Reach{Repository: "novox/web", Reached: third.MergedAs, ReachedSrc: 9})
lines := w.h.Deliveries("", "novox/web", "", true)
if len(lines) != 3 || lines[0].Machines != onMachines {
t.Errorf("deliveries = %+v", lines)
}
shown, err := w.h.Show(third.ID)
if err != nil || shown.(map[string]any)["machines"] != onMachines {
t.Errorf("show = %v, %v", shown, err)
}
if got := machines(&Delivery{State: Held, MergedAs: "x", Repository: "novox/web"}); got != "" {
t.Errorf("a held delivery says %q of the machines", got)
}
}
@@ -55,26 +55,14 @@ func (h *Holder) Work() {
}
}
// React takes every step the facts allow, after an event that can change what they are: the forge's open pull requests
// read when a check may have answered, the steps that need another's word (look), what is owed (Flush), and then
// the conditions of what stalled (conditions.go), and then the time the next look is wanted at, if any step waits on the clock. Never concurrent with itself.
// React takes every step the facts allow, after an event that can change what they are: the conditions of what stalled
// (conditions.go), and then the time the next look is wanted at, if any step waits on the clock. Never concurrent with itself.
func (h *Holder) React() {
h.reactMu.Lock()
defer h.reactMu.Unlock()
h.wanted.mu.Lock()
h.wanted.at, h.wanted.step = time.Time{}, ""
h.wanted.mu.Unlock()
if h.uncheckedDue() {
h.wantForge()
}
if h.takeForgeWanted() {
if err := h.SeeOpenPulls(); err != nil {
h.wantForge()
h.again("the forge's open pull requests, read when a check may have changed")
}
}
h.look()
h.Flush()
h.sayConditions()
h.arm()
}
@@ -1,677 +0,0 @@
package main
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// The Approve is asked as soon as the checks pass (novox/hq ADR 0421, issue 1022).
//
// Before, a reviewer marked a pull request ready (a label and a comment) and only then was the operator's Approve asked
// (issue 722, ADR 0364). The review now finishes before the push (playbook 07) and its record, a line in the pull
// request's description, is given when the pull request is opened, so the mark repeated what the push already meant and
// added a step and a wait. This owner asks the operator's Approve itself, within the event that makes it possible: the
// verdict that passes, the commit status that sets the checks. Then:
//
// - the Approve is the only act: nothing is merged before it, and nothing else is needed after it (no label, comment
// or second Approve); it is asked once for a head, and once for a group's heads together;
// - it merges through the forge's merge tool (gitea_merge_pull_request), the one path every merge takes, at the head
// the Approve names, handing the tool the heads the Approve covers (ADR 0421 §9);
// - only when its delivery is ready here: its check passed against its trunk's head. A verdict out of date is judged
// again by the checker on its trunk's move (ADR 0447 §4), and an Approve already asked stays through a trunk move
// (ADR 0421 §3): the merge is asked again on the passing verdict, with no second Approve;
// - a group member only once the group's composed check passed for its members' heads as they are;
// - a push ends the Approve and says so once on the pull request (ADR 0421 §6); the new head is asked on its own;
// - a check that failed merges nothing: it is said once on the pull request, and as a stalled line, which the
// gate says to the operator as a condition (conditions.go). So is a merge the tool refused for an hour.
//
// No part reads a pull request's description to gate anything (novox/hq ADR 0493 §6, ADR 0659). What this owner does is kept on the
// delivery (Ready), said in its log, and the merge's transitions name the Approve as their warrant.
// readyRefusedFor is how long the merge tool may refuse an approved head before it is said as stalled: none (issue 1115) —
// a refusal is said at once, and the merge is asked again at each event that can change it, with no timer. A refusal the
// gate is already answering (its trunk moved and its check was asked again) is said only once that check answered and
// the merge is still refused, or twice the repository's last check time passed (novox/hq ADR 0505 §4, `answering`).
const readyRefusedFor time.Duration = 0
// groupRefusedFor is how long the merge tool may refuse a closed group's member before it is said as stalled: none (issue 1115).
const groupRefusedFor time.Duration = 0
// waitsForHead is what a delivery waits for when its verdict was heard and its pull request's head was not (issue 1726).
const waitsForHead = "its pull request's announcement: its base, branch and title are not heard, so what it merges into is not known"
// headHeardBound is how long a delivery may wait for its pull request's announcement before it is said as stalled.
const headHeardBound = 10 * time.Minute
// groupClosed is whether a group is closed: a member reached the trunk. Under the lock.
func (h *Holder) groupClosed(id string) bool {
g := h.groups[id]
return g != nil && g.Closed
}
// ReadyRun is what this owner did with a delivery's Approve and merge: kept on the delivery and shown whole by `show`.
type ReadyRun struct {
// Head is the head the run is for.
Head string `json:"head"`
// Seen is when this owner first made the run: the checks passed.
Seen time.Time `json:"seen"`
// Waits is what the merge waits on, when it waits: a verdict, the group's composed check or the operator's Approve.
Waits string `json:"waits,omitempty"`
// Tries is how many times the merge was asked; LastTry when, and Answer what the merge tool last answered.
Tries int `json:"tries,omitempty"`
LastTry time.Time `json:"last_try"`
Answer string `json:"answer,omitempty"`
// merging is whether the member is handed to the forge bridge and not merged or refused yet: not handed twice
// meanwhile. Not kept.
merging bool
// refusedNoted is whether the refusal was written into the group's merge key. Not kept.
refusedNoted bool
// AskedKey and Asking are the ask key last asked for and how many times it was asked at those heads; they stay when the ask
// is withdrawn, so the next asking is the next number (mdc.AskingID). Written is whether that ask stands in the gate's asks.
AskedKey string `json:"asked_key,omitempty"`
Asking int `json:"asking,omitempty"`
Written bool `json:"written,omitempty"`
// MergeCommit is the commit the forge bridge reported the merge as, and SourceMoved whether source.main holds it
// (merges.go).
MergeCommit string `json:"merge_commit,omitempty"`
SourceMoved bool `json:"source_moved,omitempty"`
// Urgent is set once a refusal of this head was one that is urgent at once: its severity never steps down to a
// warning after (issue 1275, AC6).
Urgent bool `json:"urgent,omitempty"`
// Repeats is how many times in a row the merge tool refused with the same words while the trunk stayed where RepeatTrunk
// says: past maxRepeatedRefusals the merge is asked no more by a verdict, a status or a composed check, and the refusal
// is urgent (novox/hq issue 1106). A trunk's move, a new head or the operator's Approve starts the count again.
Repeats int `json:"repeats,omitempty"`
RepeatTrunk string `json:"repeat_trunk,omitempty"`
// RefusedSince is when the merge tool began refusing it without a break.
RefusedSince *time.Time `json:"refused_since,omitempty"`
// FirstRefusedOn and LastRefusedOn are when the verdict the merge was asked on was heard, at the first refusal of
// the run of refusals and at the last: a later one than the first is a fresh check answered and refused again
// (novox/hq ADR 0505 §4).
FirstRefusedOn time.Time `json:"first_refused_on,omitzero"`
LastRefusedOn time.Time `json:"last_refused_on,omitzero"`
// Merged is when the merge tool merged it.
Merged *time.Time `json:"merged,omitempty"`
// Failed is why it is not merged and will not be on this head: its check failed. Said once on the pull request.
Failed string `json:"failed,omitempty"`
// OtherBase is the branch its pull request merges into when that is not main: never asked, never merged here, said
// once on the pull request (novox/hq ADR 0398 §4, issue 948).
OtherBase string `json:"other_base,omitempty"`
// The operator's Approve of the merge into main (novox/hq ADR 0398 §3, issue 948, approval.go). AskID is the ask
// waiting for an answer, Ask the ask as made (a warrant is held to it), Heads the heads it covers by delivery id (a
// group's members together), AskedAt when and Asks how many were made for this head.
AskID string `json:"ask_id,omitempty"`
Ask *oc.Ask `json:"ask,omitempty"`
Heads map[string]string `json:"heads,omitempty"`
AskedAt time.Time `json:"asked_at"`
Asks int `json:"asks,omitempty"`
// AskCheck is the state of the check at the heads the pending ask names, as its words said it (checkAt: pass, running,
// none or fail; issue 1733): while the check gates no merge, an ask whose check reached a verdict since is asked again
// in new words, so it never keeps a claim the check no longer holds.
AskCheck string `json:"ask_check,omitempty"`
// Approved and Declined are when the operator chose, Warrant what the router's warrant says of who and how; Refused
// why an answer was not taken (it did not hold to the ask). AnsweredAsk is the ask the answer answers: an answer stands
// only for its own ask, so a Decline of an earlier ask never stands beside the Approve of a later one (declined).
Approved *time.Time `json:"approved,omitempty"`
Declined *time.Time `json:"declined,omitempty"`
AnsweredAsk string `json:"answered_ask,omitempty"`
Warrant string `json:"warrant,omitempty"`
Refused string `json:"refused,omitempty"`
// FailedAfter is a failure heard after the operator's Approve covered the head, a carry's (novox/hq ADR 0661 §5): said
// once on the pull request and as a stalled line naming the merge it follows, never a refusal of the merge; cleared by a
// later pass. FailedAfterSince is when it was heard.
FailedAfter string `json:"failed_after,omitempty"`
FailedAfterSince *time.Time `json:"failed_after_since,omitempty"`
// CannotWrite is why the operator is not asked: the forge's merging account cannot write a repository the ask covers
// (writes.go, issue 1945); CannotWriteSince since when. Said at once as a condition.
CannotWrite string `json:"cannot_write,omitempty"`
// HeldFull is since when its merge waits for room in the delivery records' ledger, which refused its record as full
// (recordfill.go): handed once the ledger is below 80 % of its cap again.
HeldFull *time.Time `json:"held_full,omitempty"`
CannotWriteSince *time.Time `json:"cannot_write_since,omitempty"`
// NotHanded is why an approved merge that waits for nothing is not handed to the forge bridge (a member of its group that
// cannot be handed, and why), NotHandedSince since when: said once in the log and as a stalled line, never silent.
NotHanded string `json:"not_handed,omitempty"`
NotHandedSince *time.Time `json:"not_handed_since,omitempty"`
}
// declined is whether the operator's Decline stands for this run: it answered the ask the run holds now, or the run holds
// no ask any more. A Decline of an earlier ask, the run asked again since at other heads (a sibling's new head), does not
// stand (2026-10-29: hq#495 declined under one ask and approved under the next was never handed). A run kept before answers
// named their ask stands declined unless an Approve came after the Decline.
func (r *ReadyRun) declined() bool {
if r == nil || r.Declined == nil {
return false
}
if r.AnsweredAsk != "" {
return r.AskID == "" || r.AskID == r.AnsweredAsk
}
return r.Approved == nil || r.Declined.After(*r.Approved)
}
// MergeBranch is the one branch this owner merges into (novox/hq ADR 0398 §4): a merge into any other is its author's.
const MergeBranch = "main"
// mergedBy is who a merge this owner made on an Approve is said to be by, in its transitions.
func (r *ReadyRun) mergedBy() string {
if r == nil || r.Merged == nil {
return ""
}
by := byOwner + ", on the operator's Approve at " + shortOf(r.Head, 12)
if r.Warrant != "" {
by += "; the merge into main approved: " + r.Warrant
}
return by
}
// pullKey is a pull request as a key: owner/repo#number.
func pullKey(p OpenPull) string {
return strings.ToLower(p.Owner+"/"+p.Repo) + fmt.Sprintf("#%d", p.Number)
}
// runOf is the delivery's run, made when it has none.
func (h *Holder) runOf(d *Delivery, now time.Time) *ReadyRun {
if d.Ready == nil {
d.Ready = &ReadyRun{Head: d.Commit, Seen: now}
h.keep(d)
}
return d.Ready
}
// readyAsks works out, under the lock, what each delivery whose checks passed needs: the operator asked, a merge asked,
// a failure said, an out of date verdict's check asked again. The asking happens outside the lock.
func (h *Holder) readyAsks(now time.Time) []func() {
var asks []func()
h.endStaleAsks()
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
d := h.deliveries[id]
if d.Number == 0 || d.State.Final() || d.MergedAs != "" {
continue // not of a pull request, or past its merge
}
if d.Ready == nil && d.State == Ready {
h.runOf(d, now)
h.Poke() // its check, if none is asked, is worth asking now (askCheck)
h.Logf("[mesh-delivery] %s is ready (%s); the operator's Approve of its merge into main is asked once it may merge",
d.ID, h.checkAt([]*Delivery{d}).Words)
}
r := d.Ready
if r == nil || r.Merged != nil || r.declined() {
continue
}
if d.State == Rejected {
if r.Failed == "" {
r.Failed = "its check failed"
if d.Check != nil {
r.Failed += ": " + oneLine(d.Check.Summary)
if j := d.Check.Judged(); j != "" {
r.Failed += " (" + j + ")"
}
}
r.Waits = ""
h.Logf("[mesh-delivery] %s passed its checks and is not merged: %s", d.ID, r.Failed)
h.keep(d)
asks = append(asks, h.sayOn(d, fmt.Sprintf("mesh-delivery: **not merged.** This pull request's checks passed at "+
"%s, and now %s. A new head, or a recheck that passes, asks the operator again.", d.Commit, r.Failed)))
}
continue
}
if d.Base == "" {
// A delivery made from its verdict before its head was heard (issue 1726): its base, branch and title are
// unknown, so what it merges into is too. It is neither asked nor passed over in silence: it waits for its
// pull request's announcement, which fills it (pullUpdated), and past headHeardBound it is stalled.
if r.Waits != waitsForHead {
r.Waits = waitsForHead
h.Logf("[mesh-delivery] %s waits: %s", d.ID, waitsForHead)
h.keep(d)
}
h.lookAt(r.Seen.Add(headHeardBound), "a delivery whose pull request was never announced")
continue
}
if d.Base != MergeBranch {
if r.OtherBase != d.Base {
r.OtherBase = d.Base
check := h.checkAt([]*Delivery{d}).Words // ready need not mean passed: the check may gate no merge (issue 1733)
h.Logf("[mesh-delivery] %s (%s) is not asked: it merges into %s, not %s", d.ID, check, d.Base, MergeBranch)
h.keep(d)
asks = append(asks, h.sayOn(d, fmt.Sprintf("mesh-delivery: **not merged.** This pull request is at %s (%s), "+
"and it merges into %s. Only a merge into %s is this owner's; a merge into another branch is its "+
"author's (novox/hq ADR 0398 §4).", d.Commit, check, d.Base, MergeBranch)))
}
continue
}
r.OtherBase = ""
r.Failed = ""
waits := h.readyWaits(d)
if waits != r.Waits {
r.Waits = waits
if waits != "" {
h.Logf("[mesh-delivery] %s waits: %s", d.ID, waits)
}
h.keep(d)
}
var approvalAsk func()
if waits == "" {
waits, approvalAsk = h.approval(d, now)
}
if waits == "" {
waits = h.passWaits(d) // no merge lands in a repository between another approved group's members (issue 1275)
}
if waits != r.Waits {
r.Waits = waits
h.keep(d)
}
if approvalAsk != nil {
asks = append(asks, approvalAsk)
}
if waits != "" || r.merging {
h.noteNotHanded(d, "", now) // what it waits for is said by its waits, or it is handed
continue
}
if !r.LastTry.IsZero() {
h.noteNotHanded(d, "", now)
continue // refused: asked again at the next event that decides whether it may merge (mergeAgainNow), with no timer
}
// A group is merged one member at a time, handed to the forge bridge, and the pass stops at the first refusal (ADR
// 0441 §3, §5); a lone pull request is a group of one.
g := h.groups[d.Group]
if d.Group == "" {
g = nil
}
if a := h.handTurn(g, d, now); a != nil {
h.noteNotHanded(d, "", now)
asks = append(asks, a)
} else {
h.noteNotHanded(d, h.notHanded(g, d), now) // a merge never handed says why (2026-10-29: silent for an hour)
}
}
asks = append(asks, h.settleMerges()...)
asks = append(asks, h.settleForgeMerges()...)
asks = append(asks, h.recordForgeMade()...)
asks = append(asks, h.seedTrunks()...)
asks = append(asks, h.withdrawnKeys()...)
asks = append(h.cancels, asks...)
h.cancels = nil
return asks
}
// mergeAgainNow lets an approved delivery's merge be asked at once: something that decides whether it may merge happened (a
// verdict, an Approve, a group's composed check), so the wait after a refusal does not hold it. Under the lock.
func (h *Holder) mergeAgainNow(d *Delivery) {
if d != nil && d.Ready != nil && d.Ready.Merged == nil {
if d.Ready.repeatedRefusal(d) {
return // the same refusal again and again with the trunk where it was: asking more changes nothing (issue 1106)
}
d.Ready.LastTry = time.Time{}
}
}
// causeChanged takes a verdict, a composed check or a status of the same heads that decides whether the merge may go: what the
// refusal waited on changed, so the count of the same words starts again and the merge is asked at once (issue 1785); only a
// refusal that truly repeats, with nothing new behind it, stops the asking (issue 1106). Under the lock.
func (h *Holder) causeChanged(d *Delivery) {
if d != nil && d.Ready != nil && d.Ready.Merged == nil {
d.Ready.Repeats = 0
}
h.mergeAgainNow(d)
}
// maxRepeatedRefusals is how many refusals in a row, with the same words and the trunk not moved, end the asking again: a
// refusal that repeats with no trunk move is not a trunk's move, and the check asked after each only brings the same words.
const maxRepeatedRefusals = 3
// repeatedRefusal is whether the merge was refused maxRepeatedRefusals times with these words and the trunk where it is
// now: it is then asked no more until something changes (a trunk's move, a new head, the operator's Approve).
func (r *ReadyRun) repeatedRefusal(d *Delivery) bool {
return r.Repeats >= maxRepeatedRefusals && r.RepeatTrunk == d.TrunkNow
}
// refusalKey is a refusal's first sentence, up to the trunk it names ("... and main is now <t>"): what follows says how long the
// last check took and the like, which differs each time and is no change of cause.
func refusalKey(answer string) string {
if i := strings.Index(answer, " is now "); i >= 0 {
if j := strings.Index(answer[i:], ". "); j >= 0 {
return answer[:i+j]
}
}
return answer
}
// noteRefusal counts a refusal: the same words with the trunk where it was count on, anything else starts the count at one.
func (r *ReadyRun) noteRefusal(d *Delivery, answer string) {
if refusalKey(answer) == refusalKey(r.Answer) && r.RepeatTrunk == d.TrunkNow && r.Repeats > 0 {
r.Repeats++
return
}
r.Repeats, r.RepeatTrunk = 1, d.TrunkNow
}
// readyWaits is what a delivery's merge waits on; empty when the Approve may be asked and the merge asked. Once the
// operator's Approve covers its heads the merge judges that Approve alone (approvedWaits); until then the check gates the
// ask (askWaits), the one place a check gates anything here (novox/hq ADR 0661 §3, §4). Under the lock.
func (h *Holder) readyWaits(d *Delivery) string {
if c := h.covering(d); c != nil && c.Approved != nil {
return h.approvedWaits(d)
}
return h.askWaits(d)
}
// askWaits is why the operator's Approve is not asked yet: the check gates the ask (novox/hq ADR 0661 §3, ADR 0421 §1). A
// ready delivery is asked once the checker's verdict at exactly the heads the ask covers passed; a proposed one whose
// verdict went out of date while its ask is pending waits for its check against the trunk's head. A group is asked only
// once its composed check passed at its members' heads (ADR 0661 §4).
func (h *Holder) askWaits(d *Delivery) string {
if d.judgedByCheck() && d.State == Proposed && d.outOfDate() != "" && h.covering(d) == nil {
return "its mesh/check is " + d.outOfDate() + ": the checker checks it again against the trunk's head"
}
switch {
case d.State == Ready:
case d.State == Proposed && d.outOfDate() != "" && d.AskedAgain != nil:
return "its check against the trunk's head is asked and has not answered"
case d.State == Proposed && d.outOfDate() != "":
return "its verdict is out of date: its check against the trunk's head is asked before the operator is"
case d.State == Proposed && d.AskedAgain != nil:
return "its check against the trunk's head is asked and has not answered"
default:
return "its check has not passed yet (" + string(d.State) + ")"
}
// On mesh/check, the checker's verdict at exactly the heads the ask covers (issue 1302, ADR 0447 §2).
if w := h.checkerWaits(d); w != "" {
return w
}
if d.Group == "" {
return ""
}
return h.groupTurnToMerge(d)
}
// approvedWaits is what a merge the operator's Approve covers waits on: nothing of the check (novox/hq ADR 0661 §3, §4).
// No verdict, carry, composed check or trunk move is waited on, and no status is read again; a group member waits only
// for every member to be ready, at the heads the Approve covers, so the group merges in one pass. A proposed delivery
// the Approve covers (a trunk moved before the Approve took its run) is ready on it. Under the lock.
func (h *Holder) approvedWaits(d *Delivery) string {
if c := h.covering(d); d.State == Proposed && d.Ready != nil && c != nil && c.Approved != nil {
now := h.Now()
h.approval(d, now) // the covering Approve taken onto its own run, so the table reads it
h.moved(d, settle(d, h.factsAt(now)))
}
if d.State != Ready {
return "it is " + string(d.State) + ", not ready yet"
}
if g := h.groups[d.Group]; d.Group != "" && g != nil {
for _, m := range h.membersOf(g) {
if m.State != Ready && m.State != Merged {
return "its group " + g.ID + " waits for " + m.ID + ", which is " + string(m.State)
}
}
}
return ""
}
// waitsCheckBeforeAsking is how the wait for the check's verdict begins, as `show` and the delivery's wait say it.
const waitsCheckBeforeAsking = "waits its check before asking the operator's Approve: "
// verdictWaits is why an Approve is not asked yet while the check gates no merge: the check at the heads the ask would cover
// has no verdict (the operator, 2026-10-27 23:54: "if you don't want me to approve it, don't send the approval request").
// A pass or a failure is asked (a failure in its words: the operator may still merge it), and nothing else: no timer stands
// in for the verdict, because the checker turns a check it lost into a failure it says aloud (issue 1733, the operator,
// 2026-10-28: "one hour is absurd"). For a group the check is the one of all its heads together. Under the lock.
func (h *Holder) verdictWaits(d *Delivery) string {
c := h.checkAt(h.askMembers(d))
if c.Key == checkSaidPass || c.Key == checkSaidFail {
return ""
}
return waitsCheckBeforeAsking + c.Words
}
// groupTurnToMerge is why a group member may not merge yet; empty when it may: the group's composed check passed for
// its members' heads as they are and is not out of date. No member waits for another's merge or delivery (novox/hq
// ADR 0418 §4): once the composed check passed and the Approve covers the heads, every member is merged in one pass.
// A group part way through that pass is merging, and its next member's turn is then (issue 1275).
func (h *Holder) groupTurnToMerge(d *Delivery) string {
g := h.groups[d.Group]
if g == nil {
return ""
}
members := h.membersOf(g)
if state, why := GroupState(g, members); !groupMayMerge(state) {
return fmt.Sprintf("its group %s is %s: %s", g.ID, state, why)
}
if c := g.Check; c == nil || (c.Verdict != "pass" && c.Verdict != "warning") || c.OutOfDate != "" ||
!sameMembers(c.Commits, commitsOf(members)) {
return "its group " + g.ID + "'s composed check has not passed for its members' heads"
}
return ""
}
// groupMayMerge is whether a group in this state hands its next member to the forge bridge: one ready, and one merging (a
// member merged, the pass goes on). Every other word holds the pass.
func groupMayMerge(state string) bool {
switch state {
case GroupReady, GroupMerging:
return true
}
return false
}
// sayOn owes a comment on a delivery's pull request: an owed act numbered among the comments on its head, written as a key
// with the delivery's other effects (owed.go). Under the lock; what it returns has nothing left to do.
func (h *Holder) sayOn(d *Delivery, body string) func() {
d.Comments++
d.Owed = append(d.Owed, Effect{Kind: EffectComment, Commit: d.Commit, Line: body, Context: strconv.Itoa(d.Comments), Since: h.Now()})
h.keep(d)
return func() {}
}
// failedAfterApprove says a failure heard after the operator's Approve covered the head: once on the pull request, and
// as the stalled line readyLines lists; the merge is not refused for it, and the operator decides the remedy, a fix or a
// revert (novox/hq ADR 0661 §5). A later pass clears it. Under the lock.
func (h *Holder) failedAfterApprove(d *Delivery, v *Verdict) {
r := d.Ready
if r == nil {
return
}
if v.Passes() {
r.FailedAfter, r.FailedAfterSince = "", nil
return
}
if r.FailedAfter != "" {
return
}
now := h.Now()
r.FailedAfter, r.FailedAfterSince = "its check failed after the operator's Approve: "+oneLine(clip(v.Summary, 120)), &now
h.Logf("[mesh-delivery] %s (%s): %s", d.ID, h.mergeFollowed(d), r.FailedAfter)
h.sayOn(d, fmt.Sprintf("mesh-delivery: **failed after the Approve.** This pull request (%s) %s. The merge is not "+
"refused for it: the operator decides the remedy, a fix or a revert.", h.mergeFollowed(d), r.FailedAfter))
}
// mergeFollowed names the merge a failure after the Approve follows: the trunk commit it merged as, or its approved head
// not merged yet.
func (h *Holder) mergeFollowed(d *Delivery) string {
if d.MergedAs != "" {
return "merged as " + shortOf(d.MergedAs, 12) + " at its head " + shortOf(d.Commit, 12)
}
return "approved at its head " + shortOf(d.Commit, 12) + ", not merged yet"
}
// readyLines are the approved heads that will not merge: a check that failed at once, a merge refused past its bound;
// as stalled lines, which the gate says as conditions (conditions.go). Under the lock.
func (h *Holder) readyLines(now time.Time) []StalledLine {
var out []StalledLine
for _, d := range h.deliveries {
r := d.Ready
if r != nil && r.FailedAfter != "" && r.FailedAfterSince != nil && d.State != Closed {
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
For: now.Sub(*r.FailedAfterSince).Round(time.Second).String(), Bound: "0s",
H2: "none: the operator decides the remedy, a fix or a revert; a later pass clears it",
Says: fmt.Sprintf("%s#%d (%s): %s", d.Repository, d.Number, h.mergeFollowed(d), r.FailedAfter)})
continue
}
if r == nil || r.Merged != nil || d.MergedAs != "" || d.State.Final() {
continue
}
switch {
case d.Base == "" && d.Number != 0 && now.Sub(r.Seen) >= headHeardBound:
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State, For: now.Sub(r.Seen).Round(time.Second).String(),
Bound: headHeardBound.String(), H2: "none: a later announcement of the pull request's head (the merge tool announces it again, the forge's announcer restarts, or a new push) or an edit of its description; a recheck does not fill it — the operator's",
Says: fmt.Sprintf("%s#%d at %s (%s) waits for %s", d.Repository, d.Number, shortOf(r.Head, 12),
h.checkAt([]*Delivery{d}).Words, waitsForHead)})
case r.CannotWrite != "" && r.Approved == nil && r.CannotWriteSince != nil:
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
For: now.Sub(*r.CannotWriteSince).Round(time.Second).String(), Bound: "0s",
H2: "none: the forge account given write on the repository (gitea_grant_write; the gitea module grants it itself once a module of it is registered), which asks the operator then",
Says: fmt.Sprintf("%s#%d at %s (%s): the operator is not asked for its Approve: %s",
d.Repository, d.Number, shortOf(r.Head, 12), h.checkAt([]*Delivery{d}).Words, r.CannotWrite)})
case r.Failed != "" && d.State == Rejected:
// Its cure is its author's new head: listed here at once (`show`), told on the pull request at once, and the
// operator's only once no new head came within the author's bound (novox/hq ADR 0505 §5).
if now.Sub(d.Since) < h.authorBound() {
continue
}
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State, For: now.Sub(d.Since).Round(time.Second).String(),
Bound: h.authorBound().String(), H2: "none: a new head from its author, or a recheck that passes",
Needs: NeedsNewHead, Author: authorOf(d), Branch: d.Branch,
Says: fmt.Sprintf("%s#%d passed its checks at %s and is not merged: %s. It needs a new head from %s, "+
"and none came for %s", d.Repository, d.Number, shortOf(r.Head, 12), r.Failed, authorOf(d),
now.Sub(d.Since).Round(time.Minute))})
case r.Refused != "":
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State, For: now.Sub(r.Seen).Round(time.Second).String(),
Bound: "0s", H2: "none: a new head, which asks the operator again — the operator's",
Says: fmt.Sprintf("%s#%d at %s is not merged: the operator's answer %s", d.Repository,
d.Number, shortOf(r.Head, 12), r.Refused)})
case r.NotHanded != "" && r.NotHandedSince != nil && r.Approved != nil:
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
For: now.Sub(*r.NotHandedSince).Round(time.Second).String(), Bound: "0s",
H2: "none: the member named is approved at its head and ready, or its group is asked anew by a new head — the operator's",
Says: fmt.Sprintf("%s#%d at %s is approved and %s", d.Repository, d.Number, shortOf(r.Head, 12), r.NotHanded)})
case h.answering(d, now):
// The trunk moved and its check was asked again: the gate is answering it, so it is no stall yet (ADR 0505
// §4). `show` lists the refusal meanwhile.
case r.RefusedSince != nil && d.Group != "" && h.groupClosed(d.Group) && now.Sub(*r.RefusedSince) >= groupRefusedFor:
// A closed group's member (novox/hq ADR 0418 §3, issue 1082): its sibling merged, and its own merge is refused
// again after each composed check. Said after the merge window, with the group and the tool's last answer.
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
For: now.Sub(*r.RefusedSince).Round(time.Second).String(), Bound: groupRefusedFor.String(),
H2: "none: the group's composed check is asked again after each refusal, and the merge each minute — the operator's",
Severity: refusalSeverity(r, now),
Says: fmt.Sprintf("group %s is half merged: %s#%d is approved at %s and the merge tool has refused it since %s: %s. "+
"Needed: the refusal's cause is met (the merge is asked again at each verdict, status or Approve), or the operator decides.",
d.Group, d.Repository, d.Number, shortOf(r.Head, 12), r.RefusedSince.UTC().Format(time.RFC3339), r.Answer)})
case r.RefusedSince != nil && now.Sub(*r.RefusedSince) >= readyRefusedFor:
out = append(out, StalledLine{ID: d.ID, Number: d.Number, State: d.State,
For: now.Sub(*r.RefusedSince).Round(time.Second).String(), Bound: readyRefusedFor.String(),
H2: "none: the merge is asked again on a verdict, an Approve, a group's composed check or a merge check's " +
"status turning success — the operator's",
Says: fmt.Sprintf("%s%s#%d is approved at %s, and the merge tool has refused it since %s: %s%s", heldInGroup(d),
d.Repository, d.Number, shortOf(r.Head, 12), r.RefusedSince.UTC().Format(time.RFC3339), r.Answer, neededOf(d)),
Severity: severityOf(d, now)})
}
}
return out
}
// mergedEvent is the module event `merged` (issue 1025): the pull request merged, its head and group, and the ask,
// option and heads the operator's Approve was bound to (the warrant the controller reads back from the router). A merge
// without an Approve names no ask and no option: no warrant is invented. Under the lock, before anything clears them.
func mergedEvent(d *Delivery, head string) map[string]any {
r := d.Ready
heads := map[string]string{}
for k, v := range r.Heads {
heads[k] = v
}
ev := map[string]any{"owner": d.Owner(), "repo": d.Repo(), "number": d.Number, "head_sha": head, "group": d.Group,
"ask_id": "", "option": "", "heads": heads}
if r.Approved != nil {
ev["ask_id"], ev["option"] = r.AskID, OptionApprove
if _, _, _, err := mdc.ParseAskKey(r.AskID); err == nil {
// The ask of the gate: the router knows it by the id its key gives, and the teller asked it (ADR 0437 §6).
ev["ask_id"], ev["asker"] = mdc.AskingID(r.AskID, r.Asking), tellerModule
} // an Approve carried over from today's module keeps its router id, and the asker the controller takes by default
}
return ev
}
// heldInGroup is the words that put a refused member in its group's pass: none for a lone delivery.
func heldInGroup(d *Delivery) string {
if d.Group == "" {
return ""
}
return "group " + d.Group + " is held, and no member after this one is merged: "
}
// neededOf is what a refused member needs, for a member of a group (issue 1275).
func neededOf(d *Delivery) string {
if d.Group == "" {
return ""
}
return ". Needed: the refusal's cause is met (the merge is asked again at each verdict, status or Approve), or the operator decides."
}
// answering is whether an approved head the merge tool refused is one the gate is already answering, and so not said as
// stalled yet (novox/hq ADR 0505 §4): refused only because its trunk moved, its check asked again, and neither has that
// check answered with the merge refused again after it, nor has twice the repository's last check time (at least the
// floor) passed since the refusals began. Any other refusal of this head, and a refusal repeated past its limit, is said
// at once (r.Urgent). Under the lock.
func (h *Holder) answering(d *Delivery, now time.Time) bool {
r := d.Ready
if r == nil || r.RefusedSince == nil || r.Urgent || !outOfDateRefusal(r.Answer) {
return false
}
if r.LastRefusedOn.After(r.FirstRefusedOn) {
return false // its fresh check answered, and the merge was refused again on it
}
return now.Sub(*r.RefusedSince) < h.answeredRefusalBound(d.Repository)
}
// outOfDateFor is how long a refusal for an out of date verdict is a warning before it is urgent (issue 1275, AC6).
const outOfDateFor = 10 * time.Minute
// outOfDateRefusal is whether the merge tool refused because the member's verdict is judged against an earlier trunk: the
// forge's "its verdict was judged against trunk <t>, and main is now <t>. The check of head <h> merged into trunk <t> was
// asked" (gitea merge.ts), whose fresh check is already running.
//
// Words that also name another cause (a status not a success, missing or failed) are not this: the other cause is urgent
// at once (ADR 0442, issue 1275 AC6).
func outOfDateRefusal(answer string) bool {
for _, other := range []string{"not a success", "(missing)", "(failure)", "(error)", "could not be asked"} {
if strings.Contains(answer, other) {
return false
}
}
// gitea merge.ts: "Its check is already asked and not yet answered" (on its way), or "The check of head <h> merged into
// trunk <t> was asked" (asked now). Its other refusals, among them "its check could not be asked", are not this.
return strings.Contains(answer, "its verdict ") && strings.Contains(answer, " is now ") &&
(strings.Contains(answer, "Its check is already asked and not yet answered") ||
strings.Contains(answer, "merged into trunk") && strings.Contains(answer, "was asked"))
}
// refusalSeverity is "warning" while a member's refusal for an out of date verdict is younger than ten minutes, as its
// fresh check runs, and "urgent" for it after that and for every other refusal at once.
func refusalSeverity(r *ReadyRun, now time.Time) string {
if !r.Urgent && r.RefusedSince != nil && outOfDateRefusal(r.Answer) && now.Sub(*r.RefusedSince) < outOfDateFor {
return "warning"
}
return "urgent"
}
// severityOf is the severity of a refused member of a group, empty for a lone delivery.
func severityOf(d *Delivery, now time.Time) string {
if d.Group == "" {
return ""
}
return refusalSeverity(d.Ready, now)
}
@@ -1,315 +0,0 @@
package main
import (
"errors"
"sort"
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
)
// **A pull request whose checks passed and whose merge the operator approved is merged by this owner** (novox/hq issues
// 722 and 1022): before, a reviewed pull request with green checks waited until a session called the merge tool, and
// again after every check the tool asked because the trunk had moved.
const (
readyHead = "1111111111111111111111111111111111111111"
pushed = "2222222222222222222222222222222222222222"
)
// openPull is an open pull request into main as gitea_open_pulls reads it.
func openPull(repo string, number int, head string) OpenPull {
owner, name, _ := strings.Cut(repo, "/")
return OpenPull{Owner: owner, Repo: name, Number: number, Base: "main", HeadSHA: head}
}
func judgedAt(repo string, number int, sha, gate, trunk string) CheckedEvent {
c := verdict(repo, number, sha, gate)
c.Trunk = trunk
return c
}
func TestAnApprovedPullRequestIsMergedThroughTheMergeTool(t *testing.T) {
w := newWorld(t)
w.op.auto = false
id := IDOf("novox/app", readyHead)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("a pull request nobody approved was merged: %v", w.forge.merges)
}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 || w.forge.merges[0] != "novox/app#7@"+readyHead {
t.Fatalf("an approved pull request was merged %v; wanted once, at the approved head", w.forge.merges)
}
d := w.delivery(id)
if d.Ready == nil || d.Ready.Merged == nil || d.Ready.Tries != 1 {
t.Fatalf("what was done with the Approve is not kept: %+v", d.Ready)
}
// Its merge's transition names the Approve as its warrant.
w.h.PullMerged(merged("novox/app", 7, readyHead, "m7m7m7m7m7m7"))
w.settleAll()
d = w.delivery(id)
var last Transition
for _, tr := range d.Transitions {
if tr.Event == EvMerged {
last = tr
}
}
if !strings.Contains(last.By, "the operator's Approve at "+readyHead[:12]) {
t.Fatalf("the merge's transition does not name the Approve: %+v", last)
}
if len(w.forge.merges) != 1 {
t.Fatalf("merged again: %v", w.forge.merges)
}
// 1733: the Approve's ask says the check's state at the heads it names, read from the checker's verdict there: "passed
// its check" only when it passed. While the check gates no merge, the ask waits for the check's verdict (the operator,
// 2026-10-27 23:54: "if you don't want me to approve it, don't send the approval request"): a pass or a failure is asked,
// the failure in its words; with no verdict nothing is asked and the delivery says it waits its check, until the verdict
// comes, however long (issue 1733: no timer stands in for it). The verdict is the
// checker's verdicts bucket value as captured on the bus (mesh-delivery-checker, `<group>.<heads hash>`).
for _, row := range []struct {
name, state, failed, then string
gated bool
after time.Duration
asks, open int
want, not []string
head string // the ask's headline starts with it
}{
{name: "gated, passed", gated: true, state: "pass", asks: 1, head: "Merge into main: app#7 Fix it",
want: []string{"app#7 Fix it passed its check. Approve merges it into main at " + readyHead[:12] + ";"},
not: []string{"before its check ends", "no verdict yet"}},
} {
t.Run(row.name, func(t *testing.T) {
v := newWorld(t)
v.op.auto = false
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
p.Title = "Fix it"
v.h.PullUpdated(p)
head := headOf("novox/app", 7, readyHead)
if row.gated {
v.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
}
v.settleAll()
if !row.gated && row.state != "" {
checkerSaysWith(v, "g", row.state, row.failed, nil, head)
v.settleAll()
}
if row.after > 0 {
v.later(row.after)
v.settleAll()
}
if row.then != "" {
checkerSays(v, "g", row.then, row.failed, head)
v.settleAll()
}
open := v.op.open()
wantOpen := min(row.asks, 1)
if row.open < 0 {
wantOpen = 0
}
if v.op.count() != row.asks || len(open) != wantOpen {
t.Fatalf("open asks %v, asked %d times; wanted asked %d times, %d open", open, v.op.count(), row.asks, wantOpen)
}
if wantOpen == 0 {
d := v.delivery(IDOf("novox/app", readyHead))
for _, w := range row.want {
if d.Ready == nil || !strings.Contains(d.Ready.Waits, w) {
t.Fatalf("the delivery's wait does not say %q: %+v", w, d.Ready)
}
}
return
}
if h := v.op.ask(open[0]).Headline; row.head != "" && !strings.HasPrefix(h, row.head) {
t.Fatalf("the headline is %q; wanted it to start %q", h, row.head)
}
e := v.op.ask(open[0]).Explanation
for _, w := range row.want {
if !strings.Contains(e, w) {
t.Fatalf("the ask does not say %q:\n%s", w, e)
}
}
for _, w := range row.not {
if strings.Contains(e, w) {
t.Fatalf("the ask says %q:\n%s", w, e)
}
}
if err := occ.Glance(e); err != nil {
t.Fatal(err)
}
})
}
}
// 1144/AC3: a refused merge is not asked again by any timer; only an event (here, mergeAgainNow called directly, as a
// verdict, an Approve or a composed check does) asks it again.
func TestAMergeRefusedIsSaidAtOnceAndAskedAgainOnlyOnAnEvent(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check is pending")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
if lines := w.h.Stalled(); len(lines) != 1 || !strings.Contains(lines[0].Says, "refused it since") {
t.Fatalf("a merge refused is not said at once: %+v", lines)
}
w.later(time.Hour)
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("asked again with no event: %v", w.forge.merges)
}
again := w.delivery(IDOf("novox/app", readyHead))
w.h.mu.Lock()
w.h.mergeAgainNow(again) // what a verdict, an Approve or a composed check does
w.h.mu.Unlock()
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("not asked again on an event: %v", w.forge.merges)
}
}
// 1144/AC1: a refused merge, approved and ready, is asked again at once when the forge's forge.status says one of
// the merge checks (mesh/merge-gate, mesh/repo-check) is success for the commit the merge was refused at — the
// delivery's approved head — and merges once the tool accepts (novox/hq mesh-catalog#366: the verdict reached this
// owner before the forge wrote the statuses, the merge asked then was refused on a pending check, and nothing asked
// it again when the statuses turned success).
func TestAMergeRefusedIsAskedAgainWhenItsCheckTurnsSuccessOnTheForge(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check (pending)")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("not asked once, refused: %v", w.forge.merges)
}
w.forge.refuse = nil
w.h.CommitStatus(ForgeStatusEvent{Owner: "novox", Repo: "app", SHA: readyHead, Context: "mesh/repo-check", State: "success"})
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("not asked again when its check turned success: %v", w.forge.merges)
}
if w.delivery(IDOf("novox/app", readyHead)).Ready.Merged == nil {
t.Fatalf("it did not merge once the tool accepted")
}
}
// 1144/AC2: a status of another context, of another commit, or a state other than success changes nothing for a
// refused merge: it is not asked again.
func TestAMergeRefusedIsNotAskedAgainOnAStatusThatDoesNotMatch(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check (pending)")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("not asked once, refused: %v", w.forge.merges)
}
w.forge.refuse = nil
for _, e := range []ForgeStatusEvent{
{Owner: "novox", Repo: "app", SHA: readyHead, Context: "some/other-check", State: "success"}, // another context
{Owner: "novox", Repo: "app", SHA: pushed, Context: "mesh/repo-check", State: "success"}, // another commit
{Owner: "novox", Repo: "app", SHA: readyHead, Context: "mesh/repo-check", State: "pending"}, // another state
} {
w.h.CommitStatus(e)
w.settleAll()
}
if len(w.forge.merges) != 1 {
t.Fatalf("asked again on a status that should change nothing: %v", w.forge.merges)
}
if w.delivery(IDOf("novox/app", readyHead)).Ready.Merged != nil {
t.Fatalf("merged on a status that should change nothing")
}
}
// 1144/AC4: the stalled line for a refused approved head names the events that ask the merge again, and no longer
// says "each minute" (removed by issue 1115; false since then — there is no timer).
func TestTheStalledLineForARefusedApprovedHeadNamesTheEventsThatAskTheMergeAgain(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: mesh/repo-check (pending)")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
lines := w.h.Stalled()
if len(lines) != 1 {
t.Fatalf("wanted one stalled line, got %+v", lines)
}
h2 := lines[0].H2
if strings.Contains(h2, "each minute") {
t.Fatalf("the stalled line still says 'each minute', false since issue 1115: %q", h2)
}
for _, want := range []string{"verdict", "Approve", "composed check", "status"} {
if !strings.Contains(h2, want) {
t.Fatalf("the stalled line does not name %q as asking the merge again: %q", want, h2)
}
}
}
func TestAGroupMergesEveryMemberInOnePassOnlyOnceComposedAndApproved(t *testing.T) {
w := newWorld(t)
w.op.auto = false
const ctlHead, catHead = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
w.h.PullUpdated(pr("novox/mesh-controller", 1, ctlHead, "feat/x", "internal/a.go"))
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catHead, "feat/x", "modules/app/module.json"))
w.h.Checked(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
if w.op.count() != 1 || len(w.forge.merges) != 0 {
t.Fatalf("composed, the group is asked once and nothing merges before the Approve: asks %d, merges %v", w.op.count(), w.forge.merges)
}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("both members are merged in the same pass on the one Approve: %v", w.forge.merges)
}
sort.Strings(w.forge.merges)
if w.forge.merges[0] != "novox/mesh-catalog#2@"+catHead || w.forge.merges[1] != "novox/mesh-controller#1@"+ctlHead {
t.Fatalf("merged %v, wanted both, each at its approved head", w.forge.merges)
}
for _, got := range w.forge.approvedHeads {
if len(got) != 2 || got[0] != "novox/mesh-catalog@"+catHead || got[1] != "novox/mesh-controller@"+ctlHead {
t.Fatalf("a member's merge was handed the approved heads %v", got)
}
}
if w.op.count() != 1 {
t.Fatalf("a second Approve was asked: %d", w.op.count())
}
}
// **A merge into another branch than main is its author's** (novox/hq ADR 0398 §4, issue 948): a green pull request
// whose base is not main is never asked or merged by this owner; it says so once on the pull request.
func TestAGreenPullRequestIntoAnotherBranchIsNotAskedOrMergedAndSaidOnce(t *testing.T) {
w := newWorld(t)
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
p.Base = "release/x"
w.h.PullUpdated(p)
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
open := openPull("novox/app", 7, readyHead)
open.Base = "release/x"
w.forge.open = []OpenPull{open}
for range 3 {
w.h.SeeOpenPulls()
w.settleAll()
w.later(2 * time.Minute)
}
if len(w.forge.merges) != 0 || w.op.count() != 0 {
t.Fatalf("a pull request into release/x was merged %v or asked %d", w.forge.merges, w.op.count())
}
if len(w.forge.comments) != 1 || !strings.Contains(w.forge.comments[0], "another branch") ||
!strings.Contains(w.forge.comments[0], "0398") {
t.Fatalf("not said exactly once on the pull request: %v", w.forge.comments)
}
}
@@ -13,7 +13,6 @@ import (
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/buscalls"
@@ -546,122 +545,6 @@ func TestTheRecordsOutcomesArriveThroughTheGatesConsumer(t *testing.T) {
})
}
// W11.3 (design 47 *The switch carries today's state in, once*): at the gate's start every group in flight becomes a
// record at its heads; a second switch (a restart, or milestone G delivered again after a revert) opens none and asks
// nothing; and from then on every merge the gate hands is in its record, whose merge commits the record's outcomes
// are found by.
func TestTheSwitchCarriesTheGroupsInFlightIntoRecordsOnce(t *testing.T) {
w := approvedGroup(t)
l := newLedgerFake(t)
w.h.Records = &Records{Ledger: l, Now: w.h.Now}
w.h.Merges = recordedMerges{MergeStore: w.merges, records: w.h.Records, members: w.h.groupMembers}
// W11.17d: what the old state owed is said as switched acts: every key of owed (G §169).
statusKey, _ := mdc.OwedKey("novox/app", 7, readyHead, statusAct("mesh/delivery"))
w.old.put(mdc.SwitchedOwed, statusKey, []byte(`{"act":"status","context":"mesh/delivery","state":"success","text":"ready","target":"none"}`), 4)
switched := func() []meshdelivery.Owed {
var out []meshdelivery.Owed
for _, o := range w.owed.said {
if _, _, _, ok := mdc.ParseSwitchedActID(o.ID); ok {
out = append(out, o)
}
}
return out
}
opened, found, err := w.h.Switch()
if err != nil || opened != 1 || found != 0 {
t.Fatalf("the first switch: %d opened, %d found, %v", opened, found, err)
}
carried := switched()
wantStatus, _ := mdc.SwitchedActID(mdc.SwitchedOwed, statusKey, 4)
if len(carried) != 1 || carried[0].ID != wantStatus || carried[0].Act != meshdelivery.OwedActStatus || carried[0].Context != "mesh/delivery" ||
carried[0].Commit != readyHead {
t.Fatalf("the switch carried %+v", carried)
}
asked := w.op.count()
opened, found, err = w.h.Switch()
if err != nil || opened != 0 || found != 1 || w.op.count() != asked {
t.Fatalf("the switch again: %d opened, %d found, %v, %d asks (was %d)", opened, found, err, w.op.count(), asked)
}
if again := switched(); len(again) != 2 || again[1].ID != wantStatus {
t.Fatalf("the switch again said %+v, wanted the same act under the same id", again)
}
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.settleAll()
records, err := w.h.Records.Current()
if err != nil || len(records) != 1 {
t.Fatalf("records %+v %v: the handed merge opened another", records, err)
}
r := records[0]
if r.Milestone != "feat/g" || r.Group == nil || r.Group.Ask == "" || len(r.Group.Pulls) != 2 || len(r.Group.Members) != 2 {
t.Fatalf("the record's group state %+v", r.Group)
}
for _, p := range r.Group.Pulls {
if p.State != string(mdc.MergeMerged) || p.Commit == "" {
t.Fatalf("a member's merge is not in the record: %+v", p)
}
}
c := &StepConsumer{Records: w.h.Records, Families: stepFamilies, Now: w.h.Now}
body := `{"v":1,"kind":"taken","key":"m.00000000000000000001","what":"revision","merges":[{"repository":"` + gCat +
`","number":2,"merge":"` + r.Group.Pulls[1].Commit + `"}],"since":"2026-11-09T20:00:00Z"}`
if err := c.Hear(stdio.Envelope{Key: "mesh-delivery-indexer.take-in.taken", Body: json.RawMessage(body)}); err != nil {
t.Fatal(err)
}
if back, _ := w.h.Records.Read(r.Instance); back.Outcomes[OutcomeKey("", "", "", "take-in")].Outcome != OutcomeTakenIn {
t.Fatalf("the take-in of the group's merge is not its record's outcome: %+v", back.Outcomes)
}
}
// W11.9 (design 47 *The delivery record*): the gate says heads-replaced when a candidate's heads were replaced or its pull
// request closed, keyed as the candidate's revisions are (the heads' hash), so the indexer and the assigner drop its graphs.
func TestACandidatesHeadsReplacedOrClosedAreSaid(t *testing.T) {
a, b, c := strings.Repeat("a", 40), strings.Repeat("b", 40), strings.Repeat("c", 40)
setup := func(t *testing.T) (*world, *[]headsReplaced) {
w := newWorld(t)
var said []headsReplaced
w.h.SaySeat = func(subject string, body any, id string) error {
if subject != meshdelivery.SubjectHeadsReplaced {
t.Errorf("said on %s", subject)
}
e := body.(headsReplaced)
if id != "heads-replaced."+e.Group+"."+e.Heads {
t.Errorf("id %s for %+v", id, e)
}
said = append(said, e)
return nil
}
w.h.PullUpdated(pr("novox/app", 7, a, "feat/x", "src/a.go"))
w.h.PullUpdated(pr("novox/lib", 3, b, "feat/x", "src/b.go"))
w.settleAll()
return w, &said
}
hashOf := func(heads ...mdc.Head) string {
h, err := mdc.HeadsHash(heads)
if err != nil {
t.Fatal(err)
}
return h
}
group := hashOf(mdc.Head{Repository: "novox/app", Number: 7, Head: a}, mdc.Head{Repository: "novox/lib", Number: 3, Head: b})
t.Run("a member's new head replaces the group's candidate", func(t *testing.T) {
w, said := setup(t)
w.h.PullUpdated(pr("novox/app", 7, c, "feat/x", "src/a.go"))
w.settleAll()
if len(*said) != 1 || (*said)[0].Group != "feat/x" || (*said)[0].Heads != group || (*said)[0].Head != c ||
(*said)[0].Closed || (*said)[0].Repository != "novox/app" || (*said)[0].Number != 7 {
t.Fatalf("said %+v (the candidate %s)", *said, group)
}
})
t.Run("a member's pull request closed replaces the group's candidate", func(t *testing.T) {
w, said := setup(t)
w.h.PullClosed(pr("novox/lib", 3, b, "feat/x"))
w.settleAll()
if len(*said) != 1 || (*said)[0].Heads != group || !(*said)[0].Closed || (*said)[0].Head != "" || (*said)[0].Number != 3 {
t.Fatalf("said %+v (the candidate %s)", *said, group)
}
})
}
// handActsFake is the hand-act log: what it took, or a refusal.
type handActsFake struct {
took []meshhandactsclient.Request
@@ -921,52 +804,6 @@ func TestTheRecordsLedgerSaysFillingFullAndRoom(t *testing.T) {
t.Fatalf("gate_records says the unread fill as %v", line["fill"])
}
})
t.Run("a merge refused for a full ledger is held, then handed with room once below 80 %", func(t *testing.T) {
w := approvedGroup(t)
l := newLedgerFake(t)
var events []said
w.h.Records = &Records{Ledger: l, Now: w.h.Now, Say: saying(&events)}
w.h.Merges = recordedMerges{MergeStore: w.merges, records: w.h.Records, members: w.h.groupMembers}
l.fill = filled(capBytes)
allGreen(w)
w.op.answer(w.op.open()[0], OptionApprove)
w.settleAll()
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("a full ledger let %v merge", w.forge.merges)
}
w.h.mu.Lock()
since := w.h.heldFull()
w.h.mu.Unlock()
if since == nil || len(events) == 0 || events[0].subject != meshdelivery.SubjectFull {
t.Fatalf("the merge is not held for room (%v), said %+v", since, events)
}
// Room for a record but past 80 %: still held.
l.fill = filled(capBytes * 85 / 100)
w.later(askAgainAfter + time.Second)
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("a ledger past 80 %% let %v merge after a full", w.forge.merges)
}
// Below 80 %: room said once, the held mark cleared, the group merged.
l.fill = filled(capBytes / 2)
w.later(askAgainAfter + time.Second)
w.settleAll()
w.settleAll()
rooms := 0
for _, e := range events {
if e.subject == meshdelivery.SubjectRoom {
rooms++
}
}
w.h.mu.Lock()
since = w.h.heldFull()
w.h.mu.Unlock()
if rooms != 1 || since != nil || len(w.forge.merges) == 0 {
t.Fatalf("room said %d times, held since %v, merges %v", rooms, since, w.forge.merges)
}
})
}
// ownersFake answers the owners' reads: the fingerprint the assignment graph names, and the fingerprint and phase of the have.
@@ -13,7 +13,6 @@ import (
"strings"
"time"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
@@ -253,41 +252,3 @@ func (rs *Records) sayEnded(r *Record) error {
return rs.say(meshdelivery.SubjectRecordEnded, recordEnded{Record: r.Instance, Milestone: r.Milestone,
ForgeMade: r.ForgeMade, Away: r.Ended.Away, At: r.Ended.At}, "record-ended."+r.Instance)
}
// headsReplaced is the seat's `heads-replaced` body: a candidate's heads replaced or its pull request closed.
type headsReplaced struct {
Group string `json:"group"`
Heads string `json:"heads"`
Repository string `json:"repository"`
Number int `json:"number"`
Head string `json:"head,omitempty"`
Closed bool `json:"closed,omitempty"`
At time.Time `json:"at"`
}
// sayHeadsReplaced says that the candidate of these members' heads means nothing any more, since one member's head moved
// to head or (closed) its pull request closed; the heads are keyed as the candidate's revisions are (HeadsHash). Under the
// lock; the say itself is the work's, outside it.
func (h *Holder) sayHeadsReplaced(group string, members []*Delivery, moved *Delivery, head string, closed bool) {
if h.SaySeat == nil || len(members) == 0 {
return
}
hash, err := mdc.HeadsHash(headsOf(members))
if err != nil {
h.refuse(fmt.Errorf("the candidate of %s cannot be said replaced: %v", group, err))
return
}
e := headsReplaced{Group: group, Heads: hash, Repository: strings.ToLower(moved.Repository), Number: moved.Number,
Closed: closed, At: h.Now().UTC()}
if !closed {
e.Head = strings.ToLower(head)
}
say := h.SaySeat
id := "heads-replaced." + group + "." + hash
h.cancels = append(h.cancels, func() {
if err := say(meshdelivery.SubjectHeadsReplaced, e, id); err != nil {
h.Logf("[mesh-delivery] the candidate %s of %s is not said replaced: %v", hash, group, err)
}
})
h.Poke()
}
@@ -144,58 +144,3 @@ func (rs *Records) fillLine() map[string]any {
}
return line
}
// heldFull is the earliest time a merge was held for a full ledger, of every delivery that waits for room. Under the
// lock.
func (h *Holder) heldFull() *time.Time {
var since *time.Time
for _, d := range h.deliveries {
if r := d.Ready; r != nil && r.HeldFull != nil && (since == nil || r.HeldFull.Before(*since)) {
since = r.HeldFull
}
}
return since
}
// roomFor clears every delivery's wait for room. Under the lock.
func (h *Holder) roomFor() {
for _, d := range h.deliveries {
if r := d.Ready; r != nil && r.HeldFull != nil {
r.HeldFull = nil
h.keep(d)
}
}
}
// holdFull keeps that target's merge waits for room. Under the lock.
func (h *Holder) holdFull(target string, now time.Time) {
if d := h.deliveries[target]; d != nil && d.Ready != nil && d.Ready.HeldFull == nil {
at := now
d.Ready.HeldFull = &at
h.keep(d)
}
}
// waitForRoom is whether a merge must wait because the records' ledger was full: while any merge waits for room, none
// is handed until RoomAgain gives it.
func (h *Holder) waitForRoom(target string) bool {
if h.Records == nil {
return false
}
h.mu.Lock()
since := h.heldFull()
h.mu.Unlock()
if since == nil {
return false
}
ok, err := h.Records.RoomAgain(*since)
if !ok {
h.giveBack(target, err)
return true
}
h.mu.Lock()
h.roomFor()
h.mu.Unlock()
h.Logf("[mesh-delivery] the records' ledger has room again: the merges held for it are handed")
return false
}
@@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"strings"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
stdio "git.novox.be/novox/mesh-sdk/go"
@@ -208,3 +209,11 @@ func recordTools(h *Holder, acts handActs) []stdio.Tool {
}},
}
}
// timeText is a time as the gate keeps it, RFC 3339; empty when not said.
func timeText(t *time.Time) string {
if t == nil || t.IsZero() {
return ""
}
return t.UTC().Format(time.RFC3339)
}
@@ -1,6 +1,6 @@
package main
// The gate's settings (novox/hq ADR 0505 §4, §5), merged by the mesh into one JSON file the env names. Each has a
// The gate's setting (design 47 *The delivery record*), merged by the mesh into one JSON file the env names. Each has a
// default here, so a gate started without the file, or with a value it cannot read, keeps the decided bound and says
// why once.
@@ -12,25 +12,14 @@ import (
"time"
)
const (
// DefaultRefusalFloor is the least a refusal the gate is already answering (the trunk moved and its check was
// asked again) waits before it is said as stalled, when the setting refusal-stall-floor-minutes gives none.
DefaultRefusalFloor = 10 * time.Minute
// DefaultAuthorBound is how long a delivery whose cure is its author's new head waits for one before the operator
// hears of it, when the setting author-new-head-minutes gives none (ADR 0505 §5).
DefaultAuthorBound = 2 * time.Hour
)
// Settings are the operator's values for this module.
type Settings struct {
RefusalFloorMinutes *int `json:"refusal-stall-floor-minutes"`
AuthorBoundMinutes *int `json:"author-new-head-minutes"`
// RecordRetentionDays is how many days a delivery record is kept from when it opened (design 47 *The delivery
// record*); nil keeps DefaultRecordRetention.
RecordRetentionDays *int `json:"record-retention"`
}
// readSettings reads the settings file; a value that is not a whole number of minutes, one or more, is refused.
// readSettings reads the settings file; a value that is not a whole number of days, from 1 to 3650, is refused.
func readSettings(path string) (Settings, error) {
var s Settings
if path == "" {
@@ -44,20 +33,6 @@ func readSettings(path string) (Settings, error) {
if err := json.Unmarshal(raw, &loose); err != nil {
return s, fmt.Errorf("the settings file is not JSON: %v", err)
}
for name, into := range map[string]**int{"refusal-stall-floor-minutes": &s.RefusalFloorMinutes,
"author-new-head-minutes": &s.AuthorBoundMinutes} {
switch v := loose[name].(type) {
case nil:
case float64:
n := int(v)
if v < 1 || v != float64(n) {
return Settings{}, fmt.Errorf("%s is not a whole number of minutes, one or more", name)
}
*into = &n
default:
return Settings{}, fmt.Errorf("%s is not a number", name)
}
}
switch v := loose["record-retention"].(type) {
case nil:
case float64:
@@ -72,28 +47,6 @@ func readSettings(path string) (Settings, error) {
return s, nil
}
// factsAt are the facts a settle reads at now: the moment. No setting switches whether a check gates a merge: the check
// gates the ask, and the merge judges the operator's Approve alone (novox/hq ADR 0661 §2, §3).
func (h *Holder) factsAt(now time.Time) Facts {
return Facts{Now: now}
}
// minutesOr is a setting's minutes, or the default when it gives none.
func minutesOr(n *int, def time.Duration) time.Duration {
if n == nil {
return def
}
return time.Duration(*n) * time.Minute
}
// refusalFloor is the least an answered refusal waits before it is said as stalled (ADR 0505 §4).
func (h *Holder) refusalFloor() time.Duration {
if h.Settings == nil {
return DefaultRefusalFloor
}
return minutesOr(h.Settings().RefusalFloorMinutes, DefaultRefusalFloor)
}
// recordRetention is how long a delivery record is kept from when it opened.
func (h *Holder) recordRetention() time.Duration {
if h.Settings == nil {
@@ -104,11 +57,3 @@ func (h *Holder) recordRetention() time.Duration {
}
return DefaultRecordRetention
}
// authorBound is how long a delivery that needs its author's new head waits before the operator hears of it (§5).
func (h *Holder) authorBound() time.Duration {
if h.Settings == nil {
return DefaultAuthorBound
}
return minutesOr(h.Settings().AuthorBoundMinutes, DefaultAuthorBound)
}
@@ -1,26 +1,49 @@
package main
// The switch to the delivery record (design 47 *The delivery record*: "The switch carries today's state in, once"): at
// the new gate's start, every group in flight and its merge as `merges` holds it become records, keyed by the
// milestone and the heads, and from then on every merge the gate hands is written into its record. It is safe to run
// twice: a group in flight whose record is there already leaves it as it is (Records.Open), and a group state the
// record holds already is not written again (Records.SetGroup), so delivering milestone G again after a revert makes no
// second record and asks nothing again.
// the new gate's start, every group in flight and its merge as the old sections hold them become records, keyed by the
// milestone and the heads. It is safe to run twice: a group in flight whose record is there already leaves it as it is
// (Records.Open), and a group state the record holds already is not written again (Records.SetGroup), so delivering milestone
// G again after a revert makes no second record and asks nothing again. The old sections are read, never written (§613).
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// milestoneOf is the milestone a delivery's record is kept under: the name its group's Approve is asked and its merge is
// handed by (askGroup), the one name the gate gives a group's delivery today.
func milestoneOf(d *Delivery) string { return askGroup(d) }
// oldDelivery is a delivery as the old section `deliveries` kept it, as far as the switch reads it.
type oldDelivery struct {
ID string `json:"id"`
Repository string `json:"repository"`
Commit string `json:"commit"`
Number int `json:"number"`
Branch string `json:"branch"`
Group string `json:"group"`
State string `json:"state"`
InSource bool `json:"in_source"`
NewerHead string `json:"newer_head"`
ClosedUnmerged bool `json:"closed_unmerged"`
}
// recordGroupOf is a merge, as `merges` holds it, as the record's group state.
// milestone is the name a delivery's record is kept under: its group's, a lone pull request's branch, its id.
func (d oldDelivery) milestone() string {
switch {
case d.Group != "":
return d.Group
case d.Branch != "":
return d.Branch
}
return d.ID
}
// recordGroupOf is a merge, as the old section merges holds it, as the record's group state.
func recordGroupOf(m Merge, members []string) RecordGroup {
g := RecordGroup{Group: m.Group, Members: members, Ask: m.Ask, Approved: m.Approved}
for _, p := range m.Pulls {
@@ -30,33 +53,6 @@ func recordGroupOf(m Merge, members []string) RecordGroup {
return g
}
// recordedMerges is the gate's merges with every merge it writes also written into its delivery record: the record holds
// the group's state, which `merges` holds today.
type recordedMerges struct {
MergeStore
records *Records
members func(group string) []string
}
// Put writes the group's merge into its delivery record first, so a ledger with no room refuses it before the merge is
// handed to the forge bridge (G §181), then into the state merges the bridge follows.
func (m recordedMerges) Put(group string, v Merge) error {
if m.records != nil && len(v.Approved) > 0 {
r, _, err := m.records.Open(group, v.Approved, false)
if err != nil {
return fmt.Errorf("the merge of %s is handed but not in its delivery record: %w", group, err)
}
var members []string
if m.members != nil {
members = m.members(group)
}
if err := m.records.SetGroup(r, recordGroupOf(v, members)); err != nil {
return err
}
}
return m.MergeStore.Put(group, v)
}
// switchGroup is one group in flight as the switch finds it: its milestone, its heads and its members.
type switchGroup struct {
milestone string
@@ -64,15 +60,31 @@ type switchGroup struct {
members []string
}
// inFlight are the groups in flight, under the lock: every milestone with a delivery not final, its live members' heads
// as `<repository>@<number>` to the full commit.
func (h *Holder) inFlight() []switchGroup {
// inFlight are the groups in flight in the old section deliveries: every milestone with a delivery not final, its live
// members' heads as `<repository>@<number>` to the full commit.
func (h *Holder) inFlight() ([]switchGroup, error) {
if h.Old == nil {
return nil, nil
}
keys, err := h.Old.Keys("deliveries")
if err != nil {
return nil, fmt.Errorf("the old section deliveries cannot be read for the switch: %w", err)
}
by := map[string]*switchGroup{}
for _, d := range h.deliveries {
if d.State.Final() && !(d.State == Merged && !d.InSource) || d.Number == 0 || d.NewerHead != "" || d.ClosedUnmerged {
for _, key := range keys {
raw, _, err := h.Old.Get("deliveries", key)
if err != nil {
return nil, fmt.Errorf("the old delivery %s cannot be read for the switch: %w", key, err)
}
var d oldDelivery
if raw == nil || json.Unmarshal(raw, &d) != nil || d.ID == "" {
continue // a mark an earlier owner kept, or no delivery
}
final := d.State == "merged" || d.State == "held" || d.State == "closed"
if final && !(d.State == "merged" && !d.InSource) || d.Number == 0 || d.NewerHead != "" || d.ClosedUnmerged {
continue
}
name := milestoneOf(d)
name := d.milestone()
g := by[name]
if g == nil {
g = &switchGroup{milestone: name, heads: map[string]string{}}
@@ -87,7 +99,7 @@ func (h *Holder) inFlight() []switchGroup {
out = append(out, *g)
}
sort.Slice(out, func(i, j int) bool { return out[i].milestone < out[j].milestone })
return out
return out, nil
}
// Switch carries today's state into the records, once and safe to run again: each group in flight becomes a record at
@@ -97,9 +109,10 @@ func (h *Holder) Switch() (opened, found int, err error) {
if h.Records == nil {
return 0, 0, nil
}
h.mu.Lock()
groups := h.inFlight()
h.mu.Unlock()
groups, err := h.inFlight()
if err != nil {
return 0, 0, err
}
for _, g := range groups {
var merge *Merge
if h.Merges != nil {
@@ -129,7 +142,7 @@ func (h *Holder) Switch() (opened, found int, err error) {
if err := h.carryOwed(); err != nil {
return opened, found, err
}
return opened, found, nil
return opened, found, h.carryApprovals()
}
// oldMerge is a group's merge as the old state merges holds it, with its key and revision; nil when it holds none.
@@ -180,23 +193,87 @@ func (h *Holder) carryOwed() error {
h.Refuse(fmt.Errorf("the owed act %s is not carried by the switch: %v", key, errors.Join(kerr, derr, ierr)))
continue
}
if err := h.Owed.Say(seatOwedOf(o, id, repository, number, head, &now)); err != nil {
said := seatOwedOf(o, id, repository, number, head, &now)
if r := h.recordHolding(repository, head); r != nil {
err = h.owe(r, said)
} else {
err = h.Owed.Say(said)
}
if err != nil {
return fmt.Errorf("the owed act %s cannot be said for the switch: %w", key, err)
}
}
return nil
}
// groupMembers are the members' ids of the milestone a merge is handed under, under the lock.
func (h *Holder) groupMembers(name string) []string {
h.mu.Lock()
defer h.mu.Unlock()
var out []string
for _, d := range h.deliveries {
if milestoneOf(d) == name && d.NewerHead == "" && !d.ClosedUnmerged {
out = append(out, d.ID)
// recordHolding is the open record that holds a pull request of the repository at the head, nil when none does.
func (h *Holder) recordHolding(repository, head string) *Record {
if h.Records == nil {
return nil
}
current, err := h.Records.Current()
if err != nil {
return nil
}
for _, r := range current {
for _, m := range membersOf(r) {
if strings.EqualFold(m.Repository, repository) && sameCommit(m.Head, head) {
return r
}
}
}
sort.Strings(out)
return out
return nil
}
// approver is who an Approve carried across the switch was given by (design 47 §542).
const approver = "the operator"
// carryApprovals says, for each key of the old section merges the old bridge had not performed, `approval-carried` of each
// pull request it had not merged, once (design 47 §542, §592): the runner takes it as that pull request's `approval` step.
// The keys stay, read no more. Said at every switch, it is the same event under the same id each time, which the bus keeps once.
func (h *Holder) carryApprovals() error {
if h.Old == nil || h.SaySeat == nil {
return nil
}
names, err := h.Old.Keys(mdc.SwitchedMerges)
if err != nil {
return fmt.Errorf("the old section merges cannot be read for the switch: %w", err)
}
now := h.Now().UTC()
for _, key := range names {
raw, _, err := h.Old.Get(mdc.SwitchedMerges, key)
if err != nil {
return fmt.Errorf("the old merge %s cannot be read for the switch: %w", key, err)
}
if raw == nil {
continue
}
m, err := DecodeMerge(raw)
if err != nil {
h.Refuse(fmt.Errorf("the old merge %s is not carried by the switch: %v", key, err))
continue
}
for approved, head := range m.Approved {
repository, number, ok := parseApprovedKey(approved)
if !ok || mergedIn(m, repository, number) {
continue
}
body := seat.ApprovalCarriedEvent{Approver: approver, At: now, Commit: head, Pull: int64(number), Repository: repository}
subject := seat.SubjectApprovalCarried(keys.EscapeName(repository), fmt.Sprint(number))
if err := h.SaySeat(subject, body, fmt.Sprintf("approval-carried.%s.%d", repository, number)); err != nil {
return fmt.Errorf("the approval of %s#%d cannot be said carried: %w", repository, number, err)
}
}
}
return nil
}
// mergedIn is whether a merge's pull request of that repository and number was merged.
func mergedIn(m Merge, repository string, number int) bool {
for _, p := range m.Pulls {
if strings.EqualFold(p.Repository, repository) && p.Number == number && p.State == mdc.MergeMerged {
return true
}
}
return false
}
@@ -1,383 +0,0 @@
package main
import (
"errors"
"fmt"
"strings"
"time"
)
// The state table (novox/hq ADR 0239 decision 2, to-be 47, cut by ADR 0539 §17): every transition a delivery may take,
// each with its guard, compiled once. The gate keeps the merge's states only: proposed, ready, rejected, merged, held
// (a merge unchecked) and closed. It asks the Approve and merges once; nothing after the merge is derived here, and a
// source revision's delivered state is the view's, from source.main, the wants and the haves. The code moves a delivery
// only through Apply, which finds the row for the delivery's state and the event, runs its guard and refuses anything
// else, naming what was asked. A test walks the table: every row it holds, and every pair it does not.
// State is where a delivery is.
type State string
// The states, in the order a delivery passes through them. None is a delivery not yet made: the rows from it are how
// one comes to exist.
const (
None State = ""
Proposed State = "proposed"
Ready State = "ready"
Rejected State = "rejected"
// Merged is a change on the trunk: the gate's last word on it.
Merged State = "merged"
// Held is a merge that reached the trunk without a passing check: only a person decides what follows.
Held State = "held"
// Closed is a delivery that ended before the merge: its pull request closed unmerged, or a newer head took its place.
Closed State = "closed"
)
// AllStates are every state a delivery can be in, for the table's own test and its verb.
var AllStates = []State{Proposed, Ready, Rejected, Merged, Held, Closed}
// Final is whether nothing follows a state.
func (s State) Final() bool { return s == Merged || s == Held || s == Closed }
// legacyState reads a state an earlier owner kept as one of today's: a change that reached the trunk is merged (held
// only when it was held for a merge without a passing check), one that ended before it closed, and a verdict taken but
// not yet acted on is proposed again.
func legacyState(d *Delivery) State {
switch d.State {
case Proposed, Ready, Rejected, Merged, Closed, None:
return d.State
case "checked":
return Proposed
case Held:
if strings.HasPrefix(d.HeldWhy, "merged without a passing check") {
return Held
}
return Merged
case "published", "delivering", "delivered", "covered", "retired", "failed":
return Merged
case "superseded", "stopped":
return Closed
}
return d.State
}
// Event is what moves a delivery.
type Event string
// The events. Observed ones are taken by settle whenever their guard holds; the others are acts — a person's, or healer
// H2's — taken only when asked.
const (
EvAnnounced Event = "announced" // the forge announced a pull request's head
EvAccepted Event = "accepted" // its verdict passes
EvRefused Event = "refused" // its verdict fails, or the check could not run
EvRecheck Event = "recheck" // a person asked for it again, or its group changed
EvReask Event = "re-ask" // healer H2 asked a check asked and not answered once more
EvLost Event = "check lost" // the build queue holds no check of its head and it was asked: asked again (issue 1274)
EvNewHead Event = "new-head" // a newer head of the same pull request
EvClosed Event = "closed" // the pull request closed unmerged
EvMerged Event = "merged" // it reached the trunk
EvMergedUnchecked Event = "merged-unchecked" // it reached the trunk without a passing check
EvTrunkMoved Event = "trunk moved" // another merge moved its trunk, and its verdict names the trunk commit before (novox/hq ADR 0323)
)
// HeldUnchecked is the why of a delivery held because its merge reached the trunk without a passing check: the one hold a
// person may settle (novox/hq ADR 0556 clause 5).
const HeldUnchecked = "merged without a passing check: only a person decides what follows"
// heldUnchecked is whether a held delivery is held for a merge without a passing check, and reached the trunk.
func heldUnchecked(d *Delivery) bool {
return d.State == Held && d.MergedAs != "" && strings.HasPrefix(d.HeldWhy, "merged without a passing check")
}
// Facts are what a guard reads beyond the delivery itself: the moment, and who asks, for an act.
type Facts struct {
Now time.Time
By string
Why string
}
// Row is one transition the table holds.
type Row struct {
From []State
Event Event
To State
// Guard says, in words, what must be true; Holds checks it, answering why not.
Guard string
Holds func(d *Delivery, f Facts) error
// Act says the row is taken only when asked — a person's or H2's — never by settle.
Act bool
// Says is the transition's why when nobody gave one: the fact that moved it, rather than the guard.
Says func(d *Delivery) string
}
func unless(cond bool, why string) error {
if cond {
return nil
}
return fmt.Errorf("%s", why)
}
var notFinal = []State{Proposed, Ready, Rejected}
// Table is every transition there is. The order within one state is the order settle tries them.
var Table = []Row{
{From: []State{None}, Event: EvAnnounced, To: Proposed,
Guard: "a pull request's head the forge announced",
Holds: func(d *Delivery, f Facts) error {
return unless(d.Number > 0 && d.Commit != "", "no pull request's head")
}},
{From: notFinal, Event: EvNewHead, To: Closed,
Guard: "a newer head of the same pull request",
Holds: func(d *Delivery, f Facts) error { return unless(d.NewerHead != "", "no newer head") }},
{From: notFinal, Event: EvClosed, To: Closed,
Guard: "the pull request closed unmerged",
Holds: func(d *Delivery, f Facts) error { return unless(d.ClosedUnmerged, "the pull request is open") }},
{From: []State{Ready}, Event: EvMerged, To: Merged,
Guard: "on the trunk its modules follow: merged there",
Holds: func(d *Delivery, f Facts) error {
return unless(d.MergedAs != "", "not on the trunk yet")
}},
// **A verdict counts only for the trunk's head** (novox/hq ADR 0323 §7): one naming a trunk commit that is no
// longer the trunk's head is kept and shown, and does not count, so the delivery stays proposed and the rows
// cannot loop with `trunk moved`; a recheck or the merge tool's ask judges it again.
{From: []State{Proposed}, Event: EvAccepted, To: Ready,
Guard: "a verdict names this commit and the trunk's head as its trunk commit, the gate passed or warned, and " +
"the repository's own check did not fail; or the operator's Approve covers this head (the merge judges it alone)",
Holds: func(d *Delivery, f Facts) error {
if d.approvedAtHead() {
return nil // novox/hq ADR 0661 §3: an Approve given stands across a trunk move, and the merge judges it alone
}
if d.Check == nil {
return errors.New("no verdict")
}
if w := d.outOfDate(); w != "" {
return errors.New("its verdict is " + w)
}
return unless(d.Check.Passes(), "the verdict does not pass")
}},
{From: []State{Proposed}, Event: EvRefused, To: Rejected,
Guard: "a verdict names this commit and the trunk's head as its trunk commit, and the gate failed or could not run, " +
"or the repository's own check failed; never once the operator's Approve covers this head",
Holds: func(d *Delivery, f Facts) error {
if d.approvedAtHead() {
// novox/hq ADR 0661 §5: a failure after the Approve is said, and does not undo it.
return errors.New("the operator's Approve covers this head: a failure is said, the merge is not refused")
}
if d.Check == nil {
return errors.New("no verdict")
}
if w := d.outOfDate(); w != "" {
return errors.New("its verdict is " + w)
}
return unless(!d.Check.Passes(), "the verdict passes")
}},
// **A ready delivery whose verdict went out of date goes back to proposed** (novox/hq ADR 0323 §7): another
// merge moved its trunk. A rejected one stays rejected, its status on the forge saying the verdict is out of date.
{From: []State{Ready}, Event: EvTrunkMoved, To: Proposed,
Guard: "its verdict names a trunk commit that is not its trunk's head, and no Approve covers this head yet",
Holds: func(d *Delivery, f Facts) error {
if d.approvedAtHead() {
// novox/hq ADR 0661 §3: an approved merge waits for no carry; its carry runs after the merge.
return errors.New("the operator's Approve covers this head: it merges onto the new trunk, its carry after")
}
return unless(d.MergedAs == "" && d.outOfDate() != "", "not merged, and its verdict is not out of date")
},
Says: func(d *Delivery) string { return d.outOfDate() }},
{From: []State{Proposed, Rejected}, Event: EvMergedUnchecked, To: Held,
Guard: "merged without a passing check — a verdict known with it is taken first: only a person decides what follows",
Holds: func(d *Delivery, f Facts) error { return unless(d.MergedAs != "", "not merged") }},
{From: []State{Rejected, Ready}, Event: EvRecheck, To: Proposed, Act: true,
Guard: "a person asked, with why, or its group changed",
Holds: func(d *Delivery, f Facts) error { return unless(f.Why != "", "a recheck says why") }},
{From: []State{Proposed}, Event: EvReask, To: Proposed, Act: true,
Guard: "its check was asked and no verdict came within its bound, and it was not re-asked before: healer H2 " +
"asks it once more, with why — after that it is the operator's",
Holds: func(d *Delivery, f Facts) error {
switch {
case f.Why == "" || f.By == "":
return errors.New("a re-ask says who and why")
case d.Check != nil:
return errors.New("its verdict is known")
case d.CheckAsk == nil:
return errors.New("its check was never asked: a look asks it, not H2")
case d.CheckAsk.Reasked > 0:
return errors.New("its check was re-asked once already: the operator's")
}
return nil
}},
// **A check the build queue lost is asked again** (novox/hq issue 1274): a proposed delivery waits for a check, and
// the queue, which the table cannot read, holds none of its head. The act (the sweep's or a person's recheck) reads
// the queue first and says why; the table keeps it to a delivery that waits for a verdict.
{From: []State{Proposed}, Event: EvLost, To: Proposed, Act: true,
Guard: "it waits for a check — no verdict yet, or one out of date — nothing of its head is waiting or in flight in " +
"the build queue, and who asks says why",
Holds: func(d *Delivery, f Facts) error {
switch {
case f.Why == "" || f.By == "":
return errors.New("a lost check's ask says who and why")
case d.MergedAs != "":
return errors.New("it is merged")
case d.Check != nil && d.outOfDate() == "":
return errors.New("its verdict is known")
}
return nil
}},
}
// rowFor is the row a state and event name; nil when the table holds none.
func rowFor(from State, ev Event, act bool) *Row {
for i := range Table {
r := &Table[i]
if r.Event != ev || r.Act != act {
continue
}
for _, s := range r.From {
if s == from {
return r
}
}
}
return nil
}
// ErrRefused is a transition the table does not hold, or whose guard does not.
type ErrRefused struct {
ID string
From State
Event Event
Why string
}
func (e ErrRefused) Error() string {
from := string(e.From)
if from == "" {
from = "nothing"
}
return fmt.Sprintf("%s: %s from %s is refused — %s", e.ID, e.Event, from, e.Why)
}
// Apply takes one transition, or refuses it. The delivery keeps the transition, bounded.
func Apply(d *Delivery, ev Event, act bool, f Facts) (Transition, error) {
r := rowFor(d.State, ev, act)
if r == nil {
kind := "observed"
if act {
kind = "asked"
}
return Transition{}, ErrRefused{ID: d.ID, From: d.State, Event: ev,
Why: "the table holds no such transition (" + kind + ")"}
}
if err := r.Holds(d, f); err != nil {
return Transition{}, ErrRefused{ID: d.ID, From: d.State, Event: ev, Why: "its guard does not hold: " + err.Error()}
}
why := f.Why
if why == "" && r.Says != nil {
why = r.Says(d)
}
if why == "" {
why = r.Guard
}
by := f.By
if by == "" && ev == EvMerged && d.MergedAs != "" {
// A merge this owner made on the operator's Approve names it as its author (novox/hq issue 722).
by = d.Ready.mergedBy()
}
t := Transition{At: f.Now, From: d.State, To: r.To, Event: ev, Why: why, By: by}
d.State, d.Since = r.To, f.Now
d.Transitions = append(d.Transitions, t)
if len(d.Transitions) > keptTransitions {
d.Transitions = d.Transitions[len(d.Transitions)-keptTransitions:]
}
return t, nil
}
// keptTransitions is how many transitions a delivery keeps; its note on the commit keeps every one.
const keptTransitions = 100
// settle takes every observed transition whose guard holds, in the table's order, until none does — so a
// delivery always stands where its facts put it, whatever order they arrived in.
func settle(d *Delivery, f Facts) []Transition {
var taken []Transition
for range len(Table) + 1 {
moved := false
for i := range Table {
r := &Table[i]
if r.Act || !contains(r.From, d.State) {
continue
}
if t, err := Apply(d, r.Event, false, f); err == nil {
taken = append(taken, t)
moved = true
break
}
}
if !moved {
break
}
}
return taken
}
func contains(states []State, s State) bool {
for _, x := range states {
if x == s {
return true
}
}
return false
}
// Bounds are how long a delivery may be in a state before `stalled` lists it, and what healer H2 may do
// then — always a transition the table holds, never one of its own (novox/hq ADR 0239 decision 9). Only a proposed
// delivery has one: its check was asked and not answered. A merge past its bound is said by the merge's own stalls.
type Bound struct {
For time.Duration
// H2 is the transitions H2 may take after the bound, by event; empty when the state is the operator's.
H2 []Event
Says string
}
// Bounds are every state's.
var Bounds = map[State]Bound{
Proposed: {For: time.Hour, H2: []Event{EvReask}, Says: "its check was asked and not answered: H2 asks it once " +
"more, then the check's own watchdog (S6) and the operator speak for it"},
}
// boundSince is when a delivery's bound runs from: its state's start, except for a proposed delivery whose
// check this owner asked — its bound is "asked and not answered", so it runs from the ask. One never asked
// runs from its start: an ask that cannot be made is still listed.
func boundSince(d *Delivery) time.Time {
if d.State == Proposed {
if d.AskedAgain != nil && d.outOfDate() != "" {
return *d.AskedAgain
}
if d.CheckAsk != nil {
return d.CheckAsk.At
}
return d.Since
}
return d.Since
}
// boundOf is a delivery's bound.
func boundOf(d *Delivery) (Bound, bool) {
if d.State == Proposed && d.outOfDate() != "" {
// Its check is out of date and not asked: nothing is running for it, and it costs nothing (ADR 0323 §7).
// Its bound starts when its check is asked: a recheck puts the verdict away; the merge tool's ask announces the
// head again, and its bound runs from then.
if d.AskedAgain == nil {
return Bound{}, false
}
b := Bounds[Proposed]
b.H2, b.Says = nil, "its verdict is "+d.outOfDate()+"; its check was asked again and not answered: the operator's"
return b, true
}
b, bounded := Bounds[d.State]
return b, bounded
}
// pastBound is whether a delivery has been where it is longer than its state's bound.
func pastBound(d *Delivery, now time.Time) bool {
b, bounded := boundOf(d)
return bounded && !d.State.Final() && (b.For == 0 || now.Sub(boundSince(d)) > b.For)
}
@@ -1,237 +0,0 @@
package main
import (
"errors"
"testing"
"time"
)
// The table walked (novox/hq ADR 0239 decision 2, cut by ADR 0539 §17): every row it holds can be taken when its guard
// holds and is refused when it does not; every pair it does not hold is refused, by name; and the rules the table exists
// for are true of the table itself, not of the code around it.
// aDeliveryFor is a delivery whose facts make the row's guard hold.
func aDeliveryFor(r Row) (*Delivery, Facts) {
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
d := &Delivery{ID: "novox/app@c0ffee000000", Repository: "novox/app", Commit: "c0ffee000000", Number: 7}
f := Facts{Now: now}
switch r.Event {
case EvNewHead:
d.NewerHead = "beef"
case EvClosed:
d.ClosedUnmerged = true
case EvMerged, EvMergedUnchecked:
d.MergedAs = "aaaa1111"
case EvAccepted:
d.Check = &Verdict{Gate: "pass"}
case EvRefused:
d.Check = &Verdict{Gate: "fail"}
case EvRecheck, EvLost:
f.Why, f.By = "a person's reason", "jochen"
case EvReask:
f.Why, f.By = "delivery stalled", byH2
d.CheckAsk = &CheckAsk{ID: "check-1", At: now.Add(-2 * time.Hour), By: byOwner}
case EvTrunkMoved:
d.Check = &Verdict{Gate: "pass", Head: "c0ffee000000", Trunk: "d28ca95bbbbb"}
d.TrunkNow = "690966dccccccccc"
}
return d, f
}
// The merge's states only (ADR 0539 §17): proposed, ready, rejected, merged, held and closed. Nothing after the merge.
func TestTheTableHoldsTheSixStatesOfTheMergeAndNothingAfter(t *testing.T) {
want := map[State]bool{Proposed: true, Ready: true, Rejected: true, Merged: true, Held: true, Closed: true}
if len(AllStates) != len(want) {
t.Fatalf("the states are %v, wanted exactly proposed, ready, rejected, merged, held and closed", AllStates)
}
for _, s := range AllStates {
if !want[s] {
t.Errorf("%q is a state of the table: ADR 0539 §17 keeps six", s)
}
}
gone := []Event{"appeared", "adopted", "checked", "go", "hold", "release", "done", "failed", "superseded", "stop",
"history", "retire", "covered", "lifted", "rewalk", "backup failed"}
for _, r := range Table {
for _, g := range gone {
if r.Event == g {
t.Errorf("the table holds the event %q: it belongs to a delivery state that is gone", g)
}
}
for _, s := range append([]State{r.To}, r.From...) {
if s != None && !want[s] {
t.Errorf("the row %v —%s→ %s names the state %q", r.From, r.Event, r.To, s)
}
}
}
for _, s := range []State{"checked", "published", "delivering", "delivered", "failed", "superseded", "stopped",
"retired", "covered"} {
if rowFor(s, EvMerged, false) != nil || contains(AllStates, s) {
t.Errorf("%q is still a state", s)
}
}
}
func TestEveryRowOfTheTableIsTakenWhenItsGuardHoldsAndRefusedWhenNot(t *testing.T) {
for _, r := range Table {
for _, from := range r.From {
d, f := aDeliveryFor(r)
d.State = from
got, err := Apply(d, r.Event, r.Act, f)
if err != nil {
t.Errorf("%s —%s→ %s was refused with its guard holding: %v", orNothing(from), r.Event, r.To, err)
continue
}
if d.State != r.To || got.From != from || got.To != r.To || len(d.Transitions) != 1 {
t.Errorf("%s —%s→ left it %s, transition %+v", orNothing(from), r.Event, d.State, got)
}
// The guard not holding: a delivery with no facts.
bare := &Delivery{ID: "novox/app@bare", State: from, Check: &Verdict{Gate: "pass"}}
if r.Event == EvAccepted {
bare.Check = &Verdict{Gate: "fail"}
}
if r.Event == EvRefused {
bare.Check = &Verdict{Gate: "pass"}
}
if _, err := Apply(bare, r.Event, r.Act, Facts{Now: f.Now}); err == nil {
t.Errorf("%s —%s→ %s was taken with no fact for its guard", orNothing(from), r.Event, r.To)
} else if bare.State != from || len(bare.Transitions) != 0 {
t.Errorf("a refused transition moved the delivery to %s", bare.State)
}
}
}
}
func TestEveryPairTheTableDoesNotHoldIsRefusedByName(t *testing.T) {
events := []Event{EvAnnounced, EvAccepted, EvRefused, EvRecheck, EvReask, EvLost, EvNewHead, EvClosed, EvMerged,
EvMergedUnchecked, EvTrunkMoved}
for _, from := range append([]State{None}, AllStates...) {
for _, ev := range events {
for _, act := range []bool{false, true} {
if rowFor(from, ev, act) != nil {
continue
}
d := &Delivery{ID: "novox/app@c0ffee", State: from, MergedAs: "x", NewerHead: "y", Check: &Verdict{Gate: "pass"},
HeldWhy: "h", ClosedUnmerged: true, Number: 1, Commit: "c"}
_, err := Apply(d, ev, act, Facts{Now: time.Now(), Why: "w", By: "b"})
var refused ErrRefused
if !errors.As(err, &refused) || refused.Event != ev || refused.From != from {
t.Errorf("%s —%s (act %v)→ was not refused by name: %v", orNothing(from), ev, act, err)
}
if d.State != from {
t.Errorf("a pair the table does not hold moved %s to %s", orNothing(from), d.State)
}
}
}
}
}
// The rules the table is for, read off the table.
func TestTheTableKeepsItsRules(t *testing.T) {
for _, r := range Table {
for _, from := range r.From {
// Nothing else leaves a final state.
if from.Final() {
t.Errorf("%s is final and the table leaves it by %s", from, r.Event)
}
}
// Only a ready delivery is merged, and nothing but its merge, or a merge without a passing check, puts one on the trunk.
if r.To == Merged && !(r.Event == EvMerged && len(r.From) == 1 && r.From[0] == Ready) {
t.Errorf("%v —%s→ merged: only a ready delivery's merge merges", r.From, r.Event)
}
if r.To == Held && r.Event != EvMergedUnchecked {
t.Errorf("%v —%s→ held: only a merge without a passing check holds", r.From, r.Event)
}
}
// Every H2 transition is a row the table holds from that state.
for state, b := range Bounds {
for _, ev := range b.H2 {
if rowFor(state, ev, false) == nil && rowFor(state, ev, true) == nil {
t.Errorf("H2 may take %s from %s, which the table does not hold", ev, state)
}
}
}
// H2 re-asks a check once: a second re-ask is refused by the table itself.
d0 := &Delivery{ID: "novox/app@c0", State: Proposed, Number: 1, CheckAsk: &CheckAsk{ID: "check-1", Reasked: 1}}
if _, err := Apply(d0, EvReask, true, Facts{Now: time.Now(), By: byH2, Why: "stalled"}); err == nil {
t.Fatal("a check re-asked once was re-asked again")
}
// Once the operator's Approve covers the head (novox/hq ADR 0661 §3, §5): a proposed delivery is accepted with no
// verdict or a failed one, none is refused, and a trunk's move puts no ready one back to proposed.
at := time.Now()
approved := func(d *Delivery) *Delivery {
d.Ready = &ReadyRun{Approved: &at, Heads: map[string]string{d.ID: d.Commit}}
return d
}
f := Facts{Now: at}
for _, check := range []*Verdict{nil, {Gate: "fail"}} {
p := approved(&Delivery{ID: "novox/app@c0", Commit: "c0", State: Proposed, Number: 1, Check: check})
if _, err := Apply(p, EvRefused, false, f); err == nil {
t.Fatalf("approved, a verdict %+v refused the delivery", check)
}
if _, err := Apply(p, EvAccepted, false, f); err != nil || p.State != Ready {
t.Fatalf("approved, a delivery with the verdict %+v is %s: %v", check, p.State, err)
}
unapproved := &Delivery{ID: "novox/app@c0", Commit: "c0", State: Proposed, Number: 1, Check: check}
if _, err := Apply(unapproved, EvAccepted, false, f); err == nil {
t.Fatalf("with no Approve, a delivery with the verdict %+v was accepted: the check gates the ask", check)
}
}
stale := approved(&Delivery{ID: "novox/app@c0", Commit: "c0", State: Ready, Check: &Verdict{Gate: "pass", Trunk: "d28ca95bbbbb"}, TrunkNow: "690966dccccc"})
if _, err := Apply(stale, EvTrunkMoved, false, f); err == nil {
t.Fatal("approved, a trunk's move put a ready delivery back to proposed")
}
stale.Ready = nil
if _, err := Apply(stale, EvTrunkMoved, false, f); err != nil || stale.State != Proposed {
t.Fatalf("with no Approve, a trunk's move left a ready delivery %s: %v", stale.State, err)
}
// A ready delivery off the trunk is refused its merge, whatever else is true of it.
d := &Delivery{ID: "novox/app@c0", State: Ready}
if _, err := Apply(d, EvMerged, false, Facts{Now: time.Now()}); err == nil {
t.Fatal("a commit not on the trunk was merged")
}
}
// A delivery an earlier owner kept in a state that is gone reads as the merge's state it came to (ADR 0539 §17).
func TestAStateAnEarlierOwnerKeptReadsAsTheMergeStateItCameTo(t *testing.T) {
for kept, want := range map[State]State{
"checked": Proposed, "published": Merged, "delivering": Merged, "delivered": Merged, "covered": Merged,
"retired": Merged, "failed": Merged, "superseded": Closed, "stopped": Closed,
Proposed: Proposed, Ready: Ready, Rejected: Rejected, Merged: Merged, Closed: Closed,
} {
if got := legacyState(&Delivery{State: kept}); got != want {
t.Errorf("kept as %q, read as %q, wanted %q", kept, got, want)
}
}
if got := legacyState(&Delivery{State: Held, HeldWhy: "merged without a passing check: only a person decides"}); got != Held {
t.Errorf("a merge without a passing check was read as %q", got)
}
if got := legacyState(&Delivery{State: Held, HeldWhy: "its group's composed check did not pass"}); got != Merged {
t.Errorf("a delivery held for a walk's sake was read as %q", got)
}
}
// settle stands a delivery where its facts put it, whatever order they came in.
func TestSettleIsTheOrderOfTheFactsNotOfTheirArrival(t *testing.T) {
now := time.Now()
d := &Delivery{ID: "novox/app@c0", Number: 1, Commit: "c0"}
if _, err := Apply(d, EvAnnounced, false, Facts{Now: now}); err != nil {
t.Fatal(err)
}
// The verdict and the merge, both known before anything settled.
d.Check = &Verdict{Gate: "pass"}
d.MergedAs = "m1"
ts := settle(d, Facts{Now: now})
var path []State
for _, t := range ts {
path = append(path, t.To)
}
want := []State{Ready, Merged}
if len(path) != len(want) {
t.Fatalf("settled through %v, wanted %v", path, want)
}
for i := range want {
if path[i] != want[i] {
t.Fatalf("settled through %v, wanted %v", path, want)
}
}
}
@@ -1,90 +0,0 @@
package main
import (
"errors"
"strings"
"testing"
"time"
)
// **A step that waits on the clock says when it is looked at again** (novox/hq ADR 0406 §1, issue 975): there is no
// tick to find it, so each grace, bound and retry is registered (lookAt) when a look meets it, and the one timer fires
// a look for the earliest. A look that meets nothing waiting registers nothing.
func (w *world) wantedAt() (time.Time, string) {
w.h.wanted.mu.Lock()
defer w.h.wanted.mu.Unlock()
return w.h.wanted.at, w.h.wanted.step
}
func TestALookThatMeetsNothingWaitingOnTheClockWantsNoOtherLook(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.h.React()
if at, step := w.wantedAt(); !at.IsZero() {
t.Fatalf("a look with nothing waiting on the clock wants another at %s for %q", at, step)
}
}
func TestAProposedDeliveryWhoseHeadHoldsAPassingMeshCheckAtTheForgeAdoptsItAndAsksNoCheck(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
id := IDOf("novox/app", readyHead)
op := openPull("novox/app", 7, readyHead)
op.Required = []string{ctxCheck}
op.Checks = map[string]HeadCheck{ctxCheck: {State: "success", Description: "pass " + readyHead[:12] + " | the checker's"}}
w.forge.open = []OpenPull{op}
w.h.React()
w.settleAll()
if d := w.delivery(id); d.Check == nil || !d.Check.Passes() || d.State != Ready {
t.Fatalf("the standing verdict was not adopted: %+v (%s)", d.Check, d.State)
}
}
// 1144/AC3: no timer asks a refused merge again; only an event does (here, mergeAgainNow called directly).
func TestAMergeTheForgeRefusedIsAskedAgainOnlyOnAnEventWithNoTimer(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.refuse = errors.New("the forge refused gitea_merge_pull_request: not mergeable yet")
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.wantForge()
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("merges %v", w.forge.merges)
}
if at, step := w.wantedAt(); !at.IsZero() {
t.Fatalf("the refused merge wants a timer at %s for %q; it waits for the next event", at, step)
}
w.forge.refuse = nil
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("asked again at a look with no event: %v", w.forge.merges)
}
again := w.delivery(IDOf("novox/app", readyHead))
w.h.mu.Lock()
w.h.mergeAgainNow(again)
w.h.mu.Unlock()
w.settleAll()
if len(w.forge.merges) != 2 || w.delivery(IDOf("novox/app", readyHead)).Ready.Merged == nil {
t.Fatalf("the refused merge was not asked again at the timer: %v", w.forge.merges)
}
}
func TestAnEffectTheBusCouldNotTakeIsTriedAgainAtItsTimeByTheTimerAlone(t *testing.T) {
w := newWorld(t)
w.owed.down = true
w.h.PullUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go"))
w.h.React()
at, step := w.wantedAt()
if at.IsZero() || !strings.Contains(step, "owed") || at.After(w.now.Add(retryFirst)) {
t.Fatalf("the look wanted at %s for %q; wanted within %s for the owed effect", at, step, retryFirst)
}
w.owed.down = false
w.later(retryFirst + time.Second)
w.h.React()
if len(w.forge.notes) == 0 {
t.Fatal("the owed effect was not tried again at its time")
}
}
@@ -1,381 +0,0 @@
package main
import (
"fmt"
"slices"
"sort"
"strconv"
"strings"
"time"
)
// A pull request the mesh was never asked to check (novox/hq issue 347).
//
// The forge's announcer is the only way the mesh hears of a pull request's head, and on 2026-10-09 it missed one: a
// pull request opened after its push moved nothing the announcer read, and waited unchecked, unable to merge, with
// nothing saying so. This owner asks the forge itself for every open pull request when it starts and when an event says
// what the forge shows may have changed — a merge check's status, a new head (events.go), not a poll
// (novox/hq ADR 0406 §1, issue 975) — and once more at a head's bound, and one whose
// base requires the merge check — mesh/merge-gate or mesh/repo-check — with neither on its head for UncheckedAfter
// since this owner first saw it so is said as a delivery held: a `stalled` line in the state Unannounced, which the
// gate says as a condition (conditions.go) like any other delivery past its bound. The delivery domain's own
// word, so no second condition and no second judge: the id is the one its delivery will have once announced.
//
// A head whose check started and never answered is said the same way (novox/hq issue 438). Counting any merge-check
// status as checked let a head that got only a pending mesh/merge-gate, or never got mesh/repo-check, wait unable to
// merge with nothing saying so. So a head is checked only when every merge check its base requires has answered —
// success, failure, error or warning, a failure being a verdict the pull request shows itself — and one with a
// required check pending or never set is said once that has lasted the delivery table's bound for a check asked and
// not answered (Bounds[Proposed]): an announced head would be said after that bound too, and it is longer than the
// controller lets one check run (its checkTimeout, 45 minutes), so a slow check or a status read a moment behind
// (issue 423) is never said. Such a head was announced — the controller set its status — so its line is not in the
// state Unannounced but Unanswered, with words of its own.
//
// A head whose delivery is held in ready or rejected is said by the same rule (novox/hq issue 441). Those states have
// no bound of their own — the verdict is known here — but the forge's status is what lets the pull request merge or
// shows why not, and a status write that lagged or was lost (issue 423), or a check asked again after the verdict,
// leaves a required check pending or never set there: the pull request waits on it, and nothing here asks again. So
// such a head is kept here like one without a delivery, and said on the same bound, as Unanswered — it was announced,
// so never Unannounced, even with no merge check on its head at all. A delivery in any other state is not: proposed
// and checked wait under bounds of their own, and the rest are past the merge, their pull request no longer open.
// Unannounced is a pull request's head the forge never announced: not a state of the table — no delivery exists —
// only what a stalled line says.
const Unannounced State = "unannounced"
// Unanswered is a pull request's head whose merge check started and never answered, and no delivery of it is held
// here (novox/hq issue 438): not a state of the table either, only what a stalled line says. The word is the table's
// own for the proposed state's bound: its check was asked and not answered.
const Unanswered State = "unanswered"
// outOfDatePrefix begins the words the forge's holder puts a merge check back to pending with when its trunk moved
// (novox/hq ADR 0323 §4): the gitea module's outOfDateWords.
const outOfDatePrefix = "out of date:"
// UncheckedAfter is how long a head may go without a merge check before it is said: the announcer passes every half
// minute and the check is asked at once, so ten minutes is a fault, never a queue.
const UncheckedAfter = 10 * time.Minute
// mergeChecks are the statuses that are the mesh's check: mesh/check, the one main requires (novox/hq ADR 0447, issue
// 1302), and the two it replaced, which hq's base (mesh/repo-check, ADR 0503 §5) and a reverted one still require. A base
// requiring any is one the mesh is meant to check; only those a base requires are waited on.
var mergeChecks = []string{ctxCheck, "mesh/merge-gate", "mesh/repo-check"}
// OpenPull is an open pull request as the forge says it (the seat git's open-pulls): its head, its base,
// the contexts the base's protection requires, the contexts its head has a status of, in any state, and each of
// those statuses with its state.
type OpenPull struct {
Owner string `json:"owner"`
Repo string `json:"repo"`
Number int `json:"number"`
Base string `json:"base"`
HeadSHA string `json:"head_sha"`
HTMLURL string `json:"html_url,omitempty"`
Required []string `json:"required"`
Statuses []string `json:"statuses"`
// Checks are the head's statuses by context, the newest of each (novox/hq issue 438). A forge module older
// than that says none: the head is then read by Statuses alone, as before, and a pending check is not told
// from an answered one.
Checks map[string]HeadCheck `json:"checks,omitempty"`
}
// HeadCheck is one status on a head as the forge keeps it: pending, success, failure, error or warning.
type HeadCheck struct {
State string `json:"state"`
Description string `json:"description,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
}
// unchecked is a head seen without an answered merge check, what it lacks, and when this owner first saw it so —
// its own time, since the forge's update time moves on every comment.
type unchecked struct {
pull OpenPull
waiting []string
// pending is when each required check that is pending was set so, as the forge says it: what tells a check
// asked again from one still waiting on the same ask.
pending map[string]string
first time.Time
// read is when the forge was read that showed it so.
read time.Time
// held is the state of the delivery of this head, ready or rejected, when one is held here (novox/hq issue
// 441); empty for a head with no delivery.
held State
}
// saidByTheForge is whether a delivery's head is said here when its required check has not answered on the forge:
// one in ready or rejected, whose state has no bound of its own (novox/hq issue 441). No delivery at all is too.
func saidByTheForge(d *Delivery) bool {
return d == nil || d.State == Ready || d.State == Rejected
}
// unanswered are the merge checks the base requires that the head has no verdict of, each as it reads — "never
// set", or pending and since when — when each pending one was set so, and whether the head has any merge check at
// all: its check started. A head on a base that requires none, or without a head, has nothing unanswered.
func (p OpenPull) unanswered() (waiting []string, pending map[string]string, started bool) {
if p.HeadSHA == "" {
return nil, nil, false
}
pending = map[string]string{}
started = slices.ContainsFunc(mergeChecks, func(c string) bool {
return slices.Contains(p.Statuses, c) || p.Checks[c].State != ""
})
for _, c := range mergeChecks {
if !slices.Contains(p.Required, c) {
continue
}
got, ok := p.Checks[c]
switch {
case p.Checks == nil && started:
// A forge that says no states: a head with any merge check counts as checked, as before issue 438,
// since nothing here says whether the one it has answered.
case !ok || got.State == "":
waiting = append(waiting, c+" never set")
case got.State == "pending" && strings.HasPrefix(got.Description, outOfDatePrefix):
// Out of date because its trunk moved, and not asked (novox/hq ADR 0323 §4, §7): nothing runs for it and
// it costs nothing, so it is not a check asked and not answered. Asked again, it is pending anew.
case got.State == "pending":
pending[c] = utcOf(got.UpdatedAt)
waiting = append(waiting, c+" pending since "+orUnknown(pending[c]))
}
}
return waiting, pending, started
}
// utcOf is a forge time in UTC, as this module's other lines give their times; one it cannot read stays as given.
func utcOf(at string) string {
t, err := time.Parse(time.RFC3339, at)
if err != nil {
return at
}
return t.UTC().Format(time.RFC3339)
}
func orUnknown(at string) string {
if at == "" {
return "a time the forge did not say"
}
return at
}
// askedAgain is whether a required check is pending on a newer ask than when this owner last saw the head: set
// pending for the first time, set pending anew, or gone back to pending after it answered. A check that answers
// never is: the head waits on what is left from the same first sight (novox/hq issue 438).
func askedAgain(was, now map[string]string) bool {
for c, at := range now {
if before, ok := was[c]; !ok || before != at {
return true
}
}
return false
}
// SeeOpenPulls asks the forge for every open pull request and keeps the heads with a required merge check not
// answered. A forge that cannot be asked changes nothing: what was seen stands, and nothing is guessed.
func (h *Holder) SeeOpenPulls() error {
open, err := h.Forge.OpenPulls()
if err != nil {
h.Logf("[mesh-delivery] the forge's open pull requests cannot be read: %v", err)
return err
}
for _, gone := range h.seeOpen(open) {
h.settleGone(gone)
}
return nil
}
// openPullsMost is the most open pull requests the seat git's open-pulls answers: it reads 20 pages of 50 and says
// nothing of the rest (mesh-catalog modules/gitea client.ts searchOpenPulls). A list that long may be cut, so what is
// missing from it is not taken as closed.
const openPullsMost = 20 * 50
// seeOpen keeps what the forge's open pull requests say, and answers the deliveries whose pull request is not among them:
// open here, closed on the forge with no event heard (a close or merge while the gate was away, or an event lost). Only
// from a complete list. Takes the lock.
func (h *Holder) seeOpen(open []OpenPull) []*Delivery {
h.mu.Lock()
defer h.mu.Unlock()
h.forgeRead = true
now := h.Now()
seen := map[string]unchecked{}
for _, p := range open {
if slices.Contains(p.Required, ctxCheck) {
// What the forge says of a head on a mesh/check base is its verdict (issue 1302): kept, and taken.
if d := h.deliveries[IDOf(p.Owner+"/"+p.Repo, p.HeadSHA)]; d != nil {
h.noteRequired(d, p.Required)
if c := p.Checks[ctxCheck]; c.State != "" {
h.takeCheckStatus(p.Owner+"/"+p.Repo, p.HeadSHA, c.State, c.Description)
}
}
}
waiting, pending, _ := p.unanswered()
if len(waiting) == 0 {
continue
}
id := IDOf(p.Owner+"/"+p.Repo, p.HeadSHA)
d := h.deliveries[id]
if !saidByTheForge(d) {
continue // a delivery with its own bound, or past its merge
}
var held State
if d != nil {
held = d.State
}
// The clock runs from when this owner first saw the head waiting, and starts again only on a newer ask: a
// check that started, or was asked again — its pending status set anew — is given its whole bound from then.
// A check that answers does not start it again, so a head is said on the bound from its first sight.
u := unchecked{pull: p, waiting: waiting, pending: pending, first: now, read: now, held: held}
if was, ok := h.unchecked[id]; ok && !askedAgain(was.pending, pending) {
u.first = was.first
} else {
h.Logf("[mesh-delivery] %s/%s#%d's head %s %s; said if so in %s", p.Owner, p.Repo, p.Number,
short(p.HeadSHA), u.what(), u.bound())
}
seen[id] = u
}
h.unchecked = seen
if len(open) >= openPullsMost {
return nil
}
listed := map[string]bool{}
for _, p := range open {
listed[strings.ToLower(p.Owner+"/"+p.Repo)+"#"+strconv.Itoa(p.Number)] = true
}
var gone []*Delivery
for _, d := range h.deliveries {
if d.State.Final() || d.MergedAs != "" || d.Number <= 0 || listed[strings.ToLower(d.Repository)+"#"+strconv.Itoa(d.Number)] {
continue
}
gone = append(gone, d)
}
sort.Slice(gone, func(i, j int) bool { return gone[i].ID < gone[j].ID })
return gone
}
// settleGone reads from the forge a pull request the open list does not hold, and takes its merge or its close as the
// forge's event would have said it (issue 1275, review round 1: a pull request closed on 2026-10-17 was still ready here
// eight days later). A merge learnt this way is past: its delivery is merged with the forge's time, moves no other
// delivery's trunk, and never moves source.main (pullMerged, late); the trunk's merges heard as they happened move
// those. A forge that cannot say changes nothing. Outside the lock.
func (h *Holder) settleGone(d *Delivery) {
h.mu.Lock()
id, owner, repo, number, head, base, branch := d.ID, d.Owner(), d.Repo(), d.Number, d.Commit, d.Base, d.Branch
h.mu.Unlock()
// Read by its number alone: the forge reads the pull request's own head, which it keeps after the branch is gone.
fc, err := h.Forge.Statuses(owner, repo, number, "", base)
if err != nil || fc == nil || fc.Pull == nil {
if err != nil {
h.Logf("[mesh-delivery] %s is not among the forge's open pull requests, and the forge cannot say what became of it: %v", id, err)
}
return
}
p := fc.Pull
if p.State != "closed" || p.Number != number {
return // open again, a state the forge did not say, or not this pull request: the next reading says
}
ev := PullEvent{Owner: owner, Repo: repo, Number: number, Base: base, Head: branch, HeadSHA: head, HTMLURL: p.HTMLURL}
if p.Merged && p.MergeCommit != "" {
ev.MergeCommit, ev.Merged, ev.MergedAt = p.MergeCommit, true, p.MergedAt
h.Logf("[mesh-delivery] %s is not among the forge's open pull requests: the forge says it merged as %s at %s", id,
shortOf(p.MergeCommit, 12), orUnknown(p.MergedAt))
h.pullMerged(ev, true)
return
}
h.Logf("[mesh-delivery] %s is not among the forge's open pull requests: the forge says it was closed unmerged", id)
h.PullClosed(ev)
}
// started is whether the head's check started, rather than was never asked: it has any merge check, or a delivery
// of it is held here, which the forge announced and the controller checked (novox/hq issue 441).
func (u unchecked) started() bool {
_, _, started := u.pull.unanswered()
return started || u.held != ""
}
// bound is how long the head may wait as it does: UncheckedAfter for a check never asked, the table's bound for a
// check asked and not answered for one that started (novox/hq issue 438).
func (u unchecked) bound() time.Duration {
if u.started() {
return Bounds[Proposed].For
}
return UncheckedAfter
}
// what is what the head lacks, in words.
func (u unchecked) what() string {
if u.started() {
return "has a merge check that has not answered: " + strings.Join(u.waiting, ", ")
}
return "has no merge check"
}
// uncheckedLines are the heads unchecked past their bound, as stalled lines; under the lock.
func (h *Holder) uncheckedLines(now time.Time) []StalledLine {
var out []StalledLine
for id, u := range h.unchecked {
d := h.deliveries[id]
if !saidByTheForge(d) || (d != nil && d.State != u.held) || now.Sub(u.first) < u.bound() {
continue // moved on since the forge was last read: the next reading says it anew
}
h2 := "none: the forge never announced it, so there is no delivery to close — the operator's"
says := fmt.Sprintf("%s/%s#%d is open on %s, which requires the merge check, and its head has had no merge "+
"check: the mesh was never asked to check it. A new commit on its branch announces it",
u.pull.Owner, u.pull.Repo, u.pull.Number, u.pull.Base)
state, waiting := Unannounced, []string(nil)
if u.started() {
state, waiting = Unanswered, u.waiting
says = fmt.Sprintf("%s/%s#%d is open on %s, which requires the merge check, and its head's check started "+
"and never answered: %s. No delivery of it is held here, so nothing asks it again. A new commit on "+
"its branch announces it and asks its check", u.pull.Owner, u.pull.Repo, u.pull.Number, u.pull.Base,
strings.Join(u.waiting, ", "))
h2 = "none: no delivery of it is held here, so there is none to close — the operator's"
}
if u.held != "" {
// A delivery is held, its verdict known, and the forge shows another (novox/hq issue 441).
says = fmt.Sprintf("%s/%s#%d is open on %s, which requires the merge check, and its delivery is %s here, "+
"but on the forge its check has not answered: %s. The pull request waits on that status and nothing "+
"asks it again. A recheck of the delivery asks its check again, as does a new commit on its branch",
u.pull.Owner, u.pull.Repo, u.pull.Number, u.pull.Base, u.held, strings.Join(u.waiting, ", "))
h2 = "none: its delivery is " + string(u.held) + ", a state the operator moves — the operator's"
}
out = append(out, StalledLine{ID: id, Number: u.pull.Number, State: state, For: now.Sub(u.first).Round(time.Second).String(),
Bound: u.bound().String(), Held: u.held,
H2: h2, Says: says, Waiting: waiting})
}
return out
}
func short(sha string) string {
if len(sha) > 12 {
return sha[:12]
}
return sha
}
// uncheckedDue is whether a head seen waiting has reached the bound it is said at without the forge being read since: it
// is read once more then, to say it only if it still waits. A head not yet at its bound is looked at again when it is.
// Where the poll used to find a status that never came, an event does not exist to find it; this read is bounded to one
// per head and its bound. Called by React.
func (h *Holder) uncheckedDue() bool {
h.mu.Lock()
defer h.mu.Unlock()
now, due := h.Now(), false
for _, u := range h.unchecked {
at := u.first.Add(u.bound())
switch {
case now.Before(at):
h.lookAt(at, "a head whose merge check has not answered, read from the forge once more at its bound")
case u.read.Before(at):
due = true
}
}
return due
}
// forgetUnchecked drops the heads seen waiting of a pull request that closed or merged.
func (h *Holder) forgetUnchecked(owner, repo string, number int) {
for id, u := range h.unchecked {
if strings.EqualFold(u.pull.Owner, owner) && strings.EqualFold(u.pull.Repo, repo) && u.pull.Number == number {
delete(h.unchecked, id)
}
}
}
@@ -1,97 +0,0 @@
package main
import (
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"testing"
"time"
)
// **A head nobody checks is found from events, not from a poll** (novox/hq ADR 0406 §1, issue 975, issues 347 and 722):
// the forge's open pull requests are read when this owner starts and when an event says something they show may have
// changed — a merge check's status, a new head, a ready mark — and once more at the bound a head's check is said.
func statusEvent(sha, context, state string) gitseat.ForgeStatusEvent {
return gitseat.ForgeStatusEvent{Repository: "novox/hq", Commit: sha, Context: context, State: state}
}
func TestAMergeCheckStatusReadsTheOpenPullRequestsAndAnotherContextDoesNot(t *testing.T) {
w := newWorld(t)
before := w.forge.openCalls
hear(t, w, gitEvent(gitseat.EventForgeStatus), statusEvent("cccc", "mesh/delivery", "success"))
if w.forge.openCalls != before {
t.Fatalf("a status of a context that is no merge check read the forge %d times", w.forge.openCalls-before)
}
hear(t, w, gitEvent(gitseat.EventForgeStatus), statusEvent("cccc", "mesh/merge-gate", "pending"))
if w.forge.openCalls != before+1 {
t.Fatalf("a merge check's status read the forge %d times, wanted once", w.forge.openCalls-before)
}
}
func TestAHeadWhoseCheckNeverAnsweredIsSaidFromTheEventsAndOneReadAtTheBound(t *testing.T) {
w := newWorld(t)
w.forge.open = []OpenPull{{Owner: "novox", Repo: "hq", Number: 1, Base: "main", HeadSHA: "cccc",
Required: []string{"mesh/merge-gate"}, Statuses: []string{"mesh/merge-gate"},
Checks: map[string]HeadCheck{"mesh/merge-gate": {State: "pending", UpdatedAt: "2026-10-06T12:00:00Z"}}}}
hear(t, w, gitEvent(gitseat.EventForgeStatus), statusEvent("cccc", "mesh/merge-gate", "pending"))
if len(w.h.Stalled()) != 0 {
t.Fatal("a head just seen waiting is said already")
}
reads := w.forge.openCalls
// Minutes pass with no event: nothing polls. At the bound, one read confirms it, and it is said.
for range 3 {
w.later(5 * time.Minute)
w.h.React()
}
w.later(Bounds[Proposed].For)
w.h.React()
w.h.React()
if got := w.forge.openCalls - reads; got != 1 {
t.Fatalf("the forge's open pull requests were read %d times between the status and the bound's end; wanted once, at the bound", got)
}
if lines := w.h.Stalled(); len(lines) != 1 || lines[0].State != Unanswered {
t.Fatalf("the head whose check never answered is not said: %+v", lines)
}
}
func TestANewlyAnnouncedHeadReadsTheForgeAndAKnownOneDoesNot(t *testing.T) {
w := newWorld(t)
before := w.forge.openCalls
hear(t, w, gitEvent(gitseat.EventPullUpdated), seatUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go")))
if w.forge.openCalls != before+2 { // the unchecked heads, and the head's standing verdict before its check is asked (issue 1115)
t.Fatalf("a new head read the forge %d times, wanted twice", w.forge.openCalls-before)
}
hear(t, w, gitEvent(gitseat.EventPullUpdated), seatUpdated(pr("novox/app", 7, readyHead, "feat/x", "src/a.go")))
if w.forge.openCalls != before+3 { // the look of a delivery awaiting its verdict reads its standing one
t.Fatalf("the same head announced again read the forge again (%d reads)", w.forge.openCalls-before)
}
}
func TestNothingReadsTheForgeWithoutAnEvent(t *testing.T) {
w := newWorld(t)
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
before := w.forge.openCalls
for range 6 {
w.later(time.Minute)
w.settleAll()
}
if w.forge.openCalls != before {
t.Fatalf("the forge's open pull requests were read %d times with no event", w.forge.openCalls-before)
}
}
func TestAClosedOrMergedPullRequestIsNoLongerSaidUnchecked(t *testing.T) {
w := newWorld(t)
w.forge.open = []OpenPull{{Owner: "novox", Repo: "hq", Number: 1, Base: "main", HeadSHA: "cccc",
Required: []string{"mesh/merge-gate"}}}
w.h.SeeOpenPulls()
w.later(11 * time.Minute)
if len(w.h.Stalled()) != 1 {
t.Fatal("not said")
}
closed := pr("novox/hq", 1, "cccc", "feat/x")
hear(t, w, gitEvent(gitseat.EventPullClosed), seatClosed(closed))
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a closed pull request is still said: %+v", lines)
}
}
@@ -1,80 +0,0 @@
package main
import (
"strings"
"testing"
"time"
)
// **A pull request the mesh was never asked to check is a delivery held, and said** (novox/hq issue 347): one open
// on a base that requires the merge check — mesh/merge-gate or mesh/repo-check, as hq requires only the second —
// whose head has had neither for ten minutes since this owner first saw it so, is a stalled line: the controller
// raises it as `delivery.<id>.stalled` (D14), the delivery domain's own word for a delivery held past its bound.
func TestAPullRequestNobodyCheckedIsAStalledDelivery(t *testing.T) {
w := newWorld(t)
head := "bfe82315f42c8f4509d0598c32569a202d5eada6"
w.forge.open = []OpenPull{
{Owner: "novox", Repo: "hq", Number: 243, Base: "main", HeadSHA: head, Required: []string{"mesh/repo-check"}},
{Owner: "novox", Repo: "sdk", Number: 14, Base: "main", HeadSHA: "aaaa", Required: []string{"mesh/merge-gate", "mesh/repo-check"},
Statuses: []string{"mesh/merge-gate"}},
{Owner: "novox", Repo: "free", Number: 1, Base: "main", HeadSHA: "bbbb", Required: nil},
}
w.h.SeeOpenPulls()
if lines := w.h.Stalled(); len(lines) != 0 {
t.Fatalf("a head just seen is stalled already: %+v", lines)
}
w.later(6 * time.Minute)
w.h.SeeOpenPulls() // seen again: its own first-seen time stands, whatever the forge's update time says
w.later(5 * time.Minute)
lines := w.h.Stalled()
if len(lines) != 1 || lines[0].ID != IDOf("novox/hq", head) || lines[0].State != Unannounced ||
!strings.Contains(lines[0].Says, "no merge check") || !strings.HasPrefix(lines[0].H2, "none") {
t.Fatalf("an unchecked pull request on a base requiring only mesh/repo-check reads %+v", lines)
}
if lines[0].Number != 243 {
t.Fatalf("the line does not name the pull request's number: %+v", lines[0])
}
if lines[0].For != "11m0s" {
t.Fatalf("its time is not counted from when this owner first saw it: %s", lines[0].For)
}
// Announced at last: a delivery of its own, with its own bound — no longer this line.
w.h.PullUpdated(pr("novox/hq", 243, head, "decision/x"))
w.h.SeeOpenPulls()
for _, l := range w.h.Stalled() {
if l.State == Unannounced {
t.Fatalf("an announced pull request is still said unannounced: %+v", l)
}
}
}
// One that gets a check, or closes, is dropped; one the forge cannot be asked about is not guessed.
func TestAnUncheckedPullRequestThatGetsACheckOrClosesIsDropped(t *testing.T) {
w := newWorld(t)
w.forge.open = []OpenPull{{Owner: "novox", Repo: "hq", Number: 1, Base: "main", HeadSHA: "cccc",
Required: []string{"mesh/merge-gate"}}}
w.h.SeeOpenPulls()
w.later(11 * time.Minute)
if len(w.h.Stalled()) != 1 {
t.Fatal("not said")
}
w.forge.open[0].Statuses = []string{"mesh/merge-gate"}
w.h.SeeOpenPulls()
if len(w.h.Stalled()) != 0 {
t.Fatal("a pull request with a check is still said")
}
w.forge.open[0].Statuses = nil
w.h.SeeOpenPulls()
w.later(11 * time.Minute)
w.forge.openDown = true
w.h.SeeOpenPulls() // the forge away: what was seen stands, nothing new is guessed
if len(w.h.Stalled()) != 1 {
t.Fatal("a forge that could not be asked dropped or invented a line")
}
w.forge.openDown = false
w.forge.open = nil
w.h.SeeOpenPulls()
if len(w.h.Stalled()) != 0 {
t.Fatal("a closed pull request is still said")
}
}
@@ -9,289 +9,301 @@ import (
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
// The seat's verbs (novox/hq ADR 0239, the mesh-delivery seat in the controller's set): six that read, and
// the acts — a person's recheck, and healer H2's close, each only by a transition the table
// holds.
// The seat's verbs that read (novox/hq ADR 0239; design 47 G §431, §172, A-24): from G's delivery the gate keeps no merge
// states, so `deliveries`, `show` and `groups` answer the delivery records as the gate keeps them (G-3 *The delivery
// record*), filtered by their arguments, with no line form of their own, and `stalled` answers the records whose open wait is
// past its bound, with that wait, and the acts owed the forge past theirs.
// Line is one delivery as `deliveries` lists it.
type Line struct {
ID string `json:"id"`
State State `json:"state"`
Since string `json:"since"`
For string `json:"for"`
Group string `json:"group,omitempty"`
Number int `json:"number,omitempty"`
Waits string `json:"waits,omitempty"`
Owed int `json:"owed,omitempty"`
Stalled bool `json:"stalled,omitempty"`
// Machines says, for a merged delivery, whether its merge is on the machines, derived from the delivery view's reach
// (reached.go, novox/hq ADR 0556 clause 4); recorded nowhere.
Machines string `json:"machines,omitempty"`
// The two states of a record, as `deliveries` filters them.
const (
stateOpen = "open"
stateEnded = "ended"
)
// listedFor is how long an ended record is listed by `deliveries` without `all`: the contract's thirty days.
const listedFor = 30 * 24 * time.Hour
// recordOpenBound is how long a record may stay open after it opened before `stalled` lists it (design 47 §148: a delivery of
// the whole mesh takes under an hour, so a day means something stands still).
const recordOpenBound = 24 * time.Hour
func stateOf(r *Record) string {
if r.Ended != nil {
return stateEnded
}
return stateOpen
}
// waitsFor is what a delivery waits for, in words.
func (h *Holder) waitsFor(d *Delivery) string {
if r := d.Ready; r != nil && r.Merged == nil && strings.HasPrefix(r.Waits, "approved;") {
// Approved and waiting for another delivery's check, or for its own verdict's carry (ADR 0548 §2, §8, §9).
return r.Waits
// groupOf is the id of the group a record is: its group's id, its milestone otherwise.
func groupOf(r *Record) string {
if r.Group != nil && r.Group.Group != "" {
return r.Group.Group
}
switch d.State {
case Proposed:
return "its check"
case Ready:
if d.Base == "" && d.Number != 0 {
return waitsForHead
return r.Milestone
}
// holdsRepository is whether one of the record's pull requests is in the repository (owner/name, any case).
func holdsRepository(r *Record, repository string) bool {
for _, m := range membersOf(r) {
if strings.EqualFold(m.Repository, repository) {
return true
}
return "its merge"
case Rejected:
return "a new head, or a recheck"
case Held:
return "a person: " + d.HeldWhy
}
return ""
return false
}
// Deliveries is the `deliveries` verb.
func (h *Holder) Deliveries(state, repository, group string, all bool) []Line {
h.mu.Lock()
defer h.mu.Unlock()
// membersOf are a record's pull requests: the group's pulls as kept, or else the members its heads name (`<repository>@<number>`
// to the commit, as an Approve names them).
func membersOf(r *Record) []seat.Member {
open := r.Ended == nil
var out []seat.Member
if r.Group != nil && len(r.Group.Pulls) > 0 {
for _, p := range r.Group.Pulls {
out = append(out, seat.Member{ID: IDOf(p.Repository, firstOf(p.Commit, p.Head)), Repository: p.Repository, State: p.State,
Number: int64(p.Number), Head: p.Head, Open: open && p.State != "merged"})
}
return out
}
ids := make([]string, 0, len(r.Heads))
for id := range r.Heads {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
repository, number, ok := parseApprovedKey(id)
if !ok {
repository = id
}
out = append(out, seat.Member{ID: IDOf(repository, r.Heads[id]), Repository: repository, Number: int64(number), Head: r.Heads[id],
State: stateOf(r), Open: open})
}
return out
}
// parseApprovedKey is a heads key `<repository>@<number>` as its repository and number.
func parseApprovedKey(key string) (string, int, bool) {
i := strings.LastIndex(key, "@")
if i < 0 {
return "", 0, false
}
var n int
if _, err := fmt.Sscanf(key[i+1:], "%d", &n); err != nil {
return "", 0, false
}
return key[:i], n, true
}
func firstOf(a, b string) string {
if a != "" {
return a
}
return b
}
// Deliveries is the `deliveries` verb: every record kept as it is kept, the open ones first, then the newest; the ended ones
// of the last thirty days (all: every one kept), narrowed by state (open or ended), repository or group.
func (h *Holder) Deliveries(state, repository, group string, all bool) (map[string]any, error) {
kept, err := h.keptRecords()
if err != nil {
return nil, err
}
now := h.Now()
var out []Line
for _, d := range h.deliveries {
if state != "" && string(d.State) != state || repository != "" && !strings.EqualFold(d.Repository, repository) ||
group != "" && d.Group != group {
continue
}
if d.State.Final() && !all && now.Sub(d.Since) > 24*time.Hour {
continue
}
l := Line{ID: d.ID, State: d.State, Since: d.Since.UTC().Format(time.RFC3339), For: now.Sub(d.Since).Round(time.Second).String(),
Group: d.Group, Number: d.Number, Waits: h.waitsFor(d), Owed: len(d.Owed), Machines: h.machines(d)}
l.Stalled = pastBound(d, now)
out = append(out, l)
}
sort.Slice(out, func(i, j int) bool {
if out[i].State.Final() != out[j].State.Final() {
return !out[i].State.Final()
}
return out[i].Since > out[j].Since
})
return out
}
// Show is the `show` verb: a delivery whole, or a group with its members.
func (h *Holder) Show(id string) (any, error) {
h.mu.Lock()
defer h.mu.Unlock()
if d := h.deliveries[id]; d != nil {
out := map[string]any{"delivery": d, "waits": h.waitsFor(d)}
if m := h.machines(d); m != "" {
out["machines"] = m
}
if g := h.groups[d.Group]; g != nil {
state, why := GroupState(g, h.membersOf(g))
out["group"] = map[string]any{"group": g, "state": state, "why": why}
}
return out, nil
}
if g := h.groups[id]; g != nil {
members := h.membersOf(g)
state, why := GroupState(g, members)
return map[string]any{"group": g, "state": state, "why": why, "members": members}, nil
}
return nil, fmt.Errorf("no delivery or group %q: `deliveries` lists them", id)
}
// Groups is the seat's verb groups: every delivery group not ended (all: those that ended too), or with only that group
// alone, ended or not, and none for a group it does not know. Each with its members in merge order (membersOf), its state
// and check, and the acts the gate said for it and its members: each unanswered act as it was said (the forge bridge's to
// perform), and each act a delivery record holds with the outcome recorded (owedended.go). Outside the lock: the records
// are read first.
func (h *Holder) Groups(all bool, only string) seat.GroupsAnswer {
var kept []*Record
if h.Records != nil {
var err error
if kept, err = h.Records.Kept(); err != nil {
return seat.GroupsAnswer{Error: "the delivery records cannot be read: " + err.Error()}
}
}
h.mu.Lock()
defer h.mu.Unlock()
out := seat.GroupsAnswer{Groups: []seat.Group{}}
for _, g := range h.groups {
if only != "" && g.ID != only {
continue
}
members := h.membersOf(g)
if len(members) == 0 {
continue
}
state, why := GroupState(g, members)
if only == "" && !all && (state == GroupMerged || state == GroupClosed) {
continue
}
l := seat.Group{ID: g.ID, State: state, Why: why, Members: []seat.Member{}, Acts: []seat.Act{}}
heads := map[string]bool{}
for _, d := range members {
l.Members = append(l.Members, seat.Member{ID: d.ID, Repository: d.Repository, State: string(d.State),
Number: int64(d.Number), Head: d.Commit, Base: d.Base,
Open: !d.State.Final() && d.MergedAs == "" && !d.ClosedUnmerged})
heads[strings.ToLower(d.Repository)+"@"+d.Commit] = true
for _, e := range d.Owed {
if o, ok := owedSaid(e, d.Repository, d.Number, d.Commit, ViewBody(d, g), d.HTMLURL, false); ok {
l.Acts = append(l.Acts, seat.Act{Owed: o})
}
}
}
for _, e := range g.Owed {
if o, ok := owedSaid(e, "", 0, "", "", "", true); ok {
l.Acts = append(l.Acts, seat.Act{Owed: o})
}
}
l.Acts = append(l.Acts, recordedActs(kept, heads)...)
if g.Check != nil {
l.Check = g.Check.Verdict
if l.Check == "" {
l.Check = "asked as " + g.Check.Asked
}
}
out.Groups = append(out.Groups, l)
}
sort.Slice(out.Groups, func(i, j int) bool { return out.Groups[i].ID < out.Groups[j].ID })
return out
}
// recordedActs are the acts the kept records hold for pull requests at these heads (`<repository>@<head>`, lower case),
// each with its outcome, ordered by id.
func recordedActs(kept []*Record, heads map[string]bool) []seat.Act {
var out []seat.Act
var out []recordView
for _, r := range kept {
for _, a := range r.Acts {
if !heads[strings.ToLower(a.Repository)+"@"+a.Head] {
continue
}
at := a.At
out = append(out, seat.Act{Outcome: a.Outcome, Words: a.Words, At: &at, Owed: seat.Owed{ID: a.ID,
Act: a.Act, Repository: a.Repository, Number: int64(a.Number), Head: a.Head}})
if state != "" && state != stateOf(r) || repository != "" && !holdsRepository(r, repository) || group != "" && groupOf(r) != group {
continue
}
if r.Ended != nil && !all && now.Sub(r.Ended.At) > listedFor {
continue
}
out = append(out, viewOf(r))
}
sort.SliceStable(out, func(i, j int) bool {
if (out[i].Record.Ended == nil) != (out[j].Record.Ended == nil) {
return out[i].Record.Ended == nil
}
return out[i].Record.Opened.After(out[j].Record.Opened)
})
if out == nil {
out = []recordView{}
}
return map[string]any{"deliveries": out}, nil
}
func (h *Holder) keptRecords() ([]*Record, error) {
if h.Records == nil {
return nil, fmt.Errorf("this gate keeps no delivery records")
}
kept, err := h.Records.Kept()
if err != nil {
return nil, fmt.Errorf("the delivery records cannot be read: %w", err)
}
return kept, nil
}
// Show is the `show` verb: the record an id names whole, as it is kept: its instance, its milestone or group, or one of its
// pull requests' ids (`<repository>@<twelve characters of the commit>`).
func (h *Holder) Show(id string) (any, error) {
kept, err := h.keptRecords()
if err != nil {
return nil, err
}
var out []recordView
for _, r := range kept {
hit := r.Instance == id || r.Milestone == id || groupOf(r) == id
for _, m := range membersOf(r) {
hit = hit || strings.EqualFold(m.ID, id)
}
if hit {
out = append(out, viewOf(r))
}
}
if len(out) == 0 {
return nil, fmt.Errorf("no delivery record or group %q: `deliveries` lists them", id)
}
return map[string]any{"records": out}, nil
}
// Groups is the seat's verb groups: every record's group not ended (all: those that ended too), or with only that group
// alone, ended or not, and none for a group it does not know. Each with its members, its state, why, and the acts the gate said
// for it with the outcome recorded in the record (owedended.go).
func (h *Holder) Groups(all bool, only string) seat.GroupsAnswer {
kept, err := h.keptRecords()
if err != nil {
return seat.GroupsAnswer{Error: err.Error()}
}
out := seat.GroupsAnswer{Groups: []seat.Group{}}
for _, r := range kept {
if only != "" && groupOf(r) != only || only == "" && !all && r.Ended != nil {
continue
}
g := seat.Group{ID: groupOf(r), State: stateOf(r), Members: membersOf(r), Acts: recordedActs(r)}
if r.Ended != nil {
g.Why = r.Ended.Why
} else {
g.Why = allRun(r)
}
out.Groups = append(out.Groups, g)
}
sort.SliceStable(out.Groups, func(i, j int) bool { return out.Groups[i].ID < out.Groups[j].ID })
return out
}
// recordedActs are the acts a record holds, each with its outcome when the forge bridge said one, ordered by id.
func recordedActs(r *Record) []seat.Act {
out := []seat.Act{}
for _, a := range r.Acts {
o := a.Owed
if o.ID == "" {
o = seat.Owed{ID: a.ID, Act: a.Act, Repository: a.Repository, Number: int64(a.Number), Head: a.Head}
}
act := seat.Act{Outcome: a.Outcome, Words: a.Words, Owed: o}
if a.Outcome != "" {
at := a.At
act.At = &at
}
out = append(out, act)
}
sort.Slice(out, func(i, j int) bool { return out[i].Owed.ID < out[j].Owed.ID })
return out
}
// StalledLine is one delivery past its state's bound.
// StalledLine is one record stalled: its open wait past its bound, or an act owed the forge unanswered past its.
type StalledLine struct {
// ID is the record's instance.
ID string `json:"id"`
// Number is the pull request's, for a head the forge never announced: the condition names it (novox/hq issue 347).
Number int `json:"number,omitempty"`
State State `json:"state"`
// Waiting are the merge checks a line of the state Unanswered waits on, each as it reads — "mesh/check
// never set", "mesh/check pending since <UTC time>" — so the controller's words name them without reading
// Says (novox/hq issue 438).
Waiting []string `json:"waiting,omitempty"`
// Checks are the same merge checks as data — context, state, since when — so the controller says their times in
// the operator's own zone, which it cannot do inside a finished sentence (novox/hq issue 443). Waiting stays
// beside them for a controller that does not read them yet.
Checks []WaitingCheck `json:"checks,omitempty"`
// Held is the state of the delivery a line of the state Unanswered is about, ready or rejected, when one is held
// here (novox/hq issues 441, 451): its check answered once, so the controller advises a recheck of it rather than
// the build queue, reading the state here and never from Says. Empty for a head with no delivery.
Held State `json:"held,omitempty"`
// Wait is the open wait: what the record waits for, or the act unanswered.
Wait string `json:"wait"`
For string `json:"for"`
Bound string `json:"bound"`
H2 string `json:"h2"`
Says string `json:"says"`
// Severity is "urgent" for a group that is held or half merged (issue 1275); empty otherwise.
// Severity is empty: nothing a record waits on is urgent.
Severity string `json:"severity,omitempty"`
// Needs is NeedsNewHead for a delivery whose cure is its author's new head (novox/hq ADR 0505 §5), with Author
// naming that author as the operator reads it and Branch its pull request's branch; empty otherwise.
Needs string `json:"needs,omitempty"`
Author string `json:"author,omitempty"`
Branch string `json:"branch,omitempty"`
}
// NeedsNewHead is what a stalled line needs when its cure is its author's new head (ADR 0505 §5).
const NeedsNewHead = "new-head"
// Stalled is the `stalled` verb: what the gate says to the operator as conditions (conditions.go).
// Stalled is the `stalled` verb: the records whose open wait is past its bound, with that wait (§148, A-24), and the acts owed
// the forge unanswered past their fifteen minutes (§165), which the condition keeper raises and the gate says (conditions.go).
func (h *Holder) Stalled() []StalledLine {
h.mu.Lock()
defer h.mu.Unlock()
out := []StalledLine{}
kept, err := h.keptRecords()
if err != nil {
h.Refuse(err)
return out
}
now := h.Now()
var out []StalledLine
for _, d := range h.deliveries {
if !pastBound(d, now) {
continue
}
b, _ := boundOf(d)
h2 := "none: the state is the operator's"
switch {
case d.State == Proposed && d.AskedAgain != nil && d.outOfDate() != "":
h2 = "none: its verdict is out of date and its check, asked again, has not answered — the operator's"
case d.State == Proposed && d.CheckAsk == nil:
h2 = "none: its check could not be asked yet — the checker has not answered it"
case d.State == Proposed && d.CheckAsk.Reasked > 0:
h2 = "none: its check was re-asked once already and still not answered — the operator's"
case d.State == Proposed:
h2 = "close, by re-ask: its check asked once more"
case len(b.H2) > 0:
var evs []string
for _, e := range b.H2 {
evs = append(evs, string(e))
for _, r := range kept {
if r.Ended == nil && now.Sub(r.Opened) > recordOpenBound {
wait := allRun(r)
if wait == "" {
wait = "its end, which every present node running its entries brings"
}
h2 = "close, by " + strings.Join(evs, " or ")
out = append(out, StalledLine{ID: r.Instance, Wait: wait, For: now.Sub(r.Opened).Round(time.Second).String(),
Bound: recordOpenBound.String(), H2: "none: what the record waits on is its owner's — `gate_record_end` ends it by hand",
Says: fmt.Sprintf("the delivery record %s has been open since %s: %s", r.Instance, r.Opened.UTC().Format(time.RFC3339), wait)})
}
out = append(out, StalledLine{ID: d.ID, State: d.State, For: now.Sub(boundSince(d)).Round(time.Second).String(),
Bound: b.For.String(), H2: h2, Says: b.Says})
out = append(out, owedLines(r, now)...)
}
out = append(out, h.withChecks(h.uncheckedLines(now))...)
out = append(out, h.readyLines(now)...)
out = append(out, h.groupLines(now)...)
out = append(out, h.owedLines(now)...) // an act owed the forge with no outcome past its bound (W11.17)
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out
}
// owedLines is the stalled line of a record's acts said and unanswered past owedBound: its oldest such act and how many more wait.
func owedLines(r *Record, now time.Time) []StalledLine {
var oldest *RecordAct
more := 0
for id := range r.Acts {
a := r.Acts[id]
if a.Outcome != "" || now.Sub(a.At) < owedBound {
continue
}
if oldest == nil {
oldest = &a
} else {
more++
if a.At.Before(oldest.At) {
oldest = &a
}
}
}
if oldest == nil {
return nil
}
says := fmt.Sprintf("the %s owed to the forge, said at %s as %s, has no outcome from the forge bridge", oldest.Act,
oldest.At.UTC().Format(time.RFC3339), oldest.ID)
if more > 0 {
says += fmt.Sprintf(", nor have %d more", more)
}
return []StalledLine{{ID: r.Instance, Wait: oldest.ID, For: now.Sub(oldest.At).Round(time.Second).String(), Bound: owedBound.String(),
H2: "none: the forge bridge's bridge_owed reads what it performed — the operator's", Says: says + "; it is said again at the gate's start"}}
}
// Status is the module's own `delivery_status`.
func (h *Holder) Status(listening string) map[string]any {
h.mu.Lock()
defer h.mu.Unlock()
count := map[State]int{}
records := map[string]int{stateOpen: 0, stateEnded: 0}
owed := 0
for _, d := range h.deliveries {
count[d.State]++
owed += len(d.Owed)
if h.Records != nil {
if kept, err := h.Records.Kept(); err == nil {
for _, r := range kept {
records[stateOf(r)]++
for _, a := range r.Acts {
if a.Outcome == "" {
owed++
}
}
}
}
}
for _, g := range h.groups {
owed += len(g.Owed)
}
byState := map[string]int{}
for s, n := range count {
byState[string(s)] = n
}
out := map[string]any{"deliveries": byState, "groups": len(h.groups), "owed": owed, "unkept": len(h.dirty),
"events": listening, "started": h.started.Format(time.RFC3339)}
// The owed effects tried since this owner started (novox/hq issue 407): done, failed and to be tried again,
// or given up, and when the last was tried. A count since the start, said with that start: two readings in
// one run give a window's tries.
out := map[string]any{"records": records, "owed": owed, "events": listening, "started": h.started.Format(time.RFC3339)}
// The owed effects tried since this owner started (novox/hq issue 407): a count since the start, said with that start.
tried := map[string]any{"since": h.started.Format(time.RFC3339), "done": h.tried.done, "failed": h.tried.failed,
"given-up": h.tried.givenUp, "tried": h.tried.done + h.tried.failed + h.tried.givenUp}
if !h.tried.last.IsZero() {
tried["last"] = h.tried.last.Format(time.RFC3339)
}
out["owed-tried"] = tried
// Each held delivery by name, with why and the verb that releases it (novox/hq issue 529): a count alone
// pointed a reader at nothing.
var held []string
for _, d := range h.deliveries {
if d.State == Held {
held = append(held, HeldLine(d))
}
}
if len(held) > 0 {
sort.Strings(held)
out["held"] = held
}
if len(h.refusals) > 0 {
from := 0
if len(h.refusals) > 10 {
@@ -301,10 +313,3 @@ func (h *Holder) Status(listening string) map[string]any {
}
return out
}
// HeldLine says a held delivery in one line: which, since when and why. A merge without a passing check is a person's to
// read; the gate moves nothing after the merge.
func HeldLine(d *Delivery) string {
line := fmt.Sprintf("%s held since %s: %s", d.ID, d.Since.UTC().Format(time.RFC3339), d.HeldWhy)
return line
}
@@ -1,67 +0,0 @@
package main
import (
"slices"
"time"
)
// The merge checks an unanswered line waits on, as data (novox/hq issue 443). The line's Waiting says them as finished
// words, with a pending check's time in UTC; the operator reads the controller's words, which give every time in the
// operator's own zone, and the controller cannot say a time again inside a sentence it did not write. So each check
// is given too as its context, its state and since when, and the controller words it.
// WaitingCheck is one merge check a stalled line of the state Unanswered waits on.
type WaitingCheck struct {
Context string `json:"context"`
// State is CheckPending or CheckNeverSet.
State string `json:"state"`
// Since is when a pending check was set so, in RFC 3339 and UTC; empty when the forge did not say, or said what
// cannot be read — never a time made up.
Since string `json:"since,omitempty"`
}
const (
CheckPending = "pending"
CheckNeverSet = "never-set"
)
// waitingChecks are the checks unanswered names, in its order and by its rule, as data.
func (p OpenPull) waitingChecks() []WaitingCheck {
if p.HeadSHA == "" {
return nil
}
started := slices.ContainsFunc(mergeChecks, func(c string) bool {
return slices.Contains(p.Statuses, c) || p.Checks[c].State != ""
})
var out []WaitingCheck
for _, c := range mergeChecks {
if !slices.Contains(p.Required, c) {
continue
}
got, ok := p.Checks[c]
switch {
case p.Checks == nil && started:
// Read as checked, as unanswered does: nothing says whether the check it has answered.
case !ok || got.State == "":
out = append(out, WaitingCheck{Context: c, State: CheckNeverSet})
case got.State == "pending":
w := WaitingCheck{Context: c, State: CheckPending}
if t, err := time.Parse(time.RFC3339, got.UpdatedAt); err == nil {
w.Since = t.UTC().Format(time.RFC3339)
}
out = append(out, w)
}
}
return out
}
// withChecks gives each unanswered line the checks it waits on as data, from the head it was said of; under the lock.
// It reads every line of the state, whichever rule said it, so a line said for another reason carries them too.
func (h *Holder) withChecks(lines []StalledLine) []StalledLine {
for i, l := range lines {
if u, ok := h.unchecked[l.ID]; ok && l.State == Unanswered && len(l.Waiting) > 0 {
lines[i].Checks = u.pull.waitingChecks()
}
}
return lines
}
@@ -1,114 +0,0 @@
package main
import (
"encoding/json"
"fmt"
"strings"
"time"
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
)
// **The operator is asked only for a merge the forge account can carry out** (novox/hq issue 1945). car-hunter #2, a
// repository of a user, was approved, and its first act after the Approve, the commit status, was refused 403: the mesh's
// merging account (the gitea module's tools, mesh-tools) only read that repository. Before the gate asks the operator's
// Approve it reads, with the forge's merge state (seatForge.mergeState), whether that account writes each repository the
// ask covers (merge.writes), as the ask is about to be written. Where it does not, nothing is asked: the delivery waits,
// saying "the forge account cannot write <repo>: gitea_grant_write", and the gate raises it as a condition at once.
//
// A repository not written is kept so, and the ask is not composed again, until the seat git's event
// repo.writable (its grant, read back from the forge) or, as a safety net, writeReadLasts after the read. A forge
// that does not say merge.writes (an older gitea module) is taken as writing: the merge says its own refusal, as before.
// writeReadLasts is how long a read of whether the forge account writes a repository holds.
const writeReadLasts = 10 * time.Minute
// writableEvent is the seat git's event of an account given write on a repository.
var writableEvent = gitEvent(gitseat.EventRepoWritable)
// writeRead is what the forge last said of the merging account on one repository it cannot write.
type writeRead struct {
says string
at time.Time
}
// cannotWriteSays is what a delivery waits on when the forge account cannot write its repository.
func cannotWriteSays(repository string) string {
return "the forge account cannot write " + repository + ": gitea_grant_write"
}
// writeWaits is what an ask about to be made waits on: a repository it covers that the forge account was read, within
// writeReadLasts, not to write. Under the lock.
func (h *Holder) writeWaits(d *Delivery, members []*Delivery, now time.Time) string {
r := d.Ready
for _, m := range members {
repo := strings.ToLower(m.Repository)
w := h.writeReads[repo]
if w == nil {
continue
}
if !now.Before(w.at.Add(writeReadLasts)) {
delete(h.writeReads, repo) // read again at the ask
continue
}
if r.CannotWrite != w.says {
r.CannotWrite, r.CannotWriteSince = w.says, &now
h.keep(d)
}
h.lookAt(w.at.Add(writeReadLasts), "a repository the forge account could not write, read again")
return w.says
}
if r.CannotWrite != "" {
r.CannotWrite, r.CannotWriteSince = "", nil
h.keep(d)
}
return ""
}
// cannotWrite reads, at the ask, whether the forge account writes each repository the ask covers, and answers what the
// first one it cannot write says; a read that fails says nothing (the merge says its own refusal, as before). Outside the
// lock; a repository not written is kept, under the lock.
func (h *Holder) cannotWrite(reads []memberRead) string {
for _, m := range reads {
fc, err := h.Forge.Statuses(m.owner, m.repo, m.number, m.head, m.base)
if err != nil || fc == nil || fc.Merge.Writes == nil || *fc.Merge.Writes {
continue
}
repo := strings.ToLower(m.owner + "/" + m.repo)
says := fc.Merge.CannotWrite
if says == "" {
says = cannotWriteSays(repo)
}
h.mu.Lock()
if h.writeReads == nil {
h.writeReads = map[string]*writeRead{}
}
h.writeReads[repo] = &writeRead{says: says, at: h.Now()}
h.mu.Unlock()
return says
}
return ""
}
// WriteGranted takes the seat git's repo.writable: the repository's read is dropped, so the next look reads it
// again, and an approved merge of it that the forge refused is asked again, its cause gone.
func (h *Holder) WriteGranted(body json.RawMessage) error {
var e gitseat.RepoWritableEvent
if err := json.Unmarshal(body, &e); err != nil || !strings.Contains(strings.Trim(e.Repository, "/"), "/") {
return fmt.Errorf("%s names no repository: %s", writableEvent, string(body))
}
repo := strings.ToLower(e.Repository)
h.mu.Lock()
defer h.mu.Unlock()
delete(h.writeReads, repo)
for _, d := range h.deliveries {
if strings.ToLower(d.Repository) != repo || d.Ready == nil || d.Ready.Approved == nil || d.Ready.Merged != nil {
continue
}
d.Ready.Repeats = 0
h.mergeAgainNow(d)
h.keep(d)
}
h.Logf("[mesh-delivery] the forge account was given write on %s: it is read again before an ask, and an approved merge of it is asked again", repo)
return nil
}
@@ -1,46 +0,0 @@
package main
import (
gitseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/git"
"strings"
"testing"
)
// Issue 1945: car-hunter #2 was asked, approved, and its commit status refused 403, since the forge's merging account only
// read the repository. 1945/AC3: a pull request whose checks passed, in a repository the merging account cannot write, is
// not asked; it waits saying so, loud as a condition at once. 1945/AC4: the gitea module's gitea.repo.writable asks it then.
func TestAnUnwritableRepositoryIsNotAskedAndSaysSoUntilTheWriteIsGranted(t *testing.T) {
w := announced(t)
conds := hearConditions(w)
w.forge.unwritable = map[string]bool{"novox/app": true}
hear(t, w, shimCheckedKey, judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.h.React()
if w.op.count() != 0 || len(w.op.open()) != 0 {
t.Fatalf("the operator was asked %d times for a merge the forge account cannot carry out", w.op.count())
}
d := w.h.deliveries[IDOf("novox/app", readyHead)]
want := "the forge account cannot write novox/app: gitea_grant_write"
if d.Ready.Waits != want || d.Ready.CannotWrite != want {
t.Fatalf("waits %q, cannot_write %q; wanted %q", d.Ready.Waits, d.Ready.CannotWrite, want)
}
if c := conds.last(d.ID); c == nil || c.State != "raised" || !strings.Contains(c.Summary+c.Explanation+c.Headline, "cannot write novox/app") {
t.Fatalf("not said as a condition: %+v", c)
}
// Looked at again with nothing changed, the forge is not read again: the read holds until the grant or its bound.
asked := len(w.forge.asked)
w.h.React()
if len(w.forge.asked) != asked || w.op.count() != 0 {
t.Fatalf("read again %d times, asked %d", len(w.forge.asked)-asked, w.op.count())
}
delete(w.forge.unwritable, "novox/app")
hear(t, w, writableEvent, gitseat.RepoWritableEvent{Repository: "novox/app", Account: "mesh-tools"})
w.h.React()
if w.op.count() != 1 {
t.Fatalf("after the write was granted the operator was asked %d times, wanted once", w.op.count())
}
if d.Ready.CannotWrite != "" || strings.Contains(d.Ready.Waits, "cannot write") {
t.Fatalf("still says it cannot write: %q / %q", d.Ready.CannotWrite, d.Ready.Waits)
}
}
@@ -1,5 +1,3 @@
{
"refusal-stall-floor-minutes": 10,
"author-new-head-minutes": 120,
"record-retention": 90
}
+7 -49
View File
@@ -17,23 +17,11 @@
}
],
"consumes": [
"git.pull-updated",
"git.pull-merged",
"git.pull-closed",
"git.forge-pull",
"git.forge-status",
"git.repo-writable",
"mesh-delivery-forge-bridge.forge.**",
"mesh-delivery-forge-bridge.owed.ended",
"mesh-delivery-teller.answered",
"mesh-delivery-view.reached",
"mesh-config.config-changed",
"mesh-delivery-indexer.take-in.taken"
],
"emits": [
"transition",
"group",
"merged"
"transition"
],
"uses": [
"operator-channel"
@@ -130,6 +118,9 @@
},
"recovery": {
"type": "object"
},
"owed": {
"type": "object"
}
},
"operations": [
@@ -236,7 +227,8 @@
"head",
"outcome",
"words",
"at"
"at",
"owed"
],
"into": "acts",
"key": "id",
@@ -311,23 +303,11 @@
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg": "0.1"
},
"invokes": [
"seat:git.pull",
"seat:git.statuses",
"seat:git.open-pulls",
"seat:git.contains",
"seat:mesh-hand-acts.hand-act",
"seat:issue-tracker.list",
"seat:mesh-wants.entry",
"seat:node-engine.composition",
"mesh-delivery-teller.teller_answers",
"mesh-delivery-view.view_reach"
"seat:node-engine.composition"
],
"state": [
"pulls",
{
"name": "asks",
"history": 5
},
{
"name": "merges",
"history": 10
@@ -341,11 +321,7 @@
"ledger-calls",
"ledger-upkeep-calls"
],
"reads": [
"mesh-delivery-checker.verdicts"
],
"tools": [
"delivery_merge_again",
"delivery_status",
"gate_consumer",
"gate_record",
@@ -354,22 +330,6 @@
"gate_records"
],
"config": {
"refusal-stall-floor-minutes": {
"type": "number",
"description": "minutes, at least, before a merge refused because its trunk moved, whose check the gate asked again, is said as stalled; twice the repository's last check time when that is longer (ADR 0505 \u00a74).",
"minimum": 1,
"maximum": 1440,
"trusted": false,
"default": 10
},
"author-new-head-minutes": {
"type": "number",
"description": "minutes a delivery that needs its author's new head (a conflict, a failed check, a rejected group) waits for one before the operator hears of it (ADR 0505 \u00a75).",
"minimum": 1,
"maximum": 10080,
"trusted": false,
"default": 120
},
"record-retention": {
"type": "number",
"description": "days a delivery record is kept from when it opened (design 47 *The delivery record*): ninety covers every trace asked of a past delivery so far; when a record is removed at its retention, the gate says freed for each graph revision it pinned. Nothing else is removed to make room.",
@@ -393,8 +353,6 @@
"path": "${dir:state}/settings.json",
"mode": "0600",
"takes-config": [
"refusal-stall-floor-minutes",
"author-new-head-minutes",
"record-retention"
],
"merge": "json",