// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package prompt import ( "testing" "git.stormux.org/storm/fenrir/internal/screen" ) func snapshot(lines ...string) screen.Snapshot { result := screen.Snapshot{Terminal: 1, Active: true, Rows: len(lines), Cols: 100, Y: len(lines) - 1} for _, line := range lines { cells := make([]rune, result.Cols) for index := range cells { cells[index] = ' ' } copy(cells, []rune(line)) result.Cells = append(result.Cells, cells...) } return result } func TestRecognizesFreshPromptsAndCompletion(t *testing.T) { for _, line := range []string{ "$", "#", ">", "bash-5.3$", "[Username@Host ~/code] $", "Username@Host:~$", "Proceed? [Y/n]", "Continue? (Yes/No)", "Is this ok [y/N]:", ":: Proceed? [Y/n]", "Password:", "[sudo] password for Username:", "Username's password:", "Please enter passphrase for key:", "Press any key to continue...", "Are you sure? [yes/no]", "Please confirm:", "CUSTOM READY", "Device 42 ready:", "Worked for 3m 35s • 8:05 PM", "Worked for 19s • 7:46 PM", "Worked for 1h 2m 3s • 12:00 AM", } { t.Run(line, func(t *testing.T) { detector, err := New([]string{"CUSTOM READY"}, []string{`^Device [0-9]+ ready:$`}) if err != nil { t.Fatal(err) } if got := detector.Observe(snapshot("working")); got != "" { t.Fatal(got) } if got := detector.Observe(snapshot(" " + line)); got != line { t.Fatal(got) } if got := detector.Observe(snapshot(" " + line)); got != "" { t.Fatal("stale line restored", got) } }) } } func TestRejectsFalseAndIncompletePrompts(t *testing.T) { for _, line := range []string{ "Price is $", "C# language", "Downloading 20%", "$ echo hello", "Password: secret", "Please enter passphrase: secret", "Worked for 19s", "Worked for 19s • 7:4", "Worked for 19s • 17:46 PM", "Worked for 19s • 7:99 PM", "Worked for 19s • 7:46 PM more", "quoted Worked for 19s • 7:46 PM", "Worked for 3m nonsense • 8:05 PM", } { t.Run(line, func(t *testing.T) { detector, _ := New(nil, nil) detector.Observe(snapshot("working")) if got := detector.Observe(snapshot(line)); got != "" { t.Fatal(got) } }) } } func TestSplitCompletionAboveCursorAndOldScroll(t *testing.T) { detector, _ := New(nil, nil) detector.Observe(snapshot("old", "working", "input")) if got := detector.Observe(snapshot("old", "Worked for 19s • 7:4", "input")); got != "" { t.Fatal(got) } line := "Worked for 19s • 7:46 PM" if got := detector.Observe(snapshot("old", line, "input")); got != line { t.Fatal(got) } if got := detector.Observe(snapshot(line, "input", "still working")); got != "" { t.Fatal("scrolled history restored", got) } // A shell prompt elsewhere on the screen is not the active prompt. if got := detector.Observe(snapshot(line, "$", "still working")); got != "" { t.Fatal(got) } } func TestSessionChangesOnlyEstablishBaseline(t *testing.T) { for _, mode := range []string{"terminal", "generation", "resize", "inactive"} { t.Run(mode, func(t *testing.T) { detector, _ := New(nil, nil) detector.Observe(snapshot("working")) next := snapshot("$") switch mode { case "terminal": next.Terminal++ case "generation": next.Generation++ case "resize": next = snapshot("", "$") case "inactive": next.Active = false } if got := detector.Observe(next); got != "" { t.Fatal(got) } next.Active = true if got := detector.Observe(next); got != "" { t.Fatal(got) } }) } }