package ckconfig import ( "bytes" "context" "errors" "fmt" "io" "strings" "testing" commandkit "gitea.maximumdirect.net/eric/CommandKit" configkit "gitea.maximumdirect.net/eric/ConfigKit" ) func TestWithScopeErrorCopiesAndRetainsCauses(t *testing.T) { first := &scopeCause{message: "first construction problem"} second := errors.New("second construction problem") original := Scope[struct{}, struct{}]{issues: []error{fmt.Errorf("wrapped: %w", first)}} copyWithError := WithScopeError(original, second) if len(original.issues) != 1 || !errors.Is(original.issues[0], first) { t.Fatalf("original issues changed: %#v", original.issues) } if len(copyWithError.issues) != 2 || !errors.Is(copyWithError.issues[0], first) || !errors.Is(copyWithError.issues[1], second) { t.Fatalf("attached issues = %#v", copyWithError.issues) } if copyWithError.issues[0] != original.issues[0] { t.Fatal("existing cause identity was not preserved") } var retained *scopeCause if !errors.As(copyWithError.issues[0], &retained) || retained != first { t.Fatalf("retained concrete cause = %#v, want %#v", retained, first) } narrowed := copyWithError narrowed.Operations = nil third := errors.New("third construction problem") withThird := WithScopeError(narrowed, third) unchanged := WithScopeError(withThird, nil) if len(unchanged.issues) != len(copyWithError.issues)+1 || !errors.Is(unchanged.issues[2], third) { t.Fatalf("repeated attachment lost issues: %#v", unchanged.issues) } } type scopeCause struct{ message string } func (e *scopeCause) Error() string { return e.message } func TestFromConfigKitRetainsNilLoaderIssue(t *testing.T) { scope := FromConfigKit[struct{}, struct{}, struct{}]("app", "Application configuration", nil) scope.Operations = nil if scope.Acquire != nil { t.Fatal("nil loader produced an acquisition callback") } if len(scope.issues) != 1 || !strings.Contains(scope.issues[0].Error(), "loader") { t.Fatalf("retained nil-loader issues = %#v", scope.issues) } } func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) { type input struct{ Label string } type state struct{ Calls int } type settings struct { Name string `yaml:"name"` } ctx, cancel := context.WithCancel(context.Background()) defer cancel() selection := Selection{ Config: commandkit.Supplied[string]{Value: "chosen.yaml", Set: true}, Overlay: commandkit.Supplied[string]{Value: "", Set: true}, } gotContext, gotSelection, gotInput, gotState := context.Context(nil), Selection{}, input{}, (*state)(nil) result, err := configkit.Load(configkit.Mapping("source.yaml", map[string]any{"name": "native"}), configkit.Options[settings]{Definition: configkit.Definition[settings]{}}) if err != nil { t.Fatal(err) } scope := FromConfigKit[input, *state, settings]("app", "Application configuration", func(got context.Context, selected Selection, in input, st *state) (*configkit.Result[settings], error) { gotContext, gotSelection, gotInput, gotState = got, selected, in, st st.Calls++ return result, nil }) if len(scope.Operations) != 4 || scope.Operations[0] != Validate || scope.Operations[1] != Show || scope.Operations[2] != Sources || scope.Operations[3] != Explain { t.Fatalf("operations = %#v", scope.Operations) } if gotContext != nil { t.Fatal("loader ran during adapter construction") } stateValue := &state{} acquired, err := scope.Acquire(ctx, Request{Operation: Show, Selection: selection}, input{Label: "typed"}, stateValue) if err != nil { t.Fatal(err) } if gotContext != ctx || gotSelection != selection || gotInput.Label != "typed" || gotState != stateValue || stateValue.Calls != 1 { t.Fatalf("loader arguments/context/state = %v, %#v, %#v, %p, %d", gotContext, gotSelection, gotInput, gotState, stateValue.Calls) } if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result { t.Fatal("adapter did not expose the native result facets directly") } var effective bytes.Buffer if err := acquired.Value.Effective.WriteEffective(&effective); err != nil { t.Fatal(err) } if !strings.Contains(effective.String(), "name: native") { t.Fatalf("native effective output = %q", effective.String()) } sources := acquired.Value.Sources.Sources() if len(sources) != 1 || sources[0].Label != "source.yaml" { t.Fatalf("native sources = %#v", sources) } path, err := configkit.ParsePath("/name") if err != nil { t.Fatal(err) } if _, ok := acquired.Value.Explanation.Explain(path); !ok { t.Fatal("native explanation facet did not resolve /name") } var explanation bytes.Buffer if err := acquired.Value.Explanation.WriteExplanation(&explanation, path); err != nil { t.Fatal(err) } if explanation.Len() == 0 { t.Fatal("native explanation writer produced no output") } } func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) { type settings struct { Name string `yaml:"name"` } _, loadErr := configkit.Load(configkit.Mapping("bad source", map[string]any{"unknown": true}), configkit.Options[settings]{Definition: configkit.Definition[settings]{}}) if loadErr == nil { t.Fatal("fixture did not produce a ConfigKit load error") } scope := FromConfigKit[struct{}, struct{}, settings]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[settings], error) { return nil, loadErr }) acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{}) if gotErr != loadErr { t.Fatalf("load error identity = %v, want original %v", gotErr, loadErr) } var typed *configkit.LoadError if !errors.As(gotErr, &typed) { t.Fatalf("load error type was not retained: %T", gotErr) } if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityError { t.Fatalf("failure diagnostics = %#v", acquired.Diagnostics) } var rendered bytes.Buffer if err := acquired.Diagnostics[0].Write(&rendered); err != nil { t.Fatal(err) } if !strings.Contains(rendered.String(), `Configuration scope "app":`) || !strings.Contains(rendered.String(), `"bad source"`) { t.Fatalf("diagnostic output lacks scope/source legend: %q", rendered.String()) } } func TestConfigKitDiagnosticsMapSeverityAndPropagateWriterFailures(t *testing.T) { warning := configKitDiagnostics("app", nil, []configkit.Diagnostic{{Severity: configkit.SeverityWarning, Message: "safe warning"}}) if len(warning) != 1 || warning[0].Severity != commandkit.SeverityWarning { t.Fatalf("warning diagnostics = %#v", warning) } withError := configKitDiagnostics("app", nil, []configkit.Diagnostic{ {Severity: configkit.SeverityWarning, Message: "safe warning"}, {Severity: configkit.SeverityError, Message: "safe error"}, }) if withError[0].Severity != commandkit.SeverityError { t.Fatalf("mixed diagnostics severity = %q", withError[0].Severity) } if err := withError[0].Write(shortWriter{}); !errors.Is(err, io.ErrShortWrite) { t.Fatalf("short writer error = %v, want %v", err, io.ErrShortWrite) } wantErr := errors.New("writer failed") if err := withError[0].Write(failingWriter{err: wantErr}); !errors.Is(err, wantErr) { t.Fatalf("writer error = %v, want %v", err, wantErr) } } type shortWriter struct{} func (shortWriter) Write(value []byte) (int, error) { return len(value) - 1, nil } type failingWriter struct{ err error } func (w failingWriter) Write([]byte) (int, error) { return 0, w.err }