Add test showing that rapidly moving a rebase todo twice moves the wrong todo

Moving a todo up or down rewrites the todo file and advances the
selection synchronously, but the commits model is only rebuilt by the
refresh, which finishes in the background. A second keypress arriving
before that reads the pre-move model at the advanced selection index —
that's the todo the first move swapped with, so the second press moves
that one back instead of moving the selected todo further. Two rapid
presses (e.g. from holding the key down) thus amount to a net no-op.

The two presses also spawn two racing refreshes whose model updates can
land in either order, so the todo list can even end up disagreeing with
the todo file. That's why the test continues the rebase and asserts the
resulting commit order instead of the displayed list: the rebase replays
the file, which is deterministic.

The test documents this currently broken behavior; the fix comes next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-07-22 08:31:11 +02:00
co-authored by Claude Fable 5
parent b96b8a9753
commit 200042a57c
2 changed files with 69 additions and 0 deletions
@@ -0,0 +1,68 @@
package interactive_rebase
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// The second keypress arrives before the refresh triggered by the first one
// has rebuilt the commits model. The handler reads the selected todo from the
// model at the already-advanced selection index, so with the stale, pre-move
// model it grabs the todo the first move swapped with and moves that one back
// down — turning the two presses into a net no-op instead of moving the
// selected todo down two slots. This is what happens when holding down the
// move-down key to move a todo several slots.
//
// We continue the rebase and assert the resulting commit order rather than
// asserting the todo list, because the two presses also spawn two racing
// refreshes whose updates can land in either order, so what the todo list
// shows in the broken state is not deterministic (it can even disagree with
// the todo file). The rebase replays what's in the file.
var MoveTodoDownWithRapidKeypresses = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Move a todo down two slots with two keypresses in rapid succession",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateNCommits(4)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit-04").IsSelected(),
Contains("commit-03"),
Contains("commit-02"),
Contains("commit-01"),
).
NavigateToLine(Contains("commit-01")).
Press(keys.Universal.Edit).
Lines(
Contains("--- Pending rebase todos ---"),
Contains("commit-04"),
Contains("commit-03"),
Contains("commit-02"),
Contains("--- Commits ---"),
Contains("commit-01").IsSelected(),
).
NavigateToLine(Contains("commit-04")).
PressRapidly(keys.Commits.MoveDownCommit, keys.Commits.MoveDownCommit).
Tap(func() {
t.Common().ContinueRebase()
}).
/* EXPECTED:
Lines(
Contains("commit-03"),
Contains("commit-02"),
Contains("commit-04"),
Contains("commit-01"),
)
ACTUAL: */
Lines(
Contains("commit-04"),
Contains("commit-03"),
Contains("commit-02"),
Contains("commit-01"),
)
},
})
+1
View File
@@ -310,6 +310,7 @@ var tests = []*components.IntegrationTest{
interactive_rebase.Move,
interactive_rebase.MoveAcrossBranchBoundaryOutsideRebase,
interactive_rebase.MoveInRebase,
interactive_rebase.MoveTodoDownWithRapidKeypresses,
interactive_rebase.MoveUpdateRefTodo,
interactive_rebase.MoveWithCustomCommentChar,
interactive_rebase.OutsideRebaseRangeSelect,