diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 4b88a9d..dec01dd 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -158,6 +158,8 @@ stage vocabulary. ## R4 — P2: Show Native Configuration Diagnostics During Processing +**Status:** Completed. Processing now presents native ConfigKit error details through CommandKit and preserves the typed load error. CLI coverage checks migration guidance, an invalid setting, and the absence of output on failure. + **Category** Missing migration guidance and actionable failure detail in diff --git a/internal/cli/process.go b/internal/cli/process.go index 0b811bf..d4f864d 100644 --- a/internal/cli/process.go +++ b/internal/cli/process.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "fmt" "sync" @@ -9,7 +10,9 @@ import ( "gitea.maximumdirect.net/eric/audita/v3/internal/core/reporting" "gitea.maximumdirect.net/eric/audita/v3/internal/core/schema" "gitea.maximumdirect.net/eric/audita/v3/internal/framework/llm" + "go.maximumdirect.net/ckconfig" commandkit "go.maximumdirect.net/commandkit" + ck "go.maximumdirect.net/configkit" ) // processResult carries the runner's transcript and ordinary report facts. @@ -24,6 +27,11 @@ func runProcess(inv commandkit.Invocation, input processInput) commandkit.Outcom if inv.Context.Err() != nil || llm.IsAssetError(err) { return commandkit.Failure("Could not prepare processing.").WithCause(err) } + var loadErr *ck.LoadError + if errors.As(err, &loadErr) { + _, explanation := ckconfig.AdaptDiagnostics("Application configuration:\n", loadErr.Sources(), loadErr.Diagnostics()) + return commandkit.Usage(loadErr.Error(), explanation.Details...).WithCause(err) + } return commandkit.Usage(err.Error()).WithCause(err) } prepared, err := prepareProcess(inv.Context, input, loaded, inv.Reporter().Output()) diff --git a/internal/cli/process_command_integration_test.go b/internal/cli/process_command_integration_test.go index 35ffe5b..4610f1d 100644 --- a/internal/cli/process_command_integration_test.go +++ b/internal/cli/process_command_integration_test.go @@ -3,14 +3,50 @@ package cli import ( "bytes" "context" + "errors" "os" "path/filepath" "strings" "testing" commandkit "go.maximumdirect.net/commandkit" + ck "go.maximumdirect.net/configkit" ) +func TestProcessShowsConfigurationLoadDiagnostics(t *testing.T) { + app, err := buildApplication() + if err != nil { + t.Fatal(err) + } + for _, tc := range []struct { + name, config, finding string + }{ + {"obsolete version", "version: 2\n", "docs/migrations/v3.md"}, + {"invalid retry budget", "version: 3\nllm: {transient_retries: -1}\n", "retry budget must be non-negative"}, + } { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + configPath := filepath.Join(dir, "config.yml") + if err := os.WriteFile(configPath, []byte(tc.config), 0600); err != nil { + t.Fatal(err) + } + output := filepath.Join(dir, "out.json") + var out, stderr bytes.Buffer + args := []string{"process", "--quiet", "--config", configPath, "--output", output, filepath.Join(dir, "transcript.json")} + result := app.Execute(context.Background(), args, commandkit.Streams{Out: &out, Err: &stderr}) + var loadErr *ck.LoadError + if result.Status != 2 || !errors.As(result.Cause, &loadErr) || out.Len() != 0 || + !strings.Contains(stderr.String(), tc.finding) || !strings.Contains(stderr.String(), configPath) { + t.Fatalf("status=%d cause=%v out=%q err=%q", result.Status, result.Cause, out.String(), stderr.String()) + } + entries, err := os.ReadDir(dir) + if err != nil || len(entries) != 1 { + t.Fatalf("outputs created on load failure: %v, err=%v", entries, err) + } + }) + } +} + func TestProcessUsesResolvedReportingMode(t *testing.T) { dir := t.TempDir() for name, contents := range map[string]string{