Add request validation API
This commit is contained in:
@@ -95,6 +95,8 @@ Completion: external consumers can call the new error-only method, all facade
|
||||
preparation plumbing is shared, and the public snapshot conversion remains only
|
||||
on `Prepare`'s success path.
|
||||
|
||||
**Completed: 2026-10-12.**
|
||||
|
||||
## Stage 2: Verify Concrete-Request Parity And Resource Boundaries
|
||||
|
||||
Scope: focused tests of the finalized roadmap's clarifications.
|
||||
|
||||
25
engine.go
25
engine.go
@@ -346,6 +346,29 @@ func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*Profile
|
||||
// relationship to ctx.Err is promised. Prepare returns no partial result on
|
||||
// error. It validates RunRequest.TransientRetries but makes no model attempts.
|
||||
func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error) {
|
||||
prepared, err := e.prepareRequest(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fromDomainPreparedRun(prepared), nil
|
||||
}
|
||||
|
||||
// ValidateRequest reports whether req passes the same preparation path used by
|
||||
// [Engine.Prepare] at the time of this call. A nil error means this concrete
|
||||
// request passed preparation. The method returns no prepared snapshot or
|
||||
// executable handle. It does not call an LLM, but preparation may still perform
|
||||
// resource I/O, including loading supplied inputs and schemas.
|
||||
//
|
||||
// Its errors and context handling follow [Engine.Prepare], including an error
|
||||
// matching ErrInvalidConfig when the Engine is nil. Success does not guarantee
|
||||
// that a provider will accept a later request. Use [Engine.PrepareExecution]
|
||||
// when execution must use the snapshot produced by preparation.
|
||||
func (e *Engine) ValidateRequest(ctx context.Context, req RunRequest) error {
|
||||
_, err := e.prepareRequest(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
func (e *Engine) prepareRequest(ctx context.Context, req RunRequest) (*domain.PreparedRun, error) {
|
||||
ctx = observation.WithoutExecutionState(ctx)
|
||||
if e == nil || e.internal == nil {
|
||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||
@@ -360,7 +383,7 @@ func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, err
|
||||
if err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
return fromDomainPreparedRun(prepared), nil
|
||||
return prepared, nil
|
||||
}
|
||||
|
||||
// PrepareExecution completely prepares a prompt request without calling the
|
||||
|
||||
83
validate_request_contract_test.go
Normal file
83
validate_request_contract_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package promptkit_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
func TestValidateRequestMatchesPrepareContract(t *testing.T) {
|
||||
var modelCalls atomic.Int64
|
||||
profiles := map[string]any{
|
||||
"offline": map[string]any{"endpoint": "http://example.test/v1", "model": "offline-model"},
|
||||
}
|
||||
newEngine := func(t *testing.T, definition map[string]any) *promptkit.Engine {
|
||||
t.Helper()
|
||||
return requirementsEngine(t, oneVersionPrompt("synthetic", "v1", definition), profiles,
|
||||
promptkit.WithLLMClient(countingLLMClient{calls: &modelCalls}))
|
||||
}
|
||||
testParity := func(t *testing.T, engine *promptkit.Engine, request promptkit.RunRequest, want error) {
|
||||
t.Helper()
|
||||
prepared, prepareErr := engine.Prepare(context.Background(), request)
|
||||
validateErr := engine.ValidateRequest(context.Background(), request)
|
||||
if want == nil {
|
||||
if prepareErr != nil || prepared == nil {
|
||||
t.Fatalf("Prepare() = (%+v, %v), want a prepared result", prepared, prepareErr)
|
||||
}
|
||||
if validateErr != nil {
|
||||
t.Fatalf("ValidateRequest() error = %v, want nil", validateErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if prepared != nil || !errors.Is(prepareErr, want) {
|
||||
t.Fatalf("Prepare() = (%+v, %v), want error matching %v", prepared, prepareErr, want)
|
||||
}
|
||||
if !errors.Is(validateErr, want) {
|
||||
t.Fatalf("ValidateRequest() error = %v, want error matching %v", validateErr, want)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
engine := newEngine(t, contractPrompt("offline", "hello", nil, "", noValidationOutput))
|
||||
testParity(t, engine, promptkit.RunRequest{PromptID: "synthetic"}, nil)
|
||||
})
|
||||
|
||||
t.Run("nil engine", func(t *testing.T) {
|
||||
var engine *promptkit.Engine
|
||||
request := promptkit.RunRequest{PromptID: "synthetic"}
|
||||
prepared, prepareErr := engine.Prepare(context.Background(), request)
|
||||
validateErr := engine.ValidateRequest(context.Background(), request)
|
||||
if prepared != nil || !errors.Is(prepareErr, promptkit.ErrInvalidConfig) {
|
||||
t.Fatalf("Prepare() = (%+v, %v), want ErrInvalidConfig", prepared, prepareErr)
|
||||
}
|
||||
if !errors.Is(validateErr, promptkit.ErrInvalidConfig) {
|
||||
t.Fatalf("ValidateRequest() error = %v, want ErrInvalidConfig", validateErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid appended message", func(t *testing.T) {
|
||||
engine := newEngine(t, contractPrompt("offline", "hello", nil, "", noValidationOutput))
|
||||
request := promptkit.RunRequest{
|
||||
PromptID: "synthetic",
|
||||
AppendedMessages: []promptkit.RenderedMessage{{
|
||||
Role: "unsupported-role", Content: "not sent to a model",
|
||||
}},
|
||||
}
|
||||
testParity(t, engine, request, promptkit.ErrInvalidRequest)
|
||||
})
|
||||
|
||||
t.Run("missing required input", func(t *testing.T) {
|
||||
definition := contractPrompt("offline", `{{input "required"}}`, []any{
|
||||
map[string]any{"name": "required", "required": true},
|
||||
}, "", noValidationOutput)
|
||||
engine := newEngine(t, definition)
|
||||
testParity(t, engine, promptkit.RunRequest{PromptID: "synthetic"}, promptkit.ErrPromptRender)
|
||||
})
|
||||
|
||||
if got := modelCalls.Load(); got != 0 {
|
||||
t.Fatalf("preparation operations made %d model calls, want none", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user