ai-agent/internal/tui/model_overlay_test.go
admin 8dc496b626
Some checks failed
CI / test (push) Has been cancelled
Release / release (push) Failing after 4m36s
first commit
2026-03-08 15:40:34 +07:00

310 lines
9.2 KiB
Go

package tui
import (
"testing"
)
func TestOverlay_ESC_ClosesCompletion(t *testing.T) {
m := newTestModel(t)
// Set up active completion state.
items := []Completion{
{Label: "/help", Insert: "/help ", Category: "command"},
{Label: "/clear", Insert: "/clear ", Category: "command"},
{Label: "/model", Insert: "/model ", Category: "command"},
}
m.completionState = newCompletionState("command", items, true)
m.completionState.Index = 1
m.completionState.Selected[0] = true
m.overlay = OverlayCompletion
// Send ESC.
updated, _ := m.Update(escKey())
m = updated.(*Model)
// Verify completion state is nil.
if m.isCompletionActive() {
t.Error("completionState should be nil after ESC")
}
if m.overlay != OverlayNone {
t.Errorf("overlay should be OverlayNone, got %d", m.overlay)
}
}
func TestOverlay_ESC_ClearsInputToPreventRetrigger(t *testing.T) {
m := newTestModel(t)
// Simulate: user typed "/" which triggered completion, then presses ESC.
m.input.SetValue("/")
items := []Completion{
{Label: "/help", Insert: "/help ", Category: "command"},
{Label: "/clear", Insert: "/clear ", Category: "command"},
}
m.completionState = newCompletionState("command", items, false)
m.overlay = OverlayCompletion
// Press ESC to close.
updated, _ := m.Update(escKey())
m = updated.(*Model)
// Input must be cleared so auto-trigger doesn't reopen.
if m.input.Value() != "" {
t.Errorf("ESC should clear input, got %q", m.input.Value())
}
if m.isCompletionActive() {
t.Error("completion should be closed after ESC")
}
if m.overlay != OverlayNone {
t.Errorf("overlay should be OverlayNone, got %d", m.overlay)
}
}
func TestOverlay_ESC_NoRetriggerOnSubsequentUpdate(t *testing.T) {
m := newTestModel(t)
// Simulate: user typed "/" which triggered completion, then presses ESC.
m.input.SetValue("/")
items := []Completion{
{Label: "/help", Insert: "/help ", Category: "command"},
}
m.completionState = newCompletionState("command", items, false)
m.overlay = OverlayCompletion
// Press ESC.
updated, _ := m.Update(escKey())
m = updated.(*Model)
// Send another key event (e.g., a harmless key like 'a') to cycle through Update.
// This exercises the auto-trigger path at lines 968-972.
updated, _ = m.Update(charKey('a'))
m = updated.(*Model)
// Completion must NOT have re-opened.
if m.isCompletionActive() {
t.Error("completion should not re-trigger after ESC close")
}
if m.overlay != OverlayNone {
t.Errorf("overlay should still be OverlayNone, got %d", m.overlay)
}
}
func TestOverlay_ESC_ClosesHelp(t *testing.T) {
m := newTestModel(t)
m.overlay = OverlayHelp
updated, _ := m.Update(escKey())
m = updated.(*Model)
if m.overlay != OverlayNone {
t.Errorf("overlay should be OverlayNone after ESC, got %d", m.overlay)
}
}
func TestOverlay_HelpDismissal(t *testing.T) {
t.Run("question_mark_dismisses", func(t *testing.T) {
m := newTestModel(t)
m.overlay = OverlayHelp
updated, _ := m.Update(charKey('?'))
m = updated.(*Model)
if m.overlay != OverlayNone {
t.Errorf("? should dismiss help overlay, got %d", m.overlay)
}
})
t.Run("q_dismisses", func(t *testing.T) {
m := newTestModel(t)
m.overlay = OverlayHelp
updated, _ := m.Update(charKey('q'))
m = updated.(*Model)
if m.overlay != OverlayNone {
t.Errorf("q should dismiss help overlay, got %d", m.overlay)
}
})
t.Run("other_key_swallowed", func(t *testing.T) {
m := newTestModel(t)
m.overlay = OverlayHelp
updated, _ := m.Update(charKey('a'))
m = updated.(*Model)
if m.overlay != OverlayHelp {
t.Errorf("'a' should be swallowed, overlay should remain OverlayHelp, got %d", m.overlay)
}
})
}
func TestOverlay_CompletionNavigation(t *testing.T) {
setup := func(t *testing.T) *Model {
t.Helper()
m := newTestModel(t)
items := []Completion{
{Label: "/help", Insert: "/help "},
{Label: "/clear", Insert: "/clear "},
{Label: "/model", Insert: "/model "},
}
m.completionState = newCompletionState("command", items, false)
m.overlay = OverlayCompletion
return m
}
t.Run("down_moves_index", func(t *testing.T) {
m := setup(t)
updated, _ := m.Update(downKey())
m = updated.(*Model)
if m.completionState.Index != 1 {
t.Errorf("down from 0 should move to 1, got %d", m.completionState.Index)
}
})
t.Run("up_at_zero_stays", func(t *testing.T) {
m := setup(t)
updated, _ := m.Update(upKey())
m = updated.(*Model)
if m.completionState.Index != 0 {
t.Errorf("up at 0 should stay at 0, got %d", m.completionState.Index)
}
})
t.Run("down_clamped_at_end", func(t *testing.T) {
m := setup(t)
m.completionState.Index = 2
updated, _ := m.Update(downKey())
m = updated.(*Model)
if m.completionState.Index != 2 {
t.Errorf("down at last item should stay at 2, got %d", m.completionState.Index)
}
})
}
func TestOverlay_CompletionToggle(t *testing.T) {
t.Run("tab_toggles_selection_on", func(t *testing.T) {
m := newTestModel(t)
items := []Completion{
{Label: "/a", Insert: "/a "},
{Label: "/b", Insert: "/b "},
}
m.completionState = newCompletionState("attachments", items, true)
m.overlay = OverlayCompletion
updated, _ := m.Update(tabKey())
m = updated.(*Model)
if !m.completionState.Selected[0] {
t.Error("tab should toggle selection on for index 0")
}
})
t.Run("tab_toggles_selection_off", func(t *testing.T) {
m := newTestModel(t)
items := []Completion{
{Label: "/a", Insert: "/a "},
{Label: "/b", Insert: "/b "},
}
m.completionState = newCompletionState("attachments", items, true)
m.completionState.Selected[0] = true
m.overlay = OverlayCompletion
updated, _ := m.Update(tabKey())
m = updated.(*Model)
if m.completionState.Selected[0] {
t.Error("tab should toggle selection off for index 0")
}
})
t.Run("nil_selected_no_panic", func(t *testing.T) {
m := newTestModel(t)
items := []Completion{
{Label: "/a", Insert: "/a "},
}
m.completionState = newCompletionState("command", items, false)
// Selected is nil for single-select mode
m.overlay = OverlayCompletion
// Should not panic.
updated, _ := m.Update(tabKey())
_ = updated.(*Model)
})
}
func TestOverlay_CompletionAccept(t *testing.T) {
t.Run("single_select", func(t *testing.T) {
m := newTestModel(t)
items := []Completion{
{Label: "/help", Insert: "/help "},
{Label: "/clear", Insert: "/clear "},
}
m.completionState = newCompletionState("command", items, false)
m.completionState.Index = 1
m.overlay = OverlayCompletion
updated, _ := m.Update(enterKey())
m = updated.(*Model)
if m.input.Value() != "/clear " {
t.Errorf("input should be '/clear ', got %q", m.input.Value())
}
if m.isCompletionActive() {
t.Error("completion should be closed after accept")
}
if m.overlay != OverlayNone {
t.Errorf("overlay should be OverlayNone, got %d", m.overlay)
}
})
t.Run("multi_select_with_selections", func(t *testing.T) {
m := newTestModel(t)
items := []Completion{
{Label: "@file1", Insert: "@file1 "},
{Label: "@file2", Insert: "@file2 "},
{Label: "@file3", Insert: "@file3 "},
}
m.completionState = newCompletionState("attachments", items, true)
m.completionState.Selected[0] = true
m.completionState.Selected[2] = true
m.overlay = OverlayCompletion
updated, _ := m.Update(enterKey())
m = updated.(*Model)
val := m.input.Value()
// Selected items 0 and 2 should be joined.
if val == "" {
t.Error("input should not be empty with multi-select")
}
if m.isCompletionActive() {
t.Error("completion should be closed after accept")
}
})
t.Run("multi_select_empty_fallback", func(t *testing.T) {
m := newTestModel(t)
items := []Completion{
{Label: "@file1", Insert: "@file1 "},
{Label: "@file2", Insert: "@file2 "},
}
m.completionState = newCompletionState("attachments", items, true)
m.completionState.Index = 1
m.overlay = OverlayCompletion
updated, _ := m.Update(enterKey())
m = updated.(*Model)
// Fallback to current item.
if m.input.Value() != "@file2 " {
t.Errorf("should fallback to current item, got %q", m.input.Value())
}
})
}