Identify configuration source files in load errors

This commit is contained in:
2026-09-14 00:41:41 +00:00
parent 9c5e19c225
commit 6107adb140
4 changed files with 52 additions and 0 deletions

View File

@@ -223,6 +223,10 @@ go run ./cmd/notarius pipelines list \
Successful commands write their primary result to standard output. Warnings and Successful commands write their primary result to standard output. Warnings and
errors are written to standard error. errors are written to standard error.
Configuration load errors include ConfigKit's field diagnostics and source
coordinates, followed by a source-ID legend identifying the acquired files or
source labels. This includes imported files and the selected configuration profile.
For **run --json**, actionable process warnings remain on standard error and standard output is a For **run --json**, actionable process warnings remain on standard error and standard output is a
machine-readable success result only. Syntax and runtime diagnostics remain on machine-readable success result only. Syntax and runtime diagnostics remain on
standard error. Parse the result only after the process exits with status 0. standard error. Parse the result only after the process exits with status 0.

View File

@@ -137,6 +137,9 @@ normalization instead of reporting a synthetic default for the explicit value.
## Finding 4 — Include Source Filenames In Load Diagnostics ## Finding 4 — Include Source Filenames In Load Diagnostics
Status: Addressed. CLI load errors include a quoted source legend; command tests
cover imported/profile failures, source coordinates, and terminal-control escaping.
Priority: P2. Import/profile failures lack actionable file attribution. Priority: P2. Import/profile failures lack actionable file attribution.
`internal/cli/configkit_load.go:29` calls `WriteDiagnostics` with diagnostics `internal/cli/configkit_load.go:29` calls `WriteDiagnostics` with diagnostics

View File

@@ -30,6 +30,18 @@ func writeConfigLoadError(output io.Writer, err error) {
var loadError *configkit.LoadError var loadError *configkit.LoadError
if errors.As(err, &loadError) { if errors.As(err, &loadError) {
if writeErr := configkit.WriteDiagnostics(output, loadError.Diagnostics()); writeErr == nil { if writeErr := configkit.WriteDiagnostics(output, loadError.Diagnostics()); writeErr == nil {
// ConfigKit's locations contain source IDs. Keep its renderer and
// supply the acquired-source legend so imported/profile errors can
// be traced to a file. Quoting also escapes terminal controls.
for _, source := range loadError.Sources() {
label := source.Path
if label == "" {
label = source.Label
}
if _, err := fmt.Fprintf(output, "source %q: %q\n", source.ID, label); err != nil {
return
}
}
return return
} }
} }

View File

@@ -4,12 +4,45 @@ import (
"bytes" "bytes"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"testing" "testing"
configkit "gitea.maximumdirect.net/eric/ConfigKit" configkit "gitea.maximumdirect.net/eric/ConfigKit"
) )
func TestConfigLoadDiagnosticsIdentifyComposedSources(t *testing.T) {
t.Parallel()
for _, profile := range []bool{false, true} {
t.Run(strconv.FormatBool(profile), func(t *testing.T) {
root := t.TempDir()
main := filepath.Join(root, "config.yml")
fragment := filepath.Join(root, "pipeline\n\x1b.yml")
args := []string{"config", "validate", "--config", main}
if profile {
fragment = filepath.Join(root, "profiles", "development.yml")
writeConfigKitFile(t, main, "version: 5\n")
args = append(args, "--config-profile", "development")
} else {
writeConfigKitFile(t, main, "version: 5\nconfigkit:\n imports:\n - path: "+strconv.Quote(filepath.Base(fragment))+"\n")
}
writeConfigKitFile(t, fragment, "concurrency: {total_llm: invalid-private-value}\n")
var stdout, stderr bytes.Buffer
if code := RunWithOptions(args, &stdout, &stderr, commandContractOptions(t)); code != 1 {
t.Fatalf("status = %d; %s", code, stderr.String())
}
got := stderr.String()
if stdout.Len() != 0 || !strings.Contains(got, "decode.type") ||
!strings.Contains(got, "source-2:1:") ||
!strings.Contains(got, strconv.Quote(fragment)) ||
strings.Contains(got, "invalid-private-value") ||
strings.ContainsRune(got, '\x1b') {
t.Fatalf("stdout = %q; stderr = %q", stdout.String(), got)
}
})
}
}
func TestLoadConfigKitUsesDiscoveryProfileAndOverrides(t *testing.T) { func TestLoadConfigKitUsesDiscoveryProfileAndOverrides(t *testing.T) {
t.Parallel() t.Parallel()
root := t.TempDir() root := t.TempDir()