From 7cc4a33cc57dcd8bfa06724c1c86203f8e65dce3 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 22:10:42 +0100 Subject: [PATCH] =?UTF-8?q?GATE=20A-24,=20=C2=A7172,=20=C2=A7431,=20=C2=A7?= =?UTF-8?q?592,=20=C2=A7593,=20=C2=A7613:=20the=20gate=20keeps=20no=20merg?= =?UTF-8?q?e=20states;=20deliveries,=20show,=20groups=20and=20stalled=20an?= =?UTF-8?q?swer=20the=20delivery=20records?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../cmd/mesh-delivery-gate/adr350_test.go | 32 - .../cmd/mesh-delivery-gate/adr505_test.go | 221 ---- .../cmd/mesh-delivery-gate/adr661_test.go | 155 --- .../cmd/mesh-delivery-gate/approval.go | 835 ------------ .../cmd/mesh-delivery-gate/approval_test.go | 523 -------- .../cmd/mesh-delivery-gate/bridge_test.go | 311 ----- .../cmd/mesh-delivery-gate/bus_test.go | 416 +----- .../mesh-delivery-gate/checked_shim_test.go | 142 -- .../cmd/mesh-delivery-gate/checkerbus_test.go | 60 - .../cmd/mesh-delivery-gate/checks.go | 283 ---- .../cmd/mesh-delivery-gate/checks_test.go | 226 ---- .../cmd/mesh-delivery-gate/checktime.go | 34 - .../cmd/mesh-delivery-gate/conditions.go | 59 +- .../cmd/mesh-delivery-gate/configheld.go | 100 -- .../cmd/mesh-delivery-gate/effects.go | 394 ------ .../cmd/mesh-delivery-gate/effects_test.go | 62 - .../cmd/mesh-delivery-gate/events.go | 110 -- .../cmd/mesh-delivery-gate/events_test.go | 175 --- .../cmd/mesh-delivery-gate/fakes_test.go | 445 ------- .../cmd/mesh-delivery-gate/gate.go | 88 ++ .../cmd/mesh-delivery-gate/gitseat.go | 153 --- .../cmd/mesh-delivery-gate/gitseat_test.go | 117 -- .../cmd/mesh-delivery-gate/group_merge.go | 396 ------ .../cmd/mesh-delivery-gate/group_test.go | 157 --- .../cmd/mesh-delivery-gate/groupsaid_test.go | 35 - .../cmd/mesh-delivery-gate/helpers_test.go | 49 - .../cmd/mesh-delivery-gate/holder.go | 1168 ----------------- .../cmd/mesh-delivery-gate/holder_test.go | 399 ------ .../cmd/mesh-delivery-gate/issue1083_test.go | 52 - .../cmd/mesh-delivery-gate/issue1107_test.go | 63 - .../cmd/mesh-delivery-gate/issue1114_test.go | 82 -- .../cmd/mesh-delivery-gate/issue1275_test.go | 742 ----------- .../issue1275_whole_test.go | 332 ----- .../cmd/mesh-delivery-gate/issue1348_test.go | 246 ---- .../cmd/mesh-delivery-gate/issue1349_test.go | 116 -- .../cmd/mesh-delivery-gate/issue1726_test.go | 123 -- .../issue1726_trunk_test.go | 156 --- .../cmd/mesh-delivery-gate/issue326_test.go | 70 - .../cmd/mesh-delivery-gate/issue341_test.go | 226 ---- .../cmd/mesh-delivery-gate/issue403_test.go | 69 - .../cmd/mesh-delivery-gate/issue407_test.go | 105 -- .../cmd/mesh-delivery-gate/issue438_test.go | 183 --- .../cmd/mesh-delivery-gate/issue441_test.go | 234 ---- .../cmd/mesh-delivery-gate/issue443_test.go | 66 - .../cmd/mesh-delivery-gate/issue451_test.go | 48 - .../cmd/mesh-delivery-gate/issue783_test.go | 130 -- .../cmd/mesh-delivery-gate/issue888_test.go | 165 --- .../cmd/mesh-delivery-gate/issue902_test.go | 65 - .../cmd/mesh-delivery-gate/issuelinks.go | 106 -- .../cmd/mesh-delivery-gate/keeps.go | 2 +- .../cmd/mesh-delivery-gate/main.go | 196 +-- .../cmd/mesh-delivery-gate/manifest_test.go | 63 +- .../cmd/mesh-delivery-gate/merges.go | 704 +--------- .../cmd/mesh-delivery-gate/model.go | 362 +---- .../cmd/mesh-delivery-gate/onestatus.go | 403 ------ .../mesh-delivery-gate/onestatus_bus_test.go | 79 -- .../cmd/mesh-delivery-gate/onestatus_test.go | 186 --- .../cmd/mesh-delivery-gate/operator_test.go | 121 -- .../cmd/mesh-delivery-gate/owed.go | 125 +- .../cmd/mesh-delivery-gate/owed_test.go | 129 -- .../cmd/mesh-delivery-gate/owedended.go | 154 +-- .../cmd/mesh-delivery-gate/ownerreads.go | 107 -- .../cmd/mesh-delivery-gate/ownerreads_test.go | 79 -- .../cmd/mesh-delivery-gate/ports.go | 287 ---- .../cmd/mesh-delivery-gate/pullkey.go | 157 --- .../cmd/mesh-delivery-gate/pullkey_test.go | 165 --- .../cmd/mesh-delivery-gate/reached.go | 100 -- .../cmd/mesh-delivery-gate/reached_test.go | 74 -- .../cmd/mesh-delivery-gate/react.go | 16 +- .../cmd/mesh-delivery-gate/ready.go | 677 ---------- .../cmd/mesh-delivery-gate/ready_test.go | 315 ----- .../cmd/mesh-delivery-gate/record_test.go | 163 --- .../cmd/mesh-delivery-gate/recordevents.go | 39 - .../cmd/mesh-delivery-gate/recordfill.go | 55 - .../cmd/mesh-delivery-gate/recordtools.go | 9 + .../cmd/mesh-delivery-gate/settings.go | 59 +- .../cmd/mesh-delivery-gate/switch.go | 193 ++- .../cmd/mesh-delivery-gate/table.go | 383 ------ .../cmd/mesh-delivery-gate/table_test.go | 237 ---- .../cmd/mesh-delivery-gate/timers_test.go | 90 -- .../cmd/mesh-delivery-gate/unchecked.go | 381 ------ .../unchecked_events_test.go | 97 -- .../cmd/mesh-delivery-gate/unchecked_test.go | 80 -- .../cmd/mesh-delivery-gate/verbs.go | 493 +++---- .../cmd/mesh-delivery-gate/waiting_checks.go | 67 - .../cmd/mesh-delivery-gate/writes.go | 114 -- .../cmd/mesh-delivery-gate/writes_test.go | 46 - .../mesh-delivery-gate/files/settings.json | 2 - modules/mesh-delivery-gate/module.json | 56 +- 89 files changed, 619 insertions(+), 16970 deletions(-) delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr350_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr505_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr661_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bridge_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/checked_shim_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/checkerbus_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/checks.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/checks_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/checktime.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/configheld.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/effects.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/effects_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/events.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/events_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/fakes_test.go create mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/gate.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/gitseat.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/gitseat_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/group_merge.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/group_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/groupsaid_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/helpers_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/holder.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/holder_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1083_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1107_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1114_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1275_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1275_whole_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1348_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1349_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1726_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue1726_trunk_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue326_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue341_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue403_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue407_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue438_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue441_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue443_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue451_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue783_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue888_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue902_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issuelinks.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/onestatus.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/onestatus_bus_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/onestatus_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/operator_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/owed_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ownerreads.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ownerreads_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/pullkey.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/pullkey_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/reached.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/reached_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ready.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/ready_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/table_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/timers_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/unchecked.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/unchecked_events_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/unchecked_test.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/waiting_checks.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/writes.go delete mode 100644 modules/mesh-delivery-gate/cmd/mesh-delivery-gate/writes_test.go diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr350_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr350_test.go deleted file mode 100644 index d00273c48..000000000 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr350_test.go +++ /dev/null @@ -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) - } -} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr505_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr505_test.go deleted file mode 100644 index b3c0355a0..000000000 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr505_test.go +++ /dev/null @@ -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) - } -} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr661_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr661_test.go deleted file mode 100644 index 19e7d353e..000000000 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr661_test.go +++ /dev/null @@ -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) - } - } -} diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval.go deleted file mode 100644 index 9b4c5718f..000000000 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval.go +++ /dev/null @@ -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: "=" 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 (`@