package ckconfig import ( "bytes" "context" "encoding/json" "errors" "io" "strings" "testing" commandkit "go.maximumdirect.net/commandkit" configkit "go.maximumdirect.net/configkit" ) func validationDiagnostic(message string) commandkit.Diagnostic { return commandkit.Diagnostic{Severity: commandkit.SeverityWarning, Write: func(w io.Writer) error { _, err := io.WriteString(w, message+"\n"); return err }} } func TestDetailedValidationFiltersAndRetainsFailureEvidence(t *testing.T) { cause := errors.New("second scope failed") calls := []string{} reader := &sourceInventory{values: []configkit.SourceInfo{{ID: "primary", Label: "main", Path: "main.yml"}, {ID: "related", Label: "included", Path: "include.yml"}, {ID: "unused", Label: "unused"}}} records := []configkit.Diagnostic{ {Code: "chosen", Severity: configkit.SeverityWarning, Path: configkit.Keys("a/b"), Message: "selected warning", Primary: &configkit.Location{SourceID: "primary", Line: 2, Column: 3}, Related: []configkit.Location{{SourceID: "related", Line: 7, Column: 8}}}, {Code: "other", Severity: configkit.SeverityWarning, Path: configkit.Keys("other"), Message: "other warning"}, } scopes := []Scope[struct{}, struct{}]{ {Name: "app", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { calls = append(calls, "app") return commandkit.Acquired[View]{Value: View{Sources: reader, Findings: &Findings{Records: records, Messages: []commandkit.Diagnostic{validationDiagnostic("native warnings")}}}, Diagnostics: []commandkit.Diagnostic{validationDiagnostic("application warning")}}, nil }}, {Name: "second", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { calls = append(calls, "second") return commandkit.Acquired[View]{Value: View{Findings: &Findings{Records: []configkit.Diagnostic{{Code: "failed", Severity: configkit.SeverityError, Message: "failed finding"}}}}, Explanation: commandkit.Explanation{Message: "terminal failure"}}, cause }}, {Name: "third", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { calls = append(calls, "third") return commandkit.Acquired[View]{}, nil }}, } app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: scopes})) var out, diag bytes.Buffer result := app.Execute(context.Background(), []string{"config", "validate", "--scope=all", "--details", "--severity=warning", "--code=chosen", "--format=json", "--verbose"}, commandkit.Streams{Out: &out, Err: &diag}) decoder := json.NewDecoder(&out) var report validationReport if err := decoder.Decode(&report); err != nil { t.Fatal(err) } if err := decoder.Decode(new(any)); err != io.EOF { t.Fatalf("extra JSON: %v", err) } if !errors.Is(result.Cause, cause) || result.Kind != commandkit.OutcomeOperational || len(calls) != 2 || calls[0] != "app" || calls[1] != "second" || len(report.Scopes) != 2 || report.Scopes[0].Outcome != "valid" || report.Scopes[1].Outcome != "failed" { t.Fatalf("result=%#v calls=%v report=%#v", result, calls, report) } first, second := report.Scopes[0].Details, report.Scopes[1].Details if first == nil || first.State != "present" || len(first.Findings) != 1 || first.Findings[0].Path != "/a~1b" || len(first.Sources) != 2 || first.Sources[0].ID != "primary" || first.Sources[1].ID != "related" || second == nil || second.State != "filtered" || len(second.Findings) != 0 { t.Fatalf("details=%#v %#v", first, second) } if strings.Contains(diag.String(), "native warnings") || !strings.Contains(diag.String(), "application warning") || !strings.Contains(diag.String(), "terminal failure") { t.Fatalf("diagnostics=%q", diag.String()) } before := len(calls) for _, args := range [][]string{{"--severity=warning"}, {"--code=x"}, {"--details", "--severity=unknown"}} { result = app.Execute(context.Background(), append([]string{"config", "validate"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard}) if result.Kind != commandkit.OutcomeUsage || len(calls) != before { t.Fatalf("invalid %v: %#v", args, result) } } } func TestValidationCancellationAndDeliveryRetainReachedScopes(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() calls := 0 scopes := []Scope[struct{}, struct{}]{ {Name: "app", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { calls++ return commandkit.Acquired[View]{Value: View{Findings: &Findings{}}}, nil }}, {Name: "second", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { calls++ cancel() return commandkit.Acquired[View]{}, nil }}, {Name: "third", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { calls++ return commandkit.Acquired[View]{}, nil }}, } app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: scopes})) var out bytes.Buffer result := app.Execute(ctx, []string{"config", "validate", "--scope=all", "--details", "--format=json"}, commandkit.Streams{Out: &out, Err: io.Discard}) var report validationReport if err := json.Unmarshal(out.Bytes(), &report); err != nil { t.Fatal(err) } if !errors.Is(result.Cause, context.Canceled) || calls != 2 || len(report.Scopes) != 2 || report.Scopes[0].Details.State != "empty" || report.Scopes[1].Details.State != "unavailable" || report.Scopes[1].Outcome != "canceled" { t.Fatalf("result=%#v calls=%d report=%#v", result, calls, report) } failure := errors.New("load failure") scope := Scope[struct{}, struct{}]{Name: "app", Operations: []Operation{Validate}, Acquire: func(context.Context, Request, struct{}, struct{}) (commandkit.Acquired[View], error) { return commandkit.Acquired[View]{}, failure }} app = newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) result = app.Execute(context.Background(), []string{"config", "validate", "--details"}, commandkit.Streams{Out: shortWriter{}, Err: io.Discard}) if !errors.Is(result.Cause, failure) || result.Presentation.Output.State != commandkit.DeliveryIncomplete { t.Fatalf("failure and delivery: %#v", result) } out.Reset() result = app.Execute(context.Background(), []string{"config", "validate", "--details", "--quiet"}, commandkit.Streams{Out: &out, Err: io.Discard}) if !errors.Is(result.Cause, failure) || out.Len() != 0 { t.Fatalf("quiet failure: %#v %q", result, out.String()) } } func TestDetailedValidationReportsNativeLoadError(t *testing.T) { _, failure := configkit.Load[map[string]any](configkit.Bytes("invalid source", []byte("invalid: ["))) var native *configkit.LoadError if !errors.As(failure, &native) { t.Fatalf("fixture: %v", failure) } scope := FromConfigKit("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[map[string]any], error) { return nil, failure }) app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}})) var out bytes.Buffer result := app.Execute(context.Background(), []string{"config", "validate", "--details", "--format=json"}, commandkit.Streams{Out: &out, Err: io.Discard}) var report validationReport if err := json.Unmarshal(out.Bytes(), &report); err != nil { t.Fatal(err) } if !errors.Is(result.Cause, failure) || len(report.Scopes) != 1 || report.Scopes[0].Outcome != "failed" || report.Scopes[0].Details.State != "present" || len(report.Scopes[0].Details.Findings) != len(native.Diagnostics()) { t.Fatalf("native failure: %#v %q", result, out.String()) } }