Separate ConfigKit messages from explanations
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Invocation reporting implementation plan
|
# Invocation reporting implementation plan
|
||||||
|
|
||||||
Status: Stages 1–9 complete. Stage 10 is next.
|
Status: Stages 1–10 complete. Stage 11 is next.
|
||||||
|
|
||||||
## Objective and authority
|
## Objective and authority
|
||||||
|
|
||||||
@@ -380,7 +380,7 @@ root tests remains deferred until the legacy external callers are reconciled.
|
|||||||
|
|
||||||
## Stage 10: Separate ConfigKit provider messages and explanations
|
## Stage 10: Separate ConfigKit provider messages and explanations
|
||||||
|
|
||||||
Status: pending.
|
Status: complete.
|
||||||
|
|
||||||
Primary files:
|
Primary files:
|
||||||
[configkit_adapter.go](../../integrations/ckconfig/configkit_adapter.go),
|
[configkit_adapter.go](../../integrations/ckconfig/configkit_adapter.go),
|
||||||
@@ -408,6 +408,14 @@ context, passive acquisition, retained causes, and data alongside errors.
|
|||||||
Use native results as fixtures without testing ConfigKit's resolution or
|
Use native results as fixtures without testing ConfigKit's resolution or
|
||||||
diagnostic classification algorithms.
|
diagnostic classification algorithms.
|
||||||
|
|
||||||
|
The ConfigKit adapter now carries independent warnings and terminal native
|
||||||
|
records separately, preserves partial values and wrapped errors, and snapshots
|
||||||
|
native writer inputs. Focused tests cover mixed records, source and comparison
|
||||||
|
side context, related locations, partial values, error identity, and messages
|
||||||
|
retained through cancellation. The ckconfig suite cannot run yet because
|
||||||
|
`diff.go`, `explain.go`, and `family.go` still use APIs scheduled for later
|
||||||
|
migration; Stage 20 will validate the assembled modules.
|
||||||
|
|
||||||
## Stage 11: Migrate configuration show and validate
|
## Stage 11: Migrate configuration show and validate
|
||||||
|
|
||||||
Status: pending.
|
Status: pending.
|
||||||
|
|||||||
@@ -192,14 +192,14 @@ func TestFromConfigKitCompareRetainsSideDiagnosticsAndLoadError(t *testing.T) {
|
|||||||
if !errors.As(gotErr, &typed) || typed != loadErr {
|
if !errors.As(gotErr, &typed) || typed != loadErr {
|
||||||
t.Fatalf("load error type not preserved: %#v", typed)
|
t.Fatalf("load error type not preserved: %#v", typed)
|
||||||
}
|
}
|
||||||
if len(acquired.Diagnostics) != 2 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning || acquired.Diagnostics[1].Severity != commandkit.SeverityError {
|
if len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityWarning || len(acquired.Explanation.Details) != 1 {
|
||||||
t.Fatalf("retained diagnostics = %#v", acquired.Diagnostics)
|
t.Fatalf("retained diagnostics/explanation = %#v / %#v", acquired.Diagnostics, acquired.Explanation)
|
||||||
}
|
}
|
||||||
var leftText, rightText bytes.Buffer
|
var leftText, rightText bytes.Buffer
|
||||||
if err := acquired.Diagnostics[0].Write(&leftText); err != nil {
|
if err := acquired.Diagnostics[0].Write(&leftText); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := acquired.Diagnostics[1].Write(&rightText); err != nil {
|
if err := acquired.Explanation.Details[0](&rightText); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(leftText.String(), `Configuration scope "app" left:`) || !strings.Contains(leftText.String(), `"left-source"`) {
|
if !strings.Contains(leftText.String(), `Configuration scope "app" left:`) || !strings.Contains(leftText.String(), `"left-source"`) {
|
||||||
@@ -231,8 +231,15 @@ func TestFromConfigKitCompareStopsAfterLeftLoadFailure(t *testing.T) {
|
|||||||
t.Fatalf("left failure/right load = %v / %v", err, rightCalled)
|
t.Fatalf("left failure/right load = %v / %v", err, rightCalled)
|
||||||
}
|
}
|
||||||
var typed *configkit.LoadError
|
var typed *configkit.LoadError
|
||||||
if !errors.As(err, &typed) || typed != loadErr || len(acquired.Diagnostics) != 1 || acquired.Diagnostics[0].Severity != commandkit.SeverityError {
|
if !errors.As(err, &typed) || typed != loadErr || len(acquired.Explanation.Details) != 1 {
|
||||||
t.Fatalf("left failure identity/diagnostics = %v / %#v", err, acquired.Diagnostics)
|
t.Fatalf("left failure identity/explanation = %v / %#v", err, acquired.Explanation)
|
||||||
|
}
|
||||||
|
var terminal bytes.Buffer
|
||||||
|
if err := acquired.Explanation.Details[0](&terminal); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(terminal.String(), `Configuration scope "app" left:`) || !strings.Contains(terminal.String(), `"left-source"`) {
|
||||||
|
t.Fatalf("left terminal context lost side/source identity: %q", terminal.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,53 +33,58 @@ func FromConfigKit[I, S, T any](
|
|||||||
|
|
||||||
scope.Acquire = func(ctx context.Context, request Request, input I, state S) (commandkit.Acquired[View], error) {
|
scope.Acquire = func(ctx context.Context, request Request, input I, state S) (commandkit.Acquired[View], error) {
|
||||||
result, err := load(ctx, request.Selection, input, state)
|
result, err := load(ctx, request.Selection, input, state)
|
||||||
|
acquired := commandkit.Acquired[View]{}
|
||||||
|
if result != nil {
|
||||||
|
acquired.Value = View{Effective: result, Sources: result, Explanation: result}
|
||||||
|
acquired.Diagnostics, acquired.Explanation = configKitDiagnostics(name, result.Sources(), result.Diagnostics())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
if errors.As(err, &loadError) && loadError != nil {
|
if errors.As(err, &loadError) && loadError != nil {
|
||||||
return commandkit.Acquired[View]{Diagnostics: configKitDiagnostics(name, loadError.Sources(), loadError.Diagnostics())}, err
|
warnings, explanation := configKitDiagnostics(name, loadError.Sources(), loadError.Diagnostics())
|
||||||
|
acquired.Diagnostics = append(acquired.Diagnostics, warnings...)
|
||||||
|
acquired.Explanation = appendConfigKitExplanation(acquired.Explanation, explanation)
|
||||||
}
|
}
|
||||||
return commandkit.Acquired[View]{}, err
|
return acquired, err
|
||||||
}
|
}
|
||||||
if result == nil {
|
if result == nil {
|
||||||
return commandkit.Acquired[View]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name)
|
return commandkit.Acquired[View]{}, fmt.Errorf("ConfigKit scope %q returned a nil result", name)
|
||||||
}
|
}
|
||||||
|
return acquired, nil
|
||||||
sources := result.Sources()
|
|
||||||
return commandkit.Acquired[View]{
|
|
||||||
Value: View{Effective: result, Sources: result, Explanation: result},
|
|
||||||
Diagnostics: configKitDiagnostics(name, sources, result.Diagnostics()),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
scope.Compare = func(ctx context.Context, request ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) {
|
scope.Compare = func(ctx context.Context, request ComparisonRequest[I], state S) (commandkit.Acquired[[]configkit.Difference], error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return commandkit.Acquired[[]configkit.Difference]{}, fmt.Errorf("ConfigKit scope %q comparison before left load: %w", name, err)
|
return commandkit.Acquired[[]configkit.Difference]{}, fmt.Errorf("ConfigKit scope %q comparison before left load: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
left, leftDiagnostics, err := loadComparisonSide(ctx, name, "left", load, request.Left, state)
|
acquired := commandkit.Acquired[[]configkit.Difference]{}
|
||||||
diagnostics := append([]commandkit.Diagnostic(nil), leftDiagnostics...)
|
left, leftDiagnostics, leftExplanation, err := loadComparisonSide(ctx, name, "left", load, request.Left, state)
|
||||||
|
acquired.Diagnostics = append(acquired.Diagnostics, leftDiagnostics...)
|
||||||
|
acquired.Explanation = appendConfigKitExplanation(acquired.Explanation, leftExplanation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, err
|
return acquired, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("ConfigKit scope %q comparison after left load: %w", name, err)
|
return acquired, fmt.Errorf("ConfigKit scope %q comparison after left load: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
right, rightDiagnostics, err := loadComparisonSide(ctx, name, "right", load, request.Right, state)
|
right, rightDiagnostics, rightExplanation, err := loadComparisonSide(ctx, name, "right", load, request.Right, state)
|
||||||
diagnostics = append(diagnostics, rightDiagnostics...)
|
acquired.Diagnostics = append(acquired.Diagnostics, rightDiagnostics...)
|
||||||
|
acquired.Explanation = appendConfigKitExplanation(acquired.Explanation, rightExplanation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, err
|
return acquired, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("ConfigKit scope %q comparison before native diff: %w", name, err)
|
return acquired, fmt.Errorf("ConfigKit scope %q comparison before native diff: %w", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
differences, err := configkit.Diff(left, right)
|
acquired.Value, err = configkit.Diff(left, right)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: diagnostics}, fmt.Errorf("ConfigKit scope %q native diff: %w", name, err)
|
return acquired, fmt.Errorf("ConfigKit scope %q native diff: %w", name, err)
|
||||||
}
|
}
|
||||||
return commandkit.Acquired[[]configkit.Difference]{Value: differences, Diagnostics: diagnostics}, nil
|
return acquired, nil
|
||||||
}
|
}
|
||||||
return scope
|
return scope
|
||||||
}
|
}
|
||||||
@@ -90,67 +95,110 @@ func loadComparisonSide[I, S, T any](
|
|||||||
load func(context.Context, Selection, I, S) (*configkit.Result[T], error),
|
load func(context.Context, Selection, I, S) (*configkit.Result[T], error),
|
||||||
request ComparisonSide[I],
|
request ComparisonSide[I],
|
||||||
state S,
|
state S,
|
||||||
) (*configkit.Result[T], []commandkit.Diagnostic, error) {
|
) (*configkit.Result[T], []commandkit.Diagnostic, commandkit.Explanation, error) {
|
||||||
result, err := load(ctx, request.Selection, request.Input, state)
|
result, err := load(ctx, request.Selection, request.Input, state)
|
||||||
|
var diagnostics []commandkit.Diagnostic
|
||||||
|
var explanation commandkit.Explanation
|
||||||
|
if result != nil {
|
||||||
|
warnings, terminal := configKitComparisonDiagnostics(scope, side, result.Sources(), result.Diagnostics())
|
||||||
|
diagnostics = append(diagnostics, warnings...)
|
||||||
|
explanation = appendConfigKitExplanation(explanation, terminal)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
var diagnostics []commandkit.Diagnostic
|
|
||||||
if errors.As(err, &loadError) && loadError != nil {
|
if errors.As(err, &loadError) && loadError != nil {
|
||||||
diagnostics = configKitComparisonDiagnostics(scope, side, loadError.Sources(), loadError.Diagnostics())
|
warnings, terminal := configKitComparisonDiagnostics(scope, side, loadError.Sources(), loadError.Diagnostics())
|
||||||
|
diagnostics = append(diagnostics, warnings...)
|
||||||
|
explanation = appendConfigKitExplanation(explanation, terminal)
|
||||||
}
|
}
|
||||||
return nil, diagnostics, fmt.Errorf("ConfigKit scope %q %s load: %w", scope, side, err)
|
return result, diagnostics, explanation, fmt.Errorf("ConfigKit scope %q %s load: %w", scope, side, err)
|
||||||
}
|
}
|
||||||
if result == nil {
|
if result == nil {
|
||||||
return nil, nil, fmt.Errorf("ConfigKit scope %q %s loader returned a nil result", scope, side)
|
return nil, diagnostics, explanation, fmt.Errorf("ConfigKit scope %q %s loader returned a nil result", scope, side)
|
||||||
}
|
}
|
||||||
return result, configKitComparisonDiagnostics(scope, side, result.Sources(), result.Diagnostics()), nil
|
return result, diagnostics, explanation, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func configKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
|
func configKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) ([]commandkit.Diagnostic, commandkit.Explanation) {
|
||||||
return configKitDiagnosticsWithHeading("Configuration scope "+strconv.Quote(scope)+":\n", sources, records)
|
return configKitDiagnosticsWithHeading("Configuration scope "+strconv.Quote(scope)+":\n", sources, records)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configKitComparisonDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
|
func configKitComparisonDiagnostics(scope, side string, sources []configkit.SourceInfo, records []configkit.Diagnostic) ([]commandkit.Diagnostic, commandkit.Explanation) {
|
||||||
return configKitDiagnosticsWithHeading("Configuration scope "+strconv.Quote(scope)+" "+side+":\n", sources, records)
|
return configKitDiagnosticsWithHeading("Configuration scope "+strconv.Quote(scope)+" "+side+":\n", sources, records)
|
||||||
}
|
}
|
||||||
|
|
||||||
func configKitDiagnosticsWithHeading(heading string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
|
func configKitDiagnosticsWithHeading(heading string, sources []configkit.SourceInfo, records []configkit.Diagnostic) ([]commandkit.Diagnostic, commandkit.Explanation) {
|
||||||
if len(records) == 0 {
|
if len(records) == 0 {
|
||||||
return nil
|
return nil, commandkit.Explanation{}
|
||||||
}
|
}
|
||||||
|
|
||||||
severity := commandkit.SeverityWarning
|
sources = append([]configkit.SourceInfo(nil), sources...)
|
||||||
|
records = cloneConfigKitDiagnostics(records)
|
||||||
|
warnings := make([]configkit.Diagnostic, 0, len(records))
|
||||||
|
terminal := make([]configkit.Diagnostic, 0, len(records))
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
if record.Severity == configkit.SeverityError {
|
if record.Severity == configkit.SeverityError {
|
||||||
severity = commandkit.SeverityError
|
terminal = append(terminal, record)
|
||||||
break
|
} else {
|
||||||
|
warnings = append(warnings, record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture the upstream snapshots so later calls to Write do not depend on
|
if len(warnings) == 0 {
|
||||||
// slices returned by the provider.
|
return nil, explanationWithConfigKitRecords(heading, sources, terminal)
|
||||||
sources = append([]configkit.SourceInfo(nil), sources...)
|
}
|
||||||
records = append([]configkit.Diagnostic(nil), records...)
|
warning := commandkit.Diagnostic{
|
||||||
return []commandkit.Diagnostic{{
|
Severity: commandkit.SeverityWarning,
|
||||||
Severity: severity,
|
Write: configKitDiagnosticWriter(heading, sources, warnings),
|
||||||
Write: func(writer io.Writer) error {
|
}
|
||||||
if err := writeDiagnosticText(writer, heading); err != nil {
|
return []commandkit.Diagnostic{warning}, explanationWithConfigKitRecords(heading, sources, terminal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func explanationWithConfigKitRecords(heading string, sources []configkit.SourceInfo, records []configkit.Diagnostic) commandkit.Explanation {
|
||||||
|
if len(records) == 0 {
|
||||||
|
return commandkit.Explanation{}
|
||||||
|
}
|
||||||
|
return commandkit.Explanation{Details: []commandkit.Detail{configKitDiagnosticWriter(heading, sources, records)}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configKitDiagnosticWriter(heading string, sources []configkit.SourceInfo, records []configkit.Diagnostic) commandkit.Detail {
|
||||||
|
return func(writer io.Writer) error {
|
||||||
|
if err := writeDiagnosticText(writer, heading); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(sources) > 0 {
|
||||||
|
if err := writeDiagnosticText(writer, "Source IDs:\n"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(sources) > 0 {
|
for _, source := range sources {
|
||||||
if err := writeDiagnosticText(writer, "Source IDs:\n"); err != nil {
|
line := " " + strconv.Quote(source.ID) + ": label=" + strconv.Quote(source.Label) + ", path=" + strconv.Quote(source.Path) + "\n"
|
||||||
|
if err := writeDiagnosticText(writer, line); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, source := range sources {
|
|
||||||
line := " " + strconv.Quote(source.ID) + ": label=" + strconv.Quote(source.Label) + ", path=" + strconv.Quote(source.Path) + "\n"
|
|
||||||
if err := writeDiagnosticText(writer, line); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return configkit.WriteDiagnostics(writer, records)
|
}
|
||||||
},
|
return configkit.WriteDiagnostics(writer, records)
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneConfigKitDiagnostics(records []configkit.Diagnostic) []configkit.Diagnostic {
|
||||||
|
cloned := append([]configkit.Diagnostic(nil), records...)
|
||||||
|
for index := range cloned {
|
||||||
|
if records[index].Primary != nil {
|
||||||
|
primary := *records[index].Primary
|
||||||
|
cloned[index].Primary = &primary
|
||||||
|
}
|
||||||
|
cloned[index].Related = append([]configkit.Location(nil), records[index].Related...)
|
||||||
|
}
|
||||||
|
return cloned
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendConfigKitExplanation(left, right commandkit.Explanation) commandkit.Explanation {
|
||||||
|
if left.Message == "" {
|
||||||
|
left.Message = right.Message
|
||||||
|
}
|
||||||
|
left.Details = append(left.Details, right.Details...)
|
||||||
|
return left
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeDiagnosticText(writer io.Writer, value string) error {
|
func writeDiagnosticText(writer io.Writer, value string) error {
|
||||||
|
|||||||
@@ -98,12 +98,13 @@ type Scope[I, S any] struct {
|
|||||||
// Operations selects the standard handlers exposed for this scope.
|
// Operations selects the standard handlers exposed for this scope.
|
||||||
Operations []Operation
|
Operations []Operation
|
||||||
// Acquire loads the selected configuration for one ordinary inspection
|
// Acquire loads the selected configuration for one ordinary inspection
|
||||||
// invocation.
|
// invocation. It returns independent diagnostics and terminal context
|
||||||
|
// separately from the native value and optional operation error.
|
||||||
Acquire func(context.Context, Request, I, S) (commandkit.Acquired[View], error)
|
Acquire func(context.Context, Request, I, S) (commandkit.Acquired[View], error)
|
||||||
// Compare compares two selections for this scope using one shared state.
|
// Compare compares two selections for this scope using one shared state.
|
||||||
// Its Acquired result carries native difference records and diagnostics;
|
// Its Acquired result carries native difference records, independent
|
||||||
// returned errors retain their supported cause identity. It is required when
|
// diagnostics, and terminal context; returned errors retain their supported
|
||||||
// Diff is declared in Operations.
|
// cause identity. It is required when Diff is declared in Operations.
|
||||||
Compare func(context.Context, ComparisonRequest[I], S) (commandkit.Acquired[[]configkit.Difference], error)
|
Compare func(context.Context, ComparisonRequest[I], S) (commandkit.Acquired[[]configkit.Difference], error)
|
||||||
|
|
||||||
issues []error
|
issues []error
|
||||||
|
|||||||
@@ -126,14 +126,20 @@ func TestFromConfigKitPassesInvocationValuesAndNativeFacets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
||||||
type settings struct {
|
path := configkit.Keys("schema_version")
|
||||||
Name string `yaml:"name"`
|
_, loadErr := configkit.Load(configkit.Bytes("bad source", []byte("{}\n")), configkit.Options[struct{}]{
|
||||||
}
|
Definition: configkit.Definition[struct{}]{Version: &configkit.VersionRule{
|
||||||
_, loadErr := configkit.Load(configkit.Mapping("bad source", map[string]any{"unknown": true}), configkit.Options[settings]{Definition: configkit.Definition[settings]{}})
|
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 {
|
if loadErr == nil {
|
||||||
t.Fatal("fixture did not produce a ConfigKit load error")
|
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
|
return nil, loadErr
|
||||||
})
|
})
|
||||||
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
||||||
@@ -144,38 +150,121 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
|||||||
if !errors.As(gotErr, &typed) {
|
if !errors.As(gotErr, &typed) {
|
||||||
t.Fatalf("load error type was not retained: %T", gotErr)
|
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)
|
t.Fatalf("failure diagnostics = %#v", acquired.Diagnostics)
|
||||||
}
|
}
|
||||||
var rendered bytes.Buffer
|
if acquired.Explanation.Message != "" || len(acquired.Explanation.Details) != 1 {
|
||||||
if err := acquired.Diagnostics[0].Write(&rendered); err != nil {
|
t.Fatalf("failure explanation = %#v", acquired.Explanation)
|
||||||
|
}
|
||||||
|
var warning, terminal bytes.Buffer
|
||||||
|
if err := acquired.Diagnostics[0].Write(&warning); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(rendered.String(), `Configuration scope "app":`) || !strings.Contains(rendered.String(), `"bad source"`) {
|
if err := acquired.Explanation.Details[0](&terminal); err != nil {
|
||||||
t.Fatalf("diagnostic output lacks scope/source legend: %q", rendered.String())
|
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) {
|
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 {
|
if len(warning) != 1 || warning[0].Severity != commandkit.SeverityWarning {
|
||||||
t.Fatalf("warning diagnostics = %#v", warning)
|
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.SeverityWarning, Message: "safe warning"},
|
||||||
{Severity: configkit.SeverityError, Message: "safe error"},
|
{Severity: configkit.SeverityError, Message: "safe error"},
|
||||||
})
|
})
|
||||||
if withError[0].Severity != commandkit.SeverityError {
|
if len(withWarning) != 1 || withWarning[0].Severity != commandkit.SeverityWarning || len(withExplanation.Details) != 1 {
|
||||||
t.Fatalf("mixed diagnostics severity = %q", withError[0].Severity)
|
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)
|
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")
|
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)
|
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{}
|
type shortWriter struct{}
|
||||||
|
|||||||
Reference in New Issue
Block a user