Separate ConfigKit messages from explanations
This commit is contained in:
@@ -126,14 +126,20 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) {
|
||||
}
|
||||
|
||||
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]{}})
|
||||
path := configkit.Keys("schema_version")
|
||||
_, loadErr := configkit.Load(configkit.Bytes("bad source", []byte("{}\n")), configkit.Options[struct{}]{
|
||||
Definition: configkit.Definition[struct{}]{Version: &configkit.VersionRule{
|
||||
Path: path,
|
||||
Check: func(_ configkit.Version, validation *configkit.Validation) error {
|
||||
validation.Warning(path, "review schema compatibility")
|
||||
return errors.New("private checker error")
|
||||
},
|
||||
}},
|
||||
})
|
||||
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) {
|
||||
scope := FromConfigKit[struct{}, struct{}, struct{}]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[struct{}], error) {
|
||||
return nil, loadErr
|
||||
})
|
||||
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
||||
@@ -144,38 +150,121 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
||||
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 {
|
||||
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning {
|
||||
t.Fatalf("failure diagnostics = %#v", acquired.Diagnostics)
|
||||
}
|
||||
var rendered bytes.Buffer
|
||||
if err := acquired.Diagnostics[0].Write(&rendered); err != nil {
|
||||
if acquired.Explanation.Message != "" || len(acquired.Explanation.Details) != 1 {
|
||||
t.Fatalf("failure explanation = %#v", acquired.Explanation)
|
||||
}
|
||||
var warning, terminal bytes.Buffer
|
||||
if err := acquired.Diagnostics[0].Write(&warning); 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())
|
||||
if err := acquired.Explanation.Details[0](&terminal); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(warning.String(), `Configuration scope "app":`) || !strings.Contains(warning.String(), `review schema compatibility`) {
|
||||
t.Fatalf("warning output lost its scope or record: %q", warning.String())
|
||||
}
|
||||
if !strings.Contains(terminal.String(), `Configuration scope "app":`) || !strings.Contains(terminal.String(), `"bad source"`) || strings.Contains(terminal.String(), "review schema compatibility") {
|
||||
t.Fatalf("terminal explanation lost native context or included an independent warning: %q", terminal.String())
|
||||
}
|
||||
if strings.Contains(terminal.String(), "private checker error") {
|
||||
t.Fatalf("terminal explanation exposed the upstream cause: %q", terminal.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromConfigKitPreservesValueAndDiagnosticsAlongsideError(t *testing.T) {
|
||||
result, err := adapterSettingsLoad(configkit.Mapping("partial source", map[string]any{"greeting": "Hello"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loadErr := errors.New("loader returned a value with an error")
|
||||
scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[adapterSettings], error) {
|
||||
return result, loadErr
|
||||
})
|
||||
|
||||
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
||||
if gotErr != loadErr {
|
||||
t.Fatalf("load error identity = %v, want %v", gotErr, loadErr)
|
||||
}
|
||||
if acquired.Value.Effective != result || acquired.Value.Sources != result || acquired.Value.Explanation != result {
|
||||
t.Fatalf("acquired value facets were lost: %#v", acquired.Value)
|
||||
}
|
||||
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning {
|
||||
t.Fatalf("independent diagnostics = %#v", acquired.Diagnostics)
|
||||
}
|
||||
if len(acquired.Explanation.Details) != 0 {
|
||||
t.Fatalf("warning-only result acquired terminal context: %#v", acquired.Explanation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigKitDiagnosticsMapSeverityAndPropagateWriterFailures(t *testing.T) {
|
||||
warning := configKitDiagnostics("app", nil, []configkit.Diagnostic{{Severity: configkit.SeverityWarning, Message: "safe warning"}})
|
||||
warning, noExplanation := 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{
|
||||
if noExplanation.Message != "" || len(noExplanation.Details) != 0 {
|
||||
t.Fatalf("warning-only explanation = %#v", noExplanation)
|
||||
}
|
||||
withWarning, withExplanation := 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 len(withWarning) != 1 || withWarning[0].Severity != commandkit.SeverityWarning || len(withExplanation.Details) != 1 {
|
||||
t.Fatalf("mixed diagnostics = %#v, explanation = %#v", withWarning, withExplanation)
|
||||
}
|
||||
|
||||
if err := withError[0].Write(shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
|
||||
if err := withWarning[0].Write(shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
|
||||
t.Fatalf("short writer error = %v, want %v", err, io.ErrShortWrite)
|
||||
}
|
||||
if err := withExplanation.Details[0](shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
|
||||
t.Fatalf("terminal 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) {
|
||||
if err := withWarning[0].Write(failingWriter{err: wantErr}); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("writer error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if err := withExplanation.Details[0](failingWriter{err: wantErr}); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("terminal writer error = %v, want %v", err, wantErr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestConfigKitTerminalContextRetainsRelatedLocationsAndSnapshots(t *testing.T) {
|
||||
primary := &configkit.Location{SourceID: "primary-source", Line: 4, Column: 2}
|
||||
related := []configkit.Location{{SourceID: "related-source", Line: 2, Column: 1}}
|
||||
sources := []configkit.SourceInfo{
|
||||
{ID: "primary-source", Label: "primary", Path: "primary.yml"},
|
||||
{ID: "related-source", Label: "related", Path: "related.yml"},
|
||||
}
|
||||
records := []configkit.Diagnostic{{
|
||||
Severity: configkit.SeverityError,
|
||||
Path: configkit.Keys("name"),
|
||||
Message: "native terminal context",
|
||||
Primary: primary,
|
||||
Related: related,
|
||||
}}
|
||||
_, explanation := configKitDiagnostics("app", sources, records)
|
||||
primary.SourceID = "mutated-primary"
|
||||
related[0].SourceID = "mutated-related"
|
||||
sources[0].Path = "mutated.yml"
|
||||
|
||||
if len(explanation.Details) != 1 {
|
||||
t.Fatalf("explanation = %#v", explanation)
|
||||
}
|
||||
var output bytes.Buffer
|
||||
if err := explanation.Details[0](&output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, fragment := range []string{"primary.yml", "primary-source", "related-source", "native terminal context"} {
|
||||
if !strings.Contains(output.String(), fragment) {
|
||||
t.Errorf("terminal context %q omits %q", output.String(), fragment)
|
||||
}
|
||||
}
|
||||
if strings.Contains(output.String(), "mutated-") || strings.Contains(output.String(), "mutated.yml") {
|
||||
t.Fatalf("terminal renderer retained caller mutations: %q", output.String())
|
||||
}
|
||||
}
|
||||
|
||||
type shortWriter struct{}
|
||||
|
||||
Reference in New Issue
Block a user