458 lines
20 KiB
Go
458 lines
20 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
func TestCompileSequencePolicyNormalizesRegistrations(t *testing.T) {
|
|
parsed, err := ParsePath("/items/0/a~1b")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
registrations []sequenceRegistration
|
|
want map[string]string
|
|
wantError bool
|
|
}{
|
|
{
|
|
name: "identical registrations coalesce",
|
|
registrations: []sequenceRegistration{
|
|
{path: Keys("items"), identityField: "id"},
|
|
{path: Keys("items"), identityField: "id"},
|
|
},
|
|
want: map[string]string{"/items": "id"},
|
|
},
|
|
{
|
|
name: "different identity fields conflict",
|
|
registrations: []sequenceRegistration{
|
|
{path: Keys("items"), identityField: "id"},
|
|
{path: Keys("items"), identityField: "name"},
|
|
},
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "empty identity field is literal",
|
|
registrations: []sequenceRegistration{{path: Keys("items"), identityField: ""}},
|
|
want: map[string]string{"/items": ""},
|
|
},
|
|
{
|
|
name: "numeric and escaped tokens remain literal keys",
|
|
registrations: []sequenceRegistration{{path: parsed, identityField: "id"}},
|
|
want: map[string]string{"/items/0/a~1b": "id"},
|
|
},
|
|
{
|
|
name: "explicit index is invalid",
|
|
registrations: []sequenceRegistration{{path: Keys("items").Index(0), identityField: "id"}},
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "unused and root registrations are accepted",
|
|
registrations: []sequenceRegistration{
|
|
{path: Path{}, identityField: "id"},
|
|
{path: Keys("unused"), identityField: "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 := compileSequencePolicy(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
|
|
}
|
|
if len(policy.identityFields) != len(test.want) {
|
|
t.Fatalf("compiled fields = %#v, want %#v", policy.identityFields, test.want)
|
|
}
|
|
for path, field := range test.want {
|
|
if got := policy.identityFields[path]; got != field {
|
|
t.Errorf("identity field at %q = %q, want %q", path, got, 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 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: Definition[config]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items").Index(0), identityField: "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: Definition[config]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items").Index(0), identityField: "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 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]{sequenceRegistrations: []sequenceRegistration{{path: Keys("items"), identityField: "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("strict underlay group appends while separate tiers reset identities", 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: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
|
|
}
|
|
result, err := testGroupedLoad(request)
|
|
if err != nil || len(result.Config.Items) != 1 || result.Config.Items[0].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 clears lower sequence", 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: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
|
|
}
|
|
result, err = testGroupedLoad(request)
|
|
if err != nil || len(result.Config.Items) != 0 {
|
|
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: mustCompileSequencePolicy(t, definition.sequenceRegistrations),
|
|
}
|
|
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 validates identities and CLI may introduce duplicates", 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 err != nil || len(result.Config.Items) != 2 {
|
|
t.Fatalf("replacement 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 err != nil {
|
|
t.Fatalf("resolver replacement error code = %s", loadCode(t, err))
|
|
}
|
|
if len(result.Config.Items) != 2 {
|
|
t.Fatalf("resolver replacement = %#v, %v", result, err)
|
|
}
|
|
})
|
|
|
|
t.Run("application validation can reject replacement duplicates", func(t *testing.T) {
|
|
checked := definition
|
|
checked.Validate = func(value *config, validation *Validation) {
|
|
seen := make(map[string]bool)
|
|
for index, entry := range value.Items {
|
|
if seen[entry.ID] {
|
|
validation.Error(Keys("items").Index(index).Key("id"), "identity must be unique")
|
|
}
|
|
seen[entry.ID] = 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 != CodeValidateError {
|
|
t.Fatalf("application validation error = %s", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func mustCompileSequencePolicy(t *testing.T, registrations []sequenceRegistration) sequencePolicy {
|
|
t.Helper()
|
|
policy, diagnostics := compileSequencePolicy(registrations)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatal(diagnostics)
|
|
}
|
|
return policy
|
|
}
|