diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index d543cdb..eb2d22e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -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. diff --git a/internal/engine/destination_import_test.go b/internal/engine/destination_import_test.go index 1a49b30..da51370 100644 --- a/internal/engine/destination_import_test.go +++ b/internal/engine/destination_import_test.go @@ -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) + } + }) }