Capture PromptKit selectors before retaining callbacks

This commit is contained in:
2026-10-11 13:02:54 +00:00
parent 6d660585d1
commit fba917f5bd
3 changed files with 59 additions and 2 deletions

View File

@@ -3,7 +3,7 @@
Reviewed on 2026-10-11, against committed implementation
`cc574915ee17e2d738f27874d2c8857aa1f51a47`. The implementation sprint starts
after planning commit `0d23676144ab1ce5047924e98f1ab6a2a9d6aeb6`.
All issues below are open. P1 identifies a functional contract violation; P2
Completion status and evidence are recorded per issue below. P1 identifies a functional contract violation; P2
identifies architecture or documentation corrections; P3 identifies smaller
maintenance corrections. These priorities do not add publication gates.
@@ -38,7 +38,14 @@ provider caches, or permanent validation gates to resolve these issues.
## R1: Capture PromptKit selector handles before retaining the suite
**Status:** Open. **Priority:** P1.
**Status:** Completed (2026-10-11). **Priority:** P1.
**Completed work:** the enabled family copies its selector-handle collection
once, preserving original identities for both generated commands and retained
callbacks. Empty-suite behavior is unchanged. The public consumer regression
replaces a source element after capture and exercises prompt/profile inspection
in two apps reusing the declaration. It failed before the fix and passes after
it; the complete ckprompt suite passes offline with Go 1.26.8.
**Evidence:** [ckprompt/command.go](../../integrations/ckprompt/command.go),
`Command`, `newDefinitionCommand`, and `prepareDefinitionInput`.

View File

@@ -37,6 +37,7 @@ func Command[I, S any](suite Suite[I, S]) commandkit.Command {
return command
}
suite.Selectors = append([]commandkit.Flag[I](nil), suite.Selectors...)
showCommands := make([]commandkit.Command, 0, 2)
if suite.InspectPrompt != nil {
showCommands = append(showCommands, newDefinitionCommand(suite, "prompt", true))

View File

@@ -17,6 +17,55 @@ import (
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: