Add reusable PromptKit request flags
This commit is contained in:
@@ -31,6 +31,8 @@ type catalogInput[I any] struct {
|
||||
type requirementsInput[I any] struct {
|
||||
selection ckconfig.Selection
|
||||
inspection promptkit.RequirementInspectionRequest
|
||||
resources ResourceInput
|
||||
inputs PromptInputs
|
||||
captured commandkit.FlagValues[I]
|
||||
selected I
|
||||
}
|
||||
@@ -281,24 +283,12 @@ func newRequirementsCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Overlay = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "profile", Usage: "Profile identity for discovery."}, stringDefault(""), parseDefinitionString,
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).inspection.ProfileID = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "session-id", Usage: "Session context for discovery."}, stringDefault(""), parseDefinitionString,
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).inspection.SessionID = value.Value
|
||||
}),
|
||||
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "var", Usage: "Caller variable assignment KEY=VALUE."}, []variableAssignment(nil), parseVariableAssignment,
|
||||
func(input **requirementsInput[I], value commandkit.Supplied[[]variableAssignment]) {
|
||||
variables := make(map[string]string, len(value.Value))
|
||||
for _, assignment := range value.Value {
|
||||
variables[assignment.key] = assignment.value
|
||||
}
|
||||
(*input).inspection.Vars = variables
|
||||
}),
|
||||
)
|
||||
|
||||
resources := NewResourceFlags(func(input **requirementsInput[I]) *ResourceInput { return &(*input).resources })
|
||||
inputs := NewPromptInputFlags(func(input **requirementsInput[I]) *PromptInputs { return &(*input).inputs })
|
||||
flags = append(flags, resources.Profile, resources.SessionID, inputs.Variable)
|
||||
|
||||
spec := commandkit.CommandSpec[*requirementsInput[I]]{
|
||||
Name: "show",
|
||||
Summary: "Inspect requirements for a named PromptKit prompt.",
|
||||
@@ -313,6 +303,9 @@ func newRequirementsCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
}),
|
||||
},
|
||||
CheckInput: func(input *requirementsInput[I]) error {
|
||||
input.inspection.ProfileID = input.resources.Profile.Value
|
||||
input.inspection.SessionID = input.resources.SessionID.Value
|
||||
input.inspection.Vars = input.inputs.Vars
|
||||
selected, err := prepareSuiteInput(suite, input.selection, input.captured)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -141,7 +141,7 @@ func TestRequirementsCommandCapturesContextAndVariableAssignments(t *testing.T)
|
||||
if familyOutcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(familyHelp.String(), "requirements") || strings.Contains(familyHelp.String(), "\n show ") || strings.Contains(familyHelp.String(), "\n list ") {
|
||||
t.Fatalf("requirements-only family help = (%#v, %q)", familyOutcome, familyHelp.String())
|
||||
}
|
||||
for _, flag := range []string{"--config", "--overlay", "--profile", "--session-id", "--var", "--format", "--campaign"} {
|
||||
for _, flag := range []string{"--config", "--overlay", "--llm-profile", "--llm-session-id", "--llm-var", "--format", "--campaign"} {
|
||||
if !strings.Contains(help.String(), flag) {
|
||||
t.Errorf("requirements help omits %s: %q", flag, help.String())
|
||||
}
|
||||
@@ -154,10 +154,10 @@ func TestRequirementsCommandCapturesContextAndVariableAssignments(t *testing.T)
|
||||
args := []string{
|
||||
"promptkit", "requirements", "show", "briefing", "--campaign", "spring",
|
||||
"--config", "before.yml", "--config=", "--overlay", "base.yml", "--overlay=",
|
||||
"--profile", "first", "--profile", "editorial",
|
||||
"--session-id", "old-session", "--session-id", "session-42", "--format", "text", "--format", "yaml",
|
||||
"--var", "note=hello, world", "--var", "empty=", "--var", "token=a=b", "--var", "=value",
|
||||
"--var", "mode=brief", "--var", "mode=full",
|
||||
"--llm-profile", "first", "--llm-profile", "editorial",
|
||||
"--llm-session-id", "old-session", "--llm-session-id", "session-42", "--format", "text", "--format", "yaml",
|
||||
"--llm-var", "note=hello, world", "--llm-var", "empty=", "--llm-var", "token=a=b", "--llm-var", "=value",
|
||||
"--llm-var", "mode=brief", "--llm-var", "mode=full",
|
||||
}
|
||||
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 {
|
||||
@@ -203,9 +203,9 @@ func TestRequirementsCommandUsageFailuresPrecedeApplicationPreparation(t *testin
|
||||
{name: "missing operand", args: []string{"promptkit", "requirements", "show"}},
|
||||
{name: "extra operand", args: []string{"promptkit", "requirements", "show", "briefing", "extra"}},
|
||||
{name: "unsupported format", args: []string{"promptkit", "requirements", "show", "briefing", "--format", "invalid"}},
|
||||
{name: "malformed variable followed by valid assignment", args: []string{"promptkit", "requirements", "show", "briefing", "--var", "broken", "--var", "mode=full"}},
|
||||
{name: "malformed variable followed by valid assignment", args: []string{"promptkit", "requirements", "show", "briefing", "--llm-var", "broken", "--llm-var", "mode=full"}},
|
||||
{name: "no built-in scope", args: []string{"promptkit", "requirements", "show", "briefing", "--scope", "all"}},
|
||||
{name: "malformed variable on help", args: []string{"help", "promptkit", "requirements", "show", "--var", "broken"}},
|
||||
{name: "malformed variable on help", args: []string{"help", "promptkit", "requirements", "show", "--llm-var", "broken"}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
newInputCalls, checkInputCalls, newStateCalls, providerCalls := 0, 0, 0, 0
|
||||
|
||||
@@ -25,13 +25,13 @@ go run . promptkit show prompt briefing --campaign spring --format yaml
|
||||
go run . pk show prompt briefing --campaign spring --fmt yaml
|
||||
go run . promptkit show profile spring --campaign spring
|
||||
go run . promptkit requirements show briefing --campaign spring
|
||||
go run . promptkit requirements show briefing --campaign spring --var mode=full --format yaml
|
||||
go run . promptkit requirements validate briefing --campaign spring --input topic=fixtures/topic.txt
|
||||
go run . promptkit requirements validate briefing --campaign spring --input topic=fixtures/topic.txt --var mode=full --input notes=fixtures/notes.txt --format yaml
|
||||
go run . promptkit requirements show briefing --campaign spring --llm-var mode=full --format yaml
|
||||
go run . promptkit requirements validate briefing --campaign spring --llm-input-file topic=fixtures/topic.txt
|
||||
go run . promptkit requirements validate briefing --campaign spring --llm-input-file topic=fixtures/topic.txt --llm-var mode=full --llm-input-file notes=fixtures/notes.txt --format yaml
|
||||
go run . promptkit requirements validate briefing --campaign spring
|
||||
go run . promptkit test text_inference --campaign spring --prompt briefing --profile spring
|
||||
go run . smoke text_inference --campaign spring --prompt briefing --profile spring
|
||||
go run . promptkit test all --campaign spring --prompt briefing --profile spring
|
||||
go run . promptkit test text_inference --campaign spring --llm-prompt briefing --llm-profile spring
|
||||
go run . smoke text_inference --campaign spring --llm-prompt briefing --llm-profile spring
|
||||
go run . promptkit test all --campaign spring --llm-prompt briefing --llm-profile spring
|
||||
go run . pk --hlep
|
||||
go run . session restore session-42 --campaign spring
|
||||
```
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestComposedRequirementsDiscoveryUsesSelectedCampaign(t *testing.T) {
|
||||
args []string
|
||||
}{
|
||||
{name: "without variables", args: []string{"promptkit", "requirements", "show", "briefing", "--campaign", "spring"}},
|
||||
{name: "with caller variable", args: []string{"promptkit", "requirements", "show", "briefing", "--campaign", "spring", "--var", "mode=full", "--format", "yaml"}},
|
||||
{name: "with caller variable", args: []string{"promptkit", "requirements", "show", "briefing", "--campaign", "spring", "--llm-var", "mode=full", "--format", "yaml"}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
@@ -204,11 +204,11 @@ func TestComposedRequestValidationUsesFileBindings(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "default mode",
|
||||
args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--input", "topic=fixtures/topic.txt"},
|
||||
args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--llm-input-file", "topic=fixtures/topic.txt"},
|
||||
},
|
||||
{
|
||||
name: "conditional notes",
|
||||
args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--input", "topic=fixtures/topic.txt", "--var", "mode=full", "--input", "notes=fixtures/notes.txt", "--format", "yaml"},
|
||||
args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--llm-input-file", "topic=fixtures/topic.txt", "--llm-var", "mode=full", "--llm-input-file", "notes=fixtures/notes.txt", "--format", "yaml"},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -393,7 +393,7 @@ func TestComposedCatalogsAndSmokeUseInjectedClientOnlyForInference(t *testing.T)
|
||||
}
|
||||
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "test", "all", "--campaign", "spring", "--prompt", "briefing", "--profile", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "test", "all", "--campaign", "spring", "--llm-prompt", "briefing", "--llm-profile", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if output.Len() == 0 || !bytes.Contains(output.Bytes(), []byte("tests:")) {
|
||||
t.Fatalf("aggregate smoke report missing: outcome=%#v output=%q", outcome, output.String())
|
||||
}
|
||||
@@ -453,7 +453,7 @@ func TestComposedSmokeReportSurvivesInferenceFailure(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var output, diagnostics bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "test", "text_inference", "--campaign", "spring", "--prompt", "briefing", "--profile", "spring", "--verbose"}, commandkit.Streams{Out: &output, Err: &diagnostics})
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "test", "text_inference", "--campaign", "spring", "--llm-prompt", "briefing", "--llm-profile", "spring", "--verbose"}, commandkit.Streams{Out: &output, Err: &diagnostics})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || outcome.Status != 1 || !errors.Is(outcome.Cause, wantCause) {
|
||||
t.Fatalf("smoke failure outcome = %#v; report %q; diagnostics %q", outcome, output.String(), diagnostics.String())
|
||||
}
|
||||
@@ -543,7 +543,7 @@ func TestComposedAliasesReachCanonicalCommands(t *testing.T) {
|
||||
}
|
||||
|
||||
output.Reset()
|
||||
outcome = app.Execute(context.Background(), []string{"smoke", "text_inference", "--campaign", "spring", "--prompt", "briefing", "--profile", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
outcome = app.Execute(context.Background(), []string{"smoke", "text_inference", "--campaign", "spring", "--llm-prompt", "briefing", "--llm-profile", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "kind: text_inference") || !strings.Contains(output.String(), "completed: true") || clientCalls != 1 {
|
||||
t.Fatalf("smoke shortcut outcome/report/client calls = (%#v, %q, %d)", outcome, output.String(), clientCalls)
|
||||
}
|
||||
|
||||
196
integrations/ckprompt/request_flags.go
Normal file
196
integrations/ckprompt/request_flags.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package ckprompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
promptkit "go.maximumdirect.net/promptkit"
|
||||
)
|
||||
|
||||
// requestFlagDefinition keeps the canonical spellings, help text, and optional
|
||||
// command-scoped aliases together for consumers of the standard flag sets.
|
||||
type requestFlagDefinition struct {
|
||||
meta commandkit.FlagMeta
|
||||
aliases []string
|
||||
}
|
||||
|
||||
var requestFlags = struct {
|
||||
Endpoint requestFlagDefinition
|
||||
Model requestFlagDefinition
|
||||
Temperature requestFlagDefinition
|
||||
MaxTokens requestFlagDefinition
|
||||
TopP requestFlagDefinition
|
||||
TimeoutSeconds requestFlagDefinition
|
||||
ServiceTier requestFlagDefinition
|
||||
ReasoningEffort requestFlagDefinition
|
||||
APIKeyEnv requestFlagDefinition
|
||||
TransientRetries requestFlagDefinition
|
||||
ExtraParams requestFlagDefinition
|
||||
Prompt requestFlagDefinition
|
||||
Profile requestFlagDefinition
|
||||
Backend requestFlagDefinition
|
||||
SessionID requestFlagDefinition
|
||||
Var requestFlagDefinition
|
||||
InputText requestFlagDefinition
|
||||
InputFile requestFlagDefinition
|
||||
}{
|
||||
Endpoint: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-endpoint", Usage: "Execution endpoint override."}, []string{"endpoint"}},
|
||||
Model: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-model", Usage: "Execution model override."}, []string{"model"}},
|
||||
Temperature: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-temperature", Usage: "Execution temperature override."}, []string{"temperature"}},
|
||||
MaxTokens: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-max-tokens", Usage: "Execution maximum tokens override."}, []string{"max-tokens"}},
|
||||
TopP: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-top-p", Usage: "Execution top-p override."}, []string{"top-p"}},
|
||||
TimeoutSeconds: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-timeout-seconds", Usage: "Execution timeout override in seconds."}, []string{"timeout-seconds"}},
|
||||
ServiceTier: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-service-tier", Usage: "Execution service tier."}, []string{"service-tier"}},
|
||||
ReasoningEffort: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-reasoning-effort", Usage: "Execution reasoning effort override."}, []string{"reasoning-effort"}},
|
||||
APIKeyEnv: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-api-key-env", Usage: "Execution API key environment variable."}, []string{"api-key-env"}},
|
||||
TransientRetries: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-transient-retries", Usage: "Inference transient retry count."}, []string{"transient-retries"}},
|
||||
ExtraParams: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-extra-params", Usage: "Execution provider parameters as a JSON object."}, []string{"extra-params"}},
|
||||
Prompt: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-prompt", Usage: "PromptKit prompt ID."}, []string{"prompt"}},
|
||||
Profile: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-profile", Usage: "PromptKit profile ID."}, []string{"profile"}},
|
||||
Backend: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-backend", Usage: "PromptKit backend ID."}, []string{"backend"}},
|
||||
SessionID: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-session-id", Usage: "PromptKit session ID."}, []string{"session-id"}},
|
||||
Var: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-var", Usage: "Caller variable assignment KEY=VALUE."}, []string{"var"}},
|
||||
InputText: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-input-text", Usage: "Literal prompt input assignment NAME=TEXT."}, []string{"input-text", "llm-input", "input"}},
|
||||
InputFile: requestFlagDefinition{commandkit.FlagMeta{Name: "llm-input-file", Usage: "Prompt input file assignment NAME=PATH."}, []string{"input-file"}},
|
||||
}
|
||||
|
||||
// ExecutionInput contains request-scoped execution controls supplied on the CLI.
|
||||
// A nil override or retry count leaves the corresponding native default intact.
|
||||
type ExecutionInput struct {
|
||||
Execution *promptkit.ExecutionTargetOverride
|
||||
TransientRetries *int
|
||||
}
|
||||
|
||||
// ExecutionFlags exposes independently selectable execution-control bindings.
|
||||
type ExecutionFlags[I any] struct {
|
||||
Endpoint, Model, Temperature, MaxTokens, TopP, TimeoutSeconds commandkit.Flag[I]
|
||||
ServiceTier, ReasoningEffort, APIKeyEnv, TransientRetries, ExtraParams commandkit.Flag[I]
|
||||
}
|
||||
|
||||
// All returns all execution controls.
|
||||
func (f ExecutionFlags[I]) All() []commandkit.Flag[I] {
|
||||
return []commandkit.Flag[I]{f.Endpoint, f.Model, f.Temperature, f.MaxTokens, f.TopP, f.TimeoutSeconds, f.ServiceTier, f.ReasoningEffort, f.APIKeyEnv, f.TransientRetries, f.ExtraParams}
|
||||
}
|
||||
|
||||
// NewExecutionFlags binds execution controls to a field of the caller's input.
|
||||
func NewExecutionFlags[I any](destination func(*I) *ExecutionInput) ExecutionFlags[I] {
|
||||
target := func(input *I) *promptkit.ExecutionTargetOverride {
|
||||
value := destination(input)
|
||||
if value.Execution == nil {
|
||||
value.Execution = &promptkit.ExecutionTargetOverride{}
|
||||
}
|
||||
return value.Execution
|
||||
}
|
||||
return ExecutionFlags[I]{
|
||||
Endpoint: executionFlag(requestFlags.Endpoint.meta, parseDefinitionString, func(i *I, v string) { target(i).Endpoint = v }),
|
||||
Model: executionFlag(requestFlags.Model.meta, parseDefinitionString, func(i *I, v string) { target(i).Model = v }),
|
||||
Temperature: executionFlag(requestFlags.Temperature.meta, parseExecutionFloat, func(i *I, v float64) { target(i).Temperature = &v }),
|
||||
MaxTokens: executionFlag(requestFlags.MaxTokens.meta, strconv.Atoi, func(i *I, v int) { target(i).MaxTokens = &v }),
|
||||
TopP: executionFlag(requestFlags.TopP.meta, parseExecutionFloat, func(i *I, v float64) { target(i).TopP = &v }),
|
||||
TimeoutSeconds: executionFlag(requestFlags.TimeoutSeconds.meta, strconv.Atoi, func(i *I, v int) { target(i).TimeoutSeconds = &v }),
|
||||
ServiceTier: executionFlag(requestFlags.ServiceTier.meta, parseDefinitionString, func(i *I, v string) { target(i).ServiceTier = v }),
|
||||
ReasoningEffort: executionFlag(requestFlags.ReasoningEffort.meta, parseDefinitionString, func(i *I, v string) { target(i).ReasoningEffort = &v }),
|
||||
APIKeyEnv: executionFlag(requestFlags.APIKeyEnv.meta, parseDefinitionString, func(i *I, v string) { target(i).APIKeyEnv = v }),
|
||||
TransientRetries: executionFlag(requestFlags.TransientRetries.meta, strconv.Atoi, func(i *I, v int) { destination(i).TransientRetries = &v }),
|
||||
ExtraParams: executionFlag(requestFlags.ExtraParams.meta, parseExtraParams, func(i *I, v map[string]any) { target(i).ExtraParams = v }),
|
||||
}
|
||||
}
|
||||
|
||||
func executionFlag[I, V any](meta commandkit.FlagMeta, parse func(string) (V, error), set func(*I, V)) commandkit.Flag[I] {
|
||||
return commandkit.ValueFlag(meta, (*V)(nil), parse, func(i *I, value commandkit.Supplied[V]) {
|
||||
if value.Set {
|
||||
set(i, value.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseExtraParams(raw string) (map[string]any, error) {
|
||||
value, err := commandkit.ParseCLIJSON(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
object, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("provider parameters must be a JSON object")
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
|
||||
// ResourceInput retains whether each resource identifier was explicitly supplied.
|
||||
type ResourceInput struct {
|
||||
Prompt, Profile, Backend, SessionID commandkit.Supplied[string]
|
||||
}
|
||||
|
||||
// ResourceFlags exposes independently selectable resource and session bindings.
|
||||
type ResourceFlags[I any] struct {
|
||||
Prompt, Profile, Backend, SessionID commandkit.Flag[I]
|
||||
}
|
||||
|
||||
// All returns all resource bindings.
|
||||
func (f ResourceFlags[I]) All() []commandkit.Flag[I] {
|
||||
return []commandkit.Flag[I]{f.Prompt, f.Profile, f.Backend, f.SessionID}
|
||||
}
|
||||
|
||||
// NewResourceFlags binds resource identifiers to a field of the caller's input.
|
||||
func NewResourceFlags[I any](destination func(*I) *ResourceInput) ResourceFlags[I] {
|
||||
flag := func(meta commandkit.FlagMeta, selectField func(*ResourceInput) *commandkit.Supplied[string]) commandkit.Flag[I] {
|
||||
return commandkit.ValueFlag(meta, (*string)(nil), parseDefinitionString, func(i *I, v commandkit.Supplied[string]) { *selectField(destination(i)) = v })
|
||||
}
|
||||
return ResourceFlags[I]{
|
||||
Prompt: flag(requestFlags.Prompt.meta, func(v *ResourceInput) *commandkit.Supplied[string] { return &v.Prompt }),
|
||||
Profile: flag(requestFlags.Profile.meta, func(v *ResourceInput) *commandkit.Supplied[string] { return &v.Profile }),
|
||||
Backend: flag(requestFlags.Backend.meta, func(v *ResourceInput) *commandkit.Supplied[string] { return &v.Backend }),
|
||||
SessionID: flag(requestFlags.SessionID.meta, func(v *ResourceInput) *commandkit.Supplied[string] { return &v.SessionID }),
|
||||
}
|
||||
}
|
||||
|
||||
// PromptInputs contains caller variables and native artifact references.
|
||||
type PromptInputs struct {
|
||||
Vars map[string]string
|
||||
Inputs map[string]promptkit.ArtifactRef
|
||||
}
|
||||
|
||||
// PromptInputFlags exposes variable and named artifact input bindings.
|
||||
type PromptInputFlags[I any] struct {
|
||||
Variable, Text, File commandkit.Flag[I]
|
||||
}
|
||||
|
||||
// All returns all variable and input bindings.
|
||||
func (f PromptInputFlags[I]) All() []commandkit.Flag[I] {
|
||||
return []commandkit.Flag[I]{f.Variable, f.Text, f.File}
|
||||
}
|
||||
|
||||
// NewPromptInputFlags binds caller variables and input references to a field of
|
||||
// the caller's input.
|
||||
func NewPromptInputFlags[I any](destination func(*I) *PromptInputs) PromptInputFlags[I] {
|
||||
variable := commandkit.RepeatFlag(requestFlags.Var.meta, []variableAssignment(nil), parseVariableAssignment, func(i *I, v commandkit.Supplied[[]variableAssignment]) {
|
||||
if !v.Set {
|
||||
return
|
||||
}
|
||||
values := make(map[string]string, len(v.Value))
|
||||
for _, assignment := range v.Value {
|
||||
values[assignment.key] = assignment.value
|
||||
}
|
||||
destination(i).Vars = values
|
||||
})
|
||||
inputFlag := func(meta commandkit.FlagMeta, makeRef func(string) promptkit.ArtifactRef) commandkit.Flag[I] {
|
||||
return commandkit.RepeatFlag(meta, []inputAssignment(nil), parseInputAssignment, func(i *I, v commandkit.Supplied[[]inputAssignment]) {
|
||||
if !v.Set {
|
||||
return
|
||||
}
|
||||
input := destination(i)
|
||||
if input.Inputs == nil {
|
||||
input.Inputs = make(map[string]promptkit.ArtifactRef)
|
||||
}
|
||||
for _, assignment := range v.Value {
|
||||
input.Inputs[assignment.name] = makeRef(assignment.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
return PromptInputFlags[I]{Variable: variable, Text: inputFlag(requestFlags.InputText.meta, promptkit.Inline), File: inputFlag(requestFlags.InputFile.meta, promptkit.File)}
|
||||
}
|
||||
|
||||
func parseExecutionFloat(value string) (float64, error) {
|
||||
return strconv.ParseFloat(value, 64)
|
||||
}
|
||||
78
integrations/ckprompt/request_flags_test.go
Normal file
78
integrations/ckprompt/request_flags_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package ckprompt_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/ckprompt"
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
promptkit "go.maximumdirect.net/promptkit"
|
||||
)
|
||||
|
||||
func TestReusableRequestFlagsOnApplicationCommand(t *testing.T) {
|
||||
type input struct {
|
||||
execution ckprompt.ExecutionInput
|
||||
resources ckprompt.ResourceInput
|
||||
prompt ckprompt.PromptInputs
|
||||
}
|
||||
execution := ckprompt.NewExecutionFlags(func(i *input) *ckprompt.ExecutionInput { return &i.execution })
|
||||
resources := ckprompt.NewResourceFlags(func(i *input) *ckprompt.ResourceInput { return &i.resources })
|
||||
inputs := ckprompt.NewPromptInputFlags(func(i *input) *ckprompt.PromptInputs { return &i.prompt })
|
||||
flags := append(execution.All(), resources.Profile, inputs.Variable)
|
||||
var received input
|
||||
calls := 0
|
||||
command := commandkit.NewCommand(commandkit.CommandSpec[input]{
|
||||
Name: "run", Flags: flags,
|
||||
Run: func(_ commandkit.Invocation, i input) commandkit.Outcome {
|
||||
calls++
|
||||
received = i
|
||||
return commandkit.Success()
|
||||
},
|
||||
})
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "app"}, command)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
run := func(args ...string) commandkit.Outcome {
|
||||
return app.Execute(context.Background(), append([]string{"run"}, args...), commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
}
|
||||
if result := run(); result.Kind != commandkit.OutcomeSuccess || received.execution.Execution != nil || received.execution.TransientRetries != nil || received.resources.Profile.Set {
|
||||
t.Fatalf("absent values: %#v %#v", result, received)
|
||||
}
|
||||
if result := run("--llm-profile=", "--llm-temperature=0", "--llm-reasoning-effort=", "--llm-transient-retries=0", "--llm-extra-params={\"n\":1,\"d\":1.5}", "--llm-var=x="); result.Kind != commandkit.OutcomeSuccess {
|
||||
t.Fatal(result)
|
||||
}
|
||||
e := received.execution
|
||||
if !received.resources.Profile.Set || received.resources.Profile.Value != "" || e.Execution == nil || e.Execution.Temperature == nil || *e.Execution.Temperature != 0 || e.Execution.ReasoningEffort == nil || *e.Execution.ReasoningEffort != "" || e.TransientRetries == nil || *e.TransientRetries != 0 || e.Execution.ExtraParams["n"] != int64(1) || e.Execution.ExtraParams["d"] != float64(1.5) || received.prompt.Vars["x"] != "" {
|
||||
t.Fatalf("explicit values: %#v", received)
|
||||
}
|
||||
if result := run("--llm-extra-params={}"); result.Kind != commandkit.OutcomeSuccess || received.execution.Execution == nil || received.execution.Execution.ExtraParams == nil || len(received.execution.Execution.ExtraParams) != 0 {
|
||||
t.Fatalf("empty object: %#v %#v", result, received)
|
||||
}
|
||||
for _, raw := range []string{"null", "[]", "1", "{invalid}"} {
|
||||
before := calls
|
||||
if result := run("--llm-extra-params=" + raw); result.Kind != commandkit.OutcomeUsage || calls != before {
|
||||
t.Fatalf("invalid object %q: %#v", raw, result)
|
||||
}
|
||||
}
|
||||
if result := run("--llm-backend=unused"); result.Kind != commandkit.OutcomeUsage {
|
||||
t.Fatalf("unselected flag: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidationForwardsExecutionControls(t *testing.T) {
|
||||
var received promptkit.RunRequest
|
||||
suite := ckprompt.Suite[struct{}, struct{}]{ValidateRequest: func(_ context.Context, req ckprompt.ValidationRequest, _, _ struct{}) (commandkit.Acquired[struct{}], error) {
|
||||
received = req.Request
|
||||
return commandkit.Acquired[struct{}]{}, nil
|
||||
}}
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "app"}, ckprompt.Command(suite))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result := app.Execute(context.Background(), []string{"promptkit", "requirements", "validate", "briefing", "--llm-profile=", "--llm-max-tokens=0", "--llm-extra-params={}", "--llm-transient-retries=0"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeSuccess || received.Execution == nil || received.Execution.MaxTokens == nil || *received.Execution.MaxTokens != 0 || received.Execution.ExtraParams == nil || received.TransientRetries == nil || *received.TransientRetries != 0 {
|
||||
t.Fatalf("result/request = %#v %#v", result, received)
|
||||
}
|
||||
}
|
||||
@@ -108,17 +108,17 @@ func TestRequirementsCommandForwardsNativeRequestAndProjectsInspection(t *testin
|
||||
{
|
||||
name: "conditional discovery",
|
||||
request: promptkit.RequirementInspectionRequest{PromptID: "briefing", ProfileID: "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--profile", "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--llm-profile", "editorial"},
|
||||
},
|
||||
{
|
||||
name: "supplied branch variable",
|
||||
request: promptkit.RequirementInspectionRequest{PromptID: "briefing", ProfileID: "editorial", Vars: map[string]string{"mode": "full"}},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--profile", "editorial", "--var", "mode=full", "--format", "yaml"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--llm-profile", "editorial", "--llm-var", "mode=full", "--format", "yaml"},
|
||||
},
|
||||
{
|
||||
name: "prompt with a required variable",
|
||||
request: promptkit.RequirementInspectionRequest{PromptID: "required-variable", ProfileID: "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "required-variable", "--profile", "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "required-variable", "--llm-profile", "editorial"},
|
||||
},
|
||||
{
|
||||
name: "default profile selection",
|
||||
@@ -128,22 +128,22 @@ func TestRequirementsCommandForwardsNativeRequestAndProjectsInspection(t *testin
|
||||
{
|
||||
name: "first distinct prompt ID",
|
||||
request: promptkit.RequirementInspectionRequest{PromptID: "ambiguous-first", ProfileID: "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "ambiguous-first", "--profile", "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "ambiguous-first", "--llm-profile", "editorial"},
|
||||
},
|
||||
{
|
||||
name: "second distinct prompt ID",
|
||||
request: promptkit.RequirementInspectionRequest{PromptID: "ambiguous-second", ProfileID: "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "ambiguous-second", "--profile", "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "ambiguous-second", "--llm-profile", "editorial"},
|
||||
},
|
||||
{
|
||||
name: "session template context",
|
||||
request: promptkit.RequirementInspectionRequest{PromptID: "briefing", ProfileID: "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--profile", "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--llm-profile", "editorial"},
|
||||
},
|
||||
{
|
||||
name: "session ID context",
|
||||
request: promptkit.RequirementInspectionRequest{PromptID: "briefing", ProfileID: "editorial", SessionID: "session-42"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--profile", "editorial", "--session-id", "session-42"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--llm-profile", "editorial", "--llm-session-id", "session-42"},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -164,17 +164,17 @@ func TestRequirementsCommandPreservesNativeSelectionErrors(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "missing prompt",
|
||||
args: []string{"promptkit", "requirements", "show", "missing", "--profile", "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "missing", "--llm-profile", "editorial"},
|
||||
cause: promptkit.ErrPromptNotFound,
|
||||
},
|
||||
{
|
||||
name: "missing profile",
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--profile", "missing"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--llm-profile", "missing"},
|
||||
cause: promptkit.ErrProfileNotFound,
|
||||
},
|
||||
{
|
||||
name: "removed prompt version flag",
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--version", "2", "--profile", "editorial"},
|
||||
args: []string{"promptkit", "requirements", "show", "briefing", "--version", "2", "--llm-profile", "editorial"},
|
||||
cause: nil,
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestSmokeAggregateCoordinatesConcreteRequestsAndRetainsErrors(t *testing.T)
|
||||
app := newPromptTestApp(t, Command(suite))
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{
|
||||
"promptkit", "test", "all", "--model=base-model", "--temperature=0.25", "--transient-retries=3",
|
||||
"promptkit", "test", "all", "--llm-model=base-model", "--llm-temperature=0.25", "--llm-transient-retries=3",
|
||||
}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeOperational || !errors.Is(outcome.Cause, nativeErr) {
|
||||
t.Fatalf("aggregate outcome = %#v", outcome)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"go.maximumdirect.net/ckconfig"
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
@@ -15,6 +14,8 @@ type smokeCommandInput[I any] struct {
|
||||
selection ckconfig.Selection
|
||||
kind string
|
||||
request promptkit.SmokeTestRequest
|
||||
execution ExecutionInput
|
||||
resources ResourceInput
|
||||
captured commandkit.FlagValues[I]
|
||||
selected I
|
||||
}
|
||||
@@ -35,98 +36,13 @@ func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).selection.Overlay = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "backend", Usage: "PromptKit backend ID."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.BackendID = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "prompt", Usage: "PromptKit prompt ID."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.PromptID = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "profile", Usage: "PromptKit profile ID."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.ProfileID = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "endpoint", Usage: "Execution endpoint override."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.Endpoint = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "model", Usage: "Execution model override."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.Model = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "temperature", Usage: "Execution temperature override."}, (*float64)(nil), parseSmokeFloat,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[float64]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.Temperature = &value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "max-tokens", Usage: "Execution maximum tokens override."}, (*int)(nil), strconv.Atoi,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[int]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.MaxTokens = &value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "top-p", Usage: "Execution top-p override."}, (*float64)(nil), parseSmokeFloat,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[float64]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.TopP = &value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "timeout-seconds", Usage: "Execution timeout override."}, (*int)(nil), strconv.Atoi,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[int]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.TimeoutSeconds = &value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "service-tier", Usage: "Execution service tier."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.ServiceTier = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "reasoning-effort", Usage: "Execution reasoning effort override."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.ReasoningEffort = &value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "api-key-env", Usage: "Execution API key environment variable."}, (*string)(nil), parseDefinitionString,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[string]) {
|
||||
if !value.Set {
|
||||
return
|
||||
}
|
||||
execution := smokeExecution(input)
|
||||
execution.APIKeyEnv = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "transient-retries", Usage: "Inference transient retry count."}, (*int)(nil), strconv.Atoi,
|
||||
func(input **smokeCommandInput[I], value commandkit.Supplied[int]) {
|
||||
if value.Set {
|
||||
(*input).request.TransientRetries = &value.Value
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
resources := NewResourceFlags(func(input **smokeCommandInput[I]) *ResourceInput { return &(*input).resources })
|
||||
execution := NewExecutionFlags(func(input **smokeCommandInput[I]) *ExecutionInput { return &(*input).execution })
|
||||
flags = append(flags, resources.Backend, resources.Prompt, resources.Profile)
|
||||
flags = append(flags, execution.All()...)
|
||||
|
||||
return commandkit.NewCommand(commandkit.CommandSpec[*smokeCommandInput[I]]{
|
||||
Name: "test",
|
||||
Summary: "Run one PromptKit smoke test.",
|
||||
@@ -142,6 +58,11 @@ func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
if input == nil {
|
||||
return errors.New("PromptKit smoke-test input must not be nil")
|
||||
}
|
||||
input.request.BackendID = input.resources.Backend.Value
|
||||
input.request.PromptID = input.resources.Prompt.Value
|
||||
input.request.ProfileID = input.resources.Profile.Value
|
||||
input.request.Execution = input.execution.Execution
|
||||
input.request.TransientRetries = input.execution.TransientRetries
|
||||
if input.kind != "all" {
|
||||
input.request.Kind = promptkit.SmokeTestKind(input.kind)
|
||||
}
|
||||
@@ -158,17 +79,6 @@ func newSmokeTestCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
})
|
||||
}
|
||||
|
||||
func smokeExecution[I any](input **smokeCommandInput[I]) *promptkit.ExecutionTargetOverride {
|
||||
if (*input).request.Execution == nil {
|
||||
(*input).request.Execution = &promptkit.ExecutionTargetOverride{}
|
||||
}
|
||||
return (*input).request.Execution
|
||||
}
|
||||
|
||||
func parseSmokeFloat(value string) (float64, error) {
|
||||
return strconv.ParseFloat(value, 64)
|
||||
}
|
||||
|
||||
func runSmokeTest[I, S any](invocation commandkit.Invocation, suite Suite[I, S], input *smokeCommandInput[I]) commandkit.Outcome {
|
||||
if err := promptKitContextError(invocation.Context); err != nil {
|
||||
return definitionFailure("PromptKit smoke testing was canceled.", commandkit.Explanation{}, err)
|
||||
|
||||
@@ -53,9 +53,9 @@ func TestSmokeTestCommandBindsRequestAndRendersFindings(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
outcome = app.Execute(context.Background(), []string{
|
||||
"promptkit", "test", "authentication", "--config=config.yml", "--overlay=overlay.yml", "--campaign=spring",
|
||||
"--backend=local", "--prompt=briefing", "--profile=editorial", "--endpoint=https://example.invalid",
|
||||
"--model=fixture", "--temperature=0", "--max-tokens=0", "--top-p=0", "--timeout-seconds=0",
|
||||
"--service-tier=", "--reasoning-effort=", "--api-key-env=TOKEN", "--transient-retries=0",
|
||||
"--llm-backend=local", "--llm-prompt=briefing", "--llm-profile=editorial", "--llm-endpoint=https://example.invalid",
|
||||
"--llm-model=fixture", "--llm-temperature=0", "--llm-max-tokens=0", "--llm-top-p=0", "--llm-timeout-seconds=0",
|
||||
"--llm-service-tier=", "--llm-reasoning-effort=", "--llm-api-key-env=TOKEN", "--llm-transient-retries=0",
|
||||
}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || providerCalls != 1 {
|
||||
t.Fatalf("smoke outcome = (%#v, %q), calls=%d", outcome, output.String(), providerCalls)
|
||||
|
||||
@@ -37,9 +37,9 @@ func TestSmokeTestCommandRetainsSingletonValuesAndResetsInvocations(t *testing.T
|
||||
first := app.Execute(context.Background(), []string{
|
||||
"promptkit", "test", "authentication", "--config=old.yml", "--config=final.yml",
|
||||
"--overlay=old-overlay.yml", "--overlay=final-overlay.yml", "--campaign=old", "--campaign=final",
|
||||
"--backend=old-backend", "--backend=final-backend", "--prompt=old-prompt", "--prompt=final-prompt",
|
||||
"--profile=old-profile", "--profile=final-profile", "--model=old-model", "--model=final-model",
|
||||
"--temperature=0", "--transient-retries=0",
|
||||
"--llm-backend=old-backend", "--llm-backend=final-backend", "--llm-prompt=old-prompt", "--llm-prompt=final-prompt",
|
||||
"--llm-profile=old-profile", "--llm-profile=final-profile", "--llm-model=old-model", "--llm-model=final-model",
|
||||
"--llm-temperature=0", "--llm-transient-retries=0",
|
||||
}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if first.Kind != commandkit.OutcomeSuccess || len(requests) != 1 {
|
||||
t.Fatalf("first smoke invocation = %#v, requests=%#v", first, requests)
|
||||
@@ -73,11 +73,11 @@ func TestSmokeTestCommandUsageAndInputFailuresStopProviderWork(t *testing.T) {
|
||||
}{
|
||||
{name: "missing kind", args: []string{"promptkit", "test"}},
|
||||
{name: "extra operand", args: []string{"promptkit", "test", "authentication", "extra"}},
|
||||
{name: "malformed temperature", args: []string{"promptkit", "test", "authentication", "--temperature=warm"}},
|
||||
{name: "malformed token count", args: []string{"promptkit", "test", "authentication", "--max-tokens=many"}},
|
||||
{name: "malformed top-p", args: []string{"promptkit", "test", "authentication", "--top-p=high"}},
|
||||
{name: "malformed timeout", args: []string{"promptkit", "test", "authentication", "--timeout-seconds=soon"}},
|
||||
{name: "malformed retries", args: []string{"promptkit", "test", "authentication", "--transient-retries=several"}},
|
||||
{name: "malformed temperature", args: []string{"promptkit", "test", "authentication", "--llm-temperature=warm"}},
|
||||
{name: "malformed token count", args: []string{"promptkit", "test", "authentication", "--llm-max-tokens=many"}},
|
||||
{name: "malformed top-p", args: []string{"promptkit", "test", "authentication", "--llm-top-p=high"}},
|
||||
{name: "malformed timeout", args: []string{"promptkit", "test", "authentication", "--llm-timeout-seconds=soon"}},
|
||||
{name: "malformed retries", args: []string{"promptkit", "test", "authentication", "--llm-transient-retries=several"}},
|
||||
{name: "unsupported format", args: []string{"promptkit", "test", "authentication", "--format=invalid"}},
|
||||
{name: "unknown flag", args: []string{"promptkit", "test", "authentication", "--unknown=value"}},
|
||||
} {
|
||||
|
||||
@@ -3,6 +3,7 @@ package ckprompt
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
@@ -331,3 +332,25 @@ func TestWithRequestValidationCancellationBoundaries(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidationBuilderReplacesCLIExecutionControls(t *testing.T) {
|
||||
engine := adapterEngine(t)
|
||||
calls := 0
|
||||
suite := WithRequestValidation(Suite[struct{}, struct{}]{},
|
||||
func(context.Context, ckconfig.Selection, struct{}, struct{}) (*promptkit.Engine, error) {
|
||||
return engine, nil
|
||||
},
|
||||
func(_ context.Context, seed ValidationRequest, _, _ struct{}) (promptkit.RunRequest, error) {
|
||||
calls++
|
||||
if seed.Request.Execution == nil || seed.Request.Execution.MaxTokens == nil || *seed.Request.Execution.MaxTokens != -1 {
|
||||
t.Fatalf("builder seed = %#v", seed.Request)
|
||||
}
|
||||
return promptkit.RunRequest{PromptID: "empty", ProfileID: "default"}, nil
|
||||
},
|
||||
)
|
||||
app := newPromptTestApp(t, Command(suite))
|
||||
result := app.Execute(context.Background(), []string{"promptkit", "requirements", "validate", "empty", "--llm-max-tokens=-1"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if result.Kind != commandkit.OutcomeSuccess || calls != 1 {
|
||||
t.Fatalf("builder replacement = %#v, calls = %d", result, calls)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@ import (
|
||||
"fmt"
|
||||
|
||||
commandkit "go.maximumdirect.net/commandkit"
|
||||
promptkit "go.maximumdirect.net/promptkit"
|
||||
)
|
||||
|
||||
type validationCommandInput[I any] struct {
|
||||
promptID string
|
||||
request ValidationRequest
|
||||
captured commandkit.FlagValues[I]
|
||||
selected I
|
||||
promptID string
|
||||
request ValidationRequest
|
||||
execution ExecutionInput
|
||||
resources ResourceInput
|
||||
inputs PromptInputs
|
||||
captured commandkit.FlagValues[I]
|
||||
selected I
|
||||
}
|
||||
|
||||
type requestValidationReport struct {
|
||||
@@ -36,32 +38,14 @@ func newValidationCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.Selection.Overlay = value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "profile", Usage: "Profile identity for validation."}, stringDefault(""), parseDefinitionString,
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.Request.ProfileID = value.Value
|
||||
}),
|
||||
commandkit.ValueFlag(commandkit.FlagMeta{Name: "session-id", Usage: "Session context for validation."}, stringDefault(""), parseDefinitionString,
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[string]) {
|
||||
(*input).request.Request.SessionID = value.Value
|
||||
}),
|
||||
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "var", Usage: "Caller variable assignment KEY=VALUE."}, []variableAssignment(nil), parseVariableAssignment,
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[[]variableAssignment]) {
|
||||
variables := make(map[string]string, len(value.Value))
|
||||
for _, assignment := range value.Value {
|
||||
variables[assignment.key] = assignment.value
|
||||
}
|
||||
(*input).request.Request.Vars = variables
|
||||
}),
|
||||
commandkit.RepeatFlag(commandkit.FlagMeta{Name: "input", Usage: "Prompt input file assignment NAME=PATH."}, []inputAssignment(nil), parseInputAssignment,
|
||||
func(input **validationCommandInput[I], value commandkit.Supplied[[]inputAssignment]) {
|
||||
inputs := make(map[string]promptkit.ArtifactRef, len(value.Value))
|
||||
for _, assignment := range value.Value {
|
||||
inputs[assignment.name] = promptkit.File(assignment.path)
|
||||
}
|
||||
(*input).request.Request.Inputs = inputs
|
||||
}),
|
||||
)
|
||||
|
||||
resources := NewResourceFlags(func(input **validationCommandInput[I]) *ResourceInput { return &(*input).resources })
|
||||
execution := NewExecutionFlags(func(input **validationCommandInput[I]) *ExecutionInput { return &(*input).execution })
|
||||
inputs := NewPromptInputFlags(func(input **validationCommandInput[I]) *PromptInputs { return &(*input).inputs })
|
||||
flags = append(flags, resources.Profile, resources.SessionID, inputs.Variable, inputs.Text, inputs.File)
|
||||
flags = append(flags, execution.All()...)
|
||||
|
||||
return commandkit.NewCommand(commandkit.CommandSpec[*validationCommandInput[I]]{
|
||||
Name: "validate",
|
||||
Summary: "Validate a concrete PromptKit request without executing it.",
|
||||
@@ -80,6 +64,12 @@ func newValidationCommand[I, S any](suite Suite[I, S]) commandkit.Command {
|
||||
if input == nil {
|
||||
return errors.New("PromptKit validation input must not be nil")
|
||||
}
|
||||
input.request.Request.ProfileID = input.resources.Profile.Value
|
||||
input.request.Request.SessionID = input.resources.SessionID.Value
|
||||
input.request.Request.Vars = input.inputs.Vars
|
||||
input.request.Request.Inputs = input.inputs.Inputs
|
||||
input.request.Request.Execution = input.execution.Execution
|
||||
input.request.Request.TransientRetries = input.execution.TransientRetries
|
||||
selected, err := prepareSuiteInput(suite, input.request.Selection, input.captured)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -64,8 +64,8 @@ func TestValidationCommandComposesWithRequirementsShow(t *testing.T) {
|
||||
outcome = app.Execute(context.Background(), []string{
|
||||
"promptkit", "requirements", "validate", "briefing",
|
||||
"--config", "config.yml", "--overlay", "overlay.yml", "--campaign", "spring",
|
||||
"--profile", "editorial", "--session-id", "session-1",
|
||||
"--var", "topic=roadmap", "--input", "notes=fixtures/notes=final.txt", "--format", "yaml",
|
||||
"--llm-profile", "editorial", "--llm-session-id", "session-1",
|
||||
"--llm-var", "topic=roadmap", "--llm-input-file", "notes=fixtures/notes=final.txt", "--format", "yaml",
|
||||
}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 || validateCalls != 1 {
|
||||
t.Fatalf("validation outcome = (%#v, %q), calls=%d", outcome, output.String(), validateCalls)
|
||||
@@ -168,7 +168,7 @@ func TestValidationCommandConstructionAndCaptureLifecycle(t *testing.T) {
|
||||
}
|
||||
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "requirements", "validate", "empty", "--profile", "default", "--campaign", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "requirements", "validate", "empty", "--llm-profile", "default", "--campaign", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || newInputCalls != 1 || newStateCalls != 1 || checkCalls != 1 || buildCalls != 1 || loadCalls != 1 {
|
||||
t.Fatalf("captured command = %#v, callback counts input/state/check/build/load=%d/%d/%d/%d/%d", outcome, newInputCalls, newStateCalls, checkCalls, buildCalls, loadCalls)
|
||||
}
|
||||
@@ -197,9 +197,9 @@ func TestValidationCommandPreservesPresenceAndRepeatedAssignmentsPerInvocation(t
|
||||
|
||||
firstArgs := []string{
|
||||
"promptkit", "requirements", "validate", "briefing", "--config", "", "--overlay", "",
|
||||
"--profile", "", "--session-id", "", "--campaign", "spring",
|
||||
"--var", "=", "--var", "phrase= two words, three ", "--var", "duplicate=first", "--var", "duplicate=last=value",
|
||||
"--input", "=", "--input", "file= path, with commas=and=equals ", "--input", "duplicate=first.txt", "--input", "duplicate=last=path.txt",
|
||||
"--llm-profile", "", "--llm-session-id", "", "--campaign", "spring",
|
||||
"--llm-var", "=", "--llm-var", "phrase= two words, three ", "--llm-var", "duplicate=first", "--llm-var", "duplicate=last=value",
|
||||
"--llm-input-file", "=", "--llm-input-file", "file= path, with commas=and=equals ", "--llm-input-file", "duplicate=first.txt", "--llm-input-file", "duplicate=last=path.txt",
|
||||
}
|
||||
outcome := app.Execute(context.Background(), firstArgs, commandkit.Streams{Out: io.Discard, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || len(requests) != 1 {
|
||||
@@ -257,8 +257,8 @@ func TestValidationCommandUsageAndInputFailuresStopProviderWork(t *testing.T) {
|
||||
{name: "unsupported format", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--format", "invalid"}},
|
||||
{name: "unknown flag", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--unknown", "value"}},
|
||||
{name: "removed prompt version flag", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--version", "2"}},
|
||||
{name: "malformed variable before valid assignment", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--var", "broken", "--var", "mode=full"}},
|
||||
{name: "malformed input before valid assignment", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--input", "broken", "--input", "notes=file.txt"}},
|
||||
{name: "malformed variable before valid assignment", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--llm-var", "broken", "--llm-var", "mode=full"}},
|
||||
{name: "malformed input before valid assignment", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--llm-input-file", "broken", "--llm-input-file", "notes=file.txt"}},
|
||||
{name: "missing required selector", args: []string{"promptkit", "requirements", "validate", "briefing"}},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -325,7 +325,7 @@ func TestValidationCommandUsesNativeOutcomesAndMinimalConfirmation(t *testing.T)
|
||||
app := newPromptTestApp(t, Command(suite))
|
||||
|
||||
var output bytes.Buffer
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "requirements", "validate", "empty", "--profile", "default"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
outcome := app.Execute(context.Background(), []string{"promptkit", "requirements", "validate", "empty", "--llm-profile", "default"}, commandkit.Streams{Out: &output, Err: io.Discard})
|
||||
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 {
|
||||
t.Fatalf("native request validation = %#v, output=%q", outcome, output.String())
|
||||
}
|
||||
@@ -342,8 +342,8 @@ func TestValidationCommandUsesNativeOutcomesAndMinimalConfirmation(t *testing.T)
|
||||
args []string
|
||||
want error
|
||||
}{
|
||||
{name: "missing required input", args: []string{"promptkit", "requirements", "validate", "briefing", "--profile", "default"}, want: promptkit.ErrPromptRender},
|
||||
{name: "invalid profile selection", args: []string{"promptkit", "requirements", "validate", "briefing", "--profile", "missing", "--input", "topic=unused.txt"}, want: promptkit.ErrProfileNotFound},
|
||||
{name: "missing required input", args: []string{"promptkit", "requirements", "validate", "briefing", "--llm-profile", "default"}, want: promptkit.ErrPromptRender},
|
||||
{name: "invalid profile selection", args: []string{"promptkit", "requirements", "validate", "briefing", "--llm-profile", "missing", "--llm-input-file", "topic=unused.txt"}, want: promptkit.ErrProfileNotFound},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
output.Reset()
|
||||
|
||||
Reference in New Issue
Block a user