Cover validation command lifecycle and grammar

This commit is contained in:
2026-10-12 22:18:50 +00:00
parent 686a7ba0ab
commit 346bcaeddf
2 changed files with 240 additions and 1 deletions

View File

@@ -3,8 +3,10 @@ package ckprompt
import (
"bytes"
"context"
"errors"
"io"
"reflect"
"strings"
"testing"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
@@ -88,3 +90,227 @@ func TestValidationCommandComposesWithRequirementsShow(t *testing.T) {
t.Fatalf("validation confirmation = %#v", report)
}
}
func TestValidationCommandIsAvailableIndependentlyAndOwnsItsFamily(t *testing.T) {
validationOnly := newPromptTestApp(t, Command(Suite[struct{}, struct{}]{ValidateRequest: func(context.Context, ValidationRequest, struct{}, struct{}) (commandkit.Acquired[struct{}], error) {
return commandkit.Acquired[struct{}]{}, nil
}}))
var help bytes.Buffer
outcome := validationOnly.Execute(context.Background(), []string{"help", "promptkit", "requirements"}, commandkit.Streams{Out: &help, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(help.String(), "validate") || strings.Contains(help.String(), "show") {
t.Fatalf("validation-only availability = (%#v, %q)", outcome, help.String())
}
declaration := Command(Suite[struct{}, struct{}]{ValidateRequest: func(context.Context, ValidationRequest, struct{}, struct{}) (commandkit.Acquired[struct{}], error) {
return commandkit.Acquired[struct{}]{}, nil
}})
other := commandkit.Group("promptkit", "An application command tree.", commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "child", Summary: "A child."}))
app := newPromptTestApp(t, other)
if err := app.Register(declaration); err == nil {
t.Fatal("validation-only command installed over an existing PromptKit namespace")
}
if err := newPromptTestApp(t, declaration).Register(other); err == nil {
t.Fatal("later application command installed over validation-only PromptKit namespace")
}
if _, err := commandkit.New(commandkit.Identity{Name: "nested"}, commandkit.Group("outer", "Outer group.", declaration)); err == nil {
t.Fatal("validation-only root family was accepted beneath another group")
}
}
func TestValidationCommandConstructionAndCaptureLifecycle(t *testing.T) {
type input struct{ campaign commandkit.Supplied[string] }
type state struct{}
newInputCalls, newStateCalls, checkCalls := 0, 0, 0
buildCalls, loadCalls := 0, 0
engine := adapterEngine(t)
oldSelector := commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign", Required: true}, (*string)(nil), parseDefinitionString,
func(target *input, value commandkit.Supplied[string]) { target.campaign = value })
newSelector := commandkit.ValueFlag(commandkit.FlagMeta{Name: "replacement"}, (*string)(nil), parseDefinitionString,
func(*input, commandkit.Supplied[string]) {})
suite := WithRequestValidation(Suite[input, state]{
NewInput: func() input { newInputCalls++; return input{} },
NewState: func() state { newStateCalls++; return state{} },
Selectors: []commandkit.Flag[input]{oldSelector},
CheckInput: func(_ ckconfig.Selection, value input) error {
checkCalls++
if !value.campaign.Set || value.campaign.Value != "spring" {
return errors.New("captured campaign selector was not applied")
}
return nil
},
}, func(context.Context, ckconfig.Selection, input, state) (*promptkit.Engine, error) {
loadCalls++
return engine, nil
}, func(_ context.Context, request ValidationRequest, _ input, _ state) (promptkit.RunRequest, error) {
buildCalls++
return request.Request, nil
})
declaration := Command(suite)
if newInputCalls != 0 || newStateCalls != 0 || checkCalls != 0 || buildCalls != 0 || loadCalls != 0 {
t.Fatal("command construction invoked application or adapter callbacks")
}
// Command captured the original callback and binding metadata.
suite.ValidateRequest = func(context.Context, ValidationRequest, input, state) (commandkit.Acquired[struct{}], error) {
return commandkit.Acquired[struct{}]{}, errors.New("replacement callback was captured")
}
suite.Selectors[0] = newSelector
app := newPromptTestApp(t, declaration)
for _, args := range [][]string{{"help", "promptkit", "requirements", "validate"}, {"version"}} {
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("construction lifecycle command %v = %#v", args, outcome)
}
}
if newInputCalls != 0 || newStateCalls != 0 || checkCalls != 0 || buildCalls != 0 || loadCalls != 0 {
t.Fatal("installation, help, or version invoked application or adapter callbacks")
}
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})
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)
}
}
func TestValidationCommandPreservesPresenceAndRepeatedAssignmentsPerInvocation(t *testing.T) {
type input struct{ campaign commandkit.Supplied[string] }
type state struct{ sequence int }
campaignFlag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign"}, (*string)(nil), parseDefinitionString,
func(target *input, value commandkit.Supplied[string]) { target.campaign = value })
var requests []ValidationRequest
var selectedInputs []input
newInputCalls, checkCalls, newStateCalls := 0, 0, 0
suite := Suite[input, state]{
NewInput: func() input { newInputCalls++; return input{} },
NewState: func() state { newStateCalls++; return state{sequence: newStateCalls} },
Selectors: []commandkit.Flag[input]{campaignFlag},
CheckInput: func(ckconfig.Selection, input) error { checkCalls++; return nil },
ValidateRequest: func(_ context.Context, request ValidationRequest, value input, _ state) (commandkit.Acquired[struct{}], error) {
requests = append(requests, request)
selectedInputs = append(selectedInputs, value)
return commandkit.Acquired[struct{}]{}, nil
},
}
app := newPromptTestApp(t, Command(suite))
firstArgs := []string{
"promptkit", "requirements", "validate", "briefing", "--config", "", "--overlay", "",
"--version", "", "--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",
}
outcome := app.Execute(context.Background(), firstArgs, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || len(requests) != 1 {
t.Fatalf("first repeated assignment invocation = %#v, requests=%d", outcome, len(requests))
}
first := requests[0]
if first.Selection.Config.Value != "" || !first.Selection.Config.Set || first.Selection.Overlay.Value != "" || !first.Selection.Overlay.Set {
t.Errorf("explicit empty selection = %#v", first.Selection)
}
if first.Request.PromptVersion != "" || first.Request.ProfileID != "" || first.Request.SessionID != "" {
t.Errorf("empty scalar values = %#v", first.Request)
}
wantVars := map[string]string{"": "", "phrase": " two words, three ", "duplicate": "last=value"}
if !reflect.DeepEqual(first.Request.Vars, wantVars) {
t.Errorf("request vars = %#v, want %#v", first.Request.Vars, wantVars)
}
wantInputs := map[string]promptkit.ArtifactRef{
"": promptkit.File(""),
"file": promptkit.File(" path, with commas=and=equals "),
"duplicate": promptkit.File("last=path.txt"),
}
if !reflect.DeepEqual(first.Request.Inputs, wantInputs) {
t.Errorf("request inputs = %#v, want %#v", first.Request.Inputs, wantInputs)
}
if !selectedInputs[0].campaign.Set || selectedInputs[0].campaign.Value != "spring" {
t.Errorf("first selector input = %#v", selectedInputs[0])
}
outcome = app.Execute(context.Background(), []string{"promptkit", "requirements", "validate", "briefing"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || len(requests) != 2 {
t.Fatalf("second invocation = %#v, requests=%d", outcome, len(requests))
}
second := requests[1]
if second.Selection.Config.Set || second.Selection.Overlay.Set || second.Request.PromptVersion != "" || second.Request.ProfileID != "" || second.Request.SessionID != "" || len(second.Request.Vars) != 0 || len(second.Request.Inputs) != 0 {
t.Errorf("first invocation values leaked into second request: %#v", second)
}
if selectedInputs[1].campaign.Set || selectedInputs[1].campaign.Value != "" {
t.Errorf("first selector leaked into second input: %#v", selectedInputs[1])
}
if newInputCalls != 2 || checkCalls != 2 || newStateCalls != 2 {
t.Errorf("per-invocation counts input/check/state = %d/%d/%d", newInputCalls, checkCalls, newStateCalls)
}
}
func TestValidationCommandUsageAndInputFailuresStopProviderWork(t *testing.T) {
type input struct{ campaign commandkit.Supplied[string] }
selector := commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign", Required: true}, (*string)(nil), parseDefinitionString,
func(target *input, value commandkit.Supplied[string]) { target.campaign = value })
for _, test := range []struct {
name string
args []string
}{
{name: "missing operand", args: []string{"promptkit", "requirements", "validate"}},
{name: "extra operand", args: []string{"promptkit", "requirements", "validate", "briefing", "extra", "--campaign", "spring"}},
{name: "unsupported format", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--format", "text"}},
{name: "unknown flag", args: []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring", "--unknown", "value"}},
{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: "missing required selector", args: []string{"promptkit", "requirements", "validate", "briefing"}},
} {
t.Run(test.name, func(t *testing.T) {
newInputCalls, checkCalls, newStateCalls, providerCalls := 0, 0, 0, 0
suite := Suite[input, struct{}]{
NewInput: func() input { newInputCalls++; return input{} },
NewState: func() struct{} { newStateCalls++; return struct{}{} },
Selectors: []commandkit.Flag[input]{selector},
CheckInput: func(ckconfig.Selection, input) error { checkCalls++; return nil },
ValidateRequest: func(context.Context, ValidationRequest, input, struct{}) (commandkit.Acquired[struct{}], error) {
providerCalls++
return commandkit.Acquired[struct{}]{}, nil
},
}
outcome := newPromptTestApp(t, Command(suite)).Execute(context.Background(), test.args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
wantInputCalls := 0
if test.name == "missing required selector" {
// prepareSuiteInput initializes the caller input before core validates
// captured required bindings; CheckInput and provider work still stop.
wantInputCalls = 1
}
if outcome.Kind != commandkit.OutcomeUsage || outcome.Status != 2 || newInputCalls != wantInputCalls || checkCalls != 0 || newStateCalls != 0 || providerCalls != 0 {
t.Fatalf("preflight failure = %#v, input/check/state/provider=%d/%d/%d/%d", outcome, newInputCalls, checkCalls, newStateCalls, providerCalls)
}
})
}
newStateCalls, providerCalls := 0, 0
suite := Suite[input, struct{}]{
NewInput: func() input { return input{} },
NewState: func() struct{} { newStateCalls++; return struct{}{} },
Selectors: []commandkit.Flag[input]{selector},
CheckInput: func(ckconfig.Selection, input) error { return errors.New("input rejected") },
ValidateRequest: func(context.Context, ValidationRequest, input, struct{}) (commandkit.Acquired[struct{}], error) {
providerCalls++
return commandkit.Acquired[struct{}]{}, nil
},
}
outcome := newPromptTestApp(t, Command(suite)).Execute(context.Background(), []string{"promptkit", "requirements", "validate", "briefing", "--campaign", "spring"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || newStateCalls != 0 || providerCalls != 0 {
t.Fatalf("input check failure reached state/provider: %#v state=%d provider=%d", outcome, newStateCalls, providerCalls)
}
}
func TestValidationCommandSuppliesZeroValueFactoriesIncludingNilPointers(t *testing.T) {
var gotInput *int
var gotState *int
suite := Suite[*int, *int]{ValidateRequest: func(_ context.Context, _ ValidationRequest, input, state *int) (commandkit.Acquired[struct{}], error) {
gotInput, gotState = input, state
return commandkit.Acquired[struct{}]{}, nil
}}
app := newPromptTestApp(t, Command(suite))
outcome := app.Execute(context.Background(), []string{"promptkit", "requirements", "validate", "briefing"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || gotInput != nil || gotState != nil {
t.Fatalf("omitted factories = outcome %#v, input=%v state=%v", outcome, gotInput, gotState)
}
}