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) } }