package engine import ( "errors" "strings" "testing" "testing/fstest" ) func TestCompileSequenceIdentityPolicyNormalizesRegistrations(t *testing.T) { tests := []struct { name string registrations []sequenceIdentityRegistration want map[string]string wantError bool }{ { name: "identical registrations coalesce", registrations: []sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}, }, want: map[string]string{"/items": "id"}, }, { name: "different identity fields conflict", registrations: []sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"name"}}, }, wantError: true, }, { name: "empty identity field is literal", registrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{""}}}, want: map[string]string{"/items": ""}, }, { name: "numeric and escaped tokens remain literal keys", registrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items"), literalSequenceSelector("0"), literalSequenceSelector("a/b")}, identityFields: []string{"id"}}}, want: map[string]string{"/items/0/a~1b": "id"}, }, { name: "unused and root registrations are accepted", registrations: []sequenceIdentityRegistration{ {selector: nil, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("unused")}, identityFields: []string{"name"}}, }, want: map[string]string{"": "id", "/unused": "name"}, }, {name: "zero option policy", want: map[string]string{}}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { policy, diagnostics := compileSequenceIdentityPolicy(test.registrations) if (len(diagnostics) != 0) != test.wantError { t.Fatalf("diagnostics = %#v, wantError = %t", diagnostics, test.wantError) } if test.wantError { if diagnostics[0].Code != CodeOptionsInvalid { t.Fatalf("diagnostic code = %q", diagnostics[0].Code) } return } for path, field := range test.want { parsed, err := ParsePath(path) if err != nil { t.Fatal(err) } gotFields, ok := policy.identityFieldsAt(parsed) if !ok || len(gotFields) != 1 || gotFields[0] != field { t.Errorf("identity fields at %q = %#v, want [%q]", path, gotFields, field) } } }) } } func TestCompileSequenceIdentityPolicyMatchesExactAndDynamicSelectors(t *testing.T) { policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{literalSequenceSelector("prompts")}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), anyMappingSequenceSelector(), literalSequenceSelector("prompts")}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), literalSequenceSelector("weather"), literalSequenceSelector("prompts")}, identityFields: []string{"id"}}, }) if len(diagnostics) != 0 { t.Fatalf("diagnostics = %#v", diagnostics) } for _, path := range []Path{Keys("prompts"), Keys("collections", "weather", "prompts")} { fields, matched := policy.identityFieldsAt(path) if !matched || !equalSequenceIdentityFields(fields, []string{"id"}) { t.Errorf("identityFieldsAt(%q) = %#v, %t", path.String(), fields, matched) } } if _, matched := policy.identityFieldsAt(Keys("collections", "weather", "other")); matched { t.Fatal("selector matched a different destination") } } func TestDynamicSequenceIdentityScopesRemainIndependent(t *testing.T) { policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{{ selector: sequenceIdentitySelector{literalSequenceSelector("collections"), anyMappingSequenceSelector(), literalSequenceSelector("prompts")}, identityFields: []string{"id"}, }}) if len(diagnostics) != 0 { t.Fatalf("diagnostics = %#v", diagnostics) } input := parseCompositionDocument(t, "source", "collections:\n weatherreporter:\n prompts: [{id: shared}]\n homewatch:\n prompts: [{id: shared}]\n") if _, diagnostics := composeDocumentsWithPolicy(policy, input); len(diagnostics) != 0 { t.Fatalf("same identity in distinct matched destinations conflicted: %#v", diagnostics) } } func TestCompileSequenceIdentityPolicyMatchesMultipleWildcardLevelsAndScopes(t *testing.T) { policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{ literalSequenceSelector("collections"), anyMappingSequenceSelector(), literalSequenceSelector("groups"), anyMappingSequenceSelector(), literalSequenceSelector("prompts"), }, identityFields: []string{"namespace", "id"}}, }) if len(diagnostics) != 0 { t.Fatalf("diagnostics = %#v", diagnostics) } for _, path := range []Path{ Keys("collections", "weather", "groups", "one", "prompts"), Keys("collections", "weather", "groups", "two", "prompts"), } { fields, matched := policy.identityFieldsAt(path) if !matched || !equalSequenceIdentityFields(fields, []string{"id", "namespace"}) { t.Errorf("identityFieldsAt(%q) = %#v, %t", path.String(), fields, matched) } } if !policy.registered(Keys("collections", "weather", "groups", "two", "prompts")) { t.Fatal("dynamic registration did not match a separate uniqueness scope") } } func TestCompileSequenceIdentityPolicyPreservesLiteralKeysAndRejectsIndexPaths(t *testing.T) { policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{literalSequenceSelector(""), literalSequenceSelector("a/b~c.%"), literalSequenceSelector("prompts")}, identityFields: []string{"id"}}, }) if len(diagnostics) != 0 { t.Fatalf("diagnostics = %#v", diagnostics) } if _, matched := policy.identityFieldsAt(Keys("", "a/b~c.%", "prompts")); !matched { t.Fatal("selector did not preserve empty and punctuation keys") } if _, matched := policy.identityFieldsAt(Keys("", "a/b~c.%", "prompts").Index(0)); matched { t.Fatal("selector matched a sequence-index destination") } } func TestCompileSequenceIdentityPolicyCoalescesEquivalentFieldSets(t *testing.T) { selector := sequenceIdentitySelector{literalSequenceSelector("items")} policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{ {selector: selector, identityFields: []string{"namespace", "id", "id"}}, {selector: cloneSequenceIdentitySelector(selector), identityFields: []string{"id", "namespace"}}, }) if len(diagnostics) != 0 { t.Fatalf("diagnostics = %#v", diagnostics) } if leaf := policy.selectorTrie.literals["items"]; leaf == nil || !leaf.terminal { t.Fatal("equivalent selector did not compile to one terminal trie entry") } fields, matched := policy.identityFieldsAt(Keys("items")) if !matched || !equalSequenceIdentityFields(fields, []string{"id", "namespace"}) { t.Fatalf("identity fields = %#v, matched = %t", fields, matched) } } func TestCompileSequenceIdentityPolicyRejectsConflictingOverlapsWithoutInput(t *testing.T) { _, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), anyMappingSequenceSelector(), literalSequenceSelector("prompts")}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), literalSequenceSelector("weather"), literalSequenceSelector("prompts")}, identityFields: []string{"name"}}, }) if len(diagnostics) != 1 || diagnostics[0].Code != CodeOptionsInvalid { t.Fatalf("diagnostics = %#v, want overlap definition error", diagnostics) } } func TestSequenceOverlapDiagnosticsIdentifyOriginalRegistrationPositions(t *testing.T) { t.Parallel() _, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{literalSequenceSelector("invalid")}}, {selector: sequenceIdentitySelector{literalSequenceSelector("unrelated")}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), anyMappingSequenceSelector(), literalSequenceSelector("items")}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), literalSequenceSelector("foo"), literalSequenceSelector("items")}, identityFields: []string{"name"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), literalSequenceSelector("bar"), literalSequenceSelector("items")}, identityFields: []string{"code"}}, }) if len(diagnostics) != 3 { t.Fatalf("diagnostics = %#v, want the invalid registration and two conflicts", diagnostics) } for _, pair := range []string{"3 and 4", "3 and 5"} { found := false for _, diagnostic := range diagnostics { if strings.Contains(diagnostic.Message, pair) { found = true if diagnostic.Code != CodeOptionsInvalid || diagnostic.Path.String() != "" || len(diagnostic.Related) != 0 { t.Fatalf("definition overlap invented input coordinates: %#v", diagnostic) } } } if !found { t.Errorf("conflicting registration positions %s not identified: %#v", pair, diagnostics) } } } func TestCompileSequenceIdentityPolicyAllowsDisjointSelectorsAndRoot(t *testing.T) { policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"name"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), anyMappingSequenceSelector()}, identityFields: []string{"key"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("collections"), literalSequenceSelector("static"), literalSequenceSelector("prompts")}, identityFields: []string{"id"}}, }) if len(diagnostics) != 0 { t.Fatalf("diagnostics = %#v", diagnostics) } if fields, matched := policy.identityFieldsAt(Path{}); !matched || !equalSequenceIdentityFields(fields, []string{"id"}) { t.Fatalf("root selector = %#v, %t", fields, matched) } for _, path := range []Path{Keys("items"), Keys("collections", "dynamic"), Keys("collections", "static", "prompts")} { if _, matched := policy.identityFieldsAt(path); !matched { t.Errorf("disjoint selector failed to match %q", path.String()) } } } func TestCompileSequenceIdentityPolicySnapshotsSelectorAndFields(t *testing.T) { selector := sequenceIdentitySelector{literalSequenceSelector("items")} fields := []string{"id", "namespace"} policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{{selector: selector, identityFields: fields}}) if len(diagnostics) != 0 { t.Fatalf("diagnostics = %#v", diagnostics) } selector[0].key = "changed" fields[0] = "changed" if got, matched := policy.identityFieldsAt(Keys("items")); !matched || !equalSequenceIdentityFields(got, []string{"id", "namespace"}) { t.Fatalf("compiled registration changed after input mutation: %#v, %t", got, matched) } } func TestCompileSequenceIdentityPolicyRejectsInvalidSelectorsAndEmptyFields(t *testing.T) { _, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{ {selector: sequenceIdentitySelector{literalSequenceSelector("configkit")}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{{kind: sequenceSelectorSegmentKind(99)}}, identityFields: []string{"id"}}, {selector: sequenceIdentitySelector{literalSequenceSelector("unused")}, identityFields: nil}, }) if len(diagnostics) != 3 { t.Fatalf("diagnostics = %#v, want all three definition errors", diagnostics) } } func literalSequenceSelector(key string) sequenceSelectorSegment { return sequenceSelectorSegment{kind: sequenceSelectorLiteral, key: key} } func anyMappingSequenceSelector() sequenceSelectorSegment { return sequenceSelectorSegment{kind: sequenceSelectorAnyMappingKey} } func TestSequenceRegistrationPreflightDoesNotReadSources(t *testing.T) { t.Parallel() type config struct { Value string `yaml:"value"` } ordinaryReader := strings.NewReader("value: ordinary\n") _, err := Load[config](Reader("ordinary.yml", ordinaryReader), SecretBinding{}, Options[config]{ Definition: DefinitionWithSequenceIdentityRegistrations(Definition[config]{}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items").Index(0)), IdentityFields: []string{"id"}}}), }) requireSequenceRegistrationOptionError(t, err) if ordinaryReader.Len() == 0 { t.Fatal("ordinary reader was consumed before invalid registration was rejected") } mainReader := strings.NewReader("value: main\n") underlayReader := strings.NewReader("value: underlay\n") _, err = LoadLayered[config](Input{Source: Reader("main.yml", mainReader)}, SecretBinding{}, LayeredOptions[config]{ Definition: DefinitionWithSequenceIdentityRegistrations(Definition[config]{}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items").Index(0)), IdentityFields: []string{"id"}}}), Underlays: []Underlay{{Inputs: []Input{{Source: Reader("underlay.yml", underlayReader)}}}}, }) requireSequenceRegistrationOptionError(t, err) if mainReader.Len() == 0 || underlayReader.Len() == 0 { t.Fatal("layered source was consumed before invalid registration was rejected") } } func pathPointer(path Path) *Path { return &path } func requireSequenceRegistrationOptionError(t *testing.T, err error) { t.Helper() if err == nil { t.Fatal("expected invalid sequence registration error") } var loadErr *LoadError if !errors.As(err, &loadErr) { t.Fatalf("error type = %T, want *LoadError", err) } diagnostics := loadErr.Diagnostics() if len(diagnostics) != 1 || diagnostics[0].Code != CodeOptionsInvalid { t.Fatalf("diagnostics = %#v, want one options diagnostic", diagnostics) } } func TestRegisteredSequencePolicyAcrossLoadingLifecycle(t *testing.T) { t.Parallel() type item struct { ID string `yaml:"id"` } type config struct { Items []item `yaml:"items"` } definition := Definition[config]{sequenceIdentityRegistrations: []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"}}}} registered := Options[config]{Definition: definition} loadCode := func(t *testing.T, err error) Code { t.Helper() var loadErr *LoadError if !errors.As(err, &loadErr) || len(loadErr.Diagnostics()) == 0 { t.Fatalf("error = %v, want a diagnostic load error", err) } return loadErr.Diagnostics()[0].Code } t.Run("ordinary authored list is checked with one source", func(t *testing.T) { if _, err := testLoad[config](Bytes("main", []byte("items: [{id: same}, {id: same}]\n")), registered); err == nil || loadCode(t, err) != CodeComposeConflict { t.Fatalf("duplicate identity error = %v", err) } result, err := testLoad[config](Bytes("main", []byte("items: [{id: one}]\n")), registered) if err != nil || len(result.Config.Items) != 1 || result.Config.Items[0].ID != "one" { t.Fatalf("single-source load = %#v, %v", result, err) } }) t.Run("ordinary imports retain registered composition", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: fragment.yml}]}\nitems: [{id: authored}]\n") writeImportFile(t, directory, "fragment.yml", "items: [{id: imported}]\n") result, err := testLoad[config](File(mainPath), Options[config]{Imports: true, Definition: definition}) if err != nil || len(result.Config.Items) != 2 || result.Config.Items[0].ID != "imported" || result.Config.Items[1].ID != "authored" { t.Fatalf("registered imported list = %#v, %v", result, err) } }) t.Run("registered sequence policy appends across strict groups and tiers", func(t *testing.T) { request := loadRequest[config]{ main: loadInput{source: Bytes("main", []byte("items: [{id: shared}]\n"))}, underlays: []loadGroup{{inputs: []loadInput{ {source: Bytes("first", []byte("items: [{id: lower}]\n"))}, {source: Bytes("second", []byte("items: [{id: other}]\n"))}, }}}, definition: definition, sequencePolicy: mustCompileSequenceIdentityPolicy(t, definition.sequenceIdentityRegistrations), } result, err := testGroupedLoad(request) if err != nil || len(result.Config.Items) != 3 || result.Config.Items[0].ID != "lower" || result.Config.Items[1].ID != "other" || result.Config.Items[2].ID != "shared" { t.Fatalf("grouped load = %#v, %v", result, err) } request.underlays = []loadGroup{{inputs: []loadInput{ {source: Bytes("first", []byte("items: [{id: shared}]\n"))}, {source: Bytes("second", []byte("items: [{id: lower}]\n"))}, }}} request.main.source = Bytes("main", []byte("items: [{id: shared}]\n")) if _, err := testGroupedLoad(request); err != nil { t.Fatalf("same identity in a separate tier: %v", err) } }) t.Run("empty registered contribution follows the accumulated sequence mode", func(t *testing.T) { result, err := testLoad[config](Bytes("main", []byte("items: []\n")), registered) if err != nil || len(result.Config.Items) != 0 { t.Fatalf("empty sequence load = %#v, %v", result, err) } request := loadRequest[config]{ main: loadInput{source: Bytes("main", []byte("items: []\n"))}, underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("items: [{id: lower}]\n"))}}}}, definition: definition, sequencePolicy: mustCompileSequenceIdentityPolicy(t, definition.sequenceIdentityRegistrations), } result, err = testGroupedLoad(request) if err != nil || len(result.Config.Items) != 1 || result.Config.Items[0].ID != "lower" { t.Fatalf("empty higher tier = %#v, %v", result, err) } }) t.Run("unregistered list overlap remains atomic", func(t *testing.T) { request := loadRequest[config]{ main: loadInput{source: Bytes("main", []byte("items: [{id: main}]\n"))}, underlays: []loadGroup{{inputs: []loadInput{ {source: Bytes("first", []byte("items: [{id: lower}]\n"))}, {source: Bytes("second", []byte("items: [{id: other}]\n"))}, }}}, } _, err := testGroupedLoad(request) if got := loadCode(t, err); got != CodeComposeConflict { t.Fatalf("unregistered overlap error = %s", got) } }) t.Run("malformed lower tier fails before precedence replacement", func(t *testing.T) { request := loadRequest[config]{ main: loadInput{source: Bytes("main", []byte("items: [{id: main}]\n"))}, underlays: []loadGroup{{inputs: []loadInput{{source: Bytes("lower", []byte("items: [{id: same}, {id: same}]\n"))}}}}, definition: definition, sequencePolicy: mustCompileSequenceIdentityPolicy(t, definition.sequenceIdentityRegistrations), } if got := loadCode(t, func() error { _, err := testGroupedLoad(request); return err }()); got != CodeComposeConflict { t.Fatalf("hidden lower-tier error = %s", got) } overlayFilesystem := NewFilesystem("overlay", fstest.MapFS{ "overlays/testing.yml": &fstest.MapFile{Data: []byte("items: [{id: overlay}]\n")}, }) request.overlay = "testing" request.overlayFilesystem = &FilesystemContext{Root: ".", BaseDir: ".", FS: &overlayFilesystem} if got := loadCode(t, func() error { _, err := testGroupedLoad(request); return err }()); got != CodeComposeConflict { t.Fatalf("selected replacement overlay hid lower-tier error: %s", got) } }) t.Run("replacement overlay and CLI validate final identities", func(t *testing.T) { directory := t.TempDir() mainPath := writeImportFile(t, directory, "main.yml", "items: [{id: base}]\n") writeImportFile(t, directory, "overlays/testing.yml", "items: [{id: duplicate}, {id: duplicate}]\n") _, err := testLoad[config](File(mainPath), Options[config]{Definition: definition, Overlay: "testing"}) if got := loadCode(t, err); got != CodeSequenceConflict { t.Fatalf("replacement overlay error code = %s, want %s", got, CodeSequenceConflict) } result, err := testLoad[config](Bytes("main", []byte("items: [{id: base}]\n")), Options[config]{ Definition: definition, Overrides: []Override{{Path: Keys("items"), Flag: "--items", Value: []any{map[string]any{"id": "duplicate"}, map[string]any{"id": "duplicate"}}}}, }) if result != nil || loadCode(t, err) != CodeSequenceConflict { t.Fatalf("duplicate CLI value = %#v, %v", result, err) } resolved := definition resolved.Resolve = func(value *config, resolution *Resolution) error { value.Items[1].ID = value.Items[0].ID resolution.Record(Keys("items"), ChangeNormalization, "registered identities collide") return nil } result, err = testLoad[config](Bytes("main", []byte("items: [{id: first}, {id: second}]\n")), Options[config]{Definition: resolved}) if result != nil || loadCode(t, err) != CodeSequenceConflict { t.Fatalf("resolver duplicate = %#v, %v", result, err) } }) t.Run("final identity validation precedes application validation", func(t *testing.T) { checked := definition validated := false checked.Validate = func(value *config, validation *Validation) { validated = true } _, err := testLoad[config](Bytes("main", []byte("items: [{id: base}]\n")), Options[config]{ Definition: checked, Overrides: []Override{{Path: Keys("items"), Flag: "--items", Value: []any{map[string]any{"id": "duplicate"}, map[string]any{"id": "duplicate"}}}}, }) if got := loadCode(t, err); got != CodeSequenceConflict || validated { t.Fatalf("duplicate identity result = %s, validation callback invoked %v", got, validated) } }) } func mustCompileSequenceIdentityPolicy(t *testing.T, registrations []sequenceIdentityRegistration) sequencePolicy { t.Helper() policy, diagnostics := compileSequenceIdentityPolicy(registrations) if len(diagnostics) != 0 { t.Fatal(diagnostics) } return policy }