Add dynamic sequence identity selectors

This commit is contained in:
2026-10-23 13:55:25 +00:00
parent 64c51647f0
commit 298e70b552
4 changed files with 451 additions and 41 deletions

View File

@@ -85,6 +85,160 @@ func TestCompileSequencePolicyNormalizesRegistrations(t *testing.T) {
}
}
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 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 {