Hear a merge's announcement from whichever module claims the delivery seat (issue 1211)

This commit is contained in:
2026-10-19 11:59:21 +02:00
parent 3834c72747
commit 3f038b989e
11 changed files with 216 additions and 14 deletions
+12 -2
View File
@@ -448,8 +448,18 @@ var ControllerFollows = []string{
// the heads that Approve covered, kept a week to read the warrant a moved module's registration needs.
var ControllerHears = []string{DeliveryMergedSubject}
// DeliveryMergedSubject is where mesh-delivery says it merged a pull request.
var DeliveryMergedSubject = moduleEventSubject("mesh-delivery", "merged")
// DeliveryMergedEvent is the event a module says when it merged a pull request.
const DeliveryMergedEvent = "merged"
// DeliveryMergedSubject is where the delivery's owner says it merged a pull request: one wildcard subject, the
// module that claims the delivery seat being whichever the catalogue says (novox/hq ADR 0437 §3). Who said it is
// read from the subject, and whether it claims the seat is the controller's to check.
var DeliveryMergedSubject = moduleEventSubject("*", DeliveryMergedEvent)
// DeliveryMergedSubjectOf is where one module says it merged a pull request.
func DeliveryMergedSubjectOf(module string) string {
return moduleEventSubject(module, DeliveryMergedEvent)
}
// AsksSeat is the seat an ask is made on and its warrant heard from (novox/hq ADR 0259): the router's.
const AsksSeat = "operator-channel"
+102
View File
@@ -0,0 +1,102 @@
package link
import (
"bytes"
"context"
"log"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/broker"
)
// The controller hears a merge's announcement from whichever module claims the delivery seat, so the subject is a
// wildcard and the emitter is read from it (novox/hq ADR 0437 §3, issue 1211).
type keptMerges struct {
emitters []string
refuse bool
}
func (k *keptMerges) KeepDeliveryMerge(_ context.Context, emitter string, _ []byte) error {
if k.refuse {
return ErrNotTheDeliverysWord
}
k.emitters = append(k.emitters, emitter)
return nil
}
func mergeSays(t *testing.T, in *fakeInbound, to *settled, subject string) Control {
t.Helper()
m := in.sends(t, to, KindDeliveryMerged, map[string]any{"owner": "novox"}).(*fakeControl)
m.subject = subject
return m
}
// 1211/AC1: a merged announcement on the subject of the module claiming the seat is read as the delivery's word and
// kept, as today's module's is (3: unchanged).
func TestAMergedAnnouncementIsKeptWhicheverModuleSaidIt(t *testing.T) {
s, in := serving()
kept := &keptMerges{}
if err := s.KeepsDeliveryMerges(kept); err != nil {
t.Fatal(err)
}
for _, module := range []string{"mesh-delivery-gate", "mesh-delivery"} { // 1211/AC1, 1211/AC3
subject := "mesh.mod." + module + ".event.merged"
if kind, ok := kindOfSubject(subject); !ok || kind != KindDeliveryMerged {
t.Errorf("%s decodes to %q %v", subject, kind, ok)
}
if got, ok := DeliveryMergedEmitter(subject); !ok || got != module {
t.Errorf("%s: emitter %q %v", subject, got, ok)
}
to := &settled{}
s.act(t.Context(), mergeSays(t, in, to, subject))
if !to.acked {
t.Errorf("%s: not taken: %+v", module, to)
}
}
if len(kept.emitters) != 2 || kept.emitters[0] != "mesh-delivery-gate" || kept.emitters[1] != "mesh-delivery" {
t.Errorf("the keeper was told the emitters %v", kept.emitters)
}
for _, subject := range []string{"mesh.mod.a.b.event.merged", "mesh.mod.x.event.merged.more", "mesh.seat.x.event.merged", "mesh.mod..event.merged"} {
if _, ok := DeliveryMergedEmitter(subject); ok {
t.Errorf("%s read as a merge announcement", subject)
}
}
}
// 1211/AC2: an announcement from a module that claims no delivery seat is not the delivery's word: nothing is kept
// and the log names the module that said it.
func TestAMergedAnnouncementFromAModuleNotClaimingTheSeatIsNamedAndNotKept(t *testing.T) {
s, in := serving()
var said bytes.Buffer
s.log = log.New(&said, "", 0)
if err := s.KeepsDeliveryMerges(&keptMerges{refuse: true}); err != nil {
t.Fatal(err)
}
to := &settled{}
s.act(t.Context(), mergeSays(t, in, to, "mesh.mod.gitea.event.merged"))
if !to.rejected || to.acked || len(in.held) != 0 {
t.Errorf("settled %+v, held %d", to, len(in.held))
}
if !strings.Contains(said.String(), "gitea") {
t.Errorf("the log does not name the module that said it: %q", said.String())
}
}
// 1211/AC4: the controller's consumer filter hears merged from any module, on one wildcard subject.
func TestTheControllerHearsMergedFromAnyModuleOnOneSubject(t *testing.T) {
if broker.DeliveryMergedSubject != "mesh.mod.*.event.merged" {
t.Errorf("the subject is %q", broker.DeliveryMergedSubject)
}
found := false
for _, s := range broker.ControllerHears {
found = found || s == broker.DeliveryMergedSubject
}
if !found {
t.Errorf("the controller hears %v", broker.ControllerHears)
}
if got := broker.DeliveryMergedSubjectOf("mesh-delivery-gate"); got != "mesh.mod.mesh-delivery-gate.event.merged" {
t.Errorf("a module's own subject is %q", got)
}
}
+18 -2
View File
@@ -296,8 +296,6 @@ func kindOfSubject(subject string) (string, bool) {
return KindPullUpdated, true
case broker.DecidedSubject:
return KindDecided, true
case broker.DeliveryMergedSubject:
return KindDeliveryMerged, true
case BuildOutcome(), BuildOutcomeOf(TheBuildMachineBefore):
// A build's outcome is the role's event now, so it arrives on the events stream rather than
// the control branch — and is acted on by the same handler, because what the controller does
@@ -305,12 +303,30 @@ func kindOfSubject(subject string) (string, bool) {
// runs (ADR 0190): the old builder still answers on the retired seat until it is unassigned.
return KindBuilt, true
}
if _, ok := DeliveryMergedEmitter(subject); ok {
return KindDeliveryMerged, true
}
if _, ok := ProvisionerEmitter(subject); ok {
return KindProvisioner, true
}
return "", false
}
// DeliveryMergedEmitter is the module a merge's announcement came from, read from its subject
// (`mesh.mod.<module>.event.merged`); false for any other subject. The controller's own follow pattern, with `*`
// for the module, decodes too.
func DeliveryMergedEmitter(subject string) (string, bool) {
rest, ok := strings.CutPrefix(subject, "mesh.mod.")
if !ok {
return "", false
}
module, event, ok := strings.Cut(rest, ".event.")
if !ok || module == "" || strings.Contains(module, ".") || event != broker.DeliveryMergedEvent {
return "", false
}
return module, true
}
// ProvisionerEmitter is the module a provider's standing event came from, read from its subject
// (`mesh.mod.<module>.event.provisioner.<failing|recovered|retirement>`); false for any other subject. The
// controller's own follow pattern, with `*` for the module, decodes too.
+16 -2
View File
@@ -159,10 +159,16 @@ func (s *Server) Decides(d Decider) error {
// DeliveryMergeKeeper is what the controller does with mesh-delivery's word that it merged a pull request (novox/hq
// ADR 0420 §7): keep it, to be read when a moved module's build is taken in.
//
// The emitter is the module the announcement's subject names; a keeper that finds it does not claim the delivery seat
// answers ErrNotTheDeliverysWord (novox/hq ADR 0437 §3).
type DeliveryMergeKeeper interface {
KeepDeliveryMerge(ctx context.Context, body []byte) error
KeepDeliveryMerge(ctx context.Context, emitter string, body []byte) error
}
// ErrNotTheDeliverysWord is a keeper's answer to a merge's announcement from a module that claims no delivery seat.
var ErrNotTheDeliverysWord = errors.New("the module claims no delivery seat")
// KeepsDeliveryMerges says what to do about mesh-delivery's merges, and asks for them to be delivered.
func (s *Server) KeepsDeliveryMerges(k DeliveryMergeKeeper) error {
if err := s.inbound.Also(KindDeliveryMerged); err != nil {
@@ -730,7 +736,15 @@ func (s *Server) deliveryMerged(ctx context.Context, m Control) {
_ = m.Took()
return
}
err := s.deliveryMerges.KeepDeliveryMerge(ctx, m.Body())
emitter, _ := DeliveryMergedEmitter(m.Subject())
err := s.deliveryMerges.KeepDeliveryMerge(ctx, emitter, m.Body())
if errors.Is(err, ErrNotTheDeliverysWord) {
// A merged event of a module that claims no delivery seat: not the delivery's word, so nothing is kept (and
// handing it back would not change that).
s.log.Printf("a word on a merge from %q is not the delivery's, and nothing is kept: %v", emitter, err)
_ = m.Drop()
return
}
switch s.decide(ctx, m, "mesh-delivery's word on a merge", "", "", err) {
case Hold:
return