From 751787ec24061a60b80e0257fce88352d0d14f48 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Wed, 14 Oct 2026 13:50:08 +0000 Subject: [PATCH] Verify mounted inheritance and sensitivity integration --- docs/roadmap/implementation.md | 31 +- .../engine/destination_integration_test.go | 463 ++++++++++++++++++ internal/engine/inheritance_load.go | 2 +- 3 files changed, 492 insertions(+), 4 deletions(-) create mode 100644 internal/engine/destination_integration_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 43f1a34..c435579 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -374,7 +374,7 @@ placement and physical origin without consumer reconstruction. ## Stage 9 — Verify final inheritance, sensitivity, and lifecycle integration -Status: planned. Scope: complete private engine behavior before facade exposure. +Status: complete. Scope: complete private engine behavior before facade exposure. 1. Resolve sibling inheritance only after full source precedence using the existing expansion path. Cover a child and parent supplied by separate peers, @@ -393,8 +393,33 @@ Status: planned. Scope: complete private engine behavior before facade exposure. placement creates a distinct risk. Validate: destination/inheritance/secret/sensitivity/explain/diff/lifecycle tests -and everyday check. Done: all approved engine semantics work together without -per-fragment loading or a post-return processing pass. +and everyday check. Validated with +`go test ./internal/engine -run 'Destination|Inheritance|Secret|Sensitivity|Explain|Diff|Load|Version|Grouped' -count=1`, +`go test -race ./internal/engine`, and `./scripts/check.sh` (go1.26.8 +linux/amd64); all passed. + +`TestLayeredDestinationInheritanceUsesFinalSiblingScopes` verifies parent and +child contributions from separate peers, a higher-tier parent update, nested +destination-local siblings, and cancellation. `TestLayeredDestinationInheritanceDoesNotSearchOtherCollections` +keeps sibling lookup local. `TestLayeredMountedVersionProtectionCoversInheritanceAndUpperTiers` +protects inherited values, scalar/null ancestors, consumed directives, selected +overlays, and CLI overrides at mounted paths while allowing nonclaiming mapping +ancestors and unrelated inheritance. The coverage exposed an ancestor gap in +the existing inheritance-version check; it now applies the shared fragment claim +logic to the transferred parent subtree. + +`TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape` checks one +defaults/resolution/secret-lookup/validation lifecycle, final mounted decoding, +field tags and effective-path predicates, superseded history, and safe effective +output/explanation against canaries; it compares mounted and explicitly wrapped +loads for equal configuration and no diff. `TestLayeredDestinationDiffUsesBilateralSensitivity` +checks redaction from either side and recorded dependencies, and +`TestLayeredDestinationDecodeFailureReturnsNoResultOrLaterCallbacks` checks a +mounted decode failure returns no result. Stage 8's +`TestLayeredImportedInheritedValuesRetainResourceContexts` continues to verify +the declaring context for inherited relative resources. Done: approved engine +semantics integrate without per-fragment loading or a post-return processing +pass. ## Stage 10 — Expose the root-owned facade and public acceptance tests diff --git a/internal/engine/destination_integration_test.go b/internal/engine/destination_integration_test.go new file mode 100644 index 0000000..e59a279 --- /dev/null +++ b/internal/engine/destination_integration_test.go @@ -0,0 +1,463 @@ +package engine + +import ( + "bytes" + "fmt" + "reflect" + "strings" + "testing" + "testing/fstest" +) + +func TestLayeredDestinationInheritanceUsesFinalSiblingScopes(t *testing.T) { + t.Parallel() + + loaded, err := LoadLayered( + Input{Source: Bytes("main", []byte("parent:\n label: final\n settings:\n primary: {height: 4}\ncancelled:\n extends: null\n own: retained\n")), Destination: Keys("catalog", "entries")}, + testSecretBinding(), + LayeredOptions[map[string]any]{Underlays: []Underlay{{Inputs: []Input{ + {Source: Bytes("parent", []byte("parent:\n label: lower\n settings:\n primary: {width: '10'}\n secondary: {extends: primary}\ncancelled:\n extends: parent\n own: retained\n")), Destination: Keys("catalog", "entries")}, + {Source: Bytes("child", []byte("child:\n extends: parent\n settings:\n primary: {width: '20'}\n secondary: {extends: primary}\n")), Destination: Keys("catalog", "entries")}, + }}}}, + ) + if err != nil { + t.Fatalf("LoadLayered: %v", err) + } + result := testLoaded(loaded) + entries := layeredMap(t, layeredMap(t, result.Config, "catalog"), "entries") + parent := layeredMap(t, entries, "parent") + child := layeredMap(t, entries, "child") + parentSettings := layeredMap(t, parent, "settings") + childSettings := layeredMap(t, child, "settings") + if parent["label"] != "final" || child["label"] != "final" { + t.Fatalf("higher-tier parent was not inherited after precedence: parent=%#v child=%#v", parent, child) + } + if layeredMapValue(t, parentSettings, "secondary", "width") != "10" || + fmt.Sprint(layeredMapValue(t, parentSettings, "secondary", "height")) != "4" || + layeredMapValue(t, childSettings, "secondary", "width") != "20" || + fmt.Sprint(layeredMapValue(t, childSettings, "secondary", "height")) != "4" { + t.Fatalf("nested sibling scopes were not resolved at their destinations: parent=%#v child=%#v", parentSettings, childSettings) + } + cancelled := layeredMap(t, entries, "cancelled") + if len(cancelled) != 1 || cancelled["own"] != "retained" { + t.Fatalf("higher-tier cancellation did not suppress inheritance: %#v", cancelled) + } + if _, ok := result.Metadata().Lookup(Keys("catalog", "entries", "child", inheritanceKey)); ok { + t.Fatal("effective child retained its extends directive") + } +} + +func TestLayeredDestinationInheritanceDoesNotSearchOtherCollections(t *testing.T) { + t.Parallel() + + _, err := LoadLayered( + Input{Source: Bytes("main", []byte("parent: {value: foreign}\n")), Destination: Keys("catalog", "other")}, + testSecretBinding(), + LayeredOptions[map[string]any]{Underlays: []Underlay{{Inputs: []Input{{ + Source: Bytes("child", []byte("child: {extends: parent}\n")), + Destination: Keys("catalog", "entries"), + }}}}}, + ) + if got := groupedErrorCode(t, err); got != CodeInheritanceMissing { + t.Fatalf("cross-collection parent lookup error = %s, want %s", got, CodeInheritanceMissing) + } +} + +func TestLayeredMountedVersionProtectionCoversInheritanceAndUpperTiers(t *testing.T) { + t.Parallel() + + for _, test := range []struct { + name string + body string + rulePath Path + }{ + {name: "inherited protected value", body: "parent: {version: v2}\nchild: {extends: parent}\n", rulePath: Keys("catalog", "entries", "child", "version")}, + {name: "inherited protected ancestor", body: "parent: {schema: null}\nchild: {extends: parent}\n", rulePath: Keys("catalog", "entries", "child", "schema", "version")}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + _, err := LoadLayered( + Input{Source: NoSource()}, testSecretBinding(), + LayeredOptions[map[string]any]{ + Definition: Definition[map[string]any]{Version: &VersionRule{Path: test.rulePath, Check: func(Version, *Validation) error { return nil }}}, + Underlays: []Underlay{{Inputs: []Input{{Source: Bytes("underlay", []byte(test.body)), Destination: Keys("catalog", "entries")}}}}, + }, + ) + if got := groupedErrorCode(t, err); got != CodeInheritanceVersion { + t.Fatalf("inheritance version protection code = %s", got) + } + }) + } + t.Run("inheritance consumes a protected directive", func(t *testing.T) { + _, err := LoadLayered( + Input{Source: Bytes("main", []byte("parent: {value: inherited}\nchild: {extends: parent}\n")), Destination: Keys("catalog", "entries")}, + testSecretBinding(), + LayeredOptions[map[string]any]{Definition: Definition[map[string]any]{Version: &VersionRule{ + Path: Keys("catalog", "entries", "child", "extends"), + Check: func(version Version, _ *Validation) error { + if !version.Present || version.Text != "parent" { + t.Errorf("mounted protected directive value = %#v", version) + } + return nil + }, + }}}, + ) + if got := groupedErrorCode(t, err); got != CodeInheritanceVersion { + t.Fatalf("consumed directive protection code = %s", got) + } + }) + t.Run("inherited mapping ancestor without a version claim", func(t *testing.T) { + loaded, err := LoadLayered( + Input{Source: NoSource()}, testSecretBinding(), + LayeredOptions[map[string]any]{ + Definition: Definition[map[string]any]{Version: &VersionRule{Path: Keys("catalog", "entries", "child", "schema", "version"), Check: func(Version, *Validation) error { return nil }}}, + Underlays: []Underlay{{Inputs: []Input{{ + Source: Bytes("underlay", []byte("parent: {schema: {name: ordinary}}\nchild: {extends: parent}\n")), Destination: Keys("catalog", "entries"), + }}}}, + }, + ) + if err != nil { + t.Fatalf("nonclaiming inherited mapping ancestor was rejected: %v", err) + } + if got := layeredMapValue(t, testLoaded(loaded).Config, "catalog", "entries", "child", "schema", "name"); got != "ordinary" { + t.Fatalf("inherited non-version value = %#v", got) + } + }) + + for _, test := range []struct { + name string + body string + }{ + {name: "overlay protected value", body: "catalog: {schema: {version: v2}}\n"}, + {name: "overlay protected ancestor", body: "catalog: {schema: null}\n"}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + filesystem := NewFilesystem("overlay", fstest.MapFS{"overlays/test.yml": &fstest.MapFile{Data: []byte(test.body)}}) + _, err := LoadLayered( + Input{Source: Bytes("main", []byte("schema: {version: v1}\n")), Destination: Keys("catalog")}, + testSecretBinding(), + LayeredOptions[map[string]any]{ + Definition: Definition[map[string]any]{Version: &VersionRule{Path: Keys("catalog", "schema", "version"), Check: func(version Version, _ *Validation) error { + if !version.Present || version.Text != "v1" { + t.Errorf("mounted main version = %#v", version) + } + return nil + }}}, + Overlay: "test", OverlayFilesystem: &FilesystemContext{Root: ".", BaseDir: ".", FS: &filesystem}, + }, + ) + if got := groupedErrorCode(t, err); got != CodeOverlayVersion { + t.Fatalf("overlay version protection code = %s", got) + } + }) + } + + for _, test := range []struct { + name string + path Path + value any + }{ + {name: "override protected value", path: Keys("catalog", "schema", "version"), value: "v2"}, + {name: "override protected ancestor", path: Keys("catalog", "schema"), value: map[string]any{"version": "v2"}}, + } { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + _, err := LoadLayered( + Input{Source: Bytes("main", []byte("schema: {version: v1}\n")), Destination: Keys("catalog")}, + testSecretBinding(), + LayeredOptions[map[string]any]{ + Definition: Definition[map[string]any]{Version: &VersionRule{Path: Keys("catalog", "schema", "version"), Check: func(Version, *Validation) error { return nil }}}, + Overrides: []Override{{Path: test.path, Value: test.value, Flag: "--config"}}, + }, + ) + if got := groupedErrorCode(t, err); got != CodeOverrideVersion { + t.Fatalf("override version protection code = %s", got) + } + }) + } + + t.Run("unrelated inheritance under the protected mapping ancestor", func(t *testing.T) { + loaded, err := LoadLayered( + Input{Source: Bytes("main", []byte("schema: {version: v1}\nentries:\n parent: {value: inherited}\n child: {extends: parent}\n")), Destination: Keys("catalog")}, + testSecretBinding(), + LayeredOptions[map[string]any]{Definition: Definition[map[string]any]{Version: &VersionRule{Path: Keys("catalog", "schema", "version"), Check: func(Version, *Validation) error { return nil }}}}, + ) + if err != nil { + t.Fatalf("unrelated mounted inheritance was rejected: %v", err) + } + child := layeredMapValue(t, testLoaded(loaded).Config, "catalog", "entries", "child", "value") + if child != "inherited" { + t.Fatalf("unrelated inheritance value = %#v", child) + } + }) +} + +type destinationDecodeProbe string + +var destinationDecodeProbeValues []string + +func (value *destinationDecodeProbe) UnmarshalText(text []byte) error { + destinationDecodeProbeValues = append(destinationDecodeProbeValues, string(text)) + *value = destinationDecodeProbe(text) + return nil +} + +func (value destinationDecodeProbe) MarshalText() ([]byte, error) { + return []byte(value), nil +} + +type destinationLifecycleConfig struct { + Defaulted string `yaml:"defaulted"` + Tenant struct { + Tagged string `yaml:"tagged" configkit:"secret"` + Dynamic string `yaml:"dynamic"` + Credential Secret `yaml:"credential"` + Derived string `yaml:"derived"` + Decoded destinationDecodeProbe `yaml:"decoded"` + } `yaml:"tenant"` +} + +func TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape(t *testing.T) { + const ( + oldTagged = "SUPERSEDED-TAGGED-CANARY" + newTagged = "CURRENT-TAGGED-CANARY" + oldDynamic = "SUPERSEDED-DYNAMIC-CANARY" + newDynamic = "CURRENT-DYNAMIC-CANARY" + credential = "RESOLVED-CREDENTIAL-CANARY" + secretName = "DESTINATION_SECRET_NAME" + decodeValue = "decoded-once" + ) + type calls struct{ defaults, resolve, lookup, validate int } + newLoad := func(mounted bool, observed *calls) (*testResult[destinationLifecycleConfig], error) { + mainBody := "tagged: " + newTagged + "\ndynamic: " + newDynamic + "\ncredential: " + secretName + "\ndecoded: " + decodeValue + "\n" + underlayBody := "tagged: " + oldTagged + "\ndynamic: " + oldDynamic + "\ncredential: OLD_DESTINATION_SECRET_NAME\ndecoded: old\n" + if !mounted { + mainBody = "tenant:\n" + indentDestinationBody(mainBody) + underlayBody = "tenant:\n" + indentDestinationBody(underlayBody) + } + main := loadInput{source: Bytes("main", []byte(mainBody))} + underlay := loadInput{source: Bytes("underlay", []byte(underlayBody))} + if mounted { + main.destination = Keys("tenant") + underlay.destination = Keys("tenant") + } + definition := Definition[destinationLifecycleConfig]{ + Defaults: func() destinationLifecycleConfig { + observed.defaults++ + return destinationLifecycleConfig{Defaulted: "from-default"} + }, + Resolve: func(configuration *destinationLifecycleConfig, resolution *Resolution) error { + observed.resolve++ + configuration.Tenant.Derived = configuration.Tenant.Dynamic + resolution.Record(Keys("tenant", "derived"), ChangeNormalization, "derive from mounted input", Keys("tenant", "dynamic")) + return nil + }, + Validate: func(configuration *destinationLifecycleConfig, _ *Validation) { + observed.validate++ + if configuration.Tenant.Decoded != destinationDecodeProbe(decodeValue) || configuration.Tenant.Dynamic != newDynamic { + t.Errorf("validation saw a non-final mounted configuration: %#v", configuration.Tenant) + } + }, + Secret: func(path Path) bool { return path.String() == "/tenant/dynamic" }, + } + loaded, err := loadGroupedWithSecretLookup(loadRequest[destinationLifecycleConfig]{ + main: main, underlays: []loadGroup{{inputs: []loadInput{underlay}}}, definition: definition, + }, testSecretBinding(), func(name string) (string, bool) { + observed.lookup++ + if name != secretName { + return "", false + } + return credential, true + }) + return testLoaded(loaded), err + } + + var observed calls + destinationDecodeProbeValues = nil + mounted, err := newLoad(true, &observed) + if err != nil { + if loadError, ok := err.(*LoadError); ok { + t.Fatalf("mounted load: %#v", loadError.Diagnostics()) + } + t.Fatalf("mounted load: %v", err) + } + if observed != (calls{defaults: 1, resolve: 1, lookup: 1, validate: 1}) { + t.Fatalf("mounted lifecycle counts = %#v", observed) + } + sawFinalDecode := false + for _, value := range destinationDecodeProbeValues { + if value == decodeValue { + sawFinalDecode = true + } else if value != "" { + t.Fatalf("decoder observed a non-final mounted value %q: %#v", value, destinationDecodeProbeValues) + } + } + if !sawFinalDecode { + t.Fatal("final mounted value was not decoded") + } + if mounted.Config.Defaulted != "from-default" || mounted.Config.Tenant.Tagged != newTagged || mounted.Config.Tenant.Dynamic != newDynamic || + mounted.Config.Tenant.Derived != newDynamic || mounted.Config.Tenant.Decoded != destinationDecodeProbe(decodeValue) || + mounted.Config.Tenant.Credential.Value() != credential { + t.Fatalf("mounted final configuration = %#v", mounted.Config) + } + for _, path := range []Path{Keys("tenant", "tagged"), Keys("tenant", "dynamic"), Keys("tenant", "derived")} { + if !mounted.Inspection.secrets[path.String()] { + t.Errorf("effective sensitivity missing at %s: %#v", path, mounted.Inspection.secrets) + } + } + for _, path := range []Path{Keys("tenant", "tagged"), Keys("tenant", "dynamic"), Keys("tenant", "derived")} { + explanation, ok := mounted.Explain(path) + if !ok || !explanation.Effective.Redacted { + t.Errorf("effective explanation at %s = %#v, %v", path, explanation, ok) + continue + } + for _, change := range explanation.History { + if strings.Contains(change.Before.YAML+change.After.YAML, oldTagged) || + strings.Contains(change.Before.YAML+change.After.YAML, newTagged) || + strings.Contains(change.Before.YAML+change.After.YAML, oldDynamic) || + strings.Contains(change.Before.YAML+change.After.YAML, newDynamic) { + t.Errorf("classified history leaked a canary at %s: %#v", path, change) + } + } + var rendered bytes.Buffer + if err := mounted.WriteExplanation(&rendered, path); err != nil || strings.Contains(rendered.String(), "CANARY") { + t.Errorf("safe explanation at %s = %q, %v", path, rendered.String(), err) + } + } + var effective bytes.Buffer + if err := mounted.WriteEffective(&effective); err != nil { + t.Fatal(err) + } + for _, canary := range []string{oldTagged, newTagged, oldDynamic, newDynamic, credential} { + if strings.Contains(effective.String(), canary) { + t.Fatalf("effective rendering exposed %q: %s", canary, effective.String()) + } + } + if serializedConfig := fmt.Sprintf("%#v", mounted.Config); strings.Contains(serializedConfig, credential) { + t.Fatalf("configuration formatting exposed the resolved credential: %s", serializedConfig) + } + for _, path := range []Path{Keys("tenant", "tagged"), Keys("tenant", "dynamic")} { + if history := fmt.Sprintf("%#v", mounted.Metadata().History(path)); strings.Contains(history, "CANARY") { + t.Fatalf("metadata history at %s exposed a canary: %s", path, history) + } + } + + var authoredCalls calls + destinationDecodeProbeValues = nil + authored, err := newLoad(false, &authoredCalls) + if err != nil { + t.Fatalf("explicitly wrapped load: %v", err) + } + if authoredCalls != (calls{defaults: 1, resolve: 1, lookup: 1, validate: 1}) { + t.Fatalf("authored lifecycle counts = %#v", authoredCalls) + } + sawFinalDecode = false + for _, value := range destinationDecodeProbeValues { + if value == decodeValue { + sawFinalDecode = true + } else if value != "" { + t.Fatalf("authored decoder observed a non-final value %q: %#v", value, destinationDecodeProbeValues) + } + } + if !sawFinalDecode { + t.Fatal("final authored value was not decoded") + } + if !reflect.DeepEqual(mounted.Config, authored.Config) { + t.Fatalf("mounted config %#v differs from explicitly wrapped config %#v", mounted.Config, authored.Config) + } + differences, err := Diff(mounted.Inspection, authored.Inspection) + if err != nil || len(differences) != 0 { + t.Fatalf("mounted versus authored diff = %#v, %v", differences, err) + } +} + +func TestLayeredDestinationDiffUsesBilateralSensitivity(t *testing.T) { + t.Parallel() + + type config struct { + Tenant struct { + Dynamic string `yaml:"dynamic"` + Derived string `yaml:"derived"` + } `yaml:"tenant"` + } + load := func(value string, classify bool) *testResult[config] { + t.Helper() + loaded, err := loadGroupedWithSecretLookup(loadRequest[config]{ + main: loadInput{source: Bytes("input", []byte("dynamic: "+value+"\n")), destination: Keys("tenant")}, + definition: Definition[config]{ + Resolve: func(configuration *config, resolution *Resolution) error { + configuration.Tenant.Derived = configuration.Tenant.Dynamic + resolution.Record(Keys("tenant", "derived"), ChangeNormalization, "derive from dynamic input", Keys("tenant", "dynamic")) + return nil + }, + Secret: func(path Path) bool { return classify && path.String() == "/tenant/dynamic" }, + }, + }, testSecretBinding(), func(string) (string, bool) { return "", false }) + if err != nil { + t.Fatalf("LoadLayered: %v", err) + } + return testLoaded(loaded) + } + left := load("LEFT-DIFF-CANARY", true) + right := load("RIGHT-DIFF-CANARY", false) + differences, err := Diff(left.Inspection, right.Inspection) + if err != nil { + t.Fatal(err) + } + if len(differences) != 2 { + t.Fatalf("differences = %#v", differences) + } + seen := make(map[string]bool) + for _, difference := range differences { + path := difference.Path.String() + seen[path] = true + if !difference.Left.Redacted || !difference.Right.Redacted || !difference.HiddenChanged || + strings.Contains(difference.Left.YAML+difference.Right.YAML, "DIFF-CANARY") { + t.Errorf("bilaterally classified difference at %s = %#v", path, difference) + } + } + for _, path := range []string{"/tenant/dynamic", "/tenant/derived"} { + if !seen[path] || !left.Inspection.secrets[path] || right.Inspection.secrets[path] { + t.Errorf("classification/difference at %s: seen=%v left=%v right=%v", path, seen[path], left.Inspection.secrets[path], right.Inspection.secrets[path]) + } + } +} + +func TestLayeredDestinationDecodeFailureReturnsNoResultOrLaterCallbacks(t *testing.T) { + t.Parallel() + + type config struct { + Tenant struct { + Count int `yaml:"count"` + } `yaml:"tenant"` + } + calls := struct{ defaults, resolve, lookup, validate int }{} + loaded, err := loadGroupedWithSecretLookup(loadRequest[config]{ + main: loadInput{source: Bytes("invalid", []byte("count: not-an-integer\n")), destination: Keys("tenant")}, + definition: Definition[config]{ + Defaults: func() config { calls.defaults++; return config{} }, + Resolve: func(*config, *Resolution) error { calls.resolve++; return nil }, + Validate: func(*config, *Validation) { calls.validate++ }, + }, + }, testSecretBinding(), func(string) (string, bool) { calls.lookup++; return "", false }) + if loaded != nil || err == nil { + t.Fatalf("mounted decode failure returned a usable result: %#v, %v", loaded, err) + } + if calls.defaults != 1 || calls.resolve != 0 || calls.lookup != 0 || calls.validate != 0 { + t.Fatalf("callbacks after mounted decode failure = %#v", calls) + } + if got := groupedErrorCode(t, err); got != CodeDecodeType { + t.Fatalf("mounted decode failure code = %s", got) + } +} + +func indentDestinationBody(body string) string { + var result strings.Builder + for _, line := range strings.Split(strings.TrimSuffix(body, "\n"), "\n") { + result.WriteString(" ") + result.WriteString(line) + result.WriteByte('\n') + } + return result.String() +} diff --git a/internal/engine/inheritance_load.go b/internal/engine/inheritance_load.go index b7dd9af..4766892 100644 --- a/internal/engine/inheritance_load.go +++ b/internal/engine/inheritance_load.go @@ -41,7 +41,7 @@ func inheritanceVersionDiagnostic(rule *VersionRule, expanded *inheritanceScope) } } else if operation.transfer != nil { if relative, within := relativeHistoryPath(operation.transfer.to, rule.Path); within { - _, affected = documentAtPath(operation.parent, relative) + affected = fragmentVersionClaim(operation.parent, &VersionRule{Path: relative}) != nil } else if pathContains(rule.Path, operation.transfer.to) { affected = true }