diff --git a/internal/catalogue/delivery_seat.go b/internal/catalogue/delivery_seat.go index c728653..4c62dec 100644 --- a/internal/catalogue/delivery_seat.go +++ b/internal/catalogue/delivery_seat.go @@ -9,7 +9,7 @@ package catalogue // DeliverySeat is the seat mesh-delivery holds. const DeliverySeat = "mesh-delivery" -// deliveryVerbs are the delivery seat's tools: five that read, and the acts of a person and of healer H2. +// deliveryVerbs are the delivery seat's tools: six that read, and the acts of a person and of healer H2. func deliveryVerbs() []Verb { return []Verb{ {Name: "deliveries", Description: "Every delivery not final, and those that ended in the last day, one line " + @@ -32,6 +32,16 @@ func deliveryVerbs() []Verb { Input: schema(map[string]string{"repository": "owner/repository", "paths": "the files it changes, comma-separated, from the repository's root", "base": "the branch it merges into (default main)"}, []string{"repository", "paths"})}, + {Name: "checks", Description: "What the mesh's checks said of a pull request's head or of one commit: each " + + "of the commit's mesh statuses (mesh/merge-gate, mesh/repo-check, mesh/delivery, …) with its state, " + + "description and when it was set; the merge check's full verdict as the controller said it — each " + + "layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's " + + "protection would let it merge, every required status being success.", + Input: schema(map[string]string{"repository": "owner/repository", + "number": "a pull request's number: its head is read", + "commit": "a commit's sha, or the start of one, instead of a pull request"}, []string{"repository"}), + // Added after the seat's first holder shipped: optional until mesh-delivery serves it everywhere. + Optional: true}, {Name: "table", Description: "The state table every delivery runs by: each transition with its guard, " + "each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", Input: schema(map[string]string{}, nil)}, diff --git a/internal/catalogue/delivery_seat_test.go b/internal/catalogue/delivery_seat_test.go new file mode 100644 index 0000000..046b93d --- /dev/null +++ b/internal/catalogue/delivery_seat_test.go @@ -0,0 +1,60 @@ +package catalogue + +import ( + "reflect" + "strings" + "testing" +) + +// The delivery seat answers "what did the mesh's checks say of this pull request?" as a verb of its own, +// `checks` (novox/hq ADR 0239): read by repository and a pull request's number or a commit. +func TestTheDeliverySeatPromisesChecks(t *testing.T) { + seat, ok := SeatNamed(DeliverySeat) + if !ok { + t.Fatal("the delivery seat is not in the set") + } + var checks *Verb + for i := range seat.Serves { + if seat.Serves[i].Name == "checks" { + checks = &seat.Serves[i] + } + } + if checks == nil { + t.Fatalf("the delivery seat promises %v and not checks", VerbNames(seat.Serves)) + } + props, _ := checks.Input["properties"].(map[string]any) + for _, arg := range []string{"repository", "number", "commit"} { + if _, has := props[arg]; !has { + t.Errorf("checks takes no %q", arg) + } + } + if req, _ := checks.Input["required"].([]string); !reflect.DeepEqual(req, []string{"repository"}) { + t.Errorf("checks requires %v, wanted only the repository", req) + } + + // Added after the seat's holder shipped, it is optional: the holder serving the ten verbs before it still + // holds the seat, and one serving all eleven does too — so the controller and the catalogue can move in + // either order, and no check of the catalogue fails on a module nobody touched between the two. + if !checks.Optional { + t.Fatal("checks is a condition of holding before its holder serves it") + } + var before []string + for _, v := range VerbNames(seat.Serves) { + if v != "checks" { + before = append(before, v) + } + } + m := Manifest{Module: "mesh-delivery", Claims: []Claim{{Name: DeliverySeat, Scope: ScopeMesh, Serves: before}}} + if err := CanHold(m, seat); err != nil { + t.Fatalf("a holder without checks yet: %v", err) + } + m.Claims[0].Serves = VerbNames(seat.Serves) + if err := CanHold(m, seat); err != nil { + t.Fatalf("a holder serving every verb: %v", err) + } + // Every other verb is still a condition of holding. + m.Claims[0].Serves = append([]string{"checks"}, before[1:]...) + if err := CanHold(m, seat); err == nil || !strings.Contains(err.Error(), before[0]) { + t.Fatalf("a holder without %s: %v", before[0], err) + } +} diff --git a/internal/catalogue/verbs.go b/internal/catalogue/verbs.go index 33e9df6..af3318f 100644 --- a/internal/catalogue/verbs.go +++ b/internal/catalogue/verbs.go @@ -22,6 +22,14 @@ type Verb struct { Description string `json:"description,omitempty"` Input map[string]any `json:"input,omitempty"` Output map[string]any `json:"output,omitempty"` + // Optional marks a verb added to a mesh seat whose holder lives in another repository (design 33 §7, + // additive within a version): a holder that serves it is accepted, and one that does not yet still + // holds the seat. Without it the addition would be a deadlock — this controller refusing the holder that + // does not serve the verb, the controller before it refusing the holder that does — and every check of + // the catalogue between the two would fail on a module nobody touched. Once every holder serves it, + // the mark is removed and the verb is a condition of holding like the rest. Never stored or said: the + // seat set's protocol is the compiled one. + Optional bool `json:"-"` } func (v *Verb) UnmarshalJSON(raw []byte) error { @@ -442,7 +450,7 @@ func unservedVerbs(tools []string, promised []Verb) []string { } var missing []string for _, v := range promised { - if !has[v.Name] { + if !has[v.Name] && !v.Optional { missing = append(missing, v.Name) } }