Add PromptKit request validation adapter
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# PromptKit request validation implementation plan
|
# PromptKit request validation implementation plan
|
||||||
|
|
||||||
Status: Stage 1 complete; implementation is ongoing.
|
Status: Stages 1 and 2 complete; implementation is ongoing.
|
||||||
|
|
||||||
## Authority, scope, and baseline
|
## Authority, scope, and baseline
|
||||||
|
|
||||||
@@ -153,6 +153,16 @@ capabilities are cleared. Run the focused tests and ckprompt suite.
|
|||||||
**Complete when:** the public API and adapter work without command dispatch,
|
**Complete when:** the public API and adapter work without command dispatch,
|
||||||
with explicit capability opt-in and the selected wiring/error behavior.
|
with explicit capability opt-in and the selected wiring/error behavior.
|
||||||
|
|
||||||
|
**Completed:** 2026-10-12. Added `ValidationRequest`, the optional suite callback,
|
||||||
|
and `WithRequestValidation`. The callback forwards a nil-builder seed through
|
||||||
|
PromptKit validation, preserves original selection and invocation objects, and
|
||||||
|
retains configuration diagnostics. Adapter tests cover suite composition,
|
||||||
|
successful nil-builder validation, builder failure before loading, independent
|
||||||
|
retention of nil-loader issues, and installation reporting after capabilities
|
||||||
|
are cleared. Focused adapter tests and
|
||||||
|
`GOTOOLCHAIN=local go test -count=1 ./...` passed from
|
||||||
|
`integrations/ckprompt`.
|
||||||
|
|
||||||
## Stage 3: Verify native delegation, request construction, and cancellation
|
## Stage 3: Verify native delegation, request construction, and cancellation
|
||||||
|
|
||||||
**Primary files:** a focused `integrations/ckprompt/validation_adapters_test.go`
|
**Primary files:** a focused `integrations/ckprompt/validation_adapters_test.go`
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ type RequirementsRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Suite describes application-owned input, shared state, and optional catalog,
|
// Suite describes application-owned input, shared state, and optional catalog,
|
||||||
// named inspection, and requirements inspection capabilities. Its private
|
// named inspection, requirements inspection, and concrete request validation
|
||||||
// issues retain adapter construction failures.
|
// capabilities. Its private issues retain adapter construction failures.
|
||||||
// Factory results and callback effects remain caller-owned: Command does not
|
// Factory results and callback effects remain caller-owned: Command does not
|
||||||
// deep-copy arbitrary objects or repair aliases returned by factories.
|
// deep-copy arbitrary objects or repair aliases returned by factories.
|
||||||
// A reached operation prepares one input, applies its captured selectors, and
|
// A reached operation prepares one input, applies its captured selectors, and
|
||||||
@@ -52,18 +52,18 @@ type RequirementsRequest struct {
|
|||||||
// Factories are not invoked during construction, installation, help, or version,
|
// Factories are not invoked during construction, installation, help, or version,
|
||||||
// and need not run after failure.
|
// and need not run after failure.
|
||||||
type Suite[I, S any] struct {
|
type Suite[I, S any] struct {
|
||||||
// NewInput is called once when definition, requirements, or catalog input
|
// NewInput is called once when definition, requirements, validation, or catalog input
|
||||||
// preparation is reached, before applying selectors and calling CheckInput.
|
// preparation is reached, before applying selectors and calling CheckInput.
|
||||||
// If nil, input is the zero value of I, including nil for pointer types.
|
// If nil, input is the zero value of I, including nil for pointer types.
|
||||||
NewInput func() I
|
NewInput func() I
|
||||||
// NewState is called once when an inspection or catalog callback is reached,
|
// NewState is called once when an inspection, validation, or catalog callback is reached,
|
||||||
// after input checks pass. If nil, state is the zero value of S, including
|
// after input checks pass. If nil, state is the zero value of S, including
|
||||||
// nil for pointer types.
|
// nil for pointer types.
|
||||||
NewState func() S
|
NewState func() S
|
||||||
// Selectors declares the original typed flags accepted by definition,
|
// Selectors declares the original typed flags accepted by definition,
|
||||||
// requirements, and catalog commands.
|
// requirements, validation, and catalog commands.
|
||||||
Selectors []commandkit.Flag[I]
|
Selectors []commandkit.Flag[I]
|
||||||
// CheckInput validates selected definition, requirements, or catalog input
|
// CheckInput validates selected definition, requirements, validation, or catalog input
|
||||||
// before state creation.
|
// before state creation.
|
||||||
CheckInput func(ckconfig.Selection, I) error
|
CheckInput func(ckconfig.Selection, I) error
|
||||||
// InspectPrompt inspects the selected prompt after input and state preparation.
|
// InspectPrompt inspects the selected prompt after input and state preparation.
|
||||||
@@ -74,6 +74,10 @@ type Suite[I, S any] struct {
|
|||||||
// state preparation. It is optional; a nil callback omits the requirements
|
// state preparation. It is optional; a nil callback omits the requirements
|
||||||
// command without disabling other capabilities.
|
// command without disabling other capabilities.
|
||||||
InspectRequirements func(context.Context, RequirementsRequest, I, S) (commandkit.Acquired[*promptkit.RequirementInspection], error)
|
InspectRequirements func(context.Context, RequirementsRequest, I, S) (commandkit.Acquired[*promptkit.RequirementInspection], error)
|
||||||
|
// ValidateRequest validates a concrete PromptKit request after input and state
|
||||||
|
// preparation. It is optional; a nil callback omits request validation without
|
||||||
|
// disabling other capabilities.
|
||||||
|
ValidateRequest func(context.Context, ValidationRequest, I, S) (commandkit.Acquired[struct{}], error)
|
||||||
// ListPrompts lists prompt identities after input and state preparation.
|
// ListPrompts lists prompt identities after input and state preparation.
|
||||||
// It is optional; a nil callback does not install a prompt catalog command.
|
// It is optional; a nil callback does not install a prompt catalog command.
|
||||||
ListPrompts func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]promptkit.PromptIdentity], error)
|
ListPrompts func(context.Context, ckconfig.Selection, I, S) (commandkit.Acquired[[]promptkit.PromptIdentity], error)
|
||||||
@@ -84,6 +88,62 @@ type Suite[I, S any] struct {
|
|||||||
issues []error
|
issues []error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithRequestValidation attaches PromptKit-backed concrete request validation
|
||||||
|
// to suite. The loader receives the request's original selection; the builder,
|
||||||
|
// when present, runs first and its returned request is passed to PromptKit
|
||||||
|
// unchanged. A nil builder forwards the seed request. PromptKit's native
|
||||||
|
// validation errors and the engine loader's configuration diagnostics are
|
||||||
|
// returned through the acquisition. A nil loader is retained as a suite wiring
|
||||||
|
// issue and does not install a validation callback. Caller-owned callback
|
||||||
|
// references and objects remain caller-owned.
|
||||||
|
func WithRequestValidation[I, S any](
|
||||||
|
suite Suite[I, S],
|
||||||
|
load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error),
|
||||||
|
build func(context.Context, ValidationRequest, I, S) (promptkit.RunRequest, error),
|
||||||
|
) Suite[I, S] {
|
||||||
|
if load == nil {
|
||||||
|
issues := append([]error(nil), suite.issues...)
|
||||||
|
suite.issues = append(issues, errors.New("PromptKit engine loader is nil"))
|
||||||
|
return suite
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.ValidateRequest = func(ctx context.Context, request ValidationRequest, input I, state S) (commandkit.Acquired[struct{}], error) {
|
||||||
|
if err := promptKitContextError(ctx); err != nil {
|
||||||
|
return commandkit.Acquired[struct{}]{}, fmt.Errorf("PromptKit request validation before building: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
concreteRequest := request.Request
|
||||||
|
if build != nil {
|
||||||
|
built, err := build(ctx, request, input, state)
|
||||||
|
if err != nil {
|
||||||
|
return commandkit.Acquired[struct{}]{}, err
|
||||||
|
}
|
||||||
|
concreteRequest = built
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := promptKitContextError(ctx); err != nil {
|
||||||
|
return commandkit.Acquired[struct{}]{}, fmt.Errorf("PromptKit request validation after building: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, "", load)
|
||||||
|
if err != nil {
|
||||||
|
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, err
|
||||||
|
}
|
||||||
|
if err := promptKitContextError(ctx); err != nil {
|
||||||
|
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit request validation before native validation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := engine.ValidateRequest(ctx, concreteRequest); err != nil {
|
||||||
|
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, err
|
||||||
|
}
|
||||||
|
if err := promptKitContextError(ctx); err != nil {
|
||||||
|
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, fmt.Errorf("PromptKit request validation after native validation: %w", err)
|
||||||
|
}
|
||||||
|
return commandkit.Acquired[struct{}]{Diagnostics: diagnostics}, nil
|
||||||
|
}
|
||||||
|
return suite
|
||||||
|
}
|
||||||
|
|
||||||
type engineLoader[I, S any] func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)
|
type engineLoader[I, S any] func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)
|
||||||
|
|
||||||
// ConfigScope adapts PromptKit's captured configuration inspection to the
|
// ConfigScope adapts PromptKit's captured configuration inspection to the
|
||||||
|
|||||||
16
integrations/ckprompt/validation.go
Normal file
16
integrations/ckprompt/validation.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package ckprompt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||||
|
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationRequest carries the configuration selection and concrete
|
||||||
|
// PromptKit request to validate.
|
||||||
|
type ValidationRequest struct {
|
||||||
|
// Selection identifies the configuration sources used to load the engine.
|
||||||
|
Selection ckconfig.Selection
|
||||||
|
// Request is the seed request, or the request returned by a validation
|
||||||
|
// builder when one is configured.
|
||||||
|
Request promptkit.RunRequest
|
||||||
|
}
|
||||||
100
integrations/ckprompt/validation_adapters_test.go
Normal file
100
integrations/ckprompt/validation_adapters_test.go
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
package ckprompt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||||
|
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||||
|
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithRequestValidationPreservesSuiteAndValidatesSeedWithoutBuilder(t *testing.T) {
|
||||||
|
type contextKey struct{}
|
||||||
|
type input struct{ value string }
|
||||||
|
type state struct{ value string }
|
||||||
|
|
||||||
|
ctx := context.WithValue(context.Background(), contextKey{}, "invocation")
|
||||||
|
engine := adapterEngine(t)
|
||||||
|
selection := ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "config.yml", Set: true}}
|
||||||
|
inspect := func(context.Context, RequirementsRequest, input, state) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
|
||||||
|
return commandkit.Acquired[*promptkit.RequirementInspection]{}, nil
|
||||||
|
}
|
||||||
|
newInput := func() input { return input{value: "factory"} }
|
||||||
|
suite := Suite[input, state]{InspectRequirements: inspect, NewInput: newInput}
|
||||||
|
loaderCalls := 0
|
||||||
|
suite = WithRequestValidation(suite, func(gotContext context.Context, gotSelection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||||
|
loaderCalls++
|
||||||
|
if gotContext != ctx || gotContext.Value(contextKey{}) != "invocation" || gotSelection != selection || gotInput.value != "input" || gotState.value != "state" {
|
||||||
|
t.Errorf("loader args = (%v, %#v, %#v, %#v)", gotContext, gotSelection, gotInput, gotState)
|
||||||
|
}
|
||||||
|
return engine, nil
|
||||||
|
}, nil)
|
||||||
|
if suite.InspectRequirements == nil || suite.NewInput == nil || suite.ValidateRequest == nil {
|
||||||
|
t.Fatalf("composed suite lost a member or omitted validation: %#v", suite)
|
||||||
|
}
|
||||||
|
|
||||||
|
acquired, err := suite.ValidateRequest(ctx, ValidationRequest{
|
||||||
|
Selection: selection,
|
||||||
|
Request: promptkit.RunRequest{PromptID: "empty", ProfileID: "default"},
|
||||||
|
}, input{value: "input"}, state{value: "state"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("validate seed request: %v", err)
|
||||||
|
}
|
||||||
|
if acquired.Value != (struct{}{}) || len(acquired.Diagnostics) != 0 || loaderCalls != 1 {
|
||||||
|
t.Fatalf("acquisition = %#v, loader calls = %d", acquired, loaderCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithRequestValidationBuilderFailurePrecedesLoading(t *testing.T) {
|
||||||
|
type input struct{ value string }
|
||||||
|
type state struct{ value string }
|
||||||
|
buildErr := errors.New("cannot build request")
|
||||||
|
loadCalls, buildCalls := 0, 0
|
||||||
|
suite := WithRequestValidation(
|
||||||
|
Suite[input, state]{},
|
||||||
|
func(context.Context, ckconfig.Selection, input, state) (*promptkit.Engine, error) {
|
||||||
|
loadCalls++
|
||||||
|
return nil, errors.New("loader should not run")
|
||||||
|
},
|
||||||
|
func(ctx context.Context, request ValidationRequest, gotInput input, gotState state) (promptkit.RunRequest, error) {
|
||||||
|
buildCalls++
|
||||||
|
if ctx == nil || request.Request.PromptID != "seed" || gotInput.value != "input" || gotState.value != "state" {
|
||||||
|
t.Errorf("builder args = (%v, %#v, %#v, %#v)", ctx, request, gotInput, gotState)
|
||||||
|
}
|
||||||
|
return promptkit.RunRequest{}, buildErr
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := suite.ValidateRequest(context.Background(), ValidationRequest{Request: promptkit.RunRequest{PromptID: "seed"}}, input{value: "input"}, state{value: "state"})
|
||||||
|
if !errors.Is(err, buildErr) || buildCalls != 1 || loadCalls != 0 {
|
||||||
|
t.Fatalf("validation = (%v), builder calls = %d, loader calls = %d", err, buildCalls, loadCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithRequestValidationRetainsNilLoaderIssueIndependently(t *testing.T) {
|
||||||
|
firstIssue := errors.New("existing adapter issue")
|
||||||
|
backing := make([]error, 1, 3)
|
||||||
|
backing[0] = firstIssue
|
||||||
|
existingCallback := func(context.Context, ValidationRequest, struct{}, struct{}) (commandkit.Acquired[struct{}], error) {
|
||||||
|
return commandkit.Acquired[struct{}]{}, nil
|
||||||
|
}
|
||||||
|
suite := Suite[struct{}, struct{}]{ValidateRequest: existingCallback, issues: backing[:1]}
|
||||||
|
composed := WithRequestValidation(suite, nil, nil)
|
||||||
|
if composed.ValidateRequest == nil || len(composed.issues) != 2 {
|
||||||
|
t.Fatalf("nil loader composition = %#v", composed)
|
||||||
|
}
|
||||||
|
if _, err := composed.ValidateRequest(context.Background(), ValidationRequest{}, struct{}{}, struct{}{}); err != nil {
|
||||||
|
t.Fatalf("nil loader replaced the existing validation callback: %v", err)
|
||||||
|
}
|
||||||
|
if !errors.Is(composed.issues[0], firstIssue) || composed.issues[1] == nil || backing[:cap(backing)][1] != nil {
|
||||||
|
t.Fatalf("issues = %#v, original backing = %#v", composed.issues, backing[:cap(backing)])
|
||||||
|
}
|
||||||
|
|
||||||
|
composed.ValidateRequest = nil
|
||||||
|
if err := newPromptTestApp(t).Register(Command(composed)); err == nil || !strings.Contains(err.Error(), "PromptKit engine loader is nil") {
|
||||||
|
t.Fatalf("installation did not report retained issue after capabilities were cleared: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user