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
|
||||
|
||||
Status: planned. Scope: assembly interactions; reuse existing algorithms.
|
||||
Status: complete. Scope: assembly interactions; reuse existing algorithms.
|
||||
|
||||
1. Cover disjoint same-destination contributions, different destinations,
|
||||
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
|
||||
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
|
||||
|
||||
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) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -307,7 +526,7 @@ func TestLayeredDestinationsDoNotChangeRepeatedSourceIdentity(t *testing.T) {
|
||||
|
||||
directory := t.TempDir()
|
||||
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(
|
||||
Input{Source: NoSource()},
|
||||
testSecretBinding(),
|
||||
@@ -323,4 +542,27 @@ func TestLayeredDestinationsDoNotChangeRepeatedSourceIdentity(t *testing.T) {
|
||||
if !ok || len(loadError.Diagnostics()) == 0 || loadError.Diagnostics()[0].Code != CodeImportRepeated {
|
||||
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