package main import ( "encoding/json" "errors" "strings" "testing" "time" ) // Issue 1826 (novox/hq ADR 0502): a value held for the open pull request that introduces its key. const thePull = "novox/mesh-catalog#434" // n8nPullDeclares is what the controller tells of the pull request: n8n gets a required number media-gid. func n8nPullDeclares() map[string]any { return map[string]any{"n8n": map[string]any{"media-gid": map[string]any{"type": "number", "description": "the media group", "required": true, "minimum": 1, "maximum": 65535}}} } // aHoldingMesh is a proposing mesh with n8n assigned to ace and declaring nothing on the trunk (the case of the // issue), and a stand-in forge. func aHoldingMesh(t *testing.T) (*Config, *memStore, *standIn, *standForge) { t.Helper() c, m, r := aProposingMesh(t) m.declare(t, "n8n", `{"module":"n8n","settings":{}}`) m.assign(t, "ace", "n8n") f := &standForge{state: map[string][2]bool{}} c.Forge = f if _, err := c.Reconcile(); err != nil { t.Fatal(err) } m.events, m.handActs = nil, nil return c, m, r, f } type standForge struct { state map[string][2]bool // pull -> open, merged head string err error } func (f *standForge) Pull(owner, repo string, n int) (bool, bool, string, error) { if f.err != nil { return false, false, "", f.err } st, ok := f.state[(PullEvent{Owner: owner, Repo: repo, Number: n}).name()] if !ok { return true, false, f.head, nil } return st[0], st[1], f.head, nil } func tell(t *testing.T, c *Config, pull, head string, declares map[string]any) *PullDeclaresAnswer { t.Helper() a, err := c.Do(Call{Action: "pull-declares", Pull: pull, Head: head, Declares: declares, Caller: ControllerCaller}) if err != nil { t.Fatalf("pull-declares: %v", err) } return a.(*PullDeclaresAnswer) } // holdIt proposes media-gid=2000 for n8n on ace held for the pull request and has the operator approve it. func holdIt(t *testing.T, c *Config, m *memStore, r *standIn) string { t.Helper() a := propose(t, c, Call{Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull, Why: "n8n's container joins the media group, held for #434"}) r.choose(t, a.Proposal, OptionApprove, nil) said, err := c.Decide() if err != nil || len(said) != 1 || !strings.Contains(said[0], "approved by the operator and held") { t.Fatalf("the Approve decided %v (%v)", said, err) } return a.Proposal } func keptCount(m *memStore) int { k, _ := m.Keys(KeptState); return len(k) } // 1826/K1: mesh-config takes what a pull request declares from the controller alone, and keeps it per pull request. func TestAPullRequestsDeclarationsAreTakenFromTheControllerAlone(t *testing.T) { c, m, _, _ := aHoldingMesh(t) for _, caller := range []string{"g14.node-tools", Unproven, ""} { if _, err := c.Do(Call{Action: "pull-declares", Pull: thePull, Head: "abc1234", Declares: n8nPullDeclares(), Caller: caller}); err == nil || !strings.Contains(err.Error(), "controller") { t.Errorf("pull-declares from %q: %v", caller, err) } } for _, bad := range []Call{{Pull: "mesh-catalog#434", Head: "a"}, {Pull: thePull}, {Pull: "a/b#0", Head: "a"}} { bad.Action, bad.Caller = "pull-declares", ControllerCaller if _, err := c.Do(bad); err == nil { t.Errorf("pull-declares %+v was taken", bad) } } a := tell(t, c, thePull, "abc1234def", n8nPullDeclares()) if len(a.Modules) != 1 || a.Modules[0] != "n8n" { t.Errorf("answered %+v", a) } e, _ := m.Get(PullsState, "novox.mesh-catalog.434") var pd PullDecl if e == nil || json.Unmarshal(e.Value, &pd) != nil || pd.Head != "abc1234def" || pd.Declares["n8n"]["media-gid"].Type != "number" || !pd.Declares["n8n"]["media-gid"].Required { t.Errorf("kept %s", func() string { if e == nil { return "nothing" } return string(e.Value) }()) } // Nothing is resolved from a pull request's declaration. if r, _ := m.Get(ResolvedState, ResolvedKey("ace", "n8n", "media-gid")); r != nil { t.Errorf("a pull request's declaration was resolved: %s", r.Value) } } // 1826/K2: a proposal for a key only the pull request declares is refused without its declaration, for a mesh or // node level, for a clear, for an undeclared key, and for a module not placed on the node; and trunk-only // proposals are judged as before. func TestAProposalNamingAPullRequestIsJudgedAgainstItsDeclaration(t *testing.T) { c, m, _, _ := aHoldingMesh(t) try := func(call Call) error { call.Action, call.Caller, call.Why = "propose", "g14.node-tools", "held for 434" _, err := c.Propose(call) return err } base := Call{Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull} if err := try(base); err == nil || !strings.Contains(err.Error(), "holds no declaration for "+thePull) { t.Errorf("without the pull request's declaration: %v", err) } // And without the pull argument it is the trunk's refusal, as before. plain := base plain.Pull = "" if err := try(plain); err == nil || !strings.Contains(err.Error(), `n8n declares no value "media-gid"`) { t.Errorf("the trunk's refusal changed: %v", err) } tell(t, c, thePull, "abc1234def", n8nPullDeclares()) for name, tc := range map[string]struct { mutate func(*Call) want string }{ "a mesh level": {func(c *Call) { c.Module, c.Node = "", "" }, "a module's own value on one node"}, "a node level": {func(c *Call) { c.Module = "" }, "a module's own value on one node"}, "a clear": {func(c *Call) { c.Clear, c.Value = true, nil }, "a clear is not held"}, "a key not added": {func(c *Call) { c.Key = "other" }, "does not add or change a value"}, "a bad value": {func(c *Call) { c.Value = 0.0 }, "below the minimum"}, "a wrong type": {func(c *Call) { c.Value = "2000" }, "is not a number"}, "another node": {func(c *Call) { c.Node = "g14" }, "is not placed on g14"}, "another pull": {func(c *Call) { c.Pull = "novox/mesh-catalog#435" }, "holds no declaration for"}, "a malformed pull": {func(c *Call) { c.Pull = "434" }, "owner/repository#number"}, } { call := base tc.mutate(&call) if err := try(call); err == nil || !strings.Contains(err.Error(), tc.want) { t.Errorf("%s: %v, want %q", name, err, tc.want) } } // A set or a clear through the verb takes no pull request. if _, err := c.Change(Call{Action: "set", Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull, Caller: "g14.node-tools", Why: "w"}); err == nil || !strings.Contains(err.Error(), "only a proposal") { t.Errorf("a set naming a pull request: %v", err) } if keptCount(m) != 0 { t.Error("something was held") } } // 1826/K2: every held value is asked whatever the key's trust; the ask says it is held and dropped on a close; on // Approve the value is kept apart, never set or resolved; history, event and hand-act say it; held answers it. func TestAHeldValueIsAskedAndKeptApartOnApprove(t *testing.T) { c, m, r, _ := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) a := propose(t, c, Call{Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull, Why: "held for #434"}) if len(a.Reaches) != 0 { t.Errorf("a held value reaches %v", a.Reaches) } ask := r.asks[a.Proposal] if err := ask.Check(c.now()); err != nil { t.Fatalf("the ask: %v", err) } for _, want := range []string{thePull, "merges", "dropped if it closes unmerged", "media-gid", "n8n", "ace"} { if !strings.Contains(ask.Explanation, want) { t.Errorf("the ask does not say %q:\n%s", want, ask.Explanation) } } if strings.Contains(ask.Explanation, "takes effect at once") || !strings.Contains(ask.Headline, "Hold") { t.Errorf("the ask: %q\n%s", ask.Headline, ask.Explanation) } if ap := ask.Options[0]; !strings.Contains(ap.Does, thePull) || ap.Level == "" { t.Errorf("the approve option does %q", ap.Does) } if keptCount(m) != 0 { t.Fatal("a proposal held something") } r.choose(t, a.Proposal, OptionApprove, nil) said, _ := c.Decide() if len(said) != 1 || !strings.Contains(said[0], "held at abc1234d for "+thePull) { t.Fatalf("decided %v", said) } // Kept apart: not a value, not resolved. if _, set := storedValue(t, m, "module.ace.n8n.media-gid"); set { t.Error("an Approve set the value") } if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "n8n", "media-gid")); e != nil { t.Errorf("an Approve resolved the value: %s", e.Value) } if keptCount(m) != 1 { t.Fatalf("%d values held", keptCount(m)) } if len(m.events) != 1 || m.events[0].Action != ActionHold || m.events[0].Pull != thePull || m.events[0].Key != "media-gid" || m.events[0].Head != "abc1234def" || len(m.events[0].Moved) != 0 || !strings.Contains(m.events[0].Approved, "approved by the operator") { t.Errorf("raised %+v", m.events) } if len(m.handActs) != 1 || !strings.Contains(m.handActs[0], thePull) { t.Errorf("hand-acts %q", m.handActs) } h, _ := c.History(Call{Module: "n8n", Node: "ace"}) if len(h) != 1 || h[0].Action != ActionHold || h[0].Pull != thePull { t.Errorf("history %+v", h) } // held answers it, at the head it is kept at, to the controller alone. if _, err := c.Do(Call{Action: "held", Caller: "g14.node-tools", Pulls: []PullAt{{thePull, "abc1234def"}}}); err == nil { t.Error("held answered a caller that is not the controller") } got, err := c.Do(Call{Action: "held", Caller: ControllerCaller, Pulls: []PullAt{{thePull, "abc1234def"}}}) if err != nil { t.Fatal(err) } ha := got.(*HeldAnswer) if len(ha.Held) != 1 || ha.Held[0].Module != "n8n" || ha.Held[0].Node != "ace" || ha.Held[0].Key != "media-gid" || string(ha.Held[0].Value) != "2000" || !strings.Contains(ha.Held[0].Approved, "approved by the operator") || ha.Held[0].Pull != thePull || len(ha.Skipped) != 0 { t.Errorf("held answered %+v", ha) } // Another head, and another pull request, are not answered. got, _ = c.Do(Call{Action: "held", Caller: ControllerCaller, Pulls: []PullAt{{thePull, "ffff0000"}, {"novox/mesh-catalog#435", "abc"}}}) if ha = got.(*HeldAnswer); len(ha.Held) != 0 || len(ha.Skipped) != 1 || !strings.Contains(ha.Skipped[0].Why, "held at") { t.Errorf("held at another head answered %+v", ha) } // The listing shows it as held, and the proposal as held for the pull request. l, _ := c.List(Call{Module: "n8n"}) if len(l.Held) != 1 || len(l.Set) != 0 { t.Errorf("the listing: held %+v set %+v", l.Held, l.Set) } rows, _ := c.Proposals(Call{}) if len(rows) != 1 || rows[0].Pull != thePull || rows[0].State != ProposalApproved { t.Errorf("proposals %+v", rows) } } // 1826/K2: an Approve is refused when the pull request's declaration changed after the operator was shown it. func TestAnApproveOfAHeldValueIsRefusedWhenTheDeclarationChanged(t *testing.T) { c, m, r, _ := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) a := propose(t, c, Call{Module: "n8n", Node: "ace", Key: "media-gid", Value: 2000.0, Pull: thePull, Why: "w"}) changed := n8nPullDeclares() changed["n8n"].(map[string]any)["media-gid"].(map[string]any)["maximum"] = 1000.0 tell(t, c, thePull, "def5678abc", changed) r.choose(t, a.Proposal, OptionApprove, nil) said, _ := c.Decide() if len(said) != 1 || !strings.Contains(said[0], "nothing is held") || keptCount(m) != 0 { t.Errorf("decided %v; %d held", said, keptCount(m)) } } // 1826/K1: at a new head a held value is kept if its key is declared the same and it still passes; otherwise it is // dropped, recorded, raised and said. func TestANewHeadKeepsAHeldValueOnlyWhileItsDeclarationIsUnchanged(t *testing.T) { c, m, r, _ := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) holdIt(t, c, m, r) m.events = nil // The same declaration at a new head (the description may differ): kept, moved to the new head. same := n8nPullDeclares() same["n8n"].(map[string]any)["media-gid"].(map[string]any)["description"] = "reworded" if a := tell(t, c, thePull, "1111111111", same); len(a.Dropped) != 0 { t.Errorf("an unchanged declaration dropped %v", a.Dropped) } got, _ := c.Do(Call{Action: "held", Caller: ControllerCaller, Pulls: []PullAt{{thePull, "1111111111"}}}) if len(got.(*HeldAnswer).Held) != 1 || len(m.events) != 0 { t.Errorf("after an unchanged head: %+v, events %+v", got, m.events) } // A changed declaration: dropped. changed := n8nPullDeclares() changed["n8n"].(map[string]any)["media-gid"].(map[string]any)["maximum"] = 1000.0 a := tell(t, c, thePull, "2222222222", changed) if len(a.Dropped) != 1 || !strings.Contains(a.Dropped[0], "declares the key otherwise") || keptCount(m) != 0 { t.Fatalf("a changed declaration: %+v, %d held", a, keptCount(m)) } if len(m.events) != 1 || m.events[0].Action != ActionDrop || m.events[0].Pull != thePull { t.Errorf("raised %+v", m.events) } got, _ = c.Do(Call{Action: "held", Caller: ControllerCaller, Pulls: []PullAt{{thePull, "2222222222"}}}) if len(got.(*HeldAnswer).Held) != 0 { t.Error("a dropped value was answered") } // A head that no longer adds the key drops it too. tell(t, c, thePull, "3333333333", n8nPullDeclares()) holdIt(t, c, m, r) if a = tell(t, c, thePull, "4444444444", map[string]any{}); len(a.Dropped) != 1 || !strings.Contains(a.Dropped[0], "no longer adds or changes") { t.Errorf("a head without the key: %+v", a) } } // 1826/K3: the merge writes the held value as the module's own value with its approval kept, before the trunk // declares the key; it resolves right after the declared-config mirror moves. func TestTheMergeAppliesAHeldValueAndItResolvesOnceTheTrunkDeclaresTheKey(t *testing.T) { c, m, r, _ := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) holdIt(t, c, m, r) approved := func() string { raw, _ := m.Get(KeptState, "novox.mesh-catalog.434.ace.n8n.media-gid") var k KeptValue _ = json.Unmarshal(raw.Value, &k) return k.Approved }() m.events, m.handActs = nil, nil said := c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434, Merged: true, HeadSHA: "abc1234def"}, true) if len(said) == 0 { t.Fatal("the merge said nothing") } v, set := storedValue(t, m, "module.ace.n8n.media-gid") if !set || string(v.Value) != "2000" || v.Approved != approved || v.Approved == "" || v.By != "the operator" || !strings.Contains(v.Why, thePull) { t.Fatalf("the merge wrote %+v", v) } if keptCount(m) != 0 { t.Error("the value is still held after the merge") } if p, _ := m.Keys(PullsState); len(p) != 0 { t.Errorf("the pull request's declarations are still kept: %v", p) } if len(m.events) != 1 || m.events[0].Action != "set" || m.events[0].Pull != thePull || m.events[0].Approved != approved || len(m.handActs) != 1 { t.Errorf("raised %+v, recorded %q", m.events, m.handActs) } // Not resolved before the trunk declares it. if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "n8n", "media-gid")); e != nil { t.Errorf("resolved before the trunk declares the key: %s", e.Value) } m.declare(t, "n8n", `{"module":"n8n","settings":{"media-gid":{"kind":"","default":null,"why":"",`+ `"type":"number","required":true,"minimum":1,"maximum":65535}}}`) if _, err := c.Reconcile(); err != nil { t.Fatal(err) } if res := m.resolved(t, "ace", "n8n", "media-gid"); string(res.Value) != "2000" || res.Source != SourceModule { t.Errorf("media-gid resolves as %+v", res) } l, _ := c.List(Call{Module: "n8n", Node: "ace"}) if len(l.Set) != 1 || l.Set[0].Approved != approved || len(l.Held) != 0 { t.Errorf("the listing: set %+v held %+v", l.Set, l.Held) } // A merge heard twice sets nothing twice. m.events = nil c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434, Merged: true, HeadSHA: "abc1234def"}, true) if len(m.events) != 0 { t.Errorf("a second merge raised %+v", m.events) } } // 1826/K3: a pull request closed unmerged drops its held values and says so; nothing is set or resolved. func TestAClosedPullRequestDropsItsHeldValuesAndSaysSo(t *testing.T) { c, m, r, f := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) holdIt(t, c, m, r) m.events = nil f.state[thePull] = [2]bool{false, false} said := c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434}, false) if len(said) == 0 || !strings.Contains(strings.Join(said, "\n"), "closed unmerged: dropped, nothing was set") { t.Errorf("said %v", said) } if _, set := storedValue(t, m, "module.ace.n8n.media-gid"); set || keptCount(m) != 0 { t.Error("a closed pull request left a value") } if len(m.events) != 1 || m.events[0].Action != ActionDrop || m.events[0].Pull != thePull || !strings.Contains(m.events[0].Why, "closed unmerged") { t.Errorf("raised %+v", m.events) } if p, _ := m.Keys(PullsState); len(p) != 0 { t.Errorf("declarations of a closed pull request kept: %v", p) } } // 1826/K3: a close event for a pull request the forge says merged is the merge. func TestACloseEventForAMergedPullRequestIsItsMerge(t *testing.T) { c, m, r, f := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) holdIt(t, c, m, r) f.state[thePull] = [2]bool{false, true} f.head = "abc1234def" c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434}, false) if v, set := storedValue(t, m, "module.ace.n8n.media-gid"); !set || string(v.Value) != "2000" { t.Errorf("the merge of a pull request announced closed set %+v", v) } } // 1826/K3: the five-minute look finds a merge, a close, and an open pull request, and says a forge that does not // answer. func TestTheLookAtTheForgeFindsAMissedMergeOrCloseAndLeavesAnOpenPullRequest(t *testing.T) { c, m, r, f := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) holdIt(t, c, m, r) if said := c.SweepPulls(); len(said) != 0 || keptCount(m) != 1 { t.Errorf("an open pull request: said %v, %d held", said, keptCount(m)) } f.err = errors.New("the forge is down") if said := c.SweepPulls(); len(said) != 1 || !strings.Contains(said[0], "does not say whether") || keptCount(m) != 1 { t.Errorf("a silent forge: said %v", said) } f.err = nil f.head = "abc1234def" f.state[thePull] = [2]bool{false, true} c.SweepPulls() if v, set := storedValue(t, m, "module.ace.n8n.media-gid"); !set || string(v.Value) != "2000" || keptCount(m) != 0 { t.Errorf("a missed merge: %+v", v) } // A missed close. tell(t, c, "novox/mesh-catalog#440", "abc", n8nPullDeclares()) f.state["novox/mesh-catalog#440"] = [2]bool{false, false} c.SweepPulls() if p, _ := m.Keys(PullsState); len(p) != 0 { t.Errorf("a missed close left %v", p) } } // 1826/K1: the new arguments are read strictly and the new actions are the verb's. func TestThePullArgumentsAreRead(t *testing.T) { call, err := callOf(map[string]any{"action": "pull-declares", "pull": " " + thePull + " ", "head": "abc", "declares": map[string]any{"n8n": map[string]any{}}, "pulls": []any{map[string]any{"pull": thePull, "head": "h"}}}) if err != nil || call.Pull != thePull || call.Head != "abc" || len(call.Declares) != 1 || len(call.Pulls) != 1 || call.Pulls[0].Head != "h" { t.Errorf("read %+v (%v)", call, err) } if _, err := callOf(map[string]any{"declares": "x"}); err == nil { t.Error("declares that is not an object was read") } if _, err := callOf(map[string]any{"pulls": "x"}); err == nil { t.Error("pulls that is not a list was read") } if _, err := callOf(map[string]any{"bogus": 1}); err == nil || !strings.Contains(err.Error(), "pull") { t.Errorf("an unknown argument: %v", err) } _ = time.Second } // 1826 review r1: the merge applies a value only if it is held at the merged head; otherwise it is dropped and said. func TestTheMergeAppliesOnlyAValueHeldAtTheMergedHead(t *testing.T) { for name, head := range map[string]string{"another head": "9999999999", "no head told": ""} { c, m, r, _ := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) holdIt(t, c, m, r) said := c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434, Merged: true, HeadSHA: head}, true) if _, set := storedValue(t, m, "module.ace.n8n.media-gid"); set || keptCount(m) != 0 || !strings.Contains(strings.Join(said, "\n"), "dropped, nothing was set") { t.Errorf("%s: set=%v held=%d said %v", name, set, keptCount(m), said) } if len(m.events) == 0 || m.events[len(m.events)-1].Action != ActionDrop { t.Errorf("%s: raised %+v", name, m.events) } } } // 1826 review r1: a later head without the declaration drops the value, so the merge has nothing to apply. func TestALaterHeadWithoutTheDeclarationLeavesNothingToApplyAtTheMerge(t *testing.T) { c, m, r, _ := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) holdIt(t, c, m, r) tell(t, c, thePull, "bbbbbbbbbb", map[string]any{}) c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434, Merged: true, HeadSHA: "bbbbbbbbbb"}, true) if _, set := storedValue(t, m, "module.ace.n8n.media-gid"); set { t.Error("a value held for a declaration the merged head lacks was set") } } // 1826 review r1: a close the forge cannot be asked about drops nothing; the five-minute look decides. func TestACloseThatTheForgeCannotBeAskedAboutDropsNothing(t *testing.T) { c, m, r, f := aHoldingMesh(t) tell(t, c, thePull, "abc1234def", n8nPullDeclares()) holdIt(t, c, m, r) f.err = errors.New("the forge is down") c.HeardPull(PullEvent{Owner: "novox", Repo: "mesh-catalog", Number: 434}, false) if keptCount(m) != 1 { t.Fatal("a close the forge could not confirm dropped the value") } f.err = nil f.state[thePull] = [2]bool{false, false} c.SweepPulls() if keptCount(m) != 0 { t.Error("the look did not drop it") } }