143 lines
7.4 KiB
Go
143 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
|
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
|
|
|
|
groupkeys "mesh-delivery-forge-bridge/internal/groups"
|
|
)
|
|
|
|
// idPub records the subjects, bodies and message ids it is handed.
|
|
type idPub struct {
|
|
subjects, ids []string
|
|
data [][]byte
|
|
}
|
|
|
|
func (p *idPub) send(_ context.Context, subject string, data []byte, id string) error {
|
|
p.subjects, p.data, p.ids = append(p.subjects, subject), append(p.data, data), append(p.ids, id)
|
|
return nil
|
|
}
|
|
|
|
func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T17, T19 (ADR 0633 §4)
|
|
ctx := context.Background()
|
|
h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40)
|
|
c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40)
|
|
merge := func(states ...seat.MergesByGroupValuePullsItem) []byte {
|
|
v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/take-in", Ask: "ask", Pulls: states,
|
|
Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return v
|
|
}
|
|
sdk := func(state meshdeliveryclient.MergeState, commit, words string) seat.MergesByGroupValuePullsItem {
|
|
return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-sdk", Number: 37, Head: h1, State: string(state), Commit: commit, Words: words}
|
|
}
|
|
dlv := func(state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem {
|
|
return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(state), Commit: commit}
|
|
}
|
|
gate := map[string][]byte{}
|
|
var logs bytes.Buffer
|
|
pub, done := &idPub{}, memRevisions{}
|
|
g := &grouper{send: pub.send, done: done, logger: log.New(&logs, "", 0),
|
|
merges: func(_ context.Context, key string) ([]byte, uint64, bool, error) {
|
|
v, ok := gate[key]
|
|
return v, 7, ok, nil
|
|
}}
|
|
mergedOf := func(repo string, number int, head, commit string) []byte {
|
|
data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/take-in", Number: number, Head: head, Merge: commit, Root: "r"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
// A lone pull request's merged message carries no group.
|
|
if _, f, ok := g.member(ctx, mergedOf("novox/hq", 4, h1, c1)); ok || f.Group != "" {
|
|
t.Errorf("a lone pull request names the group %q", f.Group)
|
|
}
|
|
// The first member: its message names the group and both members; the group is not ended.
|
|
gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), dlv(meshdeliveryclient.MergeMerging, ""))
|
|
data, f, ok := g.member(ctx, mergedOf("novox/mesh-sdk", 37, h1, c1))
|
|
if back, err := forgemsg.DecodeForge(data); !ok || err != nil || back.Group != "feat/take-in" || len(back.Members) != 2 {
|
|
t.Fatalf("a member's message: %+v %v", back, err)
|
|
}
|
|
g.published(ctx, f)
|
|
// A refusal of the other member is no end (ADR 0441 §3: the group merges on from it later).
|
|
gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(meshdeliveryclient.MergeRefused), Words: "405"})
|
|
g.ended(ctx, "feat/take-in", 8, gate["feat/take-in"])
|
|
// Merged on at the gate before its merged message is written: still no end; then its message ends the group.
|
|
gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), dlv(meshdeliveryclient.MergeMerged, c2))
|
|
g.ended(ctx, "feat/take-in", 9, gate["feat/take-in"])
|
|
if len(pub.subjects) != 0 {
|
|
t.Fatalf("an end before every member's merged message is written: %v", pub.subjects)
|
|
}
|
|
_, f, _ = g.member(ctx, mergedOf("novox/mesh-delivery", 9, h2, c2))
|
|
g.published(ctx, f)
|
|
g.ended(ctx, "feat/take-in", 10, gate["feat/take-in"]) // a later write of the gate says no second end
|
|
if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.group.merged" || pub.ids[0] != "group-end.feat/take-in."+c2 {
|
|
t.Fatalf("ends %v ids %v, want one forge.group.merged with id group-end.feat/take-in.%s", pub.subjects, pub.ids, c2)
|
|
}
|
|
end, err := forgemsg.DecodeForgeGroupEnd(pub.data[0])
|
|
if err != nil || len(end.Members) != 2 || end.Members[0].Merge != c1 || end.Members[1].Merge != c2 {
|
|
t.Errorf("the end = %+v %v, want both members in their merge order", end, err)
|
|
}
|
|
if rev, ok := done[groupkeys.Key("feat/take-in")]; !ok || rev != 7 {
|
|
t.Errorf("performed %v: want groups.feat/take-in kept", done)
|
|
}
|
|
// Again by hand: written once more, its id not the first's.
|
|
if !g.end(ctx, "feat/take-in", 10, gate["feat/take-in"], "20261104T2300") || pub.ids[1] == pub.ids[0] {
|
|
t.Errorf("the end again by hand: ids %v", pub.ids)
|
|
}
|
|
// 1585/T19: an ask to say a group's end again is answered by the bridge from the gate's merges, with the ask's time on
|
|
// the message id; not written while a member is not merged, with the reason; not for a group the gate does not hold.
|
|
pull := func(repo string, n int, head string, state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem {
|
|
return seat.MergesByGroupValuePullsItem{Repository: repo, Number: int64(n), Head: head, State: string(state), Commit: commit}
|
|
}
|
|
askMerge := func(second meshdeliveryclient.MergeState, commit2 string) []byte {
|
|
v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/take-in", Ask: "ask", Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2},
|
|
Pulls: []seat.MergesByGroupValuePullsItem{pull("novox/mesh-sdk", 37, h1, meshdeliveryclient.MergeMerged, c1), pull("novox/mesh-delivery", 9, h2, second, commit2)}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return v
|
|
}
|
|
ask := groupkeys.EndAsk{V: 1, Group: "feat/take-in", By: "operator@shanks", Why: "the indexer never took it in", At: time.Date(2026, 11, 4, 23, 0, 0, 123, time.UTC)}
|
|
for _, tc := range []struct {
|
|
name string
|
|
gate map[string][]byte
|
|
published bool
|
|
written bool
|
|
answer string
|
|
}{
|
|
{"merged and published: written again", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerged, c2)}, true, true, "written again"},
|
|
{"a member's message not written", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerged, c2)}, false, false, "not written"},
|
|
{"a member not merged", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerging, "")}, true, false, "not written"},
|
|
{"no merge at the gate", map[string][]byte{}, true, false, "holds no merge"},
|
|
} {
|
|
pub, done := &idPub{}, memRevisions{}
|
|
if tc.published {
|
|
done[groupkeys.PublishedKey("novox/mesh-sdk", 37, c1)], done[groupkeys.PublishedKey("novox/mesh-delivery", 9, c2)] = 1, 1
|
|
}
|
|
g := &grouper{send: pub.send, done: done, logger: log.New(&bytes.Buffer{}, "", 0),
|
|
merges: func(_ context.Context, key string) ([]byte, uint64, bool, error) {
|
|
v, ok := tc.gate[key]
|
|
return v, 4, ok, nil
|
|
}}
|
|
got := g.answerEnd(ctx, "feat/take-in", ask, ask.At.Add(time.Second))
|
|
if got.Written != tc.written || !strings.Contains(got.Answer, tc.answer) || !got.Done() || len(pub.ids) != map[bool]int{true: 1}[tc.written] {
|
|
t.Errorf("%s: %+v, ids %v", tc.name, got, pub.ids)
|
|
}
|
|
if tc.written && pub.ids[0] != groupkeys.EndID("feat/take-in", c2, groupkeys.AgainStamp(ask.At)) {
|
|
t.Errorf("%s: the id %s does not carry the ask's time", tc.name, pub.ids[0])
|
|
}
|
|
}
|
|
}
|