Integrate policy-aware item scopes
This commit is contained in:
229
internal/engine/item_policy_test.go
Normal file
229
internal/engine/item_policy_test.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user