Files
mesh-catalog/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/approval_test.go
T
jschoubben 9698bcd934 The merge gate reads no pull request description: the review hold goes (reviewWaits, reviewWords, the Reviewed-by line)
The operator, 2026-11-07: remove any gate that reads a description. An unreviewed pull request whose
other waits are met is asked, and the ask names no review line.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
2026-11-07 23:10:43 +01:00

524 lines
22 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// **A merge into main waits for the operator's Approve** (novox/hq ADR 0398 §3, issue 948): a marked, green delivery
// into main is asked of the operator, and merged only after an Approve whose warrant holds for that ask.
// markedGreen is a pull request into main, announced, judged green and marked ready at its head; the operator is not
// answering by himself (w.op.auto is off).
func markedGreen(t *testing.T) *world {
w := newWorld(t)
w.op.auto = false
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
p.Title = "Fix the apostrophe (issue 948, issue 12)"
w.h.PullUpdated(p)
w.h.Checked(judgedAt("novox/app", 7, readyHead, "pass", "t1t1t1t1t1t1"))
w.forge.open = []OpenPull{openPull("novox/app", 7, readyHead)}
w.h.SeeOpenPulls()
w.settleAll()
return w
}
func TestAMarkedGreenDeliveryIntoMainIsAskedAndNotMergedUntilApproved(t *testing.T) {
w := markedGreen(t)
id := IDOf("novox/app", readyHead)
if len(w.forge.merges) != 0 {
t.Fatalf("merged before the operator's Approve: %v", w.forge.merges)
}
open := w.op.open()
if len(open) != 1 {
t.Fatalf("the operator is asked %v; wanted exactly one ask", open)
}
a := w.op.ask(open[0])
if !strings.HasPrefix(a.Headline, "Merge into main: app#7") || a.Who != occ.Operator ||
len(a.Options) != 2 || a.Options[0].ID != OptionApprove || a.Options[1].ID != OptionDecline {
t.Fatalf("the ask is not the merge ask: %+v", a)
}
for _, want := range []string{"issue 948", "issue 12", "Touches: "} {
if !strings.Contains(a.Explanation, want) {
t.Fatalf("the ask does not say %q: %s", want, a.Explanation)
}
}
if err := occ.Glance(a.Explanation); err != nil {
t.Fatalf("the ask does not read at a glance: %v", err)
}
// Ticks and minutes pass without an answer: not merged, not asked again.
for range 3 {
w.later(2 * time.Minute)
w.settleAll()
}
if len(w.forge.merges) != 0 || w.op.count() != 1 {
t.Fatalf("while waiting: merges %v, asks %d", w.forge.merges, w.op.count())
}
if d := w.delivery(id); d.Ready.AskID != open[0] || !strings.Contains(d.Ready.Waits, "Approve") {
t.Fatalf("the ask is not kept on the delivery: %+v", d.Ready)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 1 || w.forge.merges[0] != "novox/app#7@"+readyHead {
t.Fatalf("after the Approve it was merged %v; wanted once, at the marked head", w.forge.merges)
}
d := w.delivery(id)
if d.Ready.Approved == nil || !strings.Contains(d.Ready.Warrant, "chose Approve") {
t.Fatalf("the warrant is not kept: %+v", d.Ready)
}
w.h.PullMerged(merged("novox/app", 7, readyHead, "m7m7m7m7m7m7"))
w.settleAll()
var last Transition
for _, tr := range w.delivery(id).Transitions {
if tr.Event == EvMerged {
last = tr
}
}
if !strings.Contains(last.By, "approved: the operator, via telegram") {
t.Fatalf("the merge's transition does not name the operator's Approve: %+v", last)
}
}
// AC3 of issue 1348: an answer naming old heads merges nothing. The teller's answer to an ask that was withdrawn when the head
// moved is still in its bucket; the gate acts on the key of the heads asked now alone.
func TestAnAnswerNamingOldHeadsMergesNothing(t *testing.T) {
w := markedGreen(t)
old := w.op.open()[0]
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
w.settleAll()
w.op.answer(old, OptionApprove) // the teller wrote it after the ask was taken away
w.settleAll()
if len(w.forge.merges) != 0 || len(w.merges.puts) != 0 {
t.Fatalf("an answer to old heads merged: %v, handed %d", w.forge.merges, len(w.merges.puts))
}
if d := w.delivery(IDOf("novox/app", readyHead)); d.Ready != nil && d.Ready.Approved != nil {
t.Fatalf("an answer to old heads approved the old delivery: %+v", d.Ready)
}
}
// An answer under a key that is not the key of the heads the ask covers is refused and said: the gate acts on its own
// ask's key alone (the heads hash is the key's last part).
func TestAnAnswerUnderAKeyThatIsNotTheHeadsKeyIsRefused(t *testing.T) {
w := markedGreen(t)
id := IDOf("novox/app", readyHead)
key := w.op.open()[0]
w.h.mu.Lock()
w.h.deliveries[id].Ready.Heads[id] = strings.Repeat("9", 40) // the heads no longer give the key
w.h.mu.Unlock()
w.op.answer(key, OptionApprove)
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("an answer under another key merged: %v", w.forge.merges)
}
d := w.delivery(id)
if !strings.Contains(d.Ready.Refused, "does not hold") || d.Ready.Approved != nil {
t.Fatalf("the refused answer is not kept: %+v", d.Ready)
}
if lines := w.h.Stalled(); len(lines) != 1 || !strings.Contains(lines[0].Says, "answer") {
t.Fatalf("a refused answer is not said loudly: %+v", lines)
}
}
func TestADeclinedMergeIsNotMergedAndSaidOnce(t *testing.T) {
w := markedGreen(t)
w.op.answer(w.op.open()[0], OptionDecline)
for range 3 {
w.later(2 * time.Minute)
w.settleAll()
}
if len(w.forge.merges) != 0 || w.op.count() != 1 {
t.Fatalf("declined, yet merges %v and asks %d", w.forge.merges, w.op.count())
}
if len(w.forge.comments) != 1 || !strings.Contains(w.forge.comments[0], "declined") ||
!strings.Contains(w.forge.comments[0], "chose Decline") {
t.Fatalf("the Decline is not said once on the pull request, with its warrant: %v", w.forge.comments)
}
}
func TestAPushAfterTheAskCancelsItAndTheNewHeadIsAskedOnItsOwnWhenItsChecksPass(t *testing.T) {
w := markedGreen(t)
old := w.op.open()[0]
w.h.PullUpdated(pr("novox/app", 7, pushed, "feat/x", "src/a.go"))
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.cancelled[0] != old || len(w.op.open()) != 0 {
t.Fatalf("the ask was not cancelled: cancelled %v, open %v", w.op.cancelled, w.op.open())
}
if len(w.forge.merges) != 0 || w.op.count() != 1 {
t.Fatalf("merges %v, asks %d", w.forge.merges, w.op.count())
}
if d := w.delivery(IDOf("novox/app", readyHead)); d.Ready.AskID != "" {
t.Fatalf("the cancelled ask is still kept as waiting: %+v", d.Ready)
}
// The new head is asked afresh once its checks pass.
w.h.Checked(judgedAt("novox/app", 7, pushed, "pass", "t1t1t1t1t1t1"))
w.settleAll()
if open := w.op.open(); len(open) != 1 || open[0] == old {
t.Fatalf("the new head was not asked afresh: %v", open)
}
// Issue 774 (2026-10-31): a pull request merged by another path than the Approve (the lead merged it once its check was
// green) left its ask in the gate's asks for a day, and the teller's ask held one of the router's three places. An ask whose
// every delivery is merged or ended is taken away at the next look, saying nothing on the pull request.
t.Run("774: an ask whose pull request merged without it is taken away", func(t *testing.T) {
w := markedGreen(t)
ask := w.op.open()[0]
comments := len(w.forge.comments)
w.h.PullMerged(merged("novox/app", 7, readyHead, "m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1m1"))
w.settleAll()
if len(w.op.open()) != 0 || len(w.op.cancelled) != 1 || w.op.cancelled[0] != ask {
t.Fatalf("the ask of a merged pull request stands: open %v, taken away %v", w.op.open(), w.op.cancelled)
}
if d := w.delivery(IDOf("novox/app", readyHead)); d.Ready.AskID != "" || d.Ready.Written {
t.Errorf("the merged delivery still keeps its ask: %+v", d.Ready)
}
w.later(2 * time.Minute)
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.count() != 1 || len(w.forge.comments) != comments {
t.Errorf("taken away %v, asks %d, comments %v: once, asking nothing again, saying nothing", w.op.cancelled, w.op.count(), w.forge.comments[comments:])
}
})
}
// The ask lapses at the router after AskLasts with nobody's answer, and the gate has no word of it: it takes its key away and
// writes it again, once, which the teller asks afresh (it asks again under the same id when the gate writes its key again).
func TestAnUnansweredAskIsWrittenAgainOnceWhenItLapses(t *testing.T) {
w := markedGreen(t)
first := w.op.open()[0]
w.later(AskLasts + time.Minute)
w.settleAll()
if w.op.count() != 2 || len(w.op.open()) != 1 || w.op.open()[0] != first || len(w.op.cancelled) != 0 {
t.Fatalf("a lapsed ask was not written again once, over the same key: %d asks, open %v, taken away %v", w.op.count(), w.op.open(), w.op.cancelled)
}
// Review round 1, G2: the router keeps an ask by its id and drops a second under it, so the second asking has an id of its own.
again := w.op.byKey[first]
againAsk := w.op.ask(first)
if again.Asking != 2 || againAsk.ID != mdc.AskingID(first, 2) || againAsk.ID == mdc.AskID(first) {
t.Fatalf("the second asking is %d under the id %s; wanted asking 2 under %s", again.Asking, againAsk.ID, mdc.AskingID(first, 2))
}
for range 3 {
w.later(2 * time.Minute)
w.settleAll()
}
if w.op.count() != 2 || len(w.forge.merges) != 0 {
t.Fatalf("asked more than once again: %d asks, merges %v", w.op.count(), w.forge.merges)
}
}
func TestAGroupIsAskedOnceForAllItsHeadsAndMergedInOnePassOnOneApprove(t *testing.T) {
w := newWorld(t)
w.op.auto = false
const ctlHead, catHead = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
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(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
w.forge.open = []OpenPull{openPull("novox/mesh-controller", 1, ctlHead),
openPull("novox/mesh-catalog", 2, catHead)}
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
if open := w.op.open(); len(open) != 1 || len(w.forge.merges) != 0 {
t.Fatalf("a group is asked once before any merge: open %v, merges %v", w.op.open(), w.forge.merges)
}
key := w.op.open()[0]
a := w.op.ask(key)
if !strings.Contains(a.Headline, "group feat") || !strings.Contains(a.Details, "mesh-controller#1") ||
!strings.Contains(a.Details, "mesh-catalog#2") {
t.Fatalf("the ask does not name the group and its members: %+v", a)
}
w.op.answer(key, OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 || w.op.count() != 1 {
t.Fatalf("both members were not merged in one pass on the one Approve: merges %v, asks %d", w.forge.merges, w.op.count())
}
}
// A push to a group member without a mark ends the group's ask (ADR 0398 §3, issue 948; review round 1): the Approve
// that follows would be for heads the group no longer has.
func TestAPushToAGroupMemberEndsTheGroupsAskAndApproveAndSaysSoOnEveryMember(t *testing.T) {
w := newWorld(t)
w.op.auto = false
const ctlHead, catHead, catPushed = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3"
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(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
w.forge.open = []OpenPull{openPull("novox/mesh-controller", 1, ctlHead),
openPull("novox/mesh-catalog", 2, catHead)}
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
first := w.op.open()
if len(first) != 1 {
t.Fatalf("asked %v", first)
}
// The catalog member is pushed; its new head has no mark yet.
w.h.PullUpdated(pr("novox/mesh-catalog", 2, catPushed, "feat/x", "modules/app/module.json"))
w.forge.open = []OpenPull{openPull("novox/mesh-controller", 1, ctlHead),
{Owner: "novox", Repo: "mesh-catalog", Number: 2, Base: "main", HeadSHA: catPushed}}
w.h.SeeOpenPulls()
w.settleAll()
if len(w.op.cancelled) != 1 || w.op.cancelled[0] != first[0] {
t.Fatalf("the group's ask was not cancelled by the push: %v", w.op.cancelled)
}
w.op.answer(first[0], OptionApprove) // a late answer to the cancelled ask
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catPushed, "pass", "t3t3t3t3t3t3"))
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-2", Verdict: "pass", Summary: "both compose again"})
w.settleAll()
if len(w.forge.merges) != 0 {
t.Fatalf("merged on an Approve of heads the group no longer has: %v", w.forge.merges)
}
}
// mergedSaid is the module events `merged` the world has said.
func mergedSaid(w *world) []map[string]any {
w.mu.Lock()
defer w.mu.Unlock()
var out []map[string]any
for _, m := range w.said {
if m["event-kind"] == "merged" {
out = append(out, m)
}
}
return out
}
// Issue 1025: a merge on the operator's Approve is announced once, with the warrant's ask, option and heads.
func TestIssue1025AMergeOnTheApproveIsAnnouncedWithItsWarrant(t *testing.T) {
w := markedGreen(t)
id := IDOf("novox/app", readyHead)
open := w.op.open()
if len(open) != 1 {
t.Fatalf("asks %v", open)
}
if got := mergedSaid(w); len(got) != 0 {
t.Fatalf("announced before the merge: %v", got)
}
d := w.delivery(id)
wantHeads := map[string]string{}
for k, v := range d.Ready.Heads {
wantHeads[k] = v
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
got := mergedSaid(w)
if len(got) != 1 {
t.Fatalf("announced %d times; wanted once: %v", len(got), got)
}
m := got[0]
if m["owner"] != "novox" || m["repo"] != "app" || m["number"] != float64(7) || m["head_sha"] != readyHead ||
m["ask_id"] != mdc.AskID(open[0]) || m["asker"] != tellerModule || m["option"] != OptionApprove {
t.Fatalf("the event does not name the pull request and its warrant: %v", m)
}
hs, _ := m["heads"].(map[string]any)
if len(hs) == 0 || len(hs) != len(wantHeads) {
t.Fatalf("heads %v; wanted %v", hs, wantHeads)
}
for k, v := range wantHeads {
if hs[k] != v {
t.Fatalf("heads %v; wanted %v", hs, wantHeads)
}
}
if g, ok := m["group"]; ok && g != "" {
t.Fatalf("a lone delivery names group %v", g)
}
w.later(2 * time.Minute)
w.settleAll()
if n := len(mergedSaid(w)); n != 1 {
t.Fatalf("announced %d times after more ticks", n)
}
}
// Issue 1025: a declined merge announces nothing.
func TestIssue1025ADeclinedMergeAnnouncesNothing(t *testing.T) {
w := markedGreen(t)
open := w.op.open()
w.op.answer(open[0], OptionDecline)
w.settleAll()
if got := mergedSaid(w); len(got) != 0 || len(w.forge.merges) != 0 {
t.Fatalf("declined, yet merges %v, announced %v", w.forge.merges, got)
}
}
// Issue 1025: a merge of a delivery in a group carries the group id and the heads the group's Approve was bound to.
func TestIssue1025AMergeInAGroupCarriesTheGroupAndItsHeads(t *testing.T) {
w := newWorld(t)
w.op.auto = false
const ctlHead, catHead = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2"
ctl, cat := IDOf("novox/mesh-controller", ctlHead), IDOf("novox/mesh-catalog", catHead)
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(judgedAt("novox/mesh-controller", 1, ctlHead, "pass", "t1t1t1t1t1t1"))
w.h.Checked(judgedAt("novox/mesh-catalog", 2, catHead, "pass", "t3t3t3t3t3t3"))
w.forge.open = []OpenPull{openPull("novox/mesh-controller", 1, ctlHead),
openPull("novox/mesh-catalog", 2, catHead)}
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: "check-1", Verdict: "pass", Summary: "both compose"})
w.settleAll()
open := w.op.open()
if len(open) != 1 {
t.Fatalf("asks %v", open)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
got := mergedSaid(w)
if len(got) != 2 {
t.Fatalf("announced %d; wanted each member's merge once, from the one pass: %v", len(got), got)
}
for _, m := range got {
hs, _ := m["heads"].(map[string]any)
if m["group"] != "feat/x" || m["ask_id"] != mdc.AskID(open[0]) || m["asker"] != tellerModule || m["option"] != OptionApprove ||
hs[ctl] != ctlHead || hs[cat] != catHead || len(hs) != 2 {
t.Fatalf("the group's merge event: %v", m)
}
}
}
// Issue 1025: the announcement is owed and kept, so it is said after a restart, and once.
func TestIssue1025AnOwedMergedIsSaidAfterARestartAndOnce(t *testing.T) {
w := newWorld(t)
p := pr("novox/app", 7, readyHead, "feat/x", "src/a.go")
w.h.PullUpdated(p)
id := IDOf("novox/app", readyHead)
w.h.mu.Lock()
d := w.h.deliveries[id]
d.Owed = append(d.Owed, Effect{Kind: EffectEmit, Event: "merged", Line: `{"owner":"novox","repo":"app","number":7,"head_sha":"` + readyHead + `"}`, Since: w.now})
if err := w.store.PutDelivery(d); err != nil {
t.Fatal(err)
}
w.h.mu.Unlock()
again := newWorldOver(t, w.store, w.forge, w.now.Add(time.Minute))
again.settleAll()
again.later(time.Hour)
again.settleAll()
if n := len(mergedSaid(again)); n != 1 {
t.Fatalf("said %d times after the restart; wanted once", n)
}
}
// Review round 1, G2: an answer given to the first asking is not taken for the second, and the second's Approve is announced
// under the second asking's id.
func TestAnAnswerToAnEarlierAskingIsNotTakenAndTheMergeNamesTheAnsweredAsking(t *testing.T) {
w := markedGreen(t)
key := w.op.open()[0]
w.later(AskLasts + time.Minute)
w.settleAll() // asked again: the second asking
w.op.mu.Lock()
w.op.answers[key] = &Answer{Option: OptionApprove, Warrant: mdc.AskingID(key, 1) + "@2026-10-06T12:00:00Z", Words: "the operator, via telegram, chose Approve"}
w.op.mu.Unlock()
w.settleAll()
if len(w.forge.merges) != 0 || len(w.merges.puts) != 0 {
t.Fatalf("an answer to the first asking merged on the second: %v", w.forge.merges)
}
w.op.answer(key, OptionApprove) // the answer to the asking asked now
w.settleAll()
if len(w.forge.merges) != 1 {
t.Fatalf("the answer to the second asking did not merge: %v", w.forge.merges)
}
say := mergedSaid(w)
if len(say) != 1 || say[0]["ask_id"] != mdc.AskingID(key, 2) {
t.Fatalf("the merge is announced %v; wanted the ask id of asking 2, %s", say, mdc.AskingID(key, 2))
}
}
// 2026-10-29 (hq#495): a group declined under one ask, a member pushed, and the group asked and approved again at the new
// heads: the member whose head did not move kept the earlier Decline beside the new Approve, and nothing was handed, with
// no word. An answer stands only for the ask it answers; whichever member is pushed, both merge on the new Approve.
func TestADeclineOfAnEarlierAskDoesNotHoldTheApproveOfTheNext(t *testing.T) {
const ctlHead, catHead, pushed = "c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1c1", "d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2", "e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3e3"
for _, push := range []struct {
repo string
number int
trunk string
}{{"novox/mesh-catalog", 2, "t3t3t3t3t3t3"}, {"novox/mesh-controller", 1, "t1t1t1t1t1t1"}} {
t.Run(push.repo, func(t *testing.T) {
w := newWorld(t)
w.op.auto = false
heads := map[string]string{"novox/mesh-controller": ctlHead, "novox/mesh-catalog": catHead}
paths := map[string]string{"novox/mesh-controller": "internal/a.go", "novox/mesh-catalog": "modules/app/module.json"}
numbers := map[string]int{"novox/mesh-controller": 1, "novox/mesh-catalog": 2}
trunks := map[string]string{"novox/mesh-controller": "t1t1t1t1t1t1", "novox/mesh-catalog": "t3t3t3t3t3t3"}
round := func(check string) {
var open []OpenPull
for _, repo := range []string{"novox/mesh-controller", "novox/mesh-catalog"} {
w.h.PullUpdated(pr(repo, numbers[repo], heads[repo], "feat/x", paths[repo]))
w.h.Checked(judgedAt(repo, numbers[repo], heads[repo], "pass", trunks[repo]))
open = append(open, openPull(repo, numbers[repo], heads[repo]))
}
w.forge.open = open
w.h.SeeOpenPulls()
w.settleAll()
w.h.Checked(CheckedEvent{Group: "feat/x", ID: check, Verdict: "pass", Summary: "both compose"})
w.settleAll()
}
round("check-1")
w.op.answer(w.op.open()[0], OptionDecline)
w.settleAll()
heads[push.repo] = pushed
round("check-2")
open := w.op.open()
if len(open) != 1 {
t.Fatalf("the group at its new heads is not asked once: %v", open)
}
w.op.answer(open[0], OptionApprove)
w.settleAll()
if len(w.forge.merges) != 2 {
t.Fatalf("approved at the new heads, yet merged %v; stalled %+v", w.forge.merges, w.h.Stalled())
}
})
}
}
// mesh-sdk no longer refuses an ask whose id is another's or that offers no option (novox/hq ADR 0627 §4, ruling 1): the gate
// holds both, when it composes its ask and when kvOperator writes it.
func TestTheGateRefusesAnAskWithAnotherIdOrNoOption(t *testing.T) {
heads := []mdc.Head{{Repository: "novox/app", Number: 7, Head: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}
key, err := mdc.MergeAskKey("feat/g", heads)
if err != nil {
t.Fatal(err)
}
option := oc.Option{ID: OptionApprove, Label: "Approve", Level: occ.Approve}
for _, c := range []struct {
name string
asking int
ask oc.Ask
refuse string
}{
{"the first asking's id", 1, oc.Ask{ID: mdc.AskingID(key, 1), Options: []oc.Option{option}}, ""},
{"the second asking's id", 2, oc.Ask{ID: mdc.AskingID(key, 2), Options: []oc.Option{option}}, ""},
// The refusals are operator-channel's client's (occ.Carried), held to the id the gate's key and asking give.
{"another id", 1, oc.Ask{ID: "another-id", Options: []oc.Option{option}}, "the id it was kept under"},
{"the first asking's id at the second asking", 2, oc.Ask{ID: mdc.AskingID(key, 1), Options: []oc.Option{option}}, "the id it was kept under"},
{"no option", 1, oc.Ask{ID: mdc.AskingID(key, 1)}, "offers no option"},
} {
a, err := gateAsk("feat/g", heads, c.asking, c.ask)
if c.refuse == "" {
if err != nil {
t.Errorf("%s: refused: %v", c.name, err)
}
continue
}
if err == nil || !strings.Contains(err.Error(), c.refuse) {
t.Errorf("%s: composed (%v), wanted a refusal saying %q", c.name, err, c.refuse)
}
// The same ask written by hand, past gateAsk, is refused where kvOperator writes it.
raw, _ := json.Marshal(c.ask)
if err := (kvOperator{}).Ask(key, Ask{Group: "feat/g", Heads: heads, Asking: c.asking, Ask: raw}); err == nil || !strings.Contains(err.Error(), c.refuse) {
t.Errorf("%s: kvOperator wrote it (%v, %v)", c.name, err, a)
}
}
// A value that carries no router's ask is refused before it is written.
if err := (kvOperator{}).Ask(key, Ask{Group: "feat/g", Heads: heads, Asking: 1, Ask: json.RawMessage(`"an ask"`)}); err == nil ||
!strings.Contains(err.Error(), "carries no operator-channel ask") {
t.Errorf("a value carrying no ask: %v", err)
}
}