Verify relative import composition and version ownership
This commit is contained in:
@@ -237,7 +237,7 @@ No limitations.
|
|||||||
|
|
||||||
## Stage 4 — Verify composition and effective version ownership
|
## Stage 4 — Verify composition and effective version ownership
|
||||||
|
|
||||||
Status: planned. Scope: assembly interactions; reuse existing algorithms.
|
Status: complete. Scope: assembly interactions; reuse existing algorithms.
|
||||||
|
|
||||||
1. Cover disjoint same-destination contributions, different destinations,
|
1. Cover disjoint same-destination contributions, different destinations,
|
||||||
ancestor overlaps, and collisions with explicitly wrapped content. Equal and
|
ancestor overlaps, and collisions with explicitly wrapped content. Equal and
|
||||||
@@ -259,6 +259,15 @@ Validate: `go test ./internal/engine -run 'Import|Destination|Compose|Overlay|Ov
|
|||||||
and everyday check. Done: relative placement changes neither tier semantics nor
|
and everyday check. Done: relative placement changes neither tier semantics nor
|
||||||
the main-owned version contract.
|
the main-owned version contract.
|
||||||
|
|
||||||
|
Evidence (2026-10-14): based on `4635ef08d5cbeec0b1a1f316cff59cf99e70eba0`,
|
||||||
|
using `go1.26.8 linux/amd64`. The engine import/destination/composition/overlay/
|
||||||
|
override/version/grouped suite, root destination/layered/version suite, and
|
||||||
|
`./scripts/check.sh` all passed. New coverage exercises strict peer behavior,
|
||||||
|
wrapped-content conflicts, lower-tier conflict preservation, nested protected
|
||||||
|
version paths, main-checker timing, and repeated/cyclic physical sources under
|
||||||
|
changed destinations. Existing destination overlay tests continue to verify
|
||||||
|
root-scoped upper tiers. No limitations.
|
||||||
|
|
||||||
## Stage 5 — Verify accumulated resource accounting
|
## Stage 5 — Verify accumulated resource accounting
|
||||||
|
|
||||||
Status: planned. Scope: limit regressions and any necessary narrow correction.
|
Status: planned. Scope: limit regressions and any necessary narrow correction.
|
||||||
|
|||||||
@@ -189,6 +189,225 @@ func TestEntryDestinationSurvivesImportSetupAndParseFailures(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRelativeImportDestinationsKeepStrictCompositionBoundaries(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
main string
|
||||||
|
files map[string]string
|
||||||
|
want map[string]any
|
||||||
|
conflict bool
|
||||||
|
wantPath string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "disjoint peers at one destination",
|
||||||
|
main: "configkit:\n imports:\n - path: left.yml\n destination: [application]\n - path: right.yml\n destination: [application]\n",
|
||||||
|
files: map[string]string{"left.yml": "left: one\n", "right.yml": "right: two\n"},
|
||||||
|
want: map[string]any{"/application/left": "one", "/application/right": "two"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "different destinations",
|
||||||
|
main: "configkit:\n imports:\n - path: left.yml\n destination: [left]\n - path: right.yml\n destination: [right]\n",
|
||||||
|
files: map[string]string{"left.yml": "value: shared\n", "right.yml": "value: shared\n"},
|
||||||
|
want: map[string]any{"/left/value": "shared", "/right/value": "shared"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ancestor and descendant overlap",
|
||||||
|
main: "configkit:\n imports:\n - path: parent.yml\n destination: [application]\n - path: child.yml\n destination: [application, branch]\n",
|
||||||
|
files: map[string]string{"parent.yml": "branch: {base: one}\n", "child.yml": "leaf: two\n"},
|
||||||
|
want: map[string]any{"/application/branch/base": "one", "/application/branch/leaf": "two"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "collision with explicitly wrapped content",
|
||||||
|
main: "configkit: {imports: [{path: fragment.yml, destination: [application]}]}\napplication: {value: root}\n",
|
||||||
|
files: map[string]string{"fragment.yml": "value: imported\n"},
|
||||||
|
conflict: true,
|
||||||
|
wantPath: "/application/value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equal peer values conflict",
|
||||||
|
main: "configkit:\n imports:\n - path: left.yml\n destination: [application]\n - path: right.yml\n destination: [application]\n",
|
||||||
|
files: map[string]string{"left.yml": "value: same\n", "right.yml": "value: same\n"},
|
||||||
|
conflict: true,
|
||||||
|
wantPath: "/application/value",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unequal peer values conflict",
|
||||||
|
main: "configkit:\n imports:\n - path: left.yml\n destination: [application]\n - path: right.yml\n destination: [application]\n",
|
||||||
|
files: map[string]string{"left.yml": "value: left\n", "right.yml": "value: right\n"},
|
||||||
|
conflict: true,
|
||||||
|
wantPath: "/application/value",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := writeImportFile(t, directory, "main.yml", test.main)
|
||||||
|
for name, body := range test.files {
|
||||||
|
writeImportFile(t, directory, name, body)
|
||||||
|
}
|
||||||
|
loaded, err := LoadLayered(
|
||||||
|
Input{Source: File(mainPath), Imports: true}, testSecretBinding(),
|
||||||
|
LayeredOptions[map[string]any]{},
|
||||||
|
)
|
||||||
|
if test.conflict {
|
||||||
|
if loaded != nil || groupedErrorCode(t, err) != CodeComposeConflict {
|
||||||
|
t.Fatalf("relative import composition = %#v, %v", loaded, err)
|
||||||
|
}
|
||||||
|
loadError := err.(*LoadError)
|
||||||
|
if diagnostics := loadError.Diagnostics(); diagnostics[0].Path.String() != test.wantPath {
|
||||||
|
t.Fatalf("conflict path = %q, want %q", diagnostics[0].Path.String(), test.wantPath)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("relative import composition: %v", err)
|
||||||
|
}
|
||||||
|
configuration := testLoaded(loaded).Config
|
||||||
|
for path, want := range test.want {
|
||||||
|
parsed, err := ParsePath(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
keys := make([]string, len(parsed.segments))
|
||||||
|
for index, segment := range parsed.segments {
|
||||||
|
keys[index] = segment.key
|
||||||
|
}
|
||||||
|
if got := layeredMapValue(t, configuration, keys...); got != want {
|
||||||
|
t.Errorf("value at %s = %#v, want %#v", path, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("upper tier cannot repair a lower import conflict", func(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
lowerPath := writeImportFile(t, directory, "lower.yml", `configkit:
|
||||||
|
imports:
|
||||||
|
- path: left.yml
|
||||||
|
destination: [application]
|
||||||
|
- path: right.yml
|
||||||
|
destination: [application]
|
||||||
|
`)
|
||||||
|
writeImportFile(t, directory, "left.yml", "value: left\n")
|
||||||
|
writeImportFile(t, directory, "right.yml", "value: right\n")
|
||||||
|
loaded, err := LoadLayered(
|
||||||
|
Input{Source: Bytes("upper", []byte("application: {value: upper}\n"))},
|
||||||
|
testSecretBinding(),
|
||||||
|
LayeredOptions[map[string]any]{Underlays: []Underlay{{Inputs: []Input{{Source: File(lowerPath), Imports: true}}}}},
|
||||||
|
)
|
||||||
|
if loaded != nil || groupedErrorCode(t, err) != CodeComposeConflict {
|
||||||
|
t.Fatalf("upper tier repaired a lower conflict: %#v, %v", loaded, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRelativeImportDestinationsUseEffectiveVersionOwnership(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
rulePath Path
|
||||||
|
fragment string
|
||||||
|
}{
|
||||||
|
{name: "direct claim", rulePath: Keys("collection", "group", "nested", "version"), fragment: "version: v2\n"},
|
||||||
|
{name: "ancestor claim", rulePath: Keys("collection", "group", "nested", "schema", "version"), fragment: "schema: null\n"},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||||
|
imports:
|
||||||
|
- path: parent.yml
|
||||||
|
destination: [group]
|
||||||
|
`)
|
||||||
|
writeImportFile(t, directory, "parent.yml", `configkit:
|
||||||
|
imports:
|
||||||
|
- path: fragment.yml
|
||||||
|
destination: [nested]
|
||||||
|
`)
|
||||||
|
writeImportFile(t, directory, "fragment.yml", test.fragment)
|
||||||
|
_, err := LoadLayered(
|
||||||
|
Input{Source: File(mainPath), Imports: true, Destination: Keys("collection")},
|
||||||
|
testSecretBinding(),
|
||||||
|
LayeredOptions[map[string]any]{Definition: Definition[map[string]any]{Version: &VersionRule{
|
||||||
|
Path: test.rulePath, Check: func(Version, *Validation) error { return nil },
|
||||||
|
}}},
|
||||||
|
)
|
||||||
|
if groupedErrorCode(t, err) != CodeImportVersion {
|
||||||
|
t.Fatalf("effective imported claim error = %v", err)
|
||||||
|
}
|
||||||
|
loadError := err.(*LoadError)
|
||||||
|
diagnostic := loadError.Diagnostics()[0]
|
||||||
|
if diagnostic.Path.String() != test.rulePath.String() || diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-3" {
|
||||||
|
t.Fatalf("effective version diagnostic = %#v", diagnostic)
|
||||||
|
}
|
||||||
|
sources := loadError.Sources()
|
||||||
|
wantDestinations := []string{"/collection", "/collection/group", "/collection/group/nested"}
|
||||||
|
if len(sources) != len(wantDestinations) {
|
||||||
|
t.Fatalf("version failure sources = %#v", sources)
|
||||||
|
}
|
||||||
|
for index, source := range sources {
|
||||||
|
if source.Destination.String() != wantDestinations[index] {
|
||||||
|
t.Errorf("source %d destination = %q, want %q", index, source.Destination.String(), wantDestinations[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRelativeImportDestinationsKeepMainVersionCheckScoped(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Collection struct {
|
||||||
|
SchemaVersion string `yaml:"schema_version"`
|
||||||
|
Group map[string]any `yaml:"group"`
|
||||||
|
} `yaml:"collection"`
|
||||||
|
}
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||||
|
imports:
|
||||||
|
- path: parent.yml
|
||||||
|
destination: [group]
|
||||||
|
schema_version: 6
|
||||||
|
`)
|
||||||
|
writeImportFile(t, directory, "parent.yml", `configkit:
|
||||||
|
imports:
|
||||||
|
- path: fragment.yml
|
||||||
|
destination: [nested]
|
||||||
|
`)
|
||||||
|
writeImportFile(t, directory, "fragment.yml", "version: 7\n")
|
||||||
|
checks := 0
|
||||||
|
loaded, err := LoadLayered(
|
||||||
|
Input{Source: File(mainPath), Imports: true, Destination: Keys("collection")},
|
||||||
|
testSecretBinding(),
|
||||||
|
LayeredOptions[config]{Definition: Definition[config]{Version: &VersionRule{
|
||||||
|
Path: Keys("collection", "schema_version"),
|
||||||
|
Check: func(version Version, _ *Validation) error {
|
||||||
|
checks++
|
||||||
|
if !version.Present || version.Text != "6" {
|
||||||
|
t.Errorf("main-owned version = %#v", version)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}}},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ordinary nested imported version was rejected: %v", err)
|
||||||
|
}
|
||||||
|
result := testLoaded(loaded)
|
||||||
|
if checks != 1 || result.Config.Collection.SchemaVersion != "6" {
|
||||||
|
t.Fatalf("main version callback/value = %d/%q", checks, result.Config.Collection.SchemaVersion)
|
||||||
|
}
|
||||||
|
nested, ok := result.Config.Collection.Group["nested"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("nested imported payload = %#v", result.Config.Collection.Group["nested"])
|
||||||
|
}
|
||||||
|
if got, ok := nested["version"].(int64); !ok || got != 7 {
|
||||||
|
t.Fatalf("ordinary nested version = %#v, want integer payload 7", nested["version"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestLayeredNativeFilesystemImportsInheritInputDestination(t *testing.T) {
|
func TestLayeredNativeFilesystemImportsInheritInputDestination(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -307,7 +526,7 @@ func TestLayeredDestinationsDoNotChangeRepeatedSourceIdentity(t *testing.T) {
|
|||||||
|
|
||||||
directory := t.TempDir()
|
directory := t.TempDir()
|
||||||
writeImportFile(t, directory, "shared.yml", "value: shared\n")
|
writeImportFile(t, directory, "shared.yml", "value: shared\n")
|
||||||
importerPath := writeImportFile(t, directory, "importer.yml", "configkit: {imports: [{path: shared.yml}]}\n")
|
importerPath := writeImportFile(t, directory, "importer.yml", "configkit: {imports: [{path: shared.yml, destination: [nested]}]}\n")
|
||||||
_, err := LoadLayered(
|
_, err := LoadLayered(
|
||||||
Input{Source: NoSource()},
|
Input{Source: NoSource()},
|
||||||
testSecretBinding(),
|
testSecretBinding(),
|
||||||
@@ -323,4 +542,27 @@ func TestLayeredDestinationsDoNotChangeRepeatedSourceIdentity(t *testing.T) {
|
|||||||
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeImportRepeated {
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeImportRepeated {
|
||||||
t.Fatalf("load error = %#v", err)
|
t.Fatalf("load error = %#v", err)
|
||||||
}
|
}
|
||||||
|
sources := loadError.Sources()
|
||||||
|
if len(sources) != 3 || sources[1].Destination.String() != "/imported/nested" || sources[2].Destination.String() != "/direct" {
|
||||||
|
t.Fatalf("repeated import sources = %#v", sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("placement does not break a physical cycle", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: parent.yml, destination: [one]}]}\n")
|
||||||
|
writeImportFile(t, directory, "parent.yml", "configkit: {imports: [{path: main.yml, destination: [two]}]}\n")
|
||||||
|
_, err := LoadLayered(Input{Source: File(mainPath), Imports: true}, testSecretBinding(), LayeredOptions[map[string]any]{})
|
||||||
|
loadError, ok := err.(*LoadError)
|
||||||
|
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeImportCycle {
|
||||||
|
t.Fatalf("cycle load error = %#v", err)
|
||||||
|
}
|
||||||
|
diagnostic := loadError.Diagnostics()[0]
|
||||||
|
if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" {
|
||||||
|
t.Fatalf("cycle primary location = %#v", diagnostic.Primary)
|
||||||
|
}
|
||||||
|
if sources := loadError.Sources(); len(sources) != 2 || sources[1].Destination.String() != "/one" {
|
||||||
|
t.Fatalf("cycle sources = %#v", sources)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user