diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 12395a3..631bdbe 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -284,7 +284,7 @@ ownership and lifecycle timing are preserved. ## Stage 6 — Reconcile diagnostic coordinate systems -Status: planned. Scope: useful paths without fictional source locations. +Status: complete. Scope: useful paths without fictional source locations. 1. Audit early acquisition, import, fragment-version, and composition failures. Use one narrow path-prefix translation where diagnostics genuinely identify @@ -300,6 +300,14 @@ Status: planned. Scope: useful paths without fictional source locations. nil results, not whole messages or callback choreography. Validate: destination/diagnostic/import/version/decode tests and everyday check. +Validated with `go test ./internal/engine -run 'LayeredDestinationDiagnostics'`, +`go test ./internal/engine -run 'Destination|Diagnostic|Import|Version|Decode'`, +and `./scripts/check.sh` (go1.26.8 linux/amd64); all passed. The mounted +conflict, decode, and imported-version assertions use effective application +paths with authored source locations; invalid destinations have no physical +location; malformed source-root directives retain their authored path and +location. No path-prefix translation was needed because value diagnostics are +already produced from placed trees and directive diagnostics from raw trees. Done: diagnostics consistently distinguish application paths from source syntax. ## Stage 7 — Complete shared placement accounting diff --git a/internal/engine/destination_diagnostic_test.go b/internal/engine/destination_diagnostic_test.go new file mode 100644 index 0000000..078790b --- /dev/null +++ b/internal/engine/destination_diagnostic_test.go @@ -0,0 +1,142 @@ +package engine + +import ( + "path/filepath" + "testing" + "testing/fstest" +) + +func TestLayeredDestinationDiagnosticsUseEffectivePathsAndAuthoredLocations(t *testing.T) { + t.Parallel() + + t.Run("strict conflict retains both source locations", func(t *testing.T) { + t.Parallel() + loaded, err := LoadLayered( + Input{Source: NoSource()}, + testSecretBinding(), + LayeredOptions[map[string]any]{Underlays: []Underlay{{Inputs: []Input{ + {Source: Bytes("left", []byte("section:\n value: left\n")), Destination: Keys("application")}, + {Source: Bytes("right", []byte("section:\n value: right\n")), Destination: Keys("application")}, + }}}}, + ) + if loaded != nil { + t.Fatalf("conflicting load returned a result: %#v", loaded) + } + diagnostic := firstDestinationDiagnostic(t, err) + if diagnostic.Code != CodeComposeConflict || diagnostic.Path.String() != "/application/section/value" { + t.Fatalf("conflict diagnostic = %#v", diagnostic) + } + if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-1" || diagnostic.Primary.Line != 2 { + t.Fatalf("primary location = %#v", diagnostic.Primary) + } + if len(diagnostic.Related) != 1 || diagnostic.Related[0].SourceID != "source-2" || diagnostic.Related[0].Line != 2 { + t.Fatalf("related locations = %#v", diagnostic.Related) + } + }) + + t.Run("decode failure keeps effective path and content line", func(t *testing.T) { + t.Parallel() + type section struct { + Count int `yaml:"count"` + } + type config struct { + Application struct { + Section section `yaml:"section"` + } `yaml:"application"` + } + loaded, err := LoadLayered( + Input{Source: Bytes("main", []byte("section:\n count: invalid\n")), Destination: Keys("application")}, + testSecretBinding(), + LayeredOptions[config]{}, + ) + if loaded != nil { + t.Fatalf("invalid load returned a result: %#v", loaded) + } + diagnostic := firstDestinationDiagnostic(t, err) + if diagnostic.Code != CodeDecodeType || diagnostic.Path.String() != "/application/section/count" { + t.Fatalf("decode diagnostic = %#v", diagnostic) + } + if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-1" || diagnostic.Primary.Line != 2 { + t.Fatalf("decode location = %#v", diagnostic.Primary) + } + }) + + t.Run("imported version failure uses effective path and import source", func(t *testing.T) { + t.Parallel() + directory := t.TempDir() + writeImportFile(t, directory, "main.yml", "configkit: {imports: [{path: fragment.yml}]}\n") + writeImportFile(t, directory, "fragment.yml", "version: forbidden\n") + rule := &VersionRule{Path: Keys("application", "version"), Check: func(Version, *Validation) error { return nil }} + loaded, err := LoadLayered( + Input{Source: NoSource()}, + testSecretBinding(), + LayeredOptions[map[string]any]{ + Definition: Definition[map[string]any]{Version: rule}, + Underlays: []Underlay{{Inputs: []Input{{ + Source: File(filepath.Join(directory, "main.yml")), Imports: true, Destination: Keys("application"), + }}}}, + }, + ) + if loaded != nil { + t.Fatalf("invalid import returned a result: %#v", loaded) + } + diagnostic := firstDestinationDiagnostic(t, err) + if diagnostic.Code != CodeImportVersion || diagnostic.Path.String() != "/application/version" { + t.Fatalf("import version diagnostic = %#v", diagnostic) + } + if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-2" || diagnostic.Primary.Line != 1 { + t.Fatalf("import version location = %#v", diagnostic.Primary) + } + }) + + t.Run("invalid destination has no fictional source location", func(t *testing.T) { + t.Parallel() + loaded, err := LoadLayered( + Input{Source: Bytes("main", []byte("value: present\n")), Destination: Keys("configkit")}, + testSecretBinding(), + LayeredOptions[map[string]any]{}, + ) + if loaded != nil { + t.Fatalf("invalid destination returned a result: %#v", loaded) + } + diagnostic := firstDestinationDiagnostic(t, err) + if diagnostic.Code != CodeOptionsInvalid || diagnostic.Path.String() != "/configkit" || diagnostic.Primary != nil { + t.Fatalf("invalid destination diagnostic = %#v", diagnostic) + } + }) + + t.Run("malformed source directive keeps its local path and location", func(t *testing.T) { + t.Parallel() + filesystem := NewFilesystem("bundle", fstest.MapFS{ + "main.yml": &fstest.MapFile{Data: []byte("configkit: {imports: [true]}\nvalue: present\n")}, + }) + loaded, err := LoadLayered( + Input{Source: filesystem.File("main.yml"), Imports: true, Destination: Keys("application")}, + testSecretBinding(), + LayeredOptions[map[string]any]{}, + ) + if loaded != nil { + t.Fatalf("malformed directive returned a result: %#v", loaded) + } + diagnostic := firstDestinationDiagnostic(t, err) + if diagnostic.Code != CodeImportInvalid || diagnostic.Path.String() != "/configkit/imports/0" { + t.Fatalf("malformed directive diagnostic = %#v", diagnostic) + } + if diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-1" || diagnostic.Primary.Line != 1 { + t.Fatalf("malformed directive location = %#v", diagnostic.Primary) + } + }) +} + +func firstDestinationDiagnostic(t *testing.T, err error) Diagnostic { + t.Helper() + loadError, ok := err.(*LoadError) + if !ok { + t.Fatalf("load error = %#v", err) + } + diagnostics := loadError.Diagnostics() + if len(diagnostics) == 0 { + t.Fatal("load error has no diagnostics") + } + return diagnostics[0] +}