Present native PromptKit operation failures
This commit is contained in:
@@ -149,7 +149,8 @@ type Suite[I, S any] struct {
|
||||
// PromptKit unchanged; the loader receives the seed's original configuration
|
||||
// selection. A nil builder forwards the seed request unchanged. A builder
|
||||
// failure prevents engine loading and preserves its supported error identity.
|
||||
// Native validation errors are returned with supported identity, and
|
||||
// Native validation errors supply terminal explanations and are returned with
|
||||
// supported identity, and
|
||||
// configuration diagnostics and terminal context from engine loading remain
|
||||
// separate in the acquisition. A nil loader is retained as a suite wiring
|
||||
// issue and does not install a validation callback. Callback references and
|
||||
@@ -192,6 +193,7 @@ func WithRequestValidation[I, S any](
|
||||
}
|
||||
|
||||
if err := engine.ValidateRequest(ctx, concreteRequest); err != nil {
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[struct{}]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
if err := promptKitContextError(ctx); err != nil {
|
||||
@@ -209,6 +211,22 @@ type promptKitAcquisition struct {
|
||||
explanation commandkit.Explanation
|
||||
}
|
||||
|
||||
// withOperationError deliberately presents a native PromptKit operation error.
|
||||
// Loader and request-builder errors remain application-owned. Native structured
|
||||
// context takes precedence when an acquisition already supplies it.
|
||||
func (messages promptKitAcquisition) withOperationError(err error) promptKitAcquisition {
|
||||
if err == nil || messages.explanation.Message != "" {
|
||||
return messages
|
||||
}
|
||||
for _, detail := range messages.explanation.Details {
|
||||
if detail != nil {
|
||||
return messages
|
||||
}
|
||||
}
|
||||
messages.explanation.Message = err.Error()
|
||||
return messages
|
||||
}
|
||||
|
||||
// ConfigScope adapts PromptKit's captured configuration inspection to the
|
||||
// standard configuration commands, including native comparison when the scope
|
||||
// declares Diff. The loader is not called during declaration construction or
|
||||
@@ -266,6 +284,7 @@ func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S)
|
||||
}
|
||||
|
||||
differences, err := leftInspection.Diff(rightInspection)
|
||||
messages = messages.withOperationError(err)
|
||||
if err != nil {
|
||||
return commandkit.Acquired[[]configkit.Difference]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, fmt.Errorf("PromptKit scope %q native configuration diff: %w", scope.Name, err)
|
||||
}
|
||||
@@ -294,7 +313,8 @@ func promptKitContextError(ctx context.Context) error {
|
||||
// listing or inspection method. Applications can narrow, wrap, or replace these
|
||||
// callbacks before Command captures them. A nil loader is retained as an
|
||||
// adapter issue. Each callback returns independent loader diagnostics and
|
||||
// terminal context alongside the native operation's value and error.
|
||||
// terminal context alongside the native operation's value and error. Native
|
||||
// operation errors supply terminal explanations when structured context is absent.
|
||||
func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] {
|
||||
if load == nil {
|
||||
return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}}
|
||||
@@ -307,6 +327,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
||||
return commandkit.Acquired[[]promptkit.PromptIdentity]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
prompts, err := engine.ListPrompts(ctx, request.Filter)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[[]promptkit.PromptIdentity]{Value: prompts, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
},
|
||||
ListProfiles: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
||||
@@ -315,6 +336,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
||||
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
profiles, err := engine.ListProfiles(ctx, request.Filter)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[[]string]{Value: profiles, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
},
|
||||
ListBackends: func(ctx context.Context, request CatalogRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
||||
@@ -323,6 +345,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
||||
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
backends, err := engine.ListBackends(ctx, request.Filter)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[[]string]{Value: backends, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
},
|
||||
ListNamespaces: func(ctx context.Context, request NamespaceRequest, input I, state S) (commandkit.Acquired[[]string], error) {
|
||||
@@ -331,6 +354,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
||||
return commandkit.Acquired[[]string]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
namespaces, err := engine.ListNamespaces(ctx, request.Kinds...)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[[]string]{Value: namespaces, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
},
|
||||
ListCollections: func(ctx context.Context, selection ckconfig.Selection, input I, state S) (commandkit.Acquired[[]promptkit.CollectionInfo], error) {
|
||||
@@ -339,6 +363,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
||||
return commandkit.Acquired[[]promptkit.CollectionInfo]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
collections, err := engine.ListCollections(ctx)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[[]promptkit.CollectionInfo]{Value: collections, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
},
|
||||
InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
@@ -347,6 +372,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
inspection, err := engine.InspectPrompt(ctx, request.ID)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
},
|
||||
InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||
@@ -355,6 +381,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
inspection, err := engine.InspectProfile(ctx, request.ID)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
},
|
||||
InspectRequirements: func(ctx context.Context, request RequirementsRequest, input I, state S) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
|
||||
@@ -363,6 +390,7 @@ func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (
|
||||
return commandkit.Acquired[*promptkit.RequirementInspection]{Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
inspection, err := engine.InspectRequirements(ctx, request.Inspection)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: inspection, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
},
|
||||
}
|
||||
@@ -390,6 +418,7 @@ func loadPromptKitEngine[I, S any](
|
||||
}
|
||||
inspection, err := engine.InspectConfiguration()
|
||||
messages := promptKitDiagnostics("promptkit", side, inspection.Sources(), inspection.Diagnostics())
|
||||
messages = messages.withOperationError(err)
|
||||
if err != nil {
|
||||
return nil, inspection, messages, err
|
||||
}
|
||||
|
||||
80
integrations/ckprompt/native_explanation_test.go
Normal file
80
integrations/ckprompt/native_explanation_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package ckprompt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
func TestBundledNativeFailuresReachTerminalExplanation(t *testing.T) {
|
||||
engine := adapterEngine(t)
|
||||
load := func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) {
|
||||
return engine, nil
|
||||
}
|
||||
for _, operation := range []string{"validation", "inspection"} {
|
||||
for _, mode := range []string{"ordinary", "quiet"} {
|
||||
t.Run(operation+"/"+mode, func(t *testing.T) {
|
||||
suite := FromEngine(load)
|
||||
suite = WithRequestValidation(suite, load, nil)
|
||||
var nativeErr error
|
||||
args := []string{"promptkit", "requirements", "validate", "briefing"}
|
||||
if operation == "validation" {
|
||||
original := suite.ValidateRequest
|
||||
suite.ValidateRequest = func(ctx context.Context, request ValidationRequest, input, state struct{}) (commandkit.Acquired[struct{}], error) {
|
||||
acquired, err := original(ctx, request, input, state)
|
||||
nativeErr = err
|
||||
acquired.Diagnostics = append(acquired.Diagnostics, commandkit.MessageDiagnostic(commandkit.SeverityWarning, "independent fixture warning"))
|
||||
return acquired, err
|
||||
}
|
||||
} else {
|
||||
args = []string{"promptkit", "show", "prompt", "missing"}
|
||||
original := suite.InspectPrompt
|
||||
suite.InspectPrompt = func(ctx context.Context, request PromptRequest, input, state struct{}) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
acquired, err := original(ctx, request, input, state)
|
||||
nativeErr = err
|
||||
return acquired, err
|
||||
}
|
||||
}
|
||||
if mode == "quiet" {
|
||||
args = append(args, "--quiet")
|
||||
}
|
||||
var diagnostics bytes.Buffer
|
||||
got := newPromptTestApp(t, Command(suite)).Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: &diagnostics})
|
||||
if nativeErr == nil {
|
||||
t.Fatal("fixture did not supply a native failure")
|
||||
}
|
||||
if got.Status != 1 || !errors.Is(got.Cause, nativeErr) || got.Explanation.Message != nativeErr.Error() {
|
||||
t.Fatalf("native error was not preserved in outcome: %#v, error=%v", got, nativeErr)
|
||||
}
|
||||
if strings.Count(diagnostics.String(), nativeErr.Error()) != 1 || strings.Contains(diagnostics.String(), "independent fixture warning") {
|
||||
t.Fatalf("terminal adaptation or warning visibility: %q", diagnostics.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeOperationExplanationPreservesSuppliedContext(t *testing.T) {
|
||||
nativeErr := errors.New("native operation detail")
|
||||
for _, explanation := range []commandkit.Explanation{
|
||||
{Message: "selected explanation"},
|
||||
{Details: []commandkit.Detail{func(w io.Writer) error { _, err := io.WriteString(w, "native structured explanation\n"); return err }}},
|
||||
} {
|
||||
messages := (promptKitAcquisition{explanation: explanation}).withOperationError(nativeErr)
|
||||
suite := Suite[struct{}, struct{}]{InspectPrompt: func(context.Context, PromptRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Explanation: messages.explanation}, nativeErr
|
||||
}}
|
||||
var diagnostics bytes.Buffer
|
||||
got := newPromptTestApp(t, Command(suite)).Execute(context.Background(), []string{"promptkit", "show", "prompt", "fixture", "--quiet"}, commandkit.Streams{Err: &diagnostics})
|
||||
if !errors.Is(got.Cause, nativeErr) || strings.Contains(diagnostics.String(), nativeErr.Error()) || strings.Count(diagnostics.String(), "explanation") != 1 {
|
||||
t.Fatalf("supplied explanation was replaced or duplicated: %#v, %q", got, diagnostics.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ type SmokeRequest struct {
|
||||
// without a builder, the seed is forwarded unchanged. Reports and errors from
|
||||
// PromptKit are both retained. A nil loader is kept as a suite wiring issue and
|
||||
// does not install a smoke-test callback. The callback also retains independent
|
||||
// engine diagnostics and terminal context with the report and error.
|
||||
// engine diagnostics and terminal context with the report and error. Native
|
||||
// operation errors supply terminal explanations when structured context is absent.
|
||||
func WithSmokeTesting[I, S any](
|
||||
suite Suite[I, S],
|
||||
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
|
||||
@@ -65,6 +66,7 @@ func WithSmokeTesting[I, S any](
|
||||
}
|
||||
|
||||
report, err := engine.SmokeTest(ctx, concreteRequest)
|
||||
messages = messages.withOperationError(err)
|
||||
return commandkit.Acquired[*promptkit.SmokeTestReport]{Value: report, Diagnostics: messages.diagnostics, Explanation: messages.explanation}, err
|
||||
}
|
||||
return suite
|
||||
|
||||
Reference in New Issue
Block a user