package configkit_test import ( "errors" "strings" "testing" "go.maximumdirect.net/configkit" ) type resolutionSequenceItem struct { ID string `yaml:"id"` Value string `yaml:"value"` } type resolutionSequenceConfig struct { Groups map[string][]resolutionSequenceItem `yaml:"groups"` } func TestPublicResolutionOrdinaryListField(t *testing.T) { type config struct { Items []resolutionSequenceItem `yaml:"items"` } definition := configkit.Definition[config]{Resolve: func(value *config, r *configkit.Resolution) error { configkit.Normalize(r, configkit.Keys("items").Index(0).Key("value"), &value.Items[0].Value, "changed", "normalize") return nil }} result, err := configkit.Load(configkit.Bytes("items", []byte("items: [{id: a, value: old}]\n")), configkit.Options[config]{Definition: definition}) if err != nil || result.Config.Items[0].Value != "changed" { t.Fatalf("result = %#v, %v", result, err) } } func TestPublicRegisteredResolutionHistoryAndOwnership(t *testing.T) { selector := configkit.SequenceSelector{configkit.SequenceLiteralKey("groups"), configkit.SequenceAnyMappingKey()} path := configkit.Keys("groups", "active") entryFixed := false definition := configkit.Definition[resolutionSequenceConfig]{ SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityMatching(selector, "id")}, Resolve: func(value *resolutionSequenceConfig, r *configkit.Resolution) error { entryFixed = r.IsExplicit(path.Index(0).Key("value")) && !r.IsExplicit(path.Index(1).Key("value")) items := value.Groups["active"] items[0], items[1] = items[1], items[0] items[1].Value = "normalized" value.Groups["active"] = items r.RecordNormalization(path, "reorder") r.RecordNormalization(path.Index(1).Key("value"), "normalize") return nil }, } source := configkit.Bytes("records", []byte("groups:\n active:\n - id: a\n value: original\n - id: b\n")) load := func() *configkit.Result[resolutionSequenceConfig] { result, err := configkit.Load(source, configkit.Options[resolutionSequenceConfig]{Definition: definition}) if err != nil { t.Fatal(err) } return result } result := load() if !entryFixed || result.Config.Groups["active"][1].Value != "normalized" { t.Fatalf("result = %#v", result.Config) } info, ok := result.Metadata().Lookup(path.Index(1).Key("value")) if !ok || !info.Explicit || info.Origin.Layer != configkit.LayerResolution { t.Fatalf("metadata = %#v, %t", info, ok) } explanation, ok := result.Explain(path.Index(1).Key("value")) if !ok { t.Fatal("missing explanation") } found := false for _, change := range explanation.History { if change.Origin.Layer == configkit.LayerResolution && change.Before.YAML == "original\n" && change.After.YAML == "normalized\n" { found = true } } if !found { t.Fatalf("history = %#v", explanation.History) } other := load() result.Config.Groups["active"][1].Value = "mutated" if len(explanation.History) != 0 { explanation.History[0].After.YAML = "mutated" } again, _ := result.Explain(path.Index(1).Key("value")) if strings.Contains(again.Effective.YAML, "mutated") || other.Config.Groups["active"][1].Value != "normalized" { t.Fatal("inspection or independent load changed") } plain, err := configkit.Load[resolutionSequenceConfig](source) if err != nil { t.Fatal(err) } differences, err := configkit.Diff(plain, result) if err != nil || len(differences) != 1 || differences[0].Path.String() != "/groups/active" { t.Fatalf("diff = %#v, %v", differences, err) } layered, err := configkit.LoadLayered(configkit.Input{Source: source}, configkit.LayeredOptions[resolutionSequenceConfig]{Definition: definition}) if err != nil || layered.Config.Groups["active"][1].Value != other.Config.Groups["active"][1].Value { t.Fatalf("layered = %#v, %v", layered, err) } } func TestPublicRegisteredMoveRequiresContainerRecord(t *testing.T) { type config struct { Items []resolutionSequenceItem `yaml:"items"` } _, err := configkit.Load(configkit.Bytes("items", []byte("items: [{id: a}, {id: b}]\n")), configkit.Options[config]{Definition: configkit.Definition[config]{ SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "id")}, Resolve: func(value *config, r *configkit.Resolution) error { value.Items[0], value.Items[1] = value.Items[1], value.Items[0] r.RecordNormalization(configkit.Keys("items").Index(1).Key("id"), "record field") return nil }, }}) var failure *configkit.LoadError if !errors.As(err, &failure) { t.Fatalf("error = %v", err) } for _, diagnostic := range failure.Diagnostics() { if diagnostic.Code == configkit.CodeResolveUnrecordedChange && diagnostic.Path.String() == "/items" { return } } t.Fatalf("diagnostics = %#v", failure.Diagnostics()) } func TestPublicRegisteredRemovalKeepsSurvivorHistory(t *testing.T) { type config struct { Items []resolutionSequenceItem `yaml:"items"` } definition := configkit.Definition[config]{ SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "id")}, Resolve: func(value *config, r *configkit.Resolution) error { value.Items = value.Items[1:] r.RecordNormalization(configkit.Keys("items"), "remove first") return nil }, } result, err := configkit.Load(configkit.Bytes("items", []byte("items:\n - id: reports\n value: report\n - id: invoices\n value: invoice\n")), configkit.Options[config]{Definition: definition}) if err != nil { t.Fatal(err) } explanation, ok := result.Explain(configkit.Keys("items").Index(0).Key("value")) if !ok { t.Fatal("missing survivor") } for _, change := range explanation.History { if change.Origin.Layer == configkit.LayerMain && change.After.YAML == "report\n" { t.Fatalf("wrong lineage = %#v", explanation.History) } } container, ok := result.Explain(configkit.Keys("items")) if !ok { t.Fatal("missing collection explanation") } found := false for _, change := range container.History { if change.Origin.Layer == configkit.LayerResolution && strings.Contains(change.Before.YAML, "reports") { found = true } } if !found { t.Fatalf("missing removal evidence = %#v", container.History) } if _, ok := result.Explain(configkit.Keys("items").Index(1)); !ok { t.Fatal("missing historical position") } } func TestPublicYAMLIdentityRemainsPositionalDuringResolution(t *testing.T) { type config struct { Items []resolutionSequenceItem `yaml:"items"` } result, err := configkit.Load(configkit.Bytes("items", []byte("config: {sequences: [{destination: [items], key: id}]}\nitems: [{id: a, value: first}, {id: b, value: second}]\n")), configkit.Options[config]{Definition: configkit.Definition[config]{Resolve: func(value *config, r *configkit.Resolution) error { value.Items[0], value.Items[1] = value.Items[1], value.Items[0] r.RecordNormalization(configkit.Keys("items"), "reorder") return nil }}}) if err != nil { t.Fatal(err) } explanation, ok := result.Explain(configkit.Keys("items").Index(0).Key("value")) if !ok { t.Fatal("missing explanation") } for _, change := range explanation.History { if change.Origin.Layer == configkit.LayerResolution && change.Before.YAML == "first\n" && change.After.YAML == "second\n" { return } } t.Fatalf("positional history = %#v", explanation.History) } func TestPublicMovedSensitiveRecordIsHidden(t *testing.T) { type config struct { Items []resolutionSequenceItem `yaml:"items"` } result, err := configkit.Load(configkit.Bytes("items", []byte("items: [{id: a, value: FACADE_PRIVATE_CANARY}, {id: b, value: public}]\n")), configkit.Options[config]{Definition: configkit.Definition[config]{ SequenceIdentities: []configkit.SequenceIdentity{configkit.SequenceIdentityAt(configkit.Keys("items"), "id")}, Sensitive: func(path configkit.Path) bool { return path.String() == "/items/0/value" }, Resolve: func(value *config, r *configkit.Resolution) error { value.Items[0], value.Items[1] = value.Items[1], value.Items[0] r.RecordNormalization(configkit.Keys("items"), "reorder") return nil }, }}) if err != nil { t.Fatal(err) } for _, path := range []configkit.Path{configkit.Keys("items"), configkit.Keys("items").Index(1).Key("value")} { explanation, ok := result.Explain(path) if !ok { t.Fatalf("missing explanation %s", path.String()) } if strings.Contains(explanation.Effective.YAML, "FACADE_PRIVATE_CANARY") { t.Fatal("effective leak") } for _, change := range explanation.History { if strings.Contains(change.Before.YAML, "FACADE_PRIVATE_CANARY") || strings.Contains(change.After.YAML, "FACADE_PRIVATE_CANARY") { t.Fatal("history leak") } } } }