package engine import ( "fmt" "reflect" "strings" "testing" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" yamladapter "gitea.maximumdirect.net/eric/ConfigKit/internal/yaml" ) func parseItemPolicyDocument(t *testing.T, sourceID, input string) *document.Node { t.Helper() root, issues := yamladapter.Parse(strings.NewReader(input), sourceID, yamladapter.Limits{ Depth: maxDocumentDepth, Nodes: maxDocumentNodes, }) if len(issues) != 0 { t.Fatalf("parse document: %#v", issues) } return root } func TestPendingItemCompositionKeepsLocalDeclarationsAndHelperIdentity(t *testing.T) { t.Parallel() declaration := sequenceDeclarationSpec{ destination: Keys("prompts"), identityFields: []string{"id"}, identityPresent: true, } itemScope := acquisitionScope{contributions: []acquiredContribution{ { document: parseItemPolicyDocument(t, "item", "name: authored\nprompts:\n - id: authored\n"), sequenceDeclarations: []sequenceDeclarationSpec{declaration}, }, { document: parseItemPolicyDocument(t, "helper", "id: supplied\nprompts:\n - id: helper\n"), }, }} outer := acquisitionScope{contributions: []acquiredContribution{{ source: SourceInfo{ID: "item"}, payload: parseItemPolicyDocument(t, "outer", "{}\n"), destination: Keys("items"), pendingItem: &itemScope, }}} composed, diagnostics := composeAcquiredScopeWithPolicy( &outer, collectionPolicyScope{}, sequencePolicy{}, newImportInclusionGraph(), nil, nil, nil, &importAccounting{}, ) if len(diagnostics) != 0 { t.Fatalf("compose pending item scope: %#v", diagnostics) } item := outer.contributions[0].payload if got, ok := documentAtPath(item, Keys("id")); !ok || got == nil || got.Text != "supplied" { t.Fatalf("helper-supplied record identity = %#v", got) } if got, ok := documentAtPath(item, Keys("prompts")); !ok || got == nil || got.Kind != document.Sequence || len(got.Sequence) != 2 { t.Fatalf("composed prompts = %#v, want two identified entries", got) } itemPolicy, ok := composed.collectionPolicies.at(Keys("items")) if !ok { t.Fatalf("projected item collection policy missing") } owned, ok := itemPolicy.recordPolicyAt(0) if !ok { t.Fatalf("projected item owner missing: %#v", itemPolicy) } if policy, ok := owned.scope.at(Keys("prompts")); !ok || !policy.identityPresent || len(policy.identityFields) != 1 || policy.identityFields[0] != "id" { t.Fatalf("item-owned prompt policy = %#v, %v", policy, ok) } if _, ok := composed.collectionPolicies.at(Keys("prompts")); ok { t.Fatal("item-local declaration escaped its owning record") } } func TestPendingItemCompositionProjectsNestedIdentityDiagnostics(t *testing.T) { t.Parallel() childScope := acquisitionScope{contributions: []acquiredContribution{ {document: parseItemPolicyDocument(t, "child", "prompts:\n - id: same\n"), sequenceDeclarations: []sequenceDeclarationSpec{{ destination: Keys("prompts"), identityFields: []string{"id"}, identityPresent: true, }}}, {document: parseItemPolicyDocument(t, "child-helper", "prompts:\n - id: same\n")}, }} itemScope := acquisitionScope{contributions: []acquiredContribution{{ document: parseItemPolicyDocument(t, "item", "name: parent\n"), }, { source: SourceInfo{ID: "child"}, payload: parseItemPolicyDocument(t, "child", "{}\n"), destination: Keys("children"), pendingItem: &childScope, }}} outer := acquisitionScope{contributions: []acquiredContribution{{ source: SourceInfo{ID: "item"}, payload: parseItemPolicyDocument(t, "outer", "{}\n"), destination: Keys("items"), pendingItem: &itemScope, }}} traversal := importTraversal{accounting: &importAccounting{}, limits: defaultImportLimits()} diagnostics := traversal.composePendingItems(&outer, sequencePolicy{}) if len(diagnostics) != 1 { t.Fatalf("diagnostics = %#v, want one conflict", diagnostics) } if diagnostics[0].Code != CodeComposeConflict || diagnostics[0].Path.String() != "/items/0/children/0/prompts" { t.Fatalf("diagnostic = %#v, want projected collection path", diagnostics[0]) } if strings.Contains(fmt.Sprint(diagnostics[0]), "same") { t.Fatalf("diagnostic disclosed identity value: %v", diagnostics[0]) } } func TestPendingItemCompositionAppliesApplicationIdentityConstraints(t *testing.T) { t.Parallel() itemScope := acquisitionScope{contributions: []acquiredContribution{{ document: parseItemPolicyDocument(t, "item", "prompts:\n - text: missing-id\n"), }}} outer := acquisitionScope{contributions: []acquiredContribution{{ source: SourceInfo{ID: "item"}, payload: parseItemPolicyDocument(t, "outer", "{}\n"), destination: Keys("items"), pendingItem: &itemScope, }}} policy, diagnostics := compileSequenceIdentityPolicy([]sequenceIdentityRegistration{{ selector: sequenceIdentitySelector{literalSequenceSelector("prompts")}, identityFields: []string{"id"}, }}) if len(diagnostics) != 0 { t.Fatalf("compile application identity: %#v", diagnostics) } traversal := importTraversal{accounting: &importAccounting{}, limits: defaultImportLimits()} diagnostics = traversal.composePendingItems(&outer, policy) if len(diagnostics) != 1 || diagnostics[0].Code != CodeComposeInvalid || diagnostics[0].Path.String() != "/items/0/prompts/0/id" { t.Fatalf("application identity diagnostics = %#v", diagnostics) } } func TestReplacingOuterCollectionRetiresDiscardedItemIdentity(t *testing.T) { t.Parallel() outerPath := Keys("items") nestedPath := Keys("items").Index(0).Key("prompts") lower := collectionPolicyScope{} outerPolicy := collectionPolicy{} outerPolicy.setRecordPolicy(newCollectionRecordPolicy()) lower.set(outerPath, outerPolicy) lower.set(nestedPath, collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) declarations := []sequenceDeclarationSpec{{ destination: outerPath, mode: sequenceModeReplace, modePresent: true, }} input := parseItemPolicyDocument(t, "replacement", "items:\n - prompts:\n - text: new\n") composed, diagnostics := composeDocumentsWithCollectionPolicy(sequencePolicy{}, lower, declarations, input) if len(diagnostics) != 0 { t.Fatalf("compose replacement: %#v", diagnostics) } policy, ok := composed.collectionPolicies.at(nestedPath) if !ok || policy.identityPresent || len(policy.recordOrder) != 0 { t.Fatalf("discarded nested owner policy = %#v, %v", policy, ok) } } func TestNestedItemPolicyProjectionUsesStableOutputIndexes(t *testing.T) { t.Parallel() itemsPath := Keys("items") outerPolicy := collectionPolicy{} retainedScope := collectionPolicyScope{} retainedScope.set(Keys("retained"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) outerPolicy.setRecordPolicy(collectionRecordPolicy{owner: newCollectionRecordPolicy().owner, scope: retainedScope}) composed := &composedDocument{collectionPolicies: collectionPolicyScope{}} composed.collectionPolicies.set(itemsPath, outerPolicy) firstScope := collectionPolicyScope{} firstScope.set(Keys("prompts"), collectionPolicy{identityFields: []string{"id"}, identityPresent: true}) secondScope := collectionPolicyScope{} secondScope.set(Keys("prompts"), collectionPolicy{identityFields: []string{"slug"}, identityPresent: true}) scope := acquisitionScope{contributions: []acquiredContribution{ {document: parseItemPolicyDocument(t, "first", "items:\n - id: first\n"), destination: itemsPath, nested: &composedDocument{collectionPolicies: firstScope}}, {document: parseItemPolicyDocument(t, "second", "items:\n - id: second\n"), destination: itemsPath, nested: &composedDocument{collectionPolicies: secondScope}}, }} projectNestedCollectionPolicies(composed, scope, nil) policy, ok := composed.collectionPolicies.at(itemsPath) if !ok || len(policy.recordOrder) != 3 { t.Fatalf("record-owned policies = %#v, %v; want retained plus two item scopes", policy, ok) } for index, wantField := range []string{"id", "id", "slug"} { record, ok := policy.recordPolicyAt(index) if !ok { t.Fatalf("record policy %d missing", index) } path := Keys("retained") if index != 0 { path = Keys("prompts") } nested, ok := record.scope.at(path) if !ok || !nested.identityPresent || len(nested.identityFields) != 1 || nested.identityFields[0] != wantField { t.Errorf("record policy %d = %#v, %v; want %q", index, nested, ok, wantField) } } } func TestOverlaySequenceTransfersIncomingItemPolicyOwnership(t *testing.T) { path := Keys("records") registered := mustCompileSequenceIdentityPolicy(t, []sequenceIdentityRegistration{{selector: sequenceIdentitySelector{literalSequenceSelector("records")}, identityFields: []string{"id"}}}) makePolicies := func(childKey string) collectionPolicyScope { local := collectionPolicyScope{} local.set(Keys("steps"), collectionPolicy{ identityFields: []string{childKey}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, }) record := newCollectionRecordPolicy() record.scope = local outer := collectionPolicy{identityFields: []string{"id"}, identityPresent: true} outer.setRecordPolicy(record) scope := collectionPolicyScope{} scope.set(path, outer) return scope } checkPolicy := func(t *testing.T, scope collectionPolicyScope, index int, key string) { t.Helper() outer, ok := scope.at(path) if !ok { t.Fatal("outer collection policy missing") } record, ok := outer.recordPolicyAt(index) if !ok { t.Fatalf("record policy %d missing", index) } local, ok := record.scope.at(Keys("steps")) if !ok || !local.identityPresent || !reflect.DeepEqual(local.identityFields, []string{key}) || !local.explicitModePresent || local.explicitMode != sequenceModeAppend { t.Fatalf("record %d nested policy = %#v, %v; want key %q and append", index, local, ok, key) } } tests := []struct { name string lower string higher string declarations []sequenceDeclarationSpec wantRecords int }{ { name: "upserting the same outer identity replaces its local policy", lower: "records: [{id: same, steps: [{id: old}]}]\n", higher: "records: [{id: same, steps: [{slug: new}]}]\n", wantRecords: 1, }, { name: "retained and appended outer records keep independent policies", lower: "records: [{id: lower, steps: [{id: old}]}]\n", higher: "records: [{id: higher, steps: [{slug: new}]}]\n", wantRecords: 2, }, { name: "whole sequence replacement transfers new local policy", lower: "records: [{id: same, steps: [{id: old}]}]\n", higher: "records: [{id: same, steps: [{slug: new}]}]\n", declarations: []sequenceDeclarationSpec{{ destination: path, mode: sequenceModeReplace, modePresent: true, }}, wantRecords: 1, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { lowerRoot := parseItemPolicyDocument(t, "lower", test.lower) incomingRoot := parseItemPolicyDocument(t, "higher", test.higher) assembly := newAssemblyAccumulator(&composedDocument{ root: lowerRoot, collectionPolicies: makePolicies("id"), origins: make(map[string]Origin), }) if diagnostics := assembly.applyOverlayWithCollectionPolicy( incomingRoot, nil, makePolicies("slug"), nil, LayerBase, registered, test.declarations, ); len(diagnostics) != 0 { t.Fatalf("overlay diagnostics = %#v", diagnostics) } outer, ok := assembly.current.collectionPolicies.at(path) if !ok || len(outer.recordOrder) != test.wantRecords { t.Fatalf("outer record policies = %#v, %v", outer, ok) } if test.wantRecords == 2 { checkPolicy(t, assembly.current.collectionPolicies, 0, "id") checkPolicy(t, assembly.current.collectionPolicies, 1, "slug") } else { checkPolicy(t, assembly.current.collectionPolicies, 0, "slug") } }) } } func TestAncestorReplacementRequiresIdentityWhenRetainedModeReturns(t *testing.T) { path := Keys("catalog", "prompts") newAssembly := func(t *testing.T) *assemblyAccumulator { t.Helper() policy := collectionPolicy{ identityFields: []string{"id"}, identityPresent: true, explicitMode: sequenceModeAppend, explicitModePresent: true, } scope := collectionPolicyScope{} scope.set(path, policy) return newAssemblyAccumulator(&composedDocument{ root: parseItemPolicyDocument(t, "base", "catalog:\n prompts: [{id: old}]\n"), collectionPolicies: scope, origins: make(map[string]Origin), }) } reintroduceAncestor := func(t *testing.T, assembly *assemblyAccumulator) { t.Helper() if diagnostics := assembly.applyOverlayWithCollectionPolicy( parseItemPolicyDocument(t, "remove", "catalog: null\n"), nil, collectionPolicyScope{}, nil, LayerBase, sequencePolicy{}, nil, ); len(diagnostics) != 0 { t.Fatalf("ancestor removal diagnostics = %#v", diagnostics) } if diagnostics := assembly.applyOverlayWithCollectionPolicy( parseItemPolicyDocument(t, "reintroduction", "catalog:\n prompts: [{id: restored, name: restored}]\n"), nil, collectionPolicyScope{}, nil, LayerBase, sequencePolicy{}, nil, ); len(diagnostics) != 0 { t.Fatalf("ancestor reintroduction diagnostics = %#v", diagnostics) } } t.Run("retained append mode requires a renewed identity", func(t *testing.T) { assembly := newAssembly(t) reintroduceAncestor(t, assembly) policy, ok := assembly.current.collectionPolicies.at(path) if !ok || policy.identityPresent || !policy.explicitModePresent || policy.explicitMode != sequenceModeAppend { t.Fatalf("reintroduced policy = %#v, %v", policy, ok) } diagnostics := assembly.applyOverlayWithCollectionPolicy( parseItemPolicyDocument(t, "next", "catalog:\n prompts: [{value: next}]\n"), nil, collectionPolicyScope{}, nil, LayerBase, sequencePolicy{}, nil, ) if len(diagnostics) != 1 || diagnostics[0].Code != CodeSequenceInvalid { t.Fatalf("append without renewed identity diagnostics = %#v", diagnostics) } }) t.Run("a renewed key restores the retained append mode", func(t *testing.T) { assembly := newAssembly(t) reintroduceAncestor(t, assembly) declaration := sequenceDeclarationSpec{ destination: path, identityFields: []string{"name"}, identityPresent: true, } diagnostics := assembly.applyOverlayWithCollectionPolicy( parseItemPolicyDocument(t, "keyed", "catalog:\n prompts: [{name: next}]\n"), nil, collectionPolicyScope{}, nil, LayerBase, sequencePolicy{}, []sequenceDeclarationSpec{declaration}, ) if len(diagnostics) != 0 { t.Fatalf("renewed identity diagnostics = %#v", diagnostics) } policy, ok := assembly.current.collectionPolicies.at(path) if !ok || !policy.identityPresent || !reflect.DeepEqual(policy.identityFields, []string{"name"}) || !policy.explicitModePresent || policy.explicitMode != sequenceModeAppend { t.Fatalf("renewed collection policy = %#v, %v", policy, ok) } }) } func TestItemSourceVersionProcessingConsumesDeclarationsBeforeComposition(t *testing.T) { t.Parallel() root := parseItemPolicyDocument(t, "item", `version: v1 configkit: sequences: - destination: [prompts] key: id prompts: - id: example `) rule := &VersionRule{Path: Keys("version"), Check: func(version Version, _ *Validation) error { if !version.Present || version.Text != "v1" { t.Errorf("version = %#v, want v1", version) } return nil }} processed := processSourceVersionWithDirectiveContext( root, rule, sourceVersionContribution, Keys("items").Index(0), sourceVersionModeRequired, rootDirectiveContextSource, ) if len(processed.diagnostics) != 0 { t.Fatalf("process item source version: %#v", processed.diagnostics) } if len(processed.sequences) != 1 || !processed.sequences[0].identityPresent { t.Fatalf("extracted item declarations = %#v", processed.sequences) } if _, ok := documentAtPath(processed.payload, Keys("version")); ok { t.Fatalf("consumed version header remains in item payload: %#v", processed.payload) } if _, ok := documentAtPath(processed.payload, Keys("configkit")); ok { t.Fatalf("consumed controls remain in item payload: %#v", processed.payload) } }