Check PromptKit selector collisions against installed flags

This commit is contained in:
2026-10-07 16:51:15 +00:00
parent c26d279b4f
commit a21dd3d89e
4 changed files with 53 additions and 3 deletions

View File

@@ -245,7 +245,7 @@ Both commands require one ID operand and accept `--config`, `--overlay`, and
`--format text|yaml` (default `text`). Prompt inspection also accepts singleton `--format text|yaml` (default `text`). Prompt inspection also accepts singleton
`--version`, defaulting to the empty string. PromptKit owns version selection: `--version`, defaulting to the empty string. PromptKit owns version selection:
an omitted version works for a single-version ID, while an ambiguous ID is an an omitted version works for a single-version ID, while an ambiguous ID is an
operational error. These commands do not accept `--scope`; their namespace is operational error. These commands have no built-in `--scope` selection; their namespace is
`promptkit`. `promptkit`.
Prompt YAML has these fields. Input order is preserved and an empty input list is Prompt YAML has these fields. Input order is preserved and an empty input list is

View File

@@ -69,6 +69,9 @@ architecture. The issues below affect behavior within the implemented structure.
- R-08 complete: unknown scopes and absent default app scopes report available - R-08 complete: unknown scopes and absent default app scopes report available
choices in standard scope order without runtime acquisition. choices in standard scope order without runtime acquisition.
- R-09 complete: PromptKit selector collisions are checked against installed
standard flags; profile-only `version` and custom `scope` selectors are usable.
## Findings and priority ## Findings and priority
P1 means a supported invocation is blocked or its report content is corrupted. P1 means a supported invocation is blocked or its report content is corrupted.

View File

@@ -128,6 +128,10 @@ func RegisterPromptKit[C any](app *App, suite PromptKitSuite[C]) error {
func normalizePromptKitSuite[C any](suite PromptKitSuite[C]) (PromptKitSuite[C], error) { func normalizePromptKitSuite[C any](suite PromptKitSuite[C]) (PromptKitSuite[C], error) {
normalized := suite normalized := suite
normalized.Selectors = append([]Selector[C](nil), suite.Selectors...) normalized.Selectors = append([]Selector[C](nil), suite.Selectors...)
standardNames := map[string]bool{"help": true}
for _, flag := range promptKitInputFlags(PromptKitSuite[C]{}, suite.InspectPrompt != nil) {
standardNames[flag.binding.meta.Name] = true
}
seenNames := make(map[string]struct{}, len(normalized.Selectors)) seenNames := make(map[string]struct{}, len(normalized.Selectors))
seenShorts := make(map[string]struct{}, len(normalized.Selectors)) seenShorts := make(map[string]struct{}, len(normalized.Selectors))
for i, selector := range normalized.Selectors { for i, selector := range normalized.Selectors {
@@ -139,8 +143,7 @@ func normalizePromptKitSuite[C any](suite PromptKitSuite[C]) (PromptKitSuite[C],
if !validFlagName(meta.Name) { if !validFlagName(meta.Name) {
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag name %q is invalid", meta.Name) return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag name %q is invalid", meta.Name)
} }
switch meta.Name { if standardNames[meta.Name] {
case "config", "overlay", "scope", "format", "version", "help":
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag --%s conflicts with a standard flag", meta.Name) return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag --%s conflicts with a standard flag", meta.Name)
} }
if meta.Short == "h" { if meta.Short == "h" {

View File

@@ -404,3 +404,47 @@ func sortedKeys(values map[string]any) []string {
} }
return keys return keys
} }
func TestPromptKitSelectorNamesCollideOnlyWithInstalledFlags(t *testing.T) {
for _, name := range []string{"version", "scope"} {
var captured commandkit.Supplied[string]
suite := commandkit.PromptKitSuite[commandkit.Supplied[string]]{
Selectors: []commandkit.Selector[commandkit.Supplied[string]]{{Binding: commandkit.ValueFlag[commandkit.Supplied[string]](commandkit.FlagMeta{Name: name}, nil, func(s string) (string, error) { return s, nil }, func(c *commandkit.Supplied[string], v commandkit.Supplied[string]) { *c = v })}},
InspectProfile: func(_ context.Context, _ commandkit.Selection, c commandkit.Supplied[string], _ string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
captured = c
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: &promptkit.ProfileInspection{}}, nil
},
}
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
if err := commandkit.RegisterPromptKit(app, suite); err != nil {
t.Fatalf("%s: %v", name, err)
}
result := app.Execute(context.Background(), []string{"promptkit", "show", "profile", "example", "--" + name, "selected"}, commandkit.Streams{})
if result.Kind != commandkit.OutcomeSuccess || captured != (commandkit.Supplied[string]{Value: "selected", Set: true}) {
t.Fatalf("%s: result=%#v captured=%#v", name, result, captured)
}
suite.InspectPrompt = func(context.Context, commandkit.Selection, commandkit.Supplied[string], string, string) (commandkit.Acquired[*promptkit.PromptInspection], error) {
return commandkit.Acquired[*promptkit.PromptInspection]{Value: &promptkit.PromptInspection{}}, nil
}
other, _ := commandkit.New(commandkit.Identity{Name: "example"})
err := commandkit.RegisterPromptKit(other, suite)
if name == "version" && err == nil {
t.Fatal("accepted collision with installed prompt version flag")
}
if name == "scope" && err != nil {
t.Fatalf("scope selector conflicts with no installed flag: %v", err)
}
}
for _, name := range []string{"config", "overlay", "format", "help"} {
app, _ := commandkit.New(commandkit.Identity{Name: "example"})
suite := commandkit.PromptKitSuite[struct{}]{
InspectProfile: func(context.Context, commandkit.Selection, struct{}, string) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
return commandkit.Acquired[*promptkit.ProfileInspection]{}, nil
},
Selectors: []commandkit.Selector[struct{}]{{Binding: commandkit.ValueFlag[struct{}](commandkit.FlagMeta{Name: name}, nil, func(s string) (string, error) { return s, nil }, func(*struct{}, commandkit.Supplied[string]) {})}},
}
if err := commandkit.RegisterPromptKit(app, suite); err == nil {
t.Fatalf("accepted standard flag collision --%s", name)
}
}
}