The gate's groups answers the seat's groups: members in merge order, the acts said with their ids and the acts recorded with their outcomes, one group alone, and none for a group it does not know (milestone 47/G, W11.10; F-forge-7, F-forge-8)
The forge bridge reads a group's members and what it owes from here, never from the gate's state. The text form of the members and composed_gate are gone with the answer of before. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ (Joined from de368344.) Builds: design 47 G §157, §304 (GATE5) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -82,20 +83,48 @@ func TestAFeaturesSinglePullRequestIsALoneDeliveryAskedAlone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A group answer says its members' gate is the composed check's (agreed with the controller, issue 977).
|
||||
func TestTheGroupsAnswerSaysItsGateIsTheComposedChecks(t *testing.T) {
|
||||
w := approvedGroup(t)
|
||||
lines := w.h.Groups(false)
|
||||
if len(lines) != 1 || !lines[0].ComposedGate {
|
||||
t.Fatalf("groups: %+v", lines)
|
||||
// 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)
|
||||
}
|
||||
raw, _ := json.Marshal(lines[0])
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(raw, &got); err != nil || got["composed_gate"] != true {
|
||||
t.Fatalf("the group line says %s", raw)
|
||||
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)
|
||||
}
|
||||
if _, ordered := got["order"]; ordered {
|
||||
t.Fatalf("a group line still says an order: %s", raw)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -336,8 +337,8 @@ func TestStalledAndCloseWorkFromTheTable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq issue 867, ADR 0382 §1 (contract A): each member of a group in `groups` also says its pull
|
||||
// request number, full head, base branch and whether it is open; `members` keeps its text form.
|
||||
// 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"
|
||||
@@ -346,28 +347,25 @@ func TestGroupsNamesEachMembersPullRequestHeadBaseAndOpen(t *testing.T) {
|
||||
pb.Base = "release/1"
|
||||
w.h.PullUpdated(pr("novox/a", 1, headA, "feat/z"))
|
||||
w.h.PullUpdated(pb)
|
||||
lines := w.h.Groups(false)
|
||||
if len(lines) != 1 || len(lines[0].Details) != 2 || len(lines[0].Members) != 2 {
|
||||
lines := w.h.Groups(false, "").Groups
|
||||
if len(lines) != 1 || len(lines[0].Members) != 2 {
|
||||
t.Fatalf("groups: %+v", lines)
|
||||
}
|
||||
got := map[string]MemberLine{}
|
||||
for _, m := range lines[0].Details {
|
||||
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 == "" {
|
||||
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)
|
||||
}
|
||||
if !strings.Contains(lines[0].Members[0], "(") {
|
||||
t.Fatalf("members lost its text form: %v", lines[0].Members)
|
||||
}
|
||||
// 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) {
|
||||
for _, m := range l.Details {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -347,13 +347,14 @@ func tools(h *Holder, l *listening) []stdio.Tool {
|
||||
"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, its composed check and its state, derived from its members. " +
|
||||
"A group has no order: it is merged in one pass, and moves source.main once (novox/hq ADR 0418, ADR 0539 clause 17); what " +
|
||||
"reached the machines is the delivery view's. On mesh/check its check is the checker's one verdict at every member's head " +
|
||||
"(ADR 0447); composed_gate says its members' gate is the controller's composed check's (ADR 0403), for a base still on it.",
|
||||
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.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{
|
||||
"all": map[string]any{"type": "string", "enum": []string{"true", "false"}}}},
|
||||
Run: func(a map[string]any) (any, error) { return h.Groups(boolArg(a, "all")), nil }},
|
||||
"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")}},
|
||||
Run: func(a map[string]any) (any, error) { return h.Groups(boolArg(a, "all"), strArg(a, "group")), nil }},
|
||||
{Name: seat + "source",
|
||||
Description: "The source revision the gate holds: every repository it names, its trunk branch and that trunk's head, the " +
|
||||
"group that moved it last and its revision. Read, never written: a node-engine reads the repositories here to call a " +
|
||||
|
||||
@@ -90,6 +90,25 @@ 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)
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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
|
||||
@@ -100,64 +102,80 @@ func (h *Holder) Show(id string) (any, error) {
|
||||
return nil, fmt.Errorf("no delivery or group %q: `deliveries` lists them", id)
|
||||
}
|
||||
|
||||
// GroupLine is one group as `groups` lists it.
|
||||
type GroupLine struct {
|
||||
ID string `json:"id"`
|
||||
State string `json:"state"`
|
||||
Why string `json:"why"`
|
||||
// ComposedGate says the group's gate is the controller's composed check's: each member's own pull request check runs
|
||||
// the repository's check alone, and the composed verdict sets its mesh/merge-gate (novox/hq ADR 0403 §1, issue 978).
|
||||
// On mesh/check the group's check is the checker's one verdict at every member's head (ADR 0447 §2, issue 1302).
|
||||
ComposedGate bool `json:"composed_gate"`
|
||||
Check string `json:"composed,omitempty"`
|
||||
Members []string `json:"members"`
|
||||
// Details are the members again, one object each, with what a reader needs to find the pull request's
|
||||
// siblings: its number, full head, base branch and whether it is open (novox/hq ADR 0382 §1). Additive:
|
||||
// `members` keeps its text form for older readers.
|
||||
Details []MemberLine `json:"member_details,omitempty"`
|
||||
}
|
||||
|
||||
// MemberLine is one member of a group, as `groups` lists it beside its text line.
|
||||
type MemberLine struct {
|
||||
ID string `json:"id"`
|
||||
Repository string `json:"repository"`
|
||||
State State `json:"state"`
|
||||
Number int `json:"number"`
|
||||
Head string `json:"head"`
|
||||
Base string `json:"base"`
|
||||
Open bool `json:"open"`
|
||||
}
|
||||
|
||||
// Groups is the `groups` verb.
|
||||
func (h *Holder) Groups(all bool) []GroupLine {
|
||||
// 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()
|
||||
var out []GroupLine
|
||||
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 !all && (state == GroupMerged || state == GroupClosed) {
|
||||
if only == "" && !all && (state == GroupMerged || state == GroupClosed) {
|
||||
continue
|
||||
}
|
||||
l := GroupLine{ID: g.ID, State: state, Why: why, ComposedGate: true}
|
||||
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, fmt.Sprintf("%s (%s)", d.ID, d.State))
|
||||
l.Details = append(l.Details, MemberLine{ID: d.ID, Repository: d.Repository, State: d.State,
|
||||
Number: d.Number, Head: d.Commit, Base: d.Base,
|
||||
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 = append(out, l)
|
||||
out.Groups = append(out.Groups, l)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
|
||||
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
|
||||
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}})
|
||||
}
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Owed.ID < out[j].Owed.ID })
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user