package reconcile import ( "sort" "strings" "testing" ) func TestNormDescription_TrimsTrailingNewlines(t *testing.T) { cases := map[string]string{ "felhom-selftest 2026": "felhom-selftest 2026", "felhom-selftest 2026\n": "felhom-selftest 2026", // PVE's single trailing \n "x\n\n": "x", // defensive: multiple "": "", "\n": "", "keep trailing spaces ": "keep trailing spaces ", // only newlines stripped } for in, want := range cases { if got := NormDescription(in); got != want { t.Errorf("NormDescription(%q) = %q, want %q", in, got, want) } } // Idempotent. if NormDescription(NormDescription("a\n")) != NormDescription("a\n") { t.Error("NormDescription not idempotent") } } func TestFieldNormalizers_DescriptionRoundTrip(t *testing.T) { n := DefaultNormalizers() // A value the agent wrote ("...Z") and read back with PVE's newline ("...Z\n") // must compare EQUAL — the whole point of the layer. if !n.Equal("description", "felhom-op", "felhom-op\n") { t.Error("description round-trip should normalize equal") } if n.Equal("description", "felhom-op", "different") { t.Error("genuinely different descriptions must not be equal") } } func TestFieldNormalizers_UnknownFieldIsIdentity(t *testing.T) { n := DefaultNormalizers() if n.Norm("cores", "2\n") != "2\n" { t.Error("a field with no normalizer must compare verbatim") } if n.Equal("cores", "2", "2\n") { t.Error("unknown field must not normalize away differences") } } // TestFieldNormalizers_ExtensibilitySeam proves the structure accepts new quirks // (boolean coercion, list ordering) the way it will as they're discovered — the task's // "structure it so other normalizers slot in." These are synthetic, not production. func TestFieldNormalizers_ExtensibilitySeam(t *testing.T) { booleanCoerce := func(s string) string { switch strings.ToLower(strings.TrimSpace(s)) { case "1", "true", "on", "yes": return "1" default: return "0" } } sortCSV := func(s string) string { parts := strings.Split(s, ",") sort.Strings(parts) return strings.Join(parts, ",") } n := FieldNormalizers{ "description": NormDescription, "onboot": booleanCoerce, "tags": sortCSV, } if !n.Equal("onboot", "true", "1") || !n.Equal("onboot", "on", "1") { t.Error("boolean coercion normalizer should equate truthy forms") } if n.Equal("onboot", "true", "0") { t.Error("boolean coercion must still distinguish true from false") } if !n.Equal("tags", "b,a,c", "a,b,c") { t.Error("list-ordering normalizer should equate reordered lists") } // The built-in still works alongside the synthetic ones. if !n.Equal("description", "d", "d\n") { t.Error("description normalizer should coexist with added ones") } }