diff --git a/cmd/interactive/palette_optional_flags_test.go b/cmd/interactive/palette_optional_flags_test.go new file mode 100644 index 0000000..fcbcf54 --- /dev/null +++ b/cmd/interactive/palette_optional_flags_test.go @@ -0,0 +1,57 @@ +package interactive + +import ( + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +// TestPaletteSelectionOpensFormForOptionalFlags 验证:选中一个"只有可选参数、 +// 无必填参数"的命令(如 branch +batch-protect 的 names/from 二选一)时, +// 仍然弹出参数表单,而非直接执行导致 "no ... provided" 错误。 +// +// 回归:之前 updatePalette 只在有缺失的 Required flag 时才弹表单, +// 导致 names/from 这类"至少一个"型参数(无法单独标 Required)的命令 +// 被直接执行,用户没机会输入参数。 +func TestPaletteSelectionOpensFormForOptionalFlags(t *testing.T) { + // 构造一个模拟 branch +batch-protect 的 shortcut:所有 flag 都非必填。 + shortcuts := map[string][]*common.Shortcut{ + "branch": { + { + Name: "batch-protect", + Description: "批量保护分支", + Flags: []common.Flag{ + {Name: "names", Usage: "分支名(逗号分隔)"}, // 无 Required + {Name: "from", Usage: "文件路径"}, // 无 Required + }, + Run: func(ctx *common.RuntimeContext) error { return nil }, + }, + }, + } + + m := newReplModel("owner", "repo") + // 注入测试 registry + m.shortcuts = shortcuts + + // 1) 输入 / 触发命令面板 + m2, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}}) + repl := m2.(replModel) + if repl.state != statePalette { + t.Fatalf("输入 / 后应在 statePalette,实际 %v", repl.state) + } + + // 2) 模拟用户选中 branch 组 + repl.palette.result = &PaletteResult{ + Group: "branch", + Shortcut: shortcuts["branch"][0], + } + + // 3) 派发一条任意消息触发 updatePalette 的结果检查 + m3, _ := repl.Update(tea.KeyMsg{Type: tea.KeyEnter}) + final := m3.(replModel) + + if final.state != stateForm { + t.Fatalf("选中带可选参数的命令应进入 stateForm,实际 %v(命令被直接执行了,用户没机会填参数)", final.state) + } +} diff --git a/cmd/interactive/repl.go b/cmd/interactive/repl.go index e688907..207f82d 100644 --- a/cmd/interactive/repl.go +++ b/cmd/interactive/repl.go @@ -250,17 +250,19 @@ func (m replModel) updatePalette(msg tea.Msg) (tea.Model, tea.Cmd) { result := m.palette.result s := result.Shortcut - // Only open the form when required flags are missing. - // Commands with only optional flags execute directly — users can pass - // flags via the direct-command syntax if needed. - missing := missingRequiredFlags(s, map[string]string{}) - if len(missing) > 0 { + // Open the form whenever the command has ANY flags — not just required + // ones. Many commands have "at-least-one-of" parameters (e.g. branch + // +batch-protect needs --names OR --from) that can't be marked Required + // individually, so skipping the form for "optional-only" commands left + // users unable to supply them and the command failed at runtime. + // Commands with no flags at all execute directly. + if len(s.Flags) > 0 { m.form = newFormModel(result.Group, s, m.width, m.height, nil) m.state = stateForm return m, nil } - // No required flags missing → execute directly + // No flags → execute directly m.state = stateExecuting return m, tea.Batch(m.executeCommand(result.Group, s, map[string]string{}), m.spinner.Tick) }