mesh/delivery-group group fix/1279-consumers-from-now rejected: a member's own check failed
mesh/merge-gate waits for its group's composed check
mesh/repo-check head 677f94e9012b merged into trunk 1325c8b452c8 beside mesh-controller#400@a0c971ce — fail: its merge-check.sh failed: --- FAIL: TestBridg…
mesh/delivery superseded: a newer head of the same pull request
381 lines
16 KiB
Go
381 lines
16 KiB
Go
package bridge
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
const head = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
// updated is the body of gitea.pull.updated as mesh-catalog modules/gitea/trunk.ts:233-250 (announcementOf) writes
|
|
// it: owner, repo, number, base_sha, title, body, base, head (the branch), head_sha, clone_url, html_url, paths,
|
|
// paths_truncated, removed, module_dirs, module_dirs_said, merge_check, merge_check_said.
|
|
func updated(extra map[string]any) []byte {
|
|
m := map[string]any{
|
|
"owner": "novox", "repo": "mesh-delivery", "number": 12,
|
|
"base_sha": "ffffffffffffffffffffffffffffffffffffffff", "title": "A change", "body": "text", "base": "main",
|
|
"head": "feat/x", "head_sha": head, "clone_url": "https://forge.example/novox/mesh-delivery.git",
|
|
"html_url": "https://forge.example/novox/mesh-delivery/pulls/12",
|
|
"paths": []string{"modules/a/x.go"}, "paths_truncated": false, "removed": []string{},
|
|
"module_dirs": []string{"modules/a"}, "module_dirs_said": true,
|
|
"merge_check": true, "merge_check_said": true,
|
|
}
|
|
for k, v := range extra {
|
|
if v == nil {
|
|
delete(m, k)
|
|
} else {
|
|
m[k] = v
|
|
}
|
|
}
|
|
b, _ := json.Marshal(m)
|
|
return b
|
|
}
|
|
|
|
// closed is the body of gitea.pull.closed as mesh-catalog modules/gitea/index.ts:127-128 writes it: owner, repo,
|
|
// number, title, head, head_sha, base, html_url.
|
|
func closed() []byte {
|
|
b, _ := json.Marshal(map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "title": "A change",
|
|
"head": "feat/x", "head_sha": head, "base": "main", "html_url": "https://forge.example/novox/mesh-delivery/pulls/12"})
|
|
return b
|
|
}
|
|
|
|
const updatedSubject = "mesh.mod.gitea.event.pull.updated"
|
|
const closedSubject = "mesh.mod.gitea.event.pull.closed"
|
|
|
|
func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion; the bus is the integration test)
|
|
msg, err := Convert(updatedSubject, updated(nil), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.Event != "forge.novox.mesh-delivery.pull" {
|
|
t.Errorf("event = %q", msg.Event)
|
|
}
|
|
f, err := delivery.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true}
|
|
if !delivery.SameForge(f, want) {
|
|
t.Errorf("message = %+v, want %+v", f, want)
|
|
}
|
|
}
|
|
|
|
func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3
|
|
msg, err := Convert(updatedSubject, updated(map[string]any{"module_dirs": nil, "module_dirs_said": nil}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, _ := delivery.DecodeForge(msg.Data)
|
|
if f.DirsKnown || len(f.Dirs) != 0 {
|
|
t.Errorf("dirs = %v known = %v, want not known", f.Dirs, f.DirsKnown)
|
|
}
|
|
// Said and empty is known and empty, not unknown.
|
|
msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs": []string{}, "module_dirs_said": true}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, _ = delivery.DecodeForge(msg.Data)
|
|
if !f.DirsKnown || len(f.Dirs) != 0 {
|
|
t.Errorf("dirs = %v known = %v, want known and empty", f.Dirs, f.DirsKnown)
|
|
}
|
|
// Dirs listed without the forge saying it could read them are not trusted.
|
|
msg, err = Convert(updatedSubject, updated(map[string]any{"module_dirs_said": nil}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f, _ = delivery.DecodeForge(msg.Data); f.DirsKnown || len(f.Dirs) != 0 {
|
|
t.Errorf("dirs %v known %v without module_dirs_said", f.Dirs, f.DirsKnown)
|
|
}
|
|
}
|
|
|
|
func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4
|
|
msg, err := Convert(closedSubject, closed(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.Event != "forge.novox.mesh-delivery.pull" {
|
|
t.Errorf("event = %q", msg.Event)
|
|
}
|
|
f, err := delivery.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Action: delivery.PullClosed}
|
|
if !delivery.SameForge(f, want) {
|
|
t.Errorf("message = %+v, want %+v", f, want)
|
|
}
|
|
}
|
|
|
|
func TestUnreadableEventsWriteNothing(t *testing.T) { // 1235/AC5
|
|
for name, c := range map[string]struct {
|
|
subject string
|
|
data []byte
|
|
why string
|
|
}{
|
|
"no owner": {updatedSubject, updated(map[string]any{"owner": nil}), "owner"},
|
|
"no repo": {updatedSubject, updated(map[string]any{"repo": nil}), "repo"},
|
|
"no number": {updatedSubject, updated(map[string]any{"number": nil}), "number"},
|
|
"number zero": {updatedSubject, updated(map[string]any{"number": 0}), "number"},
|
|
"short head": {updatedSubject, updated(map[string]any{"head_sha": "abc123"}), "head"},
|
|
"head not hex": {updatedSubject, updated(map[string]any{"head_sha": strings.Repeat("g", 40)}), "head"},
|
|
"upper-case head": {updatedSubject, updated(map[string]any{"head_sha": strings.ToUpper(head)}), "head"},
|
|
"no branch": {updatedSubject, updated(map[string]any{"head": nil}), "branch"},
|
|
"not json": {updatedSubject, []byte("{"), "json"},
|
|
"closed no head": {closedSubject, mustDrop(closed(), "head_sha"), "head"},
|
|
"closed no repo": {closedSubject, mustDrop(closed(), "repo"), "repo"},
|
|
} {
|
|
msg, err := Convert(c.subject, c.data, nil)
|
|
if err == nil {
|
|
t.Errorf("%s: a message %q was made", name, msg.Event)
|
|
continue
|
|
}
|
|
if !errors.Is(err, ErrUnreadable) {
|
|
t.Errorf("%s: error %v is not ErrUnreadable", name, err)
|
|
}
|
|
if !strings.Contains(err.Error(), c.why) || !strings.Contains(err.Error(), c.subject) {
|
|
t.Errorf("%s: error %q names neither its subject nor %q", name, err, c.why)
|
|
}
|
|
if msg.Data != nil {
|
|
t.Errorf("%s: data was made", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustDrop(body []byte, key string) []byte {
|
|
var m map[string]any
|
|
_ = json.Unmarshal(body, &m)
|
|
delete(m, key)
|
|
b, _ := json.Marshal(m)
|
|
return b
|
|
}
|
|
|
|
func TestOtherSubjectsAreNotHandled(t *testing.T) { // 1235
|
|
for _, s := range []string{"mesh.mod.gitea.event.something.else", "mesh.mod.other.event.pull.updated"} {
|
|
if _, err := Convert(s, updated(nil), nil); !errors.Is(err, ErrNotHandled) {
|
|
t.Errorf("%s: err = %v, want ErrNotHandled", s, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
const merge = "89abcdef0123456789abcdef0123456789abcdef"
|
|
|
|
// mergedBody is gitea.pull.merged as mesh-catalog modules/gitea/index.ts:186-207 writes it.
|
|
func mergedBody(extra map[string]any) []byte {
|
|
m := map[string]any{
|
|
"owner": "novox", "repo": "mesh-delivery", "number": 12, "title": "A change", "body": "text",
|
|
"head_sha": head, "head_checks": map[string]string{"mesh/merge-gate": "success"}, "head": "feat/x", "base": "main",
|
|
"merge_commit_sha": merge, "merged_at": "2026-10-19T12:00:00Z", "clone_url": "https://forge.example/novox/mesh-delivery.git",
|
|
"html_url": "https://forge.example/novox/mesh-delivery/pulls/12", "paths": []string{"modules/a/x.go"}, "paths_truncated": false,
|
|
"removed": []string{}, "module_dirs": []string{"modules/a"}, "module_dirs_said": true,
|
|
}
|
|
for k, v := range extra {
|
|
if v == nil {
|
|
delete(m, k)
|
|
} else {
|
|
m[k] = v
|
|
}
|
|
}
|
|
b, _ := json.Marshal(m)
|
|
return b
|
|
}
|
|
|
|
const mergedSubject = "mesh.mod.gitea.event.pull.merged"
|
|
|
|
type call struct{ owner, repo, sha string }
|
|
|
|
// treesOf answers as gitea_commit_trees does (issue 1234/AC4: root, and the directory to tree hash, the root spelled "").
|
|
func treesOf(calls *[]call, root string, trees map[string]string, err error) Trees {
|
|
return func(owner, repo, sha string) (string, map[string]string, error) {
|
|
*calls = append(*calls, call{owner, repo, sha})
|
|
return root, trees, err
|
|
}
|
|
}
|
|
|
|
func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1
|
|
var calls []call
|
|
answer := map[string]string{"modules/a": "t1", "modules/b": "t2"}
|
|
msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", answer, nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if msg.Event != "forge.novox.mesh-delivery.merged" {
|
|
t.Errorf("event = %q", msg.Event)
|
|
}
|
|
f, err := delivery.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Merge: merge, Root: "r0", Trees: answer}
|
|
if !delivery.SameForge(f, want) {
|
|
t.Errorf("message = %+v, want %+v", f, want)
|
|
}
|
|
if len(calls) != 1 || calls[0] != (call{"novox", "mesh-delivery", merge}) {
|
|
t.Errorf("the tool was called %v, want once with novox, mesh-delivery and the merge commit", calls)
|
|
}
|
|
}
|
|
|
|
func TestMergeIntoAnotherBranchIsAClosedPull(t *testing.T) { // 1236/AC2
|
|
var calls []call
|
|
msg, err := Convert(mergedSubject, mergedBody(map[string]any{"base": "feat/y"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
f, err := delivery.DecodeForge(msg.Data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
|
|
Head: head, Action: delivery.PullClosed}
|
|
if msg.Event != "forge.novox.mesh-delivery.pull" || !delivery.SameForge(f, want) {
|
|
t.Errorf("event %q message %+v, want a closed pull message", msg.Event, f)
|
|
}
|
|
if len(calls) != 0 {
|
|
t.Errorf("the tool was called for a merge into another branch: %v", calls)
|
|
}
|
|
}
|
|
|
|
func TestTreesRefusedWritesNothingAndSaysWhy(t *testing.T) { // 1236/AC3 (the conversion; the delay is in main_test)
|
|
var calls []call
|
|
msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "", nil, errors.New("the listing for novox/mesh-delivery was cut")))
|
|
if !errors.Is(err, ErrRetry) || msg.Data != nil {
|
|
t.Fatalf("err = %v data = %s, want ErrRetry and no message", err, msg.Data)
|
|
}
|
|
for _, w := range []string{"novox/mesh-delivery", merge, "the listing for novox/mesh-delivery was cut"} {
|
|
if !strings.Contains(err.Error(), w) {
|
|
t.Errorf("error %q does not name %q", err, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNoModuleJSONAnywhereWritesNoMergedMessage(t *testing.T) { // 1236 (notes: a repository with no module.json)
|
|
var calls []call
|
|
msg, err := Convert(mergedSubject, mergedBody(nil), treesOf(&calls, "r0", map[string]string{}, nil))
|
|
if !errors.Is(err, ErrNoModules) || msg.Data != nil {
|
|
t.Errorf("err = %v data = %s", err, msg.Data)
|
|
}
|
|
}
|
|
|
|
func TestMergedWithoutAMergeCommitIsUnreadable(t *testing.T) { // 1236
|
|
var calls []call
|
|
_, err := Convert(mergedSubject, mergedBody(map[string]any{"merge_commit_sha": "abc"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil))
|
|
if !errors.Is(err, ErrUnreadable) || len(calls) != 0 {
|
|
t.Errorf("err = %v calls = %v", err, calls)
|
|
}
|
|
}
|
|
|
|
// openAnswer is gitea_open_pulls's answer as mesh-catalog modules/gitea/client.ts:369-381 builds each entry
|
|
// (owner, repo, number, base, head, head_sha, html_url, required, statuses, checks, labels) inside {pulls}.
|
|
func openAnswer(pulls ...map[string]any) []byte {
|
|
b, _ := json.Marshal(map[string]any{"pulls": pulls})
|
|
return b
|
|
}
|
|
|
|
func openEntry(repo string, number int, base, branch, sha string) map[string]any {
|
|
return map[string]any{"owner": "novox", "repo": repo, "number": number, "base": base, "head": branch, "head_sha": sha,
|
|
"html_url": "https://forge.example/x", "required": []string{"mesh/merge-gate"}, "statuses": []string{}, "checks": map[string]string{}, "labels": []string{}}
|
|
}
|
|
|
|
func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/AC1
|
|
h2 := strings.Repeat("a", 40)
|
|
msgs, skipped, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "main", "feat/x", head), openEntry("mesh-catalog", 300, "main", "feat/x", h2)))
|
|
if err != nil || len(skipped) != 0 || len(msgs) != 2 {
|
|
t.Fatalf("msgs %d skipped %v err %v", len(msgs), skipped, err)
|
|
}
|
|
for i, want := range []delivery.Forge{
|
|
{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullUpdated},
|
|
{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: delivery.PullUpdated},
|
|
} {
|
|
f, err := delivery.DecodeForge(msgs[i].Data)
|
|
if err != nil || !delivery.SameForge(f, want) {
|
|
t.Errorf("message %d = %+v err %v, want %+v", i, f, err, want)
|
|
}
|
|
if f.DirsKnown {
|
|
t.Errorf("message %d says its directories are known", i)
|
|
}
|
|
}
|
|
if msgs[0].Event != "forge.novox.mesh-delivery.pull" || msgs[1].Event != "forge.novox.mesh-catalog.pull" {
|
|
t.Errorf("events %q %q", msgs[0].Event, msgs[1].Event)
|
|
}
|
|
}
|
|
|
|
func TestOpenPullIntoAnotherBranchGetsNoMessage(t *testing.T) { // 1237/AC4
|
|
msgs, skipped, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "feat/y", "feat/x", head)))
|
|
if err != nil || len(msgs) != 0 || len(skipped) != 0 {
|
|
t.Errorf("msgs %d skipped %v err %v, want none", len(msgs), skipped, err)
|
|
}
|
|
}
|
|
|
|
func TestOpenPullsUnreadableEntryIsNamedAndTheRestGo(t *testing.T) { // 1237
|
|
msgs, skipped, err := OpenPulls(openAnswer(openEntry("a", 1, "main", "b", "short"), openEntry("mesh-delivery", 12, "main", "feat/x", head)))
|
|
if err != nil || len(msgs) != 1 || len(skipped) != 1 || !strings.Contains(skipped[0], "novox/a#1") {
|
|
t.Errorf("msgs %d skipped %v err %v", len(msgs), skipped, err)
|
|
}
|
|
for _, bad := range []string{`{"pulls":null}`, `{}`, `[]`, `x`} {
|
|
if _, _, err := OpenPulls([]byte(bad)); err == nil {
|
|
t.Errorf("%s was taken as an answer", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOpenMessageNamesEveryOpenPullIntoTheTrunk(t *testing.T) { // 1279/AC6
|
|
h2 := strings.Repeat("a", 40)
|
|
answer := openAnswer(openEntry("mesh-delivery", 12, "main", "feat/x", head), openEntry("mesh-catalog", 300, "main", "feat/x", h2),
|
|
openEntry("mesh-catalog", 301, "feat/y", "feat/z", h2), openEntry("a", 1, "main", "b", "short"))
|
|
m, err := OpenMessage(answer)
|
|
if err != nil || m.Event != "forge.open" {
|
|
t.Fatalf("message %+v err %v", m, err)
|
|
}
|
|
f, err := delivery.DecodeForge(m.Data)
|
|
// Every one into the trunk, whether its head reads or not (it is open); none into another branch; in a fixed order.
|
|
want := []delivery.OpenPull{{Repository: "novox/a", Number: 1}, {Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}}
|
|
if err != nil || f.Kind != delivery.ForgeOpen || len(f.Open) != 3 || f.Open[0] != want[0] || f.Open[1] != want[1] || f.Open[2] != want[2] {
|
|
t.Errorf("open message = %+v err %v, want %v", f, err, want)
|
|
}
|
|
// Nothing open is a message too: the indexer then drops every candidate.
|
|
m, err = OpenMessage([]byte(`{"pulls":[]}`))
|
|
if f, derr := delivery.DecodeForge(m.Data); err != nil || derr != nil || f.Kind != delivery.ForgeOpen || len(f.Open) != 0 {
|
|
t.Errorf("empty answer: %+v %v %v", f, err, derr)
|
|
}
|
|
for _, bad := range []string{`{"pulls":null}`, `{}`, `x`} {
|
|
if _, err := OpenMessage([]byte(bad)); err == nil {
|
|
t.Errorf("%s was taken as an answer", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOnlyAHeadFORGEDoesNotHoldIsWritten(t *testing.T) { // 1279/AC6
|
|
h2 := strings.Repeat("a", 40)
|
|
msgs, _, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "main", "feat/x", head), openEntry("mesh-catalog", 300, "main", "feat/x", h2)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
held := Held{}
|
|
held.Hear(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullUpdated})
|
|
got := held.Differing(msgs)
|
|
if len(got) != 1 || got[0].Event != "forge.novox.mesh-catalog.pull" {
|
|
t.Fatalf("written: %+v, want only mesh-catalog#300", got)
|
|
}
|
|
// Another head for the same pull request differs; a closed or merged one is no longer held.
|
|
held.Hear(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h2, Action: delivery.PullUpdated})
|
|
if got := held.Differing(msgs[:1]); len(got) != 1 {
|
|
t.Errorf("a pull request held at another head was left out")
|
|
}
|
|
held.Hear(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullUpdated})
|
|
held.Hear(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullClosed})
|
|
if got := held.Differing(msgs[:1]); len(got) != 1 {
|
|
t.Errorf("a closed pull request is still held")
|
|
}
|
|
held.Hear(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullOpened})
|
|
held.Hear(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}})
|
|
if got := held.Differing(msgs[:1]); len(got) != 1 {
|
|
t.Errorf("a merged pull request is still held")
|
|
}
|
|
}
|