refactor(prompts): fold Asker into Prompter and share delete confirmations

FormPrompter is the only production prompter and implements the rich
prompt API, so Ask, AskText and ConfirmDefault become the Prompter
interface and the plain Input/Confirm fallbacks go away.

Confirmations that default to no derive their non-terminal wording
from the prompt, and single deletes share deleteConfirmed for the
prompt, the request and the done line.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-12 10:47:54 +02:00
1 parent 6b204a34c0
commit 1f4b5f868f
31 files changed
+131 -253

No files matched your search

+8 -21
View File
@@ -31,15 +31,6 @@ func accessiblePrompter(t *testing.T, input string) (*FormPrompter, *bytes.Buffe
func noEdit(context.Context, string) (Draft, error) { return Draft{}, nil }
type fakePrompt struct{ inputs int }
func (f *fakePrompt) Input(context.Context, string) (string, error) {
f.inputs++
return "provided", nil
}
func (f *fakePrompt) SecretInput(context.Context, string) (string, error) { return "", nil }
func (f *fakePrompt) Confirm(context.Context, string) (bool, error) { return false, nil }
func TestPromptPolicy(t *testing.T) {
for _, tc := range []struct {
policy Policy
@@ -54,12 +45,12 @@ func TestPromptPolicy(t *testing.T) {
} {
require.Equal(t, tc.expected, tc.policy.CanPrompt(tc.streams))
}
prompt := &fakePrompt{}
prompt := &askingPrompt{}
value := "supplied"
asked, err := RequireInput(context.Background(), &value, "title", "title", nil, Streams{}, Policy{}, prompt)
require.NoError(t, err)
require.False(t, asked)
require.Zero(t, prompt.inputs)
require.Empty(t, prompt.titles)
value = ""
_, err = RequireInput(context.Background(), &value, "title", "title", nil, Streams{}, Policy{}, prompt)
require.ErrorContains(t, err, "--title")
@@ -68,7 +59,6 @@ func TestPromptPolicy(t *testing.T) {
// askingPrompt answers Asker prompts from scripted answers. Ask skips answers that validate rejects,
// as an inline re-prompt does, and cancels when none is left.
type askingPrompt struct {
fakePrompt
answers, rejected, titles []string
accept bool
defaults []bool
@@ -87,6 +77,8 @@ func (p *askingPrompt) Ask(_ context.Context, title string, validate func(string
return "", ErrCanceled
}
func (p *askingPrompt) SecretInput(context.Context, string) (string, error) { return "", nil }
func (p *askingPrompt) AskText(_ context.Context, title, initial string) (string, error) {
p.titles = append(p.titles, title)
return initial + " edited", nil
@@ -112,14 +104,13 @@ func TestConfirmAction(t *testing.T) {
message string
rejected []string
}{
{name: "accepted by flag", c: deleteLabel, yes: true, prompt: &fakePrompt{}},
{name: "names the action without a terminal", c: deleteLabel, prompt: &fakePrompt{}, code: clierr.Usage, message: `confirmation required to delete label "bug" from alice/x; rerun with --yes`},
{name: "without a terminal or action", c: Confirmation{Prompt: "Go?"}, prompt: &fakePrompt{}, code: clierr.Usage, message: "confirmation required; rerun with --yes"},
{name: "accepted by flag", c: deleteLabel, yes: true, prompt: &askingPrompt{}},
{name: "names the action without a terminal", c: deleteLabel, prompt: &askingPrompt{}, code: clierr.Usage, message: `confirmation required to delete label "bug" from alice/x; rerun with --yes`},
{name: "names the prompted action without a terminal", c: Confirmation{Prompt: "Delete alice/x? It becomes read-only."}, prompt: &askingPrompt{}, code: clierr.Usage, message: "confirmation required to delete alice/x; rerun with --yes"},
{name: "without a terminal or action", c: Confirmation{Prompt: "Go?", DefaultYes: true}, prompt: &askingPrompt{}, code: clierr.Usage, message: "confirmation required; rerun with --yes"},
{name: "accepted", c: deleteLabel, streams: terminal, prompt: &askingPrompt{accept: true}},
{name: "declined", c: deleteLabel, streams: terminal, prompt: &askingPrompt{}, code: clierr.Canceled, message: "canceled"},
{name: "declined to a plain prompter", c: deleteLabel, streams: terminal, prompt: &fakePrompt{}, code: clierr.Canceled, message: "canceled"},
{name: "re-prompts until the name is typed", c: typed, streams: terminal, prompt: &askingPrompt{answers: []string{"alice/y", " Alice/X "}}, rejected: []string{"alice/y"}},
{name: "typed wrong to a plain prompter", c: typed, streams: terminal, prompt: &fakePrompt{}, code: clierr.Canceled, message: "canceled"},
} {
t.Run(tc.name, func(t *testing.T) {
err := ConfirmAction(context.Background(), tc.c, tc.yes, tc.streams, Policy{Enabled: true}, tc.prompt)
@@ -165,10 +156,6 @@ func TestRequireInput(t *testing.T) {
require.Equal(t, []string{"Color (hex)"}, asker.titles)
value = ""
_, err = RequireInput(context.Background(), &value, "color", "Color (hex)", hex, streams, policy, &fakePrompt{})
require.Equal(t, clierr.Usage, clierr.ExitCode(err))
require.EqualError(t, err, "--color: use six hex digits")
_, err = RequireInput(context.Background(), &value, "color", "Color (hex)", hex, Streams{}, policy, asker)
require.EqualError(t, err, "missing required input: supply --color")
}