The gate says a group again when a member joins, leaves or pushes, or its why changes, so the forge never shows a stale member count (issue 774)
On 2026-10-30 mesh-catalog #571 joined fix/held-module-and-assigner-contract at 21:59; the group stayed checking, so mesh/delivery-group kept saying "3 of 3 member(s) ready" for a group of four, and #571 had no mesh/delivery-group status until the state changed. Quick fix: compare the why too. Thorough fix: compare the why and the member ids (each names its head), so a push that keeps the count is said as well. Chose thorough: same size, covers the push case.
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1071,8 +1071,10 @@ func (h *Holder) planGroup(g *Group, members []*Delivery, now time.Time) []func(
|
||||
if held {
|
||||
reviewSaid = why
|
||||
}
|
||||
if state != g.Said || status != g.SaidStatus || reviewSaid != g.SaidReview {
|
||||
g.Said, g.SaidStatus, g.SaidReview = state, status, reviewSaid
|
||||
saidMembers := strings.Join(sortedIDs(members), " ")
|
||||
if state != g.Said || status != g.SaidStatus || reviewSaid != g.SaidReview || why != g.SaidWhy ||
|
||||
saidMembers != g.SaidMembers {
|
||||
g.Said, g.SaidStatus, g.SaidReview, g.SaidWhy, g.SaidMembers = state, status, reviewSaid, why, saidMembers
|
||||
for _, d := range members {
|
||||
g.Owed = append(g.Owed, Effect{Kind: EffectStatus, Commit: d.Commit, Context: "mesh/delivery-group",
|
||||
State: status, Line: clip(fmt.Sprintf("group %s %s: %s", g.ID, state, why), 140),
|
||||
|
||||
@@ -366,8 +366,13 @@ type Group struct {
|
||||
SaidStatus string `json:"said_status,omitempty"`
|
||||
// SaidReview is the wait for a review last said of the group while it was held for one (ADR 0496 §2): a change of
|
||||
// the members still waiting is said too, though the state did not change.
|
||||
SaidReview string `json:"said_review,omitempty"`
|
||||
Owed []Effect `json:"owed,omitempty"`
|
||||
SaidReview string `json:"said_review,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.
|
||||
@@ -404,6 +409,16 @@ func commitsOf(ds []*Delivery) map[string]string {
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user