package main import ( "os" "path/filepath" "strings" "testing" ) // The tests of ADR 0534 §1, §2 (the operator, 2026-10-23): the forge requires no status to merge. Main's rule keeps no // push, no push whitelist, the tools' account alone in its merge whitelist and no administrator merging past it, and // requires no status: the program writes it so on every repository it lists, and takes every required status off a // rule covering main in every repository of the organisation it does not list. Nothing else of an unlisted // repository's rule is sent. func narrowedRule(contexts ...any) map[string]any { return map[string]any{ "enable_push": false, "enable_push_whitelist": false, "push_whitelist_usernames": []any{}, "enable_merge_whitelist": true, "merge_whitelist_usernames": []any{tools}, "enable_status_check": len(contexts) > 0, "status_check_contexts": contexts, "block_admin_merge_override": true, } } // requiresNone is whether a rule as the fake forge keeps it requires no status. func requiresNone(rule map[string]any) bool { return rule["enable_status_check"] != true && len(strs(rule["status_check_contexts"])) == 0 } // 0534-1: a listed repository whose main requires statuses has them all taken off, the rest of the rule kept; a // second pass changes nothing. func TestListedRepositoryRequiresNoStatus(t *testing.T) { f := newFake() repo := f.add("novox/mesh-delivery", map[string]map[string]any{mainRule: narrowedRule("mesh/merge-gate", "mesh/repo-check", "other/x")}, "main") checked := f.add("novox/checked", map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main") s, _ := syncer(t, f) o := run(t, s, Value{"mesh-delivery": {}, "checked": {}}) for name, r := range map[string]*fRepo{"mesh-delivery": repo, "checked": checked} { rule := r.Rules[mainRule] if !requiresNone(rule) { t.Errorf("%s's main still requires %v", name, rule["status_check_contexts"]) } if rule["enable_push"] != false || rule["enable_merge_whitelist"] != true || strings.Join(strs(rule["merge_whitelist_usernames"]), ",") != tools || rule["block_admin_merge_override"] != true { t.Errorf("%s's main lost the rest of its rule: %v", name, rule) } if r := result(o, name); r.Did != DidPutRight { t.Errorf("%s outcome %+v, want put right", name, r) } } before := len(f.writes()) o = run(t, s, Value{"mesh-delivery": {}, "checked": {}}) if w := f.writes(); len(w) != before { t.Errorf("a second pass wrote %v", w[before:]) } if result(o, "checked").Did != DidUnchanged { t.Errorf("second outcome %+v, want unchanged", result(o, "checked")) } } // 0534-1: a repository the program makes requires no status from its first rule in the organisation on. func TestNewRepositoryRequiresNoStatus(t *testing.T) { f := newFake() s, _ := syncer(t, f) run(t, s, Value{"fresh": {Description: "d"}}) rule := f.repos["novox/fresh"].Rules[mainRule] if !requiresNone(rule) { t.Errorf("a new repository's main requires %v, want none", rule["status_check_contexts"]) } if readme := f.repos["novox/fresh"].Files["README.md"]; strings.Contains(readme, "mesh/merge-gate") || strings.Contains(readme, "mesh/repo-check") { t.Errorf("the README names the statuses of before: %q", readme) } } // 0534-1: a listed repository whose main requires nothing stays so, and is not written when the rest is right. func TestListedRuleRequiringNothingIsRight(t *testing.T) { f := newFake() none := f.add("novox/none", map[string]map[string]any{mainRule: narrowedRule()}, "main") s, _ := syncer(t, f) o := run(t, s, Value{"none": {}}) if !requiresNone(none.Rules[mainRule]) { t.Errorf("none requires %v", none.Rules[mainRule]["status_check_contexts"]) } if result(o, "none").Did != DidUnchanged { t.Errorf("outcome %+v, want unchanged", result(o, "none")) } for _, w := range f.writes() { t.Errorf("a right rule was written: %s", w) } } // 0534-1: hq, whose main required mesh/repo-check alone, requires no status either, listed or not. func TestOwnCheckAloneGoesToo(t *testing.T) { f := newFake() hq := f.add("novox/hq", map[string]map[string]any{mainRule: narrowedRule("mesh/repo-check")}, "main") lab := f.add("novox/mesh-lab", map[string]map[string]any{mainRule: narrowedRule("mesh/repo-check")}, "main") s, _ := syncer(t, f) o := run(t, s, Value{"hq": {}}) for name, repo := range map[string]*fRepo{"hq": hq, "mesh-lab": lab} { if !requiresNone(repo.Rules[mainRule]) { t.Errorf("%s requires %v, want none", name, repo.Rules[mainRule]["status_check_contexts"]) } if r := result(o, name); r.Did != DidPutRight { t.Errorf("%s outcome %+v, want put right", name, r) } } } // 0534-1: a repository the value does not list whose rule covering main requires any status has them taken off, and // only its statuses: nothing else of its rule is sent. One that requires none is not written to. func TestUnlistedRepositoryRequiringAStatusHasItTakenOff(t *testing.T) { f := newFake() catalog := f.add("novox/mesh-catalog", map[string]map[string]any{mainRule: { "enable_push": true, "enable_status_check": true, "status_check_contexts": []any{"mesh/check"}, }}, "main") toolsGo := f.add("novox/mesh-tools-go", map[string]map[string]any{mainRule: narrowedRule("mesh/merge-gate")}, "main") f.add("novox/plain", map[string]map[string]any{mainRule: {"enable_push": false}}, "main") f.add("novox/unticked", map[string]map[string]any{mainRule: {"enable_push": false, "enable_status_check": false, "status_check_contexts": []any{"mesh/check"}}}, "main") f.add("novox/norule", nil, "main") f.add("mesh-admin/elsewhere", map[string]map[string]any{mainRule: narrowedRule("mesh/merge-gate")}, "main") s, _ := syncer(t, f) o := run(t, s, Value{}) if !requiresNone(catalog.Rules[mainRule]) { t.Errorf("mesh-catalog requires %v, want none", catalog.Rules[mainRule]["status_check_contexts"]) } if catalog.Rules[mainRule]["enable_push"] != true { t.Errorf("an unlisted repository's rule was changed beyond its statuses: %v", catalog.Rules[mainRule]) } if !requiresNone(toolsGo.Rules[mainRule]) { t.Errorf("mesh-tools-go requires %v, want none", toolsGo.Rules[mainRule]["status_check_contexts"]) } want := []string{ "PATCH /api/v1/repos/novox/mesh-catalog/branch_protections/main", "PATCH /api/v1/repos/novox/mesh-tools-go/branch_protections/main", } if got := f.writes(); strings.Join(got, "\n") != strings.Join(want, "\n") { t.Errorf("writes:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n")) } for _, n := range []string{"mesh-catalog", "mesh-tools-go"} { if r := result(o, n); r.Did != DidPutRight { t.Errorf("%s outcome %+v, want put right", n, r) } } for _, n := range []string{"plain", "unticked", "norule", "elsewhere"} { if r := result(o, n); r.Name != "" { t.Errorf("%s is in the outcome though nothing was done: %+v", n, r) } } before := len(f.writes()) run(t, s, Value{}) if w := f.writes(); len(w) != before { t.Errorf("a second pass wrote %v", w[before:]) } } // 0534-1: the organisation's repositories are read page by page, so one past the first page is put right too. func TestUnlistedRepositoriesAreReadPastTheFirstPage(t *testing.T) { f := newFake() f.pageSize = 2 for _, n := range []string{"a", "b", "c", "d", "e"} { f.add("novox/"+n, map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main") } s, _ := syncer(t, f) run(t, s, Value{}) for _, n := range []string{"a", "b", "c", "d", "e"} { if rule := f.repos["novox/"+n].Rules[mainRule]; !requiresNone(rule) { t.Errorf("%s requires %v", n, rule["status_check_contexts"]) } } } // 0534-1: a refused removal is said with the repository and its step, and the pass goes on to the next. func TestRefusedRemovalIsSaid(t *testing.T) { f := newFake() f.add("novox/a", map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main") f.add("novox/b", map[string]map[string]any{mainRule: narrowedRule("mesh/check")}, "main") f.refuse = func(method, path string) (int, string) { if method == "PATCH" && strings.Contains(path, "/novox/a/") { return 403, "no" } return 0, "" } s, _ := syncer(t, f) o := run(t, s, Value{}) if r := result(o, "a"); r.Did != DidRefused || r.Step != StepNoStatus { t.Errorf("a: %+v, want refused at %s", r, StepNoStatus) } if r := result(o, "b"); r.Did != DidPutRight { t.Errorf("b: %+v, want put right", r) } } // 1301-S3: the organisation's list refused is said, and the listed repositories are still done. func TestOrganisationListRefusedIsSaid(t *testing.T) { f := newFake() f.refuse = func(method, path string) (int, string) { if strings.HasPrefix(path, "/api/v1/orgs/") { return 500, "down" } return 0, "" } s, _ := syncer(t, f) o := run(t, s, Value{"x": {}}) if result(o, "x").Did != DidMade { t.Errorf("x: %+v", result(o, "x")) } refusedList := false for _, r := range o.Refused() { refusedList = refusedList || r.Step == StepList } if !refusedList { t.Errorf("the refused list is not in the outcome: %+v", o) } } // 0534-2 (the operator, 2026-10-23 ~12:05): the operator's own forge login stays on main's merge whitelist beside the // tools' account, so the operator can always merge by hand at the forge; a rule naming the tools' account alone is put // right, and a second pass changes nothing. func TestTheOperatorsLoginStaysBesideTheTools(t *testing.T) { f := newFake() repo := f.add("novox/mesh-delivery", map[string]map[string]any{mainRule: narrowedRule()}, "main") s, _ := syncer(t, f) s.Mergers = []string{"jschoubben", tools, "jschoubben"} o := run(t, s, Value{"mesh-delivery": {}, "fresh": {}}) for name, rule := range map[string]map[string]any{"mesh-delivery": repo.Rules[mainRule], "fresh": f.repos["novox/fresh"].Rules[mainRule]} { if got := strings.Join(strs(rule["merge_whitelist_usernames"]), ","); got != tools+",jschoubben" { t.Errorf("%s's merge whitelist is %s, want %s and jschoubben", name, got, tools) } if !requiresNone(rule) || rule["enable_push"] != false { t.Errorf("%s's rule: %v", name, rule) } } if r := result(o, "mesh-delivery"); r.Did != DidPutRight { t.Errorf("outcome %+v, want put right", r) } before := len(f.writes()) run(t, s, Value{"mesh-delivery": {}, "fresh": {}}) if w := f.writes(); len(w) != before { t.Errorf("a second pass wrote %v", w[before:]) } } // 0534-2: the mergers are read from the module's runtime config file; a missing key is none; a name that is no forge login // is refused. func TestReadMergers(t *testing.T) { p := filepath.Join(t.TempDir(), "r.json") _ = os.WriteFile(p, []byte(`{"url":"","main-mergers":["jschoubben"]}`), 0o600) if m, err := ReadMergers(p); err != nil || strings.Join(m, ",") != "jschoubben" { t.Fatalf("%v %v", m, err) } _ = os.WriteFile(p, []byte(`{"url":""}`), 0o600) if m, err := ReadMergers(p); err != nil || len(m) != 0 { t.Fatalf("%v %v", m, err) } for _, bad := range []string{`["a b"]`, `["../x"]`, `[""]`, `"jschoubben"`} { _ = os.WriteFile(p, []byte(`{"main-mergers":`+bad+`}`), 0o600) if _, err := ReadMergers(p); err == nil { t.Errorf("%s was accepted", bad) } } }