From 6107adb14009736bc82e11802d02f0b18ebbcbf7 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 14 Sep 2026 00:41:41 +0000 Subject: [PATCH] Identify configuration source files in load errors --- docs/cli.md | 4 ++++ docs/roadmap/remediation.md | 3 +++ internal/cli/configkit_load.go | 12 +++++++++++ internal/cli/configkit_load_test.go | 33 +++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/docs/cli.md b/docs/cli.md index 98a719f3..3c0c41ab 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -223,6 +223,10 @@ go run ./cmd/notarius pipelines list \ Successful commands write their primary result to standard output. Warnings and 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 machine-readable success result only. Syntax and runtime diagnostics remain on standard error. Parse the result only after the process exits with status 0. diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index c8b4218d..2603e507 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -137,6 +137,9 @@ normalization instead of reporting a synthetic default for the explicit value. ## 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. `internal/cli/configkit_load.go:29` calls `WriteDiagnostics` with diagnostics diff --git a/internal/cli/configkit_load.go b/internal/cli/configkit_load.go index a4f5eece..4e17779f 100644 --- a/internal/cli/configkit_load.go +++ b/internal/cli/configkit_load.go @@ -30,6 +30,18 @@ func writeConfigLoadError(output io.Writer, err error) { var loadError *configkit.LoadError if errors.As(err, &loadError) { 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 } } diff --git a/internal/cli/configkit_load_test.go b/internal/cli/configkit_load_test.go index 808a88d5..e38c9e28 100644 --- a/internal/cli/configkit_load_test.go +++ b/internal/cli/configkit_load_test.go @@ -4,12 +4,45 @@ import ( "bytes" "os" "path/filepath" + "strconv" "strings" "testing" 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) { t.Parallel() root := t.TempDir()