Preserve ConfigKit result evidence alongside loader errors
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Shared reporting and PromptKit adoption remediation
|
# Shared reporting and PromptKit adoption remediation
|
||||||
|
|
||||||
Status: five open findings; R1–R2 complete. R1–R3 are from the architectural review of
|
Status: four open findings; R1–R3 complete. R1–R3 are from the architectural review of
|
||||||
`f83866e44a8a9066db52cbd2aa5cccde0d69a839`. R4–R7 are from the algorithmic
|
`f83866e44a8a9066db52cbd2aa5cccde0d69a839`. R4–R7 are from the algorithmic
|
||||||
audit of `f5b2d1f80c4720b762f3301c29d5050710c210d0`; R4 is a correctness
|
audit of `f5b2d1f80c4720b762f3301c29d5050710c210d0`; R4 is a correctness
|
||||||
issue and R5–R7 are efficiency opportunities for consideration.
|
issue and R5–R7 are efficiency opportunities for consideration.
|
||||||
@@ -114,6 +114,13 @@ should continue to stop domain work.
|
|||||||
|
|
||||||
## R3 — P2: A native ConfigKit result accompanying an error loses evidence
|
## R3 — P2: A native ConfigKit result accompanying an error loses evidence
|
||||||
|
|
||||||
|
**Complete.** Result-plus-error acquisition now retains sources, findings, and
|
||||||
|
ordinary messages without exposing effective, explanation, or metadata facets.
|
||||||
|
A matched LoadError still supplies the sole authoritative snapshot. Regression
|
||||||
|
checks cover application/cancellation causes, absent versus empty findings,
|
||||||
|
source and validation reports, warning delivery, and presentation failures.
|
||||||
|
The independent ckconfig suite passes with Go 1.27.0.
|
||||||
|
|
||||||
**Location:** [configkit_adapter.go](../../integrations/ckconfig/configkit_adapter.go),
|
**Location:** [configkit_adapter.go](../../integrations/ckconfig/configkit_adapter.go),
|
||||||
FromConfigKit.Acquire lines 35–47.
|
FromConfigKit.Acquire lines 35–47.
|
||||||
|
|
||||||
|
|||||||
@@ -43,17 +43,24 @@ func FromConfigKit[I, S, T any](
|
|||||||
acquired.Value.Sources = loadError
|
acquired.Value.Sources = loadError
|
||||||
acquired.Value.Findings = &Findings{Records: records, Messages: messages}
|
acquired.Value.Findings = &Findings{Records: records, Messages: messages}
|
||||||
acquired.Explanation = explanation
|
acquired.Explanation = explanation
|
||||||
}
|
|
||||||
return acquired, err
|
return acquired, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
if err != nil {
|
||||||
|
return acquired, err
|
||||||
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
metadata := result.Metadata()
|
|
||||||
records := result.Diagnostics()
|
records := result.Diagnostics()
|
||||||
messages, explanation := configKitDiagnostics(name, result.Sources(), records)
|
messages, explanation := configKitDiagnostics(name, result.Sources(), records)
|
||||||
acquired.Value = View{Effective: result, Sources: result, Explanation: result, Metadata: &metadata, Findings: &Findings{Records: records, Messages: messages}}
|
acquired.Value = View{Sources: result, Findings: &Findings{Records: records, Messages: messages}}
|
||||||
acquired.Explanation = explanation
|
acquired.Explanation = explanation
|
||||||
|
if err != nil {
|
||||||
|
return acquired, err
|
||||||
|
}
|
||||||
|
metadata := result.Metadata()
|
||||||
|
acquired.Value.Effective, acquired.Value.Explanation, acquired.Value.Metadata = result, result, &metadata
|
||||||
return acquired, nil
|
return acquired, 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) {
|
||||||
|
|||||||
@@ -140,8 +140,12 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
|||||||
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")
|
||||||
}
|
}
|
||||||
|
partial, err := configkit.Load[struct{}](configkit.Mapping("unrelated result", map[string]any{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
scope := FromConfigKit[struct{}, struct{}, struct{}]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[struct{}], error) {
|
scope := FromConfigKit[struct{}, struct{}, struct{}]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[struct{}], error) {
|
||||||
return nil, loadErr
|
return partial, loadErr
|
||||||
})
|
})
|
||||||
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
||||||
if gotErr != loadErr {
|
if gotErr != loadErr {
|
||||||
@@ -157,6 +161,9 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
|||||||
if acquired.Explanation.Message != "" || len(acquired.Explanation.Details) != 1 {
|
if acquired.Explanation.Message != "" || len(acquired.Explanation.Details) != 1 {
|
||||||
t.Fatalf("failure explanation = %#v", acquired.Explanation)
|
t.Fatalf("failure explanation = %#v", acquired.Explanation)
|
||||||
}
|
}
|
||||||
|
if !reflect.DeepEqual(acquired.Value.Sources.Sources(), typed.Sources()) || !reflect.DeepEqual(acquired.Value.Findings.Records, typed.Diagnostics()) {
|
||||||
|
t.Fatal("LoadError snapshot was replaced or duplicated")
|
||||||
|
}
|
||||||
var warning, terminal bytes.Buffer
|
var warning, terminal bytes.Buffer
|
||||||
if err := acquired.Value.Findings.Messages[0].Write(&warning); err != nil {
|
if err := acquired.Value.Findings.Messages[0].Write(&warning); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -175,25 +182,62 @@ func TestFromConfigKitRetainsLoadErrorAndDiagnostics(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFromConfigKitDoesNotExposeResultFromFailedLoad(t *testing.T) {
|
func TestFromConfigKitRetainsResultEvidenceWithApplicationFailure(t *testing.T) {
|
||||||
result, err := adapterSettingsLoad(configkit.Mapping("partial source", map[string]any{"greeting": "Hello"}))
|
result, err := adapterSettingsLoad(configkit.Mapping("partial source", map[string]any{"greeting": "Hello"}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
loadErr := errors.New("loader returned a value with an error")
|
for _, cause := range []error{errors.New("post-load check failed"), context.Canceled} {
|
||||||
scope := FromConfigKit[struct{}, struct{}, adapterSettings]("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[adapterSettings], error) {
|
t.Run(cause.Error(), func(t *testing.T) {
|
||||||
return result, loadErr
|
scope := FromConfigKit("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[adapterSettings], error) {
|
||||||
|
return result, cause
|
||||||
})
|
})
|
||||||
|
|
||||||
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
acquired, gotErr := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
||||||
if gotErr != loadErr {
|
if gotErr != cause || acquired.Value.Effective != nil || acquired.Value.Metadata != nil || acquired.Value.Explanation != nil || acquired.Value.Sources == nil || acquired.Value.Findings == nil {
|
||||||
t.Fatalf("load error identity = %v, want %v", gotErr, loadErr)
|
t.Fatalf("failed acquisition = %#v, %v", acquired, gotErr)
|
||||||
}
|
}
|
||||||
if acquired.Value.Effective != nil || acquired.Value.Metadata != nil || acquired.Value.Sources != nil || acquired.Value.Explanation != nil || acquired.Value.Findings != nil {
|
if !reflect.DeepEqual(acquired.Value.Sources.Sources(), result.Sources()) || !reflect.DeepEqual(acquired.Value.Findings.Records, result.Diagnostics()) || len(acquired.Value.Findings.Messages) != 1 || len(acquired.Explanation.Details) != 0 {
|
||||||
t.Fatalf("failed load exposed result facets: %#v", acquired.Value)
|
t.Fatalf("lost or duplicated result evidence: %#v", acquired)
|
||||||
|
}
|
||||||
|
app := newConfigTestApp(t, Command(Suite[struct{}, struct{}]{Scopes: []Scope[struct{}, struct{}]{scope}}))
|
||||||
|
for _, args := range [][]string{{"config", "sources", "--format=json"}, {"config", "validate", "--details", "--format=json"}, {"config", "validate", "--verbose"}} {
|
||||||
|
var out, diag bytes.Buffer
|
||||||
|
got := app.Execute(context.Background(), args, commandkit.Streams{Out: &out, Err: &diag})
|
||||||
|
if !errors.Is(got.Cause, cause) {
|
||||||
|
t.Fatalf("lost cause: %#v", got)
|
||||||
|
}
|
||||||
|
if args[1] == "sources" {
|
||||||
|
if !strings.Contains(out.String(), "partial source") {
|
||||||
|
t.Fatalf("lost sources: %s", out.String())
|
||||||
|
}
|
||||||
|
} else if strings.Contains(strings.Join(args, " "), "--details") {
|
||||||
|
if !strings.Contains(out.String(), result.Diagnostics()[0].Message) || strings.Contains(diag.String(), result.Diagnostics()[0].Message) {
|
||||||
|
t.Fatalf("detailed evidence: output=%s diag=%s", out.String(), diag.String())
|
||||||
|
}
|
||||||
|
} else if !strings.Contains(diag.String(), result.Diagnostics()[0].Message) {
|
||||||
|
t.Fatalf("lost ordinary warning: %s", diag.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeErr := errors.New("output failed")
|
||||||
|
got := app.Execute(context.Background(), []string{"config", "validate", "--details"}, commandkit.Streams{Out: failingOutput{err: writeErr}, Err: io.Discard})
|
||||||
|
if !errors.Is(got.Cause, cause) || !errors.Is(got.Presentation.Output.WriteError, writeErr) {
|
||||||
|
t.Fatalf("lost operation/presentation cause: %#v", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
empty, err := configkit.Load[map[string]any](configkit.Mapping("empty", map[string]any{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, result := range []*configkit.Result[map[string]any]{nil, empty} {
|
||||||
|
cause := errors.New("application failure")
|
||||||
|
scope := FromConfigKit("app", "", func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[map[string]any], error) {
|
||||||
|
return result, cause
|
||||||
|
})
|
||||||
|
got, err := scope.Acquire(context.Background(), Request{}, struct{}{}, struct{}{})
|
||||||
|
if err != cause || (got.Value.Findings == nil) != (result == nil) {
|
||||||
|
t.Fatalf("unavailable/empty evidence: %#v %v", got, err)
|
||||||
}
|
}
|
||||||
if len(acquired.Explanation.Details) != 0 {
|
|
||||||
t.Fatalf("warning-only result acquired terminal context: %#v", acquired.Explanation)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user