package engine import ( "reflect" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) func TestCollectionPolicyWithoutValueRetainsIdentityAndMode(t *testing.T) { lower := collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, } lower.setRecordPolicy(newCollectionRecordPolicy()) current := collectionPolicy{explicitMode: sequenceModeReplace, explicitModePresent: true} retained, diagnostics := collectionPolicyWithoutValue(&lower, current, nil, Keys("prompts")) if len(diagnostics) != 0 || !retained.identityPresent || !reflect.DeepEqual(retained.identityFields, []string{"id"}) { t.Fatalf("no-value replace reset identity: %#v, %#v", retained, diagnostics) } if retained.explicitMode != sequenceModeReplace || !retained.explicitModePresent || len(retained.recordOrder) != 1 { t.Fatalf("no-value policy update lost mode or record ownership: %#v", retained) } retainedRecord, _ := retained.recordPolicyAt(0) lowerRecord, _ := lower.recordPolicyAt(0) if retainedRecord.owner != lowerRecord.owner { t.Fatal("no-value policy update changed record ownership") } } func TestCollectionPolicyWithoutValueLeavesAbsentAndNonSequenceDestinationsUnconstrained(t *testing.T) { current := collectionPolicy{identityFields: []string{"id"}, identityPresent: true} for _, value := range []*document.Node{nil, parsePolicyNode(t, "value: scalar\n").Mapping[0].Value} { retained, diagnostics := collectionPolicyWithoutValue(nil, current, value, Keys("prompts")) if len(diagnostics) != 0 || !retained.identityPresent || !reflect.DeepEqual(retained.identityFields, []string{"id"}) { t.Fatalf("pending identity at absent/non-sequence destination = %#v, %#v", retained, diagnostics) } } } func TestCollectionPolicyWithoutValueValidatesRetainedSequenceImmediately(t *testing.T) { current := collectionPolicy{identityFields: []string{"id"}, identityPresent: true} valid := parsePolicyNode(t, "items: [{id: one}, {id: two}]\n").Mapping[0].Value retained, diagnostics := collectionPolicyWithoutValue(nil, current, valid, Keys("prompts")) if len(diagnostics) != 0 || !retained.identityPresent { t.Fatalf("valid retained records were rejected: %#v, %#v", retained, diagnostics) } invalid := parsePolicyNode(t, "items: [{name: missing-id}]\n").Mapping[0].Value retained, diagnostics = collectionPolicyWithoutValue(nil, current, invalid, Keys("prompts")) if !reflect.DeepEqual(retained, collectionPolicy{}) || len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid || diagnostics[0].Path.String() != "/prompts/0/id" { t.Fatalf("invalid retained records = %#v, %#v", retained, diagnostics) } } func TestCollectionPolicyWithoutValueRejectsIdentityChange(t *testing.T) { lower := collectionPolicy{identityFields: []string{"id"}, identityPresent: true} current := collectionPolicy{identityFields: []string{"name"}, identityPresent: true} retained, diagnostics := collectionPolicyWithoutValue(&lower, current, nil, Keys("prompts")) if !reflect.DeepEqual(retained, collectionPolicy{}) || len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceConflict { t.Fatalf("no-value identity change = %#v, %#v", retained, diagnostics) } } func TestCollectionPolicyLifetimePersistenceAndReplacementTable(t *testing.T) { tests := []struct { name string current collectionPolicy replaceValue bool wantIdentity []string wantIdentitySet bool wantMode sequenceMode wantRecords int }{ { name: "mode-only replace declaration does not reset retained identity", current: collectionPolicy{ explicitMode: sequenceModeReplace, explicitModePresent: true, }, wantIdentity: []string{"id"}, wantIdentitySet: true, wantMode: sequenceModeReplace, wantRecords: 1, }, { name: "whole-sequence replacement resets lower identity", replaceValue: true, wantIdentitySet: false, wantMode: sequenceModeAppend, wantRecords: 0, }, { name: "replacement can establish a new identity", current: collectionPolicy{identityFields: []string{"name"}, identityPresent: true}, replaceValue: true, wantIdentity: []string{"name"}, wantIdentitySet: true, wantMode: sequenceModeAppend, wantRecords: 0, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { lower := collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, } lower.setRecordPolicy(newCollectionRecordPolicy()) var got collectionPolicy if test.replaceValue { got = collectionPolicyForReplacement(&lower, test.current) } else { var diagnostics []Diagnostic got, diagnostics = collectionPolicyWithoutValue(&lower, test.current, nil, Keys("prompts")) if len(diagnostics) != 0 { t.Fatalf("policy diagnostics = %#v", diagnostics) } } if got.identityPresent != test.wantIdentitySet || !reflect.DeepEqual(got.identityFields, test.wantIdentity) || got.explicitMode != test.wantMode || len(got.recordOrder) != test.wantRecords { t.Fatalf("transition result = %#v", got) } }) } } func TestCollectionPolicyValueReplacementResetsIdentityButRetainsExplicitMode(t *testing.T) { lower := collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, } lower.setRecordPolicy(newCollectionRecordPolicy()) current := collectionPolicy{identityFields: []string{"name"}, identityPresent: true} replaced := collectionPolicyForReplacement(&lower, current) if !replaced.identityPresent || !reflect.DeepEqual(replaced.identityFields, []string{"name"}) { t.Fatalf("replacement identity was not reset: %#v", replaced) } if replaced.explicitMode != sequenceModeAppend || !replaced.explicitModePresent || len(replaced.recordOrder) != 0 { t.Fatalf("replacement failed to retain explicit mode or retire lower record policy: %#v", replaced) } cleared := collectionPolicyForReplacement(&lower, collectionPolicy{}) if cleared.identityPresent || len(cleared.identityFields) != 0 || cleared.explicitMode != sequenceModeAppend || !cleared.explicitModePresent { t.Fatalf("replacement without a new key did not clear YAML identity while retaining mode: %#v", cleared) } } func TestCollectionRecordRetentionAndReplacementUseOpaqueOwners(t *testing.T) { oldRecord := newCollectionRecordPolicy() oldRecord.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) retained := retainedCollectionRecordPolicy(oldRecord) if retained.owner != oldRecord.owner { t.Fatal("retained record did not keep its owner") } if got, ok := retained.scope.at(Keys("steps")); !ok || !got.identityPresent { t.Fatalf("retained record lost its local policy: %#v, %t", got, ok) } if retained.scope.collections["/steps"].policy.identityFields[0] = "changed"; oldRecord.scope.collections["/steps"].policy.identityFields[0] != "id" { t.Fatal("retained record policy aliases its source") } // A whole-record replacement at the same list position, even with equal // authored identity/source metadata, receives new ownership and no old rules. replacement := replacedCollectionRecordPolicy() if replacement.owner == oldRecord.owner || len(replacement.scope.collections) != 0 { t.Fatal("replacement record inherited discarded owner policy") } } func TestCollectionPolicyAncestorRetirementPreservesIndependentMode(t *testing.T) { var scope collectionPolicyScope owner := newCollectionRecordPolicy() owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) scope.set(Keys("catalog", "prompts"), collectionPolicyWithRecords(collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, }, owner)) scope.retireCollectionPoliciesAtOrBelow(Keys("catalog")) policy, ok := scope.at(Keys("catalog", "prompts")) if !ok || policy.identityPresent || len(policy.recordOrder) != 0 || policy.explicitMode != sequenceModeAppend || !policy.explicitModePresent { t.Fatalf("ancestor retirement = %#v, %t", policy, ok) } reintroduced, diagnostics := collectionPolicyWithoutValue(&policy, collectionPolicy{}, nil, Keys("catalog", "prompts")) if len(diagnostics) != 0 || reintroduced.identityPresent || reintroduced.explicitMode != sequenceModeAppend { t.Fatalf("retired identity was resurrected while mode persisted: %#v, %#v", reintroduced, diagnostics) } } func TestEqualOuterIdentitiesHaveDistinctRecordOwners(t *testing.T) { retained := newCollectionRecordPolicy() replacement := replacedCollectionRecordPolicy() if retained.owner == replacement.owner { t.Fatal("replacement with the same outer identity reused the old owner") } if retained.owner == nil || replacement.owner == nil { t.Fatal("record owner token is missing") } } func TestCollectionPolicyScopesKeepRootAndItemRulesIndependent(t *testing.T) { var root collectionPolicyScope root.set(Keys("steps"), collectionPolicy{identityFields: []string{"root-id"}, identityPresent: true}) owner := newCollectionRecordPolicy() owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"item-id"}, identityPresent: true}) root.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, owner)) root.retireCollectionPoliciesAtOrBelow(Keys("steps")) rootPolicy, ok := root.at(Keys("steps")) if !ok || rootPolicy.identityPresent { t.Fatalf("root-local policy was not retired: %#v", rootPolicy) } itemPolicy, ok := root.at(Keys("items")) if !ok || len(itemPolicy.recordOrder) != 1 { t.Fatalf("unrelated item owner disappeared: %#v", itemPolicy) } itemRecord, _ := itemPolicy.recordPolicyAt(0) nested, ok := itemRecord.scope.at(Keys("steps")) if !ok || !nested.identityPresent || !reflect.DeepEqual(nested.identityFields, []string{"item-id"}) { t.Fatalf("item-local policy was coupled to root destination: %#v", nested) } } func TestCollectionPolicyProjectionUsesConcretePlacement(t *testing.T) { first := newCollectionRecordPolicy() first.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"step"}, identityPresent: true}) second := newCollectionRecordPolicy() second.scope.set(Keys("steps"), collectionPolicy{explicitMode: sequenceModeAppend, explicitModePresent: true}) var scope collectionPolicyScope scope.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, first, second)) projected := projectCollectionPolicyScope(scope, Keys("mounted")) if len(projected) != 3 { t.Fatalf("projected policies = %#v", projected) } want := []string{"/mounted/items", "/mounted/items/0/steps", "/mounted/items/1/steps"} for index, policy := range projected { if policy.path.String() != want[index] { t.Fatalf("projected path[%d] = %q, want %q", index, policy.path.String(), want[index]) } } } func TestCollectionPolicyInheritanceCopiesOwnersAndState(t *testing.T) { owner := newCollectionRecordPolicy() owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) original := collectionPolicyWithRecords(collectionPolicy{}, owner) copy := cloneCollectionPolicyForInheritance(original) copyOwner := copy.recordOrder[0] if copyOwner == owner.owner { t.Fatal("inheritance copy retained the source record owner") } copy.recordPolicies[copyOwner].collections["/steps"].policy.identityFields[0] = "changed" if owner.scope.collections["/steps"].policy.identityFields[0] != "id" { t.Fatal("inheritance copy aliases source policy") } } func TestAcquisitionScopeCloneOwnsCollectionPolicyCopy(t *testing.T) { owner := newCollectionRecordPolicy() owner.scope.set(Keys("steps"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) var scope acquisitionScope scope.collectionPolicies.set(Keys("items"), collectionPolicyWithRecords(collectionPolicy{}, owner)) cloned := cloneAcquisitionScope(scope) clonedOwner := cloned.collectionPolicies.collections["/items"].policy.recordOrder[0] cloned.collectionPolicies.collections["/items"].policy.recordPolicies[clonedOwner].collections["/steps"].policy.identityFields[0] = "changed" sourceOwner := scope.collectionPolicies.collections["/items"].policy.recordOrder[0] if scope.collectionPolicies.collections["/items"].policy.recordPolicies[sourceOwner].collections["/steps"].policy.identityFields[0] != "id" { t.Fatal("acquisition scope clone aliases collection policy") } if clonedOwner != owner.owner { t.Fatal("acquisition scope clone changed record ownership") } } func collectionPolicyWithRecords(policy collectionPolicy, records ...collectionRecordPolicy) collectionPolicy { for _, record := range records { policy.setRecordPolicy(record) } return policy } func parsePolicyNode(t *testing.T, input string) *document.Node { t.Helper() parsed, diagnostics := parseSource(Bytes("collection-policy-state.yml", []byte(input)), "collection-policy-state", defaultParseLimits()) if len(diagnostics) != 0 { t.Fatalf("parse policy value: %#v", diagnostics) } return parsed.document }