Files
mesh-delivery/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go
T
jschoubben 677f94e901
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
Say which pull requests are open and drop every other candidate, so the candidates hold only open pull requests (issue 1279, ADR 0443)
2026-10-19 21:56:42 +02:00

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")
}
}