// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later package reader import ( "reflect" "testing" "time" "git.stormux.org/storm/fenrir/internal/command" "git.stormux.org/storm/fenrir/internal/presentation" "git.stormux.org/storm/fenrir/internal/screen" ) func TestDeletionEchoWithAutomaticReadingDisabled(t *testing.T) { for _, trial := range []struct { name, before, after string beforeX, afterX int want []presentation.Message }{ {"backspace", "$ abc", "$ ab", 5, 4, []presentation.Message{presentation.Character_message('c')}}, {"delete", "$ abc", "$ ac", 3, 3, []presentation.Message{presentation.Character_message('b')}}, {"midline backspace", "$ abc", "$ ac", 4, 3, []presentation.Message{presentation.Character_message('b')}}, {"unicode", "$ café", "$ caf", 6, 5, []presentation.Message{presentation.Character_message('é')}}, {"punctuation", "$ .", "$ ", 3, 2, []presentation.Message{presentation.Character_message('.')}}, {"space", "$ ab ", "$ ab", 5, 4, []presentation.Message{presentation.Character_message(' ')}}, {"word", "$ one two", "$ one ", 9, 6, []presentation.Message{{Text: "two"}}}, {"forward word", "$ one two", "$ two", 2, 2, []presentation.Message{{Text: "one "}}}, } { t.Run(trial.name, func(t *testing.T) { r := New() r.Configure(false, false) r.Configure_echo(false, false, false) before := fixture(trial.before, "background") before.X = trial.beforeX r.Update(before) r.Command(command.CurrentLine) r.Note_input(true, false, false) r.Note_deletion() // An unchanged capture and background output can precede the edit. r.Update(before) background := fixture(trial.before, "changed background") background.X = trial.beforeX if got := r.Update(background); len(got) != 0 { t.Fatal("background output escaped autoread", got) } after := fixture(trial.after, "more background") after.X = trial.afterX if got := r.Update(after); !reflect.DeepEqual(got, trial.want) { t.Fatal("deletion echo", got, trial.want) } if got := r.Update(before); len(got) != 0 { t.Fatal("redraw spoke after deletion", got) } if got := r.Update(after); len(got) != 0 { t.Fatal("deletion authorization was reused", got) } }) } } func TestDeletionEchoRejectsUnconfirmedChanges(t *testing.T) { for _, mode := range []string{"no key", "expired", "other key", "reader command", "redraw", "cursor only", "no echo", "new terminal", "new buffer", "resize", "inactive", "live region"} { t.Run(mode, func(t *testing.T) { r := New() r.Configure(false, true) before, after := fixture("$ abc", "background"), fixture("$ ab", "changed background") before.X, after.X = 5, 4 r.Update(before) if mode != "no key" { r.Note_input(true, false, false) r.Note_deletion() } switch mode { case "expired": r.progressDeletionAt = time.Now().Add(-time.Second) case "other key": r.Note_input(true, false, false) case "reader command": r.Command(command.CurrentLine) case "redraw": after = fixture("$ xx", "changed background") after.X = 4 case "cursor only": after = before after.X = 4 case "no echo": after = before case "new terminal": after.Terminal++ case "new buffer": after.Generation++ case "resize": after = fixture("$ ab") after.X = 4 case "inactive": after.Active = false case "live region": r.ensure_regions().live[0] = &screen.Region{Right: before.Cols - 1} } if got := r.Update(after); len(got) != 0 { t.Fatal("unconfirmed or excluded deletion spoke", got) } if mode == "new terminal" || mode == "new buffer" || mode == "resize" || mode == "inactive" { if got := r.Update(before); len(got) != 0 { t.Fatal("old session spoke", got) } if got := r.Update(fixture("$ ab", "background")); len(got) != 0 { t.Fatal("pending deletion survived session change", got) } } }) } }