Files
commandkit/integrations/ckprompt/composition_test.go

160 lines
7.2 KiB
Go

package ckprompt_test
import (
"bytes"
"context"
"io"
"testing"
"testing/fstest"
commandkit "gitea.maximumdirect.net/eric/CommandKit"
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckprompt"
configkit "gitea.maximumdirect.net/eric/ConfigKit"
promptkit "gitea.maximumdirect.net/eric/promptkit"
)
type compositionInput struct{ Campaign commandkit.Supplied[string] }
type compositionState struct{ token int }
func TestPublicDefinitionCommandsCaptureSelectorHandles(t *testing.T) {
newFlag := func() commandkit.Flag[compositionInput] {
return commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign", Required: true}, (*string)(nil),
func(value string) (string, error) { return value, nil },
func(input *compositionInput, value commandkit.Supplied[string]) { input.Campaign = value })
}
selectors := []commandkit.Flag[compositionInput]{newFlag()}
calls := 0
checkInput := func(input compositionInput) {
t.Helper()
calls++
if !input.Campaign.Set || input.Campaign.Value != "spring" {
t.Errorf("captured selector value = %#v", input.Campaign)
}
}
declaration := ckprompt.Command(ckprompt.Suite[compositionInput, struct{}]{
Selectors: selectors,
InspectPrompt: func(_ context.Context, request ckprompt.PromptRequest, input compositionInput, _ struct{}) (commandkit.Acquired[*promptkit.PromptInspection], error) {
checkInput(input)
return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{PromptID: request.ID}}, nil
},
InspectProfile: func(_ context.Context, request ckprompt.ProfileRequest, input compositionInput, _ struct{}) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
checkInput(input)
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{ProfileID: request.ID}}, nil
},
})
first, err := commandkit.New(commandkit.Identity{Name: "first"}, declaration)
if err != nil {
t.Fatal(err)
}
// A separately constructed lookalike must not replace the captured binding.
selectors[0] = newFlag()
second, err := commandkit.New(commandkit.Identity{Name: "second"}, declaration)
if err != nil {
t.Fatal(err)
}
for _, app := range []*commandkit.App{first, second} {
for _, resource := range []string{"prompt", "profile"} {
outcome := app.Execute(context.Background(), []string{"promptkit", "show", resource, "briefing", "--campaign", "spring"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess {
t.Fatalf("%s after selector edit: %#v", resource, outcome)
}
}
}
if calls != 4 {
t.Fatalf("inspection calls = %d, want 4", calls)
}
}
func TestPublicConfigurationAndDefinitionFamiliesRemainIndependent(t *testing.T) {
inputs, err := promptkit.FilesystemInputs("composition fixture", fstest.MapFS{
"config/prompts.yml": &fstest.MapFile{Data: []byte(`prompts:
briefing:
"1":
messages:
- role: user
content: "Hello"
output:
format: text
validation_mode: none
backends:
local:
endpoint: "https://example.invalid/v1"
profiles:
default:
backend: local
model: example
`)}}, "config")
if err != nil {
t.Fatal(err)
}
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: []configkit.Underlay{{Inputs: inputs}}}})
if err != nil {
t.Fatalf("construct engine: %v", err)
}
flag := commandkit.ValueFlag(commandkit.FlagMeta{Name: "campaign"}, (*string)(nil), func(value string) (string, error) { return value, nil },
func(input **compositionInput, value commandkit.Supplied[string]) { (*input).Campaign = value })
newInputCalls, newStateCalls := 0, 0
newInput := func() *compositionInput { newInputCalls++; return &compositionInput{} }
newState := func() *compositionState { newStateCalls++; return &compositionState{token: newStateCalls} }
var appInput, engineInput *compositionInput
var appState, configEngineState *compositionState
appAcquisitions, engineScopeAcquisitions, loaderCalls := 0, 0, 0
loadEngine := func(_ context.Context, _ ckconfig.Selection, input *compositionInput, state *compositionState) (*promptkit.Engine, error) {
loaderCalls++
if !input.Campaign.Set || input.Campaign.Value != "spring" {
t.Errorf("engine loader input = %#v", input)
}
if loaderCalls == 1 {
engineInput, configEngineState = input, state
}
return engine, nil
}
appScope := ckconfig.Scope[*compositionInput, *compositionState]{
Name: "app", Operations: []ckconfig.Operation{ckconfig.Validate},
Acquire: func(_ context.Context, _ ckconfig.Request, input *compositionInput, state *compositionState) (commandkit.Acquired[ckconfig.View], error) {
appAcquisitions++
appInput, appState = input, state
return commandkit.Acquired[ckconfig.View]{}, nil
},
}
engineScope := ckprompt.ConfigScope(loadEngine)
acquireEngineScope := engineScope.Acquire
engineScope.Acquire = func(ctx context.Context, request ckconfig.Request, input *compositionInput, state *compositionState) (commandkit.Acquired[ckconfig.View], error) {
engineScopeAcquisitions++
return acquireEngineScope(ctx, request, input, state)
}
configCommand := ckconfig.Command(ckconfig.Suite[*compositionInput, *compositionState]{
NewInput: newInput, NewState: newState,
Selectors: []ckconfig.Selector[*compositionInput]{{Binding: flag, Scopes: []string{"app", "promptkit"}}},
Scopes: []ckconfig.Scope[*compositionInput, *compositionState]{appScope, engineScope},
})
definitions := ckprompt.FromEngine(loadEngine)
definitions.NewInput = newInput
definitions.NewState = newState
definitions.Selectors = []commandkit.Flag[*compositionInput]{flag}
command, err := commandkit.New(commandkit.Identity{Name: "composition"}, configCommand, ckprompt.Command(definitions))
if err != nil {
t.Fatal(err)
}
var output bytes.Buffer
outcome := command.Execute(context.Background(), []string{"config", "validate", "--scope", "all", "--campaign", "spring"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || appAcquisitions != 1 || engineScopeAcquisitions != 1 || loaderCalls != 1 {
t.Fatalf("configuration run = (%#v, output=%q, counts=%d/%d/%d)", outcome, output.String(), appAcquisitions, engineScopeAcquisitions, loaderCalls)
}
if appInput == nil || engineInput == nil || appInput == engineInput || appState == nil || appState != configEngineState || appState.token != 1 {
t.Fatalf("scope values were not isolated/shared as selected: inputs=%p/%p states=%p/%p", appInput, engineInput, appState, configEngineState)
}
output.Reset()
outcome = command.Execute(context.Background(), []string{"promptkit", "show", "profile", "default", "--campaign", "spring", "--format", "yaml"}, commandkit.Streams{Out: &output, Err: io.Discard})
if outcome.Kind != commandkit.OutcomeSuccess || appAcquisitions != 1 || engineScopeAcquisitions != 1 || loaderCalls != 2 || !bytes.Contains(output.Bytes(), []byte("profile_id: default")) {
t.Fatalf("independent profile run = (%#v, output=%q, counts=%d/%d/%d)", outcome, output.String(), appAcquisitions, engineScopeAcquisitions, loaderCalls)
}
if newInputCalls != 3 || newStateCalls != 2 {
t.Fatalf("invocation factory calls input/state=%d/%d", newInputCalls, newStateCalls)
}
}