Carry a config value held for a pull request's new key into its composed check (issue 1826)
The controller tells mesh-config what a pull request declares, asks the approved held values with each check, and the build agent and merge gate lay them over the change's composition only: a key the trunk does not declare yet can then be approved before the merge (ADR 0502).
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
const (
|
||||
headSHA = "2222222222222222222222222222222222222222"
|
||||
baseSHA = "1111111111111111111111111111111111111111"
|
||||
)
|
||||
|
||||
// fakeManifests serves manifests by ref, and counts what the forge was asked. Issue 1826/AC1 (unit level).
|
||||
func fakeManifests(t *testing.T, at map[string]*catalogue.Manifest) {
|
||||
t.Helper()
|
||||
was := manifestAt
|
||||
t.Cleanup(func() { manifestAt = was })
|
||||
manifestsReadMu.Lock()
|
||||
clear(manifestsRead)
|
||||
manifestsReadMu.Unlock()
|
||||
manifestAt = func(_ context.Context, _, _, _, ref string) (catalogue.Manifest, bool, error) {
|
||||
if m := at[ref]; m != nil {
|
||||
return *m, true, nil
|
||||
}
|
||||
return catalogue.Manifest{}, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// fakeConfig records the calls to mesh-config and answers each action from answers.
|
||||
func fakeConfig(t *testing.T, answers map[string]string) *[]map[string]any {
|
||||
t.Helper()
|
||||
var calls []map[string]any
|
||||
was := meshConfigAsk
|
||||
t.Cleanup(func() { meshConfigAsk = was })
|
||||
meshConfigAsk = func(_ context.Context, args map[string]any) ([]byte, error) {
|
||||
calls = append(calls, args)
|
||||
return []byte(answers[args["action"].(string)]), nil
|
||||
}
|
||||
return &calls
|
||||
}
|
||||
|
||||
func manifest(module string, keys map[string]catalogue.SettingDeclaration) *catalogue.Manifest {
|
||||
return &catalogue.Manifest{Module: module, Settings: keys}
|
||||
}
|
||||
|
||||
func thePull() pullAt {
|
||||
return pullAt{Number: 434, changeSource: changeSource{Owner: "novox", Repo: "mesh-catalog", New: headSHA, Old: baseSHA,
|
||||
Paths: []string{"modules/n8n/module.json", "modules/n8n/README.md"}}}
|
||||
}
|
||||
|
||||
// 1826/AC1: a key the head adds is sent with its declaration, under the pull request and the head.
|
||||
func TestAKeyThePullRequestAddsIsToldToMeshConfig(t *testing.T) {
|
||||
fakeManifests(t, map[string]*catalogue.Manifest{
|
||||
baseSHA: manifest("n8n", map[string]catalogue.SettingDeclaration{"port": {Type: "number"}}),
|
||||
headSHA: manifest("n8n", map[string]catalogue.SettingDeclaration{"port": {Type: "number"},
|
||||
"media-gid": {Type: "number", Required: true}}),
|
||||
})
|
||||
calls := fakeConfig(t, map[string]string{"pull-declares": `{"kept":1}`})
|
||||
tellPullDeclares(context.Background(), io.Discard, []pullAt{thePull()})
|
||||
if len(*calls) != 1 {
|
||||
t.Fatalf("mesh-config was asked %d times: %v", len(*calls), *calls)
|
||||
}
|
||||
c := (*calls)[0]
|
||||
if c["action"] != "pull-declares" || c["pull"] != "novox/mesh-catalog#434" || c["head"] != headSHA {
|
||||
t.Fatalf("the call is %v", c)
|
||||
}
|
||||
body, _ := json.Marshal(c["declares"])
|
||||
var declares map[string]map[string]inventory.DeclaredMirrored
|
||||
if err := json.Unmarshal(body, &declares); err != nil || len(declares) != 1 || declares["n8n"] == nil {
|
||||
t.Fatalf("declares %s: %v", body, err)
|
||||
}
|
||||
mods := []inventory.DeclaredConfigMirrored{{Module: "n8n", Settings: declares["n8n"]}}
|
||||
if _, has := mods[0].Settings["port"]; has || len(mods[0].Settings) != 1 {
|
||||
t.Fatalf("only the added key is told, got %v", mods[0].Settings)
|
||||
}
|
||||
d := mods[0].Settings["media-gid"]
|
||||
if d.Type != "number" || !d.Required {
|
||||
t.Fatalf("the declaration is %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
// 1826/AC1: a key whose declaration the head changes is sent; one it leaves as it was is not.
|
||||
func TestAnUnchangedKeyIsNotToldAndAChangedOneIs(t *testing.T) {
|
||||
fakeManifests(t, map[string]*catalogue.Manifest{
|
||||
baseSHA: manifest("n8n", map[string]catalogue.SettingDeclaration{"port": {Type: "number"}, "x": {Type: "string"}}),
|
||||
headSHA: manifest("n8n", map[string]catalogue.SettingDeclaration{"port": {Type: "number"}, "x": {Type: "string", Pattern: "a+"}}),
|
||||
})
|
||||
mods, touched, err := pullDeclarations(context.Background(), thePull())
|
||||
if err != nil || !touched || len(mods) != 1 || len(mods[0].Settings) != 1 || mods[0].Settings["x"].Pattern != "a+" {
|
||||
t.Fatalf("%+v %v %v", mods, touched, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 1826/AC1: a pull request that changes no module.json, or removes it, tells mesh-config nothing.
|
||||
func TestAPullRequestThatDeclaresNothingSendsNothing(t *testing.T) {
|
||||
fakeManifests(t, map[string]*catalogue.Manifest{baseSHA: manifest("n8n", nil), headSHA: manifest("n8n", nil)})
|
||||
calls := fakeConfig(t, nil)
|
||||
p := thePull()
|
||||
p.Paths = []string{"modules/n8n/README.md"}
|
||||
tellPullDeclares(context.Background(), io.Discard, []pullAt{p})
|
||||
p = thePull()
|
||||
p.Removed = p.Paths
|
||||
tellPullDeclares(context.Background(), io.Discard, []pullAt{p})
|
||||
if len(*calls) != 0 {
|
||||
t.Fatalf("mesh-config was told %v", *calls)
|
||||
}
|
||||
}
|
||||
|
||||
// 1826/AC1: a module.json changed without a declaration changing is told as an empty set, so mesh-config re-judges what it holds.
|
||||
func TestAChangedManifestWithNoNewDeclarationIsToldAsEmpty(t *testing.T) {
|
||||
m := manifest("n8n", map[string]catalogue.SettingDeclaration{"port": {Type: "number"}})
|
||||
fakeManifests(t, map[string]*catalogue.Manifest{baseSHA: m, headSHA: m})
|
||||
calls := fakeConfig(t, nil)
|
||||
tellPullDeclares(context.Background(), io.Discard, []pullAt{thePull()})
|
||||
if len(*calls) != 1 {
|
||||
t.Fatalf("calls %v", *calls)
|
||||
}
|
||||
if d, _ := json.Marshal((*calls)[0]["declares"]); string(d) != "{}" {
|
||||
t.Fatalf("declares %s", d)
|
||||
}
|
||||
}
|
||||
|
||||
// 1826/AC1: mesh-config not answering is said and goes on; the check is not stopped.
|
||||
func TestMeshConfigNotAnsweringIsSaidAndGoneOn(t *testing.T) {
|
||||
fakeManifests(t, map[string]*catalogue.Manifest{baseSHA: manifest("n8n", nil),
|
||||
headSHA: manifest("n8n", map[string]catalogue.SettingDeclaration{"k": {Type: "string"}})})
|
||||
was := meshConfigAsk
|
||||
t.Cleanup(func() { meshConfigAsk = was })
|
||||
meshConfigAsk = func(context.Context, map[string]any) ([]byte, error) { return nil, io.ErrUnexpectedEOF }
|
||||
var said strings.Builder
|
||||
tellPullDeclares(context.Background(), &said, []pullAt{thePull()})
|
||||
got := heldFor(context.Background(), &said, []pullAt{thePull()})
|
||||
if len(got) != 0 || !strings.Contains(said.String(), "mesh-config") {
|
||||
t.Fatalf("held %v, said %q", got, said.String())
|
||||
}
|
||||
}
|
||||
|
||||
// 1826/AC2: the held values mesh-config answers for the check's pull requests are what the check request carries.
|
||||
func TestTheHeldValuesOfTheChecksPullRequestsAreAsked(t *testing.T) {
|
||||
calls := fakeConfig(t, map[string]string{"held": `{"held":[{"pull":"novox/mesh-catalog#434","head":"` + headSHA +
|
||||
`","node":"ace","module":"n8n","key":"media-gid","value":2000,"approved":"the operator"}]}`})
|
||||
other := pullAt{Number: 7, changeSource: changeSource{Owner: "novox", Repo: "mesh-controller", New: baseSHA}}
|
||||
got := heldFor(context.Background(), io.Discard, []pullAt{thePull(), other})
|
||||
if len(got) != 1 || got[0].Node != "ace" || got[0].Key != "media-gid" || string(got[0].Value) != "2000" {
|
||||
t.Fatalf("got %+v", got)
|
||||
}
|
||||
c := (*calls)[0]
|
||||
pulls, _ := json.Marshal(c["pulls"])
|
||||
if c["action"] != "held" || string(pulls) != `[{"pull":"novox/mesh-catalog#434","head":"`+headSHA+
|
||||
`"},{"pull":"novox/mesh-controller#7","head":"`+baseSHA+`"}]` {
|
||||
t.Fatalf("asked %v %s", c, pulls)
|
||||
}
|
||||
}
|
||||
|
||||
// 1826/AC2: a value held for a pull request the check is not of is never carried.
|
||||
func TestAHeldValueOfAnotherPullRequestIsNotCarried(t *testing.T) {
|
||||
fakeConfig(t, map[string]string{"held": `{"held":[{"pull":"novox/mesh-catalog#999","head":"x","node":"ace","module":"n8n","key":"k","value":1}]}`})
|
||||
if got := heldFor(context.Background(), io.Discard, []pullAt{thePull()}); len(got) != 0 {
|
||||
t.Fatalf("carried %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 1826/AC2: the check request carries what mesh-config answered; a member of a composed group asks none.
|
||||
func TestTheCheckRequestCarriesTheHeldValues(t *testing.T) {
|
||||
fakeManifests(t, map[string]*catalogue.Manifest{baseSHA: manifest("n8n", nil), headSHA: manifest("n8n", nil)})
|
||||
fakeConfig(t, map[string]string{"held": `{"held":[{"pull":"novox/mesh-catalog#434","node":"ace","module":"n8n","key":"media-gid","value":2000}]}`})
|
||||
request := link.BuildRequest{Check: &link.CheckRequest{}}
|
||||
withHeldConfig(context.Background(), io.Discard, &request, []pullAt{thePull()}, true)
|
||||
if len(request.Check.HeldConfig) != 1 || request.Check.HeldConfig[0].Key != "media-gid" {
|
||||
t.Fatalf("the request carries %+v", request.Check.HeldConfig)
|
||||
}
|
||||
request = link.BuildRequest{Check: &link.CheckRequest{}}
|
||||
withHeldConfig(context.Background(), io.Discard, &request, []pullAt{thePull()}, false)
|
||||
if len(request.Check.HeldConfig) != 0 {
|
||||
t.Fatalf("a composed member carries %+v", request.Check.HeldConfig)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user