From db0cb1e15e39ecd5de2d868478ded3a55c28929f Mon Sep 17 00:00:00 2001 From: jochen Date: Tue, 13 Oct 2026 00:40:24 +0200 Subject: [PATCH] =?UTF-8?q?Refuse=20a=20declaring=20module's=20mergeable?= =?UTF-8?q?=20file=20that=20names=20no=20keys=20(hq=20issue=20574,=20ADR?= =?UTF-8?q?=200326=20=C2=A73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A module that declares a setting and owns a mergeable file naming no keys still let every key reach that file. The refusal was held back in #272 because betterbird was one; mesh-catalog #252 named its keys, and every module in mesh-catalog and mesh-media-catalog main now passes, so the module check and registration refuse it again. A stored definition is still composed as it is, never left out of a machine. --- internal/catalogue/setting_takes.go | 21 +++++++++++++++---- internal/catalogue/setting_takes_test.go | 26 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/internal/catalogue/setting_takes.go b/internal/catalogue/setting_takes.go index bfc83c57..950f69d0 100644 --- a/internal/catalogue/setting_takes.go +++ b/internal/catalogue/setting_takes.go @@ -70,10 +70,23 @@ func namesItsKeys(m Manifest) bool { // reads itself, one the file keeps for itself, or one mergeable file naming its keys beside another // that names none. func TakenSettingsProblems(m Manifest) []string { - // Not yet here: refusing a module that declares a setting and owns a mergeable file naming no keys - // (ADR 0326 §3). betterbird is one, and the catalogue cannot name its keys before this controller - // runs, so that refusal follows once the catalogue does (novox/hq issue 574). - return takenSettingsProblems(m) + problems := takenSettingsProblems(m) + // **A module that declares a setting names the keys of every mergeable file** (ADR 0326 §3, ADR + // 0164 §11 and §15; novox/hq issue 574): only a module that declares none keeps today's form. Said + // where a definition is checked and registered, never where a stored one is composed: a definition + // stored before this rule is brought into the form by its next change, not left out of a machine + // meanwhile. + if len(m.Settings) > 0 { + for _, r := range m.Resources { + how, _ := r["merge"].(string) + if _, said := r[TakesSettingsField]; how != "" && !said { + problems = append(problems, fmt.Sprintf("%s declares settings, and its mergeable %v names no %s: "+ + "a module that declares a setting names the keys each mergeable file takes, an empty list "+ + "for none (novox/hq ADR 0326 §3, ADR 0164 §11)", m.Module, r["id"], TakesSettingsField)) + } + } + } + return problems } // takenSettingsProblems is every rule of ADR 0326 §2 and §4, judged where a definition is checked and diff --git a/internal/catalogue/setting_takes_test.go b/internal/catalogue/setting_takes_test.go index 06ddc3a3..a4503cea 100644 --- a/internal/catalogue/setting_takes_test.go +++ b/internal/catalogue/setting_takes_test.go @@ -194,3 +194,29 @@ func containsText(list []string, text string) bool { } return false } + +func TestAModuleThatDeclaresASettingNamesItsFilesKeys(t *testing.T) { + // ADR 0326 §3, ADR 0164 §15, novox/hq issue 574: only a module that declares no settings keeps + // today's form. One that declares a preference and owns a silent mergeable file is refused where it + // is checked — and not where a stored definition is composed, so a machine does not lose it before + // its next change. + m := twoFiles(nil, nil) + m.Settings = map[string]SettingDeclaration{"site-name": {Kind: KindPreference, Default: "Search", Why: "a name"}} + problems := TakenSettingsProblems(m) + if len(problems) != 2 || !containsText(problems, "app names no takes-settings") || + !containsText(problems, "sidecar names no takes-settings") { + t.Errorf("problems: %v", problems) + } + if err := JudgeSettings(m, nil, false); err != nil { + t.Errorf("a stored definition is composed as before, and was refused: %v", err) + } + m = twoFiles([]any{"server"}, []any{}) + m.Settings = map[string]SettingDeclaration{"site-name": {Kind: KindPreference, Default: "Search", Why: "a name"}} + if problems := TakenSettingsProblems(m); len(problems) != 0 { + t.Errorf("every mergeable file names its keys, and was refused: %v", problems) + } + // And a module that declares none keeps today's form, silent files and all. + if problems := TakenSettingsProblems(twoFiles(nil, nil)); len(problems) != 0 { + t.Errorf("a module declaring no setting was refused: %v", problems) + } +}