Add PromptKit requirements command

This commit is contained in:
2026-10-12 17:05:33 +00:00
parent 0276930b12
commit 7dce7b816b
3 changed files with 322 additions and 14 deletions

View File

@@ -5,6 +5,7 @@ import (
"context"
"errors"
"io"
"reflect"
"strings"
"testing"
@@ -29,7 +30,7 @@ func TestCommandBuildsIndependentOptionalTrees(t *testing.T) {
}}))
var output bytes.Buffer
outcome := promptOnly.Execute(context.Background(), []string{"help", "promptkit", "show"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "\n prompt ") || strings.Contains(output.String(), "\n profile ") || strings.Contains(output.String(), "config") {
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "\n prompt ") || strings.Contains(output.String(), "\n profile ") || strings.Contains(output.String(), "requirements") || strings.Contains(output.String(), "config") {
t.Fatalf("prompt-only help = (%#v, %q)", outcome, output.String())
}
@@ -49,9 +50,21 @@ func TestCommandBuildsIndependentOptionalTrees(t *testing.T) {
InspectProfile: func(context.Context, ProfileRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
return commandkit.Acquired[*promptkit.ProfileInspection]{}, nil
},
InspectRequirements: func(context.Context, RequirementsRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
return commandkit.Acquired[*promptkit.RequirementInspection]{}, nil
},
ListPrompts: func(context.Context, ckconfig.Selection, struct{}, struct{}) (commandkit.Acquired[[]promptkit.PromptIdentity], error) {
return commandkit.Acquired[[]promptkit.PromptIdentity]{}, nil
},
ListProfiles: func(context.Context, ckconfig.Selection, struct{}, struct{}) (commandkit.Acquired[[]string], error) {
return commandkit.Acquired[[]string]{}, nil
},
})
if err := newPromptTestApp(t).Register(both); err != nil {
t.Fatalf("both-capability tree failed to register: %v", err)
all := newPromptTestApp(t, both)
output.Reset()
outcome = all.Execute(context.Background(), []string{"help", "promptkit"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(output.String(), "show") || !strings.Contains(output.String(), "list") || !strings.Contains(output.String(), "requirements") {
t.Fatalf("all-capability tree = (%#v, %q)", outcome, output.String())
}
app := newPromptTestApp(t, Command(Suite[struct{}, struct{}]{Selectors: []commandkit.Flag[struct{}]{{}}}))
@@ -64,11 +77,156 @@ func TestCommandBuildsIndependentOptionalTrees(t *testing.T) {
invalid := FromEngine[int, int](nil)
invalid.ListPrompts, invalid.ListProfiles = nil, nil
invalid.InspectPrompt, invalid.InspectProfile = nil, nil
invalid.InspectRequirements = nil
if err := newPromptTestApp(t).Register(Command(invalid)); err == nil || !strings.Contains(err.Error(), "PromptKit engine loader is nil") {
t.Fatalf("empty invalid adapter lost its construction issue: %v", err)
}
}
func TestRequirementsCommandCapturesContextAndVariableAssignments(t *testing.T) {
type input struct{ campaign commandkit.Supplied[string] }
type state struct{ call int }
campaignFlag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign", Usage: "Application campaign."}, (*string)(nil), parseDefinitionString,
func(target *input, value commandkit.Supplied[string]) { target.campaign = value })
newInputCalls, checkInputCalls, newStateCalls, providerCalls := 0, 0, 0, 0
var received RequirementsRequest
var receivedInput input
var receivedState state
suite := Suite[input, state]{
NewInput: func() input {
newInputCalls++
return input{}
},
NewState: func() state {
newStateCalls++
return state{call: newStateCalls}
},
Selectors: []commandkit.Flag[input]{campaignFlag},
CheckInput: func(selection ckconfig.Selection, value input) error {
checkInputCalls++
if checkInputCalls == 1 {
if !selection.Config.Set || selection.Config.Value != "" || !selection.Overlay.Set || selection.Overlay.Value != "" {
t.Errorf("checked selection = %#v", selection)
}
if !value.campaign.Set || value.campaign.Value != "spring" {
t.Errorf("checked campaign = %#v", value.campaign)
}
} else if selection.Config.Set || selection.Overlay.Set || value.campaign.Set {
t.Errorf("second checked input retained prior invocation values: selection=%#v input=%#v", selection, value)
}
return nil
},
InspectRequirements: func(_ context.Context, request RequirementsRequest, value input, gotState state) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
providerCalls++
received, receivedInput, receivedState = request, value, gotState
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: &promptkit.RequirementInspection{
PromptID: request.Inspection.PromptID, PromptVersion: request.Inspection.PromptVersion,
ProfileID: request.Inspection.ProfileID, Complete: true,
}}, nil
},
}
app := newPromptTestApp(t, Command(suite))
streams := commandkit.Streams{Out: io.Discard, Err: io.Discard}
var help bytes.Buffer
streams.Out = &help
helpOutcome := app.Execute(context.Background(), []string{"help", "promptkit", "requirements", "show"}, streams)
if helpOutcome.Kind != commandkit.OutcomeSuccess || !strings.Contains(help.String(), "id") {
t.Fatalf("requirements-only help = (%#v, %q)", helpOutcome, help.String())
}
var familyHelp bytes.Buffer
familyOutcome := app.Execute(context.Background(), []string{"help", "promptkit"}, commandkit.Streams{Out: &familyHelp, Err: io.Discard})
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", "--version", "--profile", "--session-id", "--var", "--format", "--campaign"} {
if !strings.Contains(help.String(), flag) {
t.Errorf("requirements help omits %s: %q", flag, help.String())
}
}
if strings.Contains(help.String(), "--scope") || newInputCalls != 0 || checkInputCalls != 0 || newStateCalls != 0 || providerCalls != 0 {
t.Fatalf("help selected a scope or invoked application code: output=%q factories/checks/providers=%d/%d/%d/%d", help.String(), newInputCalls, checkInputCalls, newStateCalls, providerCalls)
}
var output bytes.Buffer
args := []string{
"promptkit", "requirements", "show", "briefing", "--campaign", "spring",
"--config", "before.yml", "--config=", "--overlay", "base.yml", "--overlay=",
"--version", "1", "--version", "2", "--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",
}
outcome := app.Execute(context.Background(), args, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || outcome.Status != 0 {
t.Fatalf("requirements invocation = %#v, output=%q", outcome, output.String())
}
if newInputCalls != 1 || checkInputCalls != 1 || newStateCalls != 1 || providerCalls != 1 {
t.Fatalf("first invocation calls = input/check/state/provider %d/%d/%d/%d", newInputCalls, checkInputCalls, newStateCalls, providerCalls)
}
if !received.Selection.Config.Set || received.Selection.Config.Value != "" || !received.Selection.Overlay.Set || received.Selection.Overlay.Value != "" {
t.Fatalf("configuration selection = %#v", received.Selection)
}
wantVariables := map[string]string{"note": "hello, world", "empty": "", "token": "a=b", "": "value", "mode": "full"}
if received.Inspection.PromptID != "briefing" || received.Inspection.PromptVersion != "2" || received.Inspection.ProfileID != "editorial" || received.Inspection.SessionID != "session-42" || !reflect.DeepEqual(received.Inspection.Vars, wantVariables) {
t.Fatalf("native request = %#v, want vars %#v", received.Inspection, wantVariables)
}
if !receivedInput.campaign.Set || receivedInput.campaign.Value != "spring" || receivedState.call != 1 {
t.Fatalf("provider input/state = %#v/%#v", receivedInput, receivedState)
}
var report map[string]any
if err := yaml.Unmarshal(output.Bytes(), &report); err != nil || report["prompt_id"] != "briefing" || report["complete"] != true {
t.Fatalf("default requirements YAML = %#v (decode error %v), output=%q", report, err, output.String())
}
output.Reset()
outcome = app.Execute(context.Background(), []string{"promptkit", "requirements", "show", "briefing"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || newInputCalls != 2 || checkInputCalls != 2 || newStateCalls != 2 || providerCalls != 2 {
t.Fatalf("second invocation = %#v, calls=%d/%d/%d/%d", outcome, newInputCalls, checkInputCalls, newStateCalls, providerCalls)
}
if len(received.Inspection.Vars) != 0 || received.Inspection.PromptVersion != "" || received.Inspection.ProfileID != "" || received.Inspection.SessionID != "" {
t.Fatalf("second invocation retained native values: %#v", received.Inspection)
}
if !strings.HasPrefix(output.String(), "prompt_id:") {
t.Fatalf("omitted format did not default to YAML: %q", output.String())
}
}
func TestRequirementsCommandUsageFailuresPrecedeApplicationPreparation(t *testing.T) {
for _, test := range []struct {
name string
args []string
}{
{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", "text"}},
{name: "malformed variable followed by valid assignment", args: []string{"promptkit", "requirements", "show", "briefing", "--var", "broken", "--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"}},
} {
t.Run(test.name, func(t *testing.T) {
newInputCalls, checkInputCalls, newStateCalls, providerCalls := 0, 0, 0, 0
suite := Suite[struct{}, struct{}]{
NewInput: func() struct{} { newInputCalls++; return struct{}{} },
CheckInput: func(ckconfig.Selection, struct{}) error {
checkInputCalls++
return nil
},
NewState: func() struct{} { newStateCalls++; return struct{}{} },
InspectRequirements: func(context.Context, RequirementsRequest, struct{}, struct{}) (commandkit.Acquired[*promptkit.RequirementInspection], error) {
providerCalls++
return commandkit.Acquired[*promptkit.RequirementInspection]{Value: &promptkit.RequirementInspection{}}, nil
},
}
var output bytes.Buffer
outcome := newPromptTestApp(t, Command(suite)).Execute(context.Background(), test.args, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeUsage || outcome.Status != 2 || newInputCalls != 0 || checkInputCalls != 0 || newStateCalls != 0 || providerCalls != 0 {
t.Fatalf("usage failure = %#v, factories/checks/providers=%d/%d/%d/%d output=%q", outcome, newInputCalls, checkInputCalls, newStateCalls, providerCalls, output.String())
}
})
}
}
func TestCatalogCommandTreesAreIndependentAndOmitShow(t *testing.T) {
prompts := Command(Suite[struct{}, struct{}]{
ListPrompts: func(context.Context, ckconfig.Selection, struct{}, struct{}) (commandkit.Acquired[[]promptkit.PromptIdentity], error) {