Check PromptKit selector collisions against installed flags
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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" {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user