Verify layered resolution provenance
This commit is contained in:
@@ -167,7 +167,7 @@ Stage 2 risks were identified.
|
|||||||
|
|
||||||
## Stage 3 — Verify layered provenance and snapshot independence
|
## Stage 3 — Verify layered provenance and snapshot independence
|
||||||
|
|
||||||
Status: not started. Dependencies: Stage 2.
|
Status: complete (2026-09-12). Dependencies: Stage 2.
|
||||||
|
|
||||||
Scope: public lifecycle/provenance, import/profile/override, and ownership tests;
|
Scope: public lifecycle/provenance, import/profile/override, and ownership tests;
|
||||||
production fixes only for gaps exposed at those boundaries.
|
production fixes only for gaps exposed at those boundaries.
|
||||||
@@ -202,6 +202,20 @@ is introduced. Any discovered defect is fixed before the stage is marked complet
|
|||||||
Validation: `go test . -run 'Resolution|Provenance|Import|Profile|Override|Metadata|Path'`,
|
Validation: `go test . -run 'Resolution|Provenance|Import|Profile|Override|Metadata|Path'`,
|
||||||
`go test -race ./...`, then the everyday checker.
|
`go test -race ./...`, then the everyday checker.
|
||||||
|
|
||||||
|
Completion record (2026-09-12): public lifecycle tests now build real temporary
|
||||||
|
main/import/profile graphs and prove scalar, sequence, composite-container,
|
||||||
|
profile, and equal-text CLI provenance through `InputInfo`/`Source`, deriving source
|
||||||
|
IDs from returned locations. Coverage also exercises byte and reader roots with
|
||||||
|
filesystem imports, a standalone non-file root, retained defaults, unknown IDs,
|
||||||
|
typed/inlined/map/array/dynamic shape matching, signed mapping keys, and invalid
|
||||||
|
sequence shapes. Recorded normalization, deletion, addition, and move operations
|
||||||
|
leave entry queries fixed while final metadata reflects resolution. Returned records,
|
||||||
|
completed results, another load, and concurrent independent loads remain detached.
|
||||||
|
No production gap was exposed. `go test . -run
|
||||||
|
'Resolution|Provenance|Import|Profile|Override|Metadata|Path'`, `go test -race
|
||||||
|
./...`, and `./scripts/check.sh` passed with Go 1.26.7 on Linux. No deviations or
|
||||||
|
remaining Stage 3 risks were identified.
|
||||||
|
|
||||||
## Stage 4 — Demonstrate source-aware normalization and safe inspection
|
## Stage 4 — Demonstrate source-aware normalization and safe inspection
|
||||||
|
|
||||||
Status: not started. Dependencies: Stage 3.
|
Status: not started. Dependencies: Stage 3.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package configkit
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -205,6 +206,295 @@ func TestSelectedProfileLoadsRemainIndependentConcurrently(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolutionInputKeepsLayeredOriginsAndSources(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
mainPath := writeImportFile(t, directory, "main.yml", `configkit:
|
||||||
|
imports: [{path: first.yml}, {path: second.yml}]
|
||||||
|
main: main
|
||||||
|
equal: same
|
||||||
|
cli_equal: same
|
||||||
|
composite: {main: main}
|
||||||
|
`)
|
||||||
|
firstPath := writeImportFile(t, directory, "first.yml", `imported: imported
|
||||||
|
sequence: [first, second]
|
||||||
|
composite: {first: first}
|
||||||
|
`)
|
||||||
|
secondPath := writeImportFile(t, directory, "second.yml", "composite: {second: second}\n")
|
||||||
|
profilePath := writeImportFile(t, directory, "profiles/testing.yml", "equal: same\nprofile: profile\n")
|
||||||
|
compositeSources := map[string]bool{
|
||||||
|
canonicalTestPath(t, mainPath): true,
|
||||||
|
canonicalTestPath(t, firstPath): true,
|
||||||
|
canonicalTestPath(t, secondPath): true,
|
||||||
|
}
|
||||||
|
type config struct {
|
||||||
|
Main string `yaml:"main"`
|
||||||
|
Imported string `yaml:"imported"`
|
||||||
|
Sequence []string `yaml:"sequence"`
|
||||||
|
Composite map[string]string `yaml:"composite"`
|
||||||
|
Equal string `yaml:"equal"`
|
||||||
|
Profile string `yaml:"profile"`
|
||||||
|
CLIEqual string `yaml:"cli_equal"`
|
||||||
|
}
|
||||||
|
var callbackErr error
|
||||||
|
result, err := Load[config](File(mainPath), Options[config]{
|
||||||
|
Imports: true, Profile: "testing", Overrides: []Override{{Path: Keys("cli_equal"), Value: "same", Flag: "--cli-equal"}},
|
||||||
|
Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||||
|
check := func(ok bool, message string) {
|
||||||
|
if !ok && callbackErr == nil {
|
||||||
|
callbackErr = errors.New(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileOwner := func(path Path, want string) FieldInfo {
|
||||||
|
info, found := resolution.InputInfo(path)
|
||||||
|
check(found && len(info.Origin.Locations) == 1, "scalar has no exact origin")
|
||||||
|
if !found || len(info.Origin.Locations) != 1 {
|
||||||
|
return FieldInfo{}
|
||||||
|
}
|
||||||
|
source, exists := resolution.Source(info.Origin.Locations[0].SourceID)
|
||||||
|
check(exists && source.Path == want, "scalar source does not match its declaration")
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
fileOwner(Keys("main"), canonicalTestPath(t, mainPath))
|
||||||
|
fileOwner(Keys("imported"), canonicalTestPath(t, firstPath))
|
||||||
|
fileOwner(Keys("sequence").Index(1), canonicalTestPath(t, firstPath))
|
||||||
|
profile := fileOwner(Keys("equal"), canonicalTestPath(t, profilePath))
|
||||||
|
fileOwner(Keys("profile"), canonicalTestPath(t, profilePath))
|
||||||
|
check(profile.Origin.Layer == LayerProfile, "profile replacement retained base ownership")
|
||||||
|
cli, found := resolution.InputInfo(Keys("cli_equal"))
|
||||||
|
check(found && cli.Origin.Layer == LayerCLI && len(cli.Origin.Locations) == 1, "CLI replacement lost ownership")
|
||||||
|
if found && len(cli.Origin.Locations) == 1 {
|
||||||
|
source, exists := resolution.Source(cli.Origin.Locations[0].SourceID)
|
||||||
|
check(exists && source.Layer == LayerCLI && source.Path == "", "CLI source has a file path or is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
composite, found := resolution.InputInfo(Keys("composite"))
|
||||||
|
check(found && len(composite.Origin.Locations) == 3, "composite mapping did not retain every contributor")
|
||||||
|
for _, location := range composite.Origin.Locations {
|
||||||
|
source, exists := resolution.Source(location.SourceID)
|
||||||
|
check(exists && compositeSources[source.Path], "composite location has no expected source record")
|
||||||
|
delete(compositeSources, source.Path)
|
||||||
|
}
|
||||||
|
check(len(compositeSources) == 0, "composite sources are incomplete")
|
||||||
|
|
||||||
|
imported, _ := resolution.InputInfo(Keys("imported"))
|
||||||
|
importedSourceID := imported.Origin.Locations[0].SourceID
|
||||||
|
imported.Origin.Locations[0].SourceID = "mutated"
|
||||||
|
importSource, _ := resolution.Source(importedSourceID)
|
||||||
|
check(importSource.ImportedFrom != nil, "import source is missing its import location")
|
||||||
|
if importSource.ImportedFrom != nil {
|
||||||
|
importSource.ImportedFrom.SourceID = "mutated"
|
||||||
|
}
|
||||||
|
again, _ := resolution.InputInfo(Keys("imported"))
|
||||||
|
againSource, _ := resolution.Source(importedSourceID)
|
||||||
|
check(again.Origin.Locations[0].SourceID == importedSourceID, "input record is aliased")
|
||||||
|
check(againSource.ImportedFrom != nil && againSource.ImportedFrom.SourceID == "source-1", "source record is aliased")
|
||||||
|
return callbackErr
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if callbackErr != nil {
|
||||||
|
t.Fatal(callbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.Main != "main" || result.Config.Imported != "imported" || !reflect.DeepEqual(result.Config.Sequence, []string{"first", "second"}) ||
|
||||||
|
result.Config.Equal != "same" || result.Config.Profile != "profile" || result.Config.CLIEqual != "same" {
|
||||||
|
t.Fatalf("configuration = %#v", result.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultField, _ := result.Metadata().Lookup(Keys("imported"))
|
||||||
|
resultSource := result.Sources()[1]
|
||||||
|
if resultField.Origin.Locations[0].SourceID != "source-2" || resultSource.ImportedFrom == nil || resultSource.ImportedFrom.SourceID != "source-1" {
|
||||||
|
t.Fatal("callback record mutation changed the completed result")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolutionInputUsesEntryShapesAcrossTypedAndDynamicValues(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type Embedded struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
}
|
||||||
|
type config struct {
|
||||||
|
Embedded `yaml:",inline"`
|
||||||
|
Mappings map[string]string `yaml:"mappings"`
|
||||||
|
Array [2]string `yaml:"array"`
|
||||||
|
Dynamic any `yaml:"dynamic"`
|
||||||
|
}
|
||||||
|
var callbackErr error
|
||||||
|
_, err := Load[config](Bytes("shapes.yml", []byte("name: inlined\nmappings: {'-0': key}\narray: [zero, one]\ndynamic: {nested: [dynamic]}\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||||
|
check := func(path Path, want bool) {
|
||||||
|
_, found := resolution.InputInfo(path)
|
||||||
|
if found != want && callbackErr == nil {
|
||||||
|
callbackErr = errors.New("entry shape lookup disagrees with its document container")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parsedMapping, _ := ParsePath("/mappings/-0")
|
||||||
|
check(Keys("name"), true)
|
||||||
|
check(parsedMapping, true)
|
||||||
|
check(Keys("mappings").Index(0), false)
|
||||||
|
check(Keys("array").Index(1), true)
|
||||||
|
check(Keys("array", "1"), false)
|
||||||
|
check(Keys("array").Index(2), false)
|
||||||
|
check(Keys("dynamic", "nested").Index(0), true)
|
||||||
|
check(Keys("dynamic", "nested", "0"), false)
|
||||||
|
negative, _ := ParsePath("/array/-1")
|
||||||
|
check(negative, false)
|
||||||
|
return callbackErr
|
||||||
|
}}})
|
||||||
|
if callbackErr != nil {
|
||||||
|
t.Fatal(callbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolutionInputDistinguishesNonFileRootsAndImportedFiles(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
importPath := writeImportFile(t, directory, "fragment.yml", "imported: file\n")
|
||||||
|
type config struct {
|
||||||
|
Root string `yaml:"root"`
|
||||||
|
Imported string `yaml:"imported"`
|
||||||
|
Default string `yaml:"default"`
|
||||||
|
}
|
||||||
|
input := "configkit: {imports: [{path: fragment.yml}]}\nroot: memory\n"
|
||||||
|
context := &FilesystemContext{BaseDir: directory, Root: directory}
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
source func() Source
|
||||||
|
}{
|
||||||
|
{name: "bytes", source: func() Source { return Bytes("memory://config.yml", []byte(input)) }},
|
||||||
|
{name: "reader", source: func() Source { return Reader("memory://config.yml", strings.NewReader(input)) }},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var callbackErr error
|
||||||
|
_, err := Load[config](test.source(), Options[config]{Imports: true, Filesystem: context, Definition: Definition[config]{
|
||||||
|
Defaults: func() config { return config{Default: "retained"} },
|
||||||
|
Resolve: func(_ *config, resolution *Resolution) error {
|
||||||
|
root, _ := resolution.InputInfo(Keys("root"))
|
||||||
|
imported, _ := resolution.InputInfo(Keys("imported"))
|
||||||
|
defaulted, _ := resolution.InputInfo(Keys("default"))
|
||||||
|
rootSource, rootFound := resolution.Source(root.Origin.Locations[0].SourceID)
|
||||||
|
importSource, importFound := resolution.Source(imported.Origin.Locations[0].SourceID)
|
||||||
|
if !rootFound || rootSource.Path != "" || !importFound || importSource.Path != canonicalTestPath(t, importPath) || defaulted.Explicit {
|
||||||
|
callbackErr = errors.New("non-file root or imported file provenance is wrong")
|
||||||
|
}
|
||||||
|
if _, found := resolution.Source("unknown"); found {
|
||||||
|
callbackErr = errors.New("unknown source was found")
|
||||||
|
}
|
||||||
|
return callbackErr
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if callbackErr != nil {
|
||||||
|
t.Fatal(callbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var callbackErr error
|
||||||
|
_, err := Load[config](Bytes("memory://standalone.yml", []byte("root: memory\n")), Options[config]{Definition: Definition[config]{
|
||||||
|
Defaults: func() config { return config{Default: "retained"} },
|
||||||
|
Resolve: func(_ *config, resolution *Resolution) error {
|
||||||
|
root, rootFound := resolution.InputInfo(Keys("root"))
|
||||||
|
defaulted, defaultFound := resolution.InputInfo(Keys("default"))
|
||||||
|
source, sourceFound := resolution.Source(root.Origin.Locations[0].SourceID)
|
||||||
|
if !rootFound || !defaultFound || !sourceFound || source.Path != "" || defaulted.Explicit {
|
||||||
|
callbackErr = errors.New("standalone non-file provenance is wrong")
|
||||||
|
}
|
||||||
|
return callbackErr
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if callbackErr != nil {
|
||||||
|
t.Fatal(callbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolutionInputRemainsFixedAcrossRecordedMutations(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Existing string `yaml:"existing"`
|
||||||
|
Values map[string]string `yaml:"values"`
|
||||||
|
}
|
||||||
|
var callbackErr error
|
||||||
|
result, err := Load[config](Bytes("mutations.yml", []byte("existing: original\nvalues: {original: move}\n")), Options[config]{Definition: Definition[config]{Resolve: func(value *config, resolution *Resolution) error {
|
||||||
|
original, originalFound := resolution.InputInfo(Keys("values", "original"))
|
||||||
|
_, destinationFound := resolution.InputInfo(Keys("values", "destination"))
|
||||||
|
existing, existingFound := resolution.InputInfo(Keys("existing"))
|
||||||
|
Normalize(resolution, Keys("existing"), &value.Existing, "normalized", "normalized existing value")
|
||||||
|
value.Values["destination"] = value.Values["original"]
|
||||||
|
delete(value.Values, "original")
|
||||||
|
resolution.Record(Keys("values"), ChangeNormalization, "moved configured value")
|
||||||
|
againOriginal, againOriginalFound := resolution.InputInfo(Keys("values", "original"))
|
||||||
|
againDestination, againDestinationFound := resolution.InputInfo(Keys("values", "destination"))
|
||||||
|
againExisting, againExistingFound := resolution.InputInfo(Keys("existing"))
|
||||||
|
if !originalFound || destinationFound || !existingFound || !againOriginalFound || againDestinationFound || !againExistingFound ||
|
||||||
|
!reflect.DeepEqual(original, againOriginal) || !reflect.DeepEqual(existing, againExisting) || !reflect.DeepEqual(againDestination, FieldInfo{}) {
|
||||||
|
callbackErr = errors.New("entry view changed during resolution")
|
||||||
|
}
|
||||||
|
return callbackErr
|
||||||
|
}}})
|
||||||
|
if callbackErr != nil {
|
||||||
|
t.Fatal(callbackErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.Existing != "normalized" || result.Config.Values["destination"] != "move" {
|
||||||
|
t.Fatalf("configuration = %#v", result.Config)
|
||||||
|
}
|
||||||
|
if _, found := result.Metadata().Lookup(Keys("values", "original")); found {
|
||||||
|
t.Fatal("final metadata retained a removed path")
|
||||||
|
}
|
||||||
|
destination, found := result.Metadata().Lookup(Keys("values", "destination"))
|
||||||
|
existing, existingFound := result.Metadata().Lookup(Keys("existing"))
|
||||||
|
if !found || destination.Origin.Layer != LayerResolution || !existingFound || existing.Origin.Layer != LayerResolution {
|
||||||
|
t.Fatalf("final metadata = destination %#v existing %#v", destination, existing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolutionInputQueriesRemainIndependentAcrossLoads(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
const loads = 16
|
||||||
|
failures := make(chan error, loads)
|
||||||
|
var group sync.WaitGroup
|
||||||
|
for index := 0; index < loads; index++ {
|
||||||
|
group.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer group.Done()
|
||||||
|
_, err := Load[config](Bytes("parallel.yml", []byte("value: stable\n")), Options[config]{Definition: Definition[config]{Resolve: func(_ *config, resolution *Resolution) error {
|
||||||
|
info, found := resolution.InputInfo(Keys("value"))
|
||||||
|
source, sourceFound := resolution.Source(info.Origin.Locations[0].SourceID)
|
||||||
|
if !found || !sourceFound || source.ID != "source-1" {
|
||||||
|
return errors.New("independent entry lookup failed")
|
||||||
|
}
|
||||||
|
info.Origin.Locations[0].SourceID = "mutated"
|
||||||
|
source.Label = "mutated"
|
||||||
|
return nil
|
||||||
|
}}})
|
||||||
|
if err != nil {
|
||||||
|
failures <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
group.Wait()
|
||||||
|
close(failures)
|
||||||
|
for err := range failures {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func assertHistoryLayersAndPaths(t *testing.T, history []Change, layers []Layer, paths []string) {
|
func assertHistoryLayersAndPaths(t *testing.T, history []Change, layers []Layer, paths []string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if len(history) != len(layers) || len(history) != len(paths) {
|
if len(history) != len(layers) || len(history) != len(paths) {
|
||||||
|
|||||||
Reference in New Issue
Block a user