diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr505_bus_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr505_bus_test.go new file mode 100644 index 00000000..0bde5945 --- /dev/null +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/adr505_bus_test.go @@ -0,0 +1,88 @@ +//go:build integration + +package main + +import ( + "context" + "encoding/json" + "strconv" + "strings" + "testing" + "time" + + sdk "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// novox/hq ADR 0505 §4 over the real bus, in the bed of bus_test.go: the gate whole, its deliveries kept in its buckets. + +// call asks the bundle one of its verbs, as the runtime does, and answers its result's text. +func (r *runtime) call(t *testing.T, name string, args map[string]any) string { + t.Helper() + r.mu.Lock() + r.next++ + id := "adr505-" + strconv.Itoa(r.next) + ch := make(chan json.RawMessage, 1) + r.wait[id] = ch + r.mu.Unlock() + r.say(map[string]any{"id": id, "method": "tools/call", "params": map[string]any{"name": name, "arguments": args}}) + select { + case raw := <-ch: + return string(raw) + case <-time.After(30 * time.Second): + t.Fatalf("%s was not answered in 30 s", name) + } + return "" +} + +// ADR 0505 §4: an approved head the merge tool refused because its trunk moved is shown by `show` at once but is not in +// `stalled` while its fresh check is unanswered; once that check passed and the merge is refused again, it is. +func TestATrunkMovedRefusalIsStalledOnlyOnceItsFreshCheckDidNotCureItOverARealBus(t *testing.T) { + b := newTestBus(t) + ctx := context.Background() + rt := b.start(t) + evctx, cancel := context.WithCancel(ctx) + defer cancel() + rt.deliverEvents(evctx) + rt.until("the module to subscribe to the events it follows", 20*time.Second, func() bool { + rt.mu.Lock() + defer rt.mu.Unlock() + return len(rt.subs) >= len(followed) + }) + rt.untilHolding() + + const head = "1111111111111111111111111111111111111111" + id := IDOf("novox/app", head) + announced := pr("novox/app", 7, head, "fix/x", "src/a.go") + announced.BaseSHA = "t1t1t1t1t1t1" + b.publish(t, "mesh.mod.gitea.event.pull.updated", announced) + b.publish(t, "mesh.seat."+ControllerSeat+".event.checked", judgedAt("novox/app", 7, head, "pass", "t1t1t1t1t1t1")) + var key string + rt.until("the ask to be written", 10*time.Second, func() bool { key = onlyAsk(b); return key != "" }) + b.answer(t, key, OptionApprove) + rt.until("the merge to be handed to the bridge", 10*time.Second, func() bool { return len(mergeAt(b, "fix/x").Pulls) > 0 }) + + refused := func() { + b.forgeSays(t, sdk.Forge{Kind: sdk.ForgeRefused, Act: "merge", Repository: "novox/app", Number: 7, Head: head, + Words: errAskedAgain.Error()}) + } + refused() + rt.until("show to name the refusal at once", 10*time.Second, func() bool { + return strings.Contains(rt.call(t, Seat+".show", map[string]any{"id": id}), "its verdict was judged against trunk") + }) + if got := rt.call(t, Seat+".stalled", map[string]any{}); strings.Contains(got, id) { + t.Fatalf("said as stalled while its fresh check runs: %s", got) + } + + // The trunk moved; the head is announced again and its fresh check passes; the merge is refused again on it. + b.publish(t, "mesh.mod.gitea.event.pull.merged", merged("novox/app", 9, "9999999999999999999999999999999999999999", "t2t2t2t2t2t2")) + b.publish(t, "mesh.mod.gitea.event.pull.updated", announced) + time.Sleep(time.Second) + b.publish(t, "mesh.seat."+ControllerSeat+".event.checked", judgedAt("novox/app", 7, head, "pass", "t2t2t2t2t2t2")) + rt.until("the fresh verdict to be heard", 10*time.Second, func() bool { + return strings.Contains(rt.call(t, Seat+".show", map[string]any{"id": id}), "t2t2t2t2t2t2") + }) + refused() + rt.until("the refusal on the fresh verdict to be said as stalled", 10*time.Second, func() bool { + return strings.Contains(rt.call(t, Seat+".stalled", map[string]any{}), id) + }) +}