Present native PromptKit operation failures

This commit is contained in:
2026-10-26 14:18:21 +00:00
parent 41c6ca8070
commit 3338b902a2
4 changed files with 122 additions and 7 deletions

View File

@@ -1,8 +1,6 @@
# Invocation reporting audit remediation
Status: five open remediation findings and two efficiency opportunities for
consideration. Findings 4 and 5 came from the interaction audit; findings 6 and 7
came from the algorithmic audit.
Status: remediation in progress; 1 of 7 findings complete.
## Review scope
@@ -65,7 +63,13 @@ findings 1–5. It made no implementation changes and ran no tests or benchmarks
## 1. Preserve native PromptKit operation failure explanations
Priority: P2. Status: open.
Priority: P2. Status: complete.
- [x] Completed: bundled native operation failures now supply terminal explanations
without printing arbitrary application causes. Explicit provider context and
independent warnings retain their presentation paths. Native-error forwarding
and single-presentation regressions pass in ordinary and quiet modes.
Validation: `GOTOOLCHAIN=local go test ./...` in `integrations/ckprompt`.
### Finding and evidence

View File

@@ -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
}

View 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())
}
}
}

View File

@@ -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