package integration import ( "strings" "testing" "github.com/charmbracelet/x/ansi" "github.com/versenilvis/iris/spec" ) func TestRenderGhostText_CursorAtEnd(t *testing.T) { o := NewOverlay() items := []spec.Suggestion{ {Cmd: "git checkout -b feature"}, } o.UpdateItems(items) // case 1: cursor at end of buffer -> should render ghost text suffix out := o.RenderGhostText("git check", false, true) if !strings.Contains(out, "out -b feature") { t.Fatalf("Expected ghost text suffix 'out -b feature', got: %q", out) } if o.LastGhostLen == 0 { t.Fatalf("Expected LastGhostLen > 0, got %d", o.LastGhostLen) } // case 2: cursor moved left (cursorAtEnd == false) -> should clear ghost text outClear := o.RenderGhostText("git check", false, false) if strings.Contains(outClear, "out -b feature") { t.Fatalf("Expected ghost text to be hidden/cleared when cursor moved left, got: %q", outClear) } if o.LastGhostLen != 0 { t.Fatalf("Expected LastGhostLen == 0 after clearing, got %d", o.LastGhostLen) } } func TestGetGhostText(t *testing.T) { o := NewOverlay() items := []spec.Suggestion{ {Cmd: "docker exec -it my-container bash"}, } o.UpdateItems(items) // case 1: cursor at end ghost := o.GetGhostText("docker e", true) expected := "xec -it my-container bash" if ghost != expected { t.Fatalf("Expected %q, got %q", expected, ghost) } // case 2: cursor not at end (moved left) ghostLeft := o.GetGhostText("docker e", false) if ghostLeft != "" { t.Fatalf("Expected empty string when cursor not at end, got %q", ghostLeft) } // case 3: user navigated menu with Up/Down arrow -> should sync with highlighted item o.SetUserNavigated(true) ghostNav := o.GetGhostText("docker e", true) if ghostNav != expected { t.Fatalf("Expected %q when user navigated menu, got %q", expected, ghostNav) } } func TestGhostText_MenuSync(t *testing.T) { o := NewOverlay() items := []spec.Suggestion{ {Cmd: "git checkout -b first"}, {Cmd: "git checkout master"}, } o.UpdateItems(items) // default item 0 ghost0 := o.GetGhostText("git check", true) if ghost0 != "out -b first" { t.Fatalf("Expected 'out -b first', got %q", ghost0) } // move cursor down to item 1 o.MoveCursor("down") ghost1 := o.GetGhostText("git check", true) if ghost1 != "out master" { t.Fatalf("Expected 'out master', got %q", ghost1) } out := o.RenderGhostText("git check", true, true) if !strings.Contains(out, "out master") { t.Fatalf("Expected RenderGhostText to render 'out master', got %q", out) } } func TestGhostText_Truncation(t *testing.T) { o := NewOverlay() longCmd := "git commit -m '" + strings.Repeat("a", 150) + "'" items := []spec.Suggestion{ {Cmd: longCmd}, } o.UpdateItems(items) o.SetPromptLen(10) // typed query length 105 -> total cursor col = 115, default width = 120 -> available cols = 5 typedQuery := "git commit -m '" + strings.Repeat("a", 90) out := o.RenderGhostText(typedQuery, false, true) if !strings.Contains(out, "…") { t.Fatalf("Expected truncated ghost text with '…', got %q", out) } } func TestHideMenu_PreservesTypedQueryForAI(t *testing.T) { o := NewOverlay() o.HideMenu("git commit") if o.GetTypedQuery() != "git commit" { t.Fatalf("Expected TypedQuery to be preserved as 'git commit', got %q", o.GetTypedQuery()) } aiSugg := spec.Suggestion{ Cmd: "git commit -m 'fix: test'", Desc: "AI suggestion", Source: "ai", Confidence: 85, } if !o.InjectAISuggestion(aiSugg) { t.Fatalf("Expected InjectAISuggestion to succeed after HideMenu") } if !o.IsVisible() || len(o.Items) == 0 || o.Items[0].Cmd != aiSugg.Cmd { t.Fatalf("Expected AI suggestion to be injected into Items[0] and Visible=true") } } func TestRenderMatchedTitle_ASCII(t *testing.T) { out := renderMatchedTitle("git status", "git", false, 40) stripped := ansi.Strip(out) if stripped != "git status"+strings.Repeat(" ", 30) { t.Fatalf("unexpected content: %q", stripped) } if !strings.Contains(out, "git") { t.Fatalf("missing match segment: %q", out) } } func TestRenderMatchedTitle_WideRunes(t *testing.T) { out := renderMatchedTitle("日記を表示する", "日記", false, 20) stripped := ansi.Strip(out) if !strings.HasPrefix(stripped, "日記を表示する") { t.Fatalf("content mangled: %q", stripped) } // split must occur after 日記 (width 4), not mid-grapheme if !strings.Contains(stripped, "を") { t.Fatalf("lost remainder: %q", stripped) } } func TestRenderMatchedTitle_CaseInsensitive(t *testing.T) { out := renderMatchedTitle("GitHub CLI", "git", false, 40) stripped := ansi.Strip(out) if !strings.HasPrefix(stripped, "GitHub CLI") { t.Fatalf("case-insensitive match mangled content: %q", stripped) } }