From 17d1db2e6c887d4d537f12cda2b94bcb66b52a09 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 5 Oct 2026 13:33:32 +0200 Subject: [PATCH] Add tests for when a view counts as loading If a message replaces a command's output while the command is still being read, the view keeps counting as loading until another command has been read to the end. Until then, the layout doesn't clamp the view's scroll position to its content, and IsSingleHunkForWholeFile returns false. If an earlier command reaches the end of its input after a later one has been asked for, the view stops counting as loading, although the later command hasn't started yet. The layout can then clamp the scroll position to the earlier command's output before the later command has been read. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/tasks/tasks_test.go | 88 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/pkg/tasks/tasks_test.go b/pkg/tasks/tasks_test.go index 41139b9f1..884f51250 100644 --- a/pkg/tasks/tasks_test.go +++ b/pkg/tasks/tasks_test.go @@ -870,3 +870,91 @@ func TestReservedTaskDoesntReplaceATaskAskedForLater(t *testing.T) { assert.Equal(t, []string{"message"}, tasksRun) } + +// A message that replaces a command task while the task is still reading its +// output leaves nothing loading content into the view. +func TestMessageEndsTheLoadingOfTheTaskItReplaces(t *testing.T) { + manager := NewViewBufferManager( + utils.NewDummyLog(), + bytes.NewBuffer(nil), + func() {}, + func() {}, + func() {}, + func() {}, + func() {}, + func() {}, + func() gocui.Task { return gocui.NewFakeTask() }, + // no UI thread in the test; run the view mutations inline + func(f func()) error { f(); return nil }, + ) + + stalled := BlockingLineReader{ + linesToYield: 3, + blocked: make(chan struct{}), + unblock: make(chan struct{}), + } + defer close(stalled.unblock) + start := func() (Cmd, io.Reader) { + // not actually starting this because it's not necessary + return ExecCmd{Cmd: exec.Command("blah")}, &stalled + } + + reservation := manager.ReserveTask() + manager.StartLoading() + _ = manager.NewReservedTask(reservation, manager.NewCmdTask(start, "", LinesToRead{100, 50, nil}, nil), "cmd") + <-stalled.blocked + assert.True(t, manager.IsLoading()) + + _ = manager.NewTask(func(TaskOpts) error { return nil }, "message") + /* EXPECTED: + assert.False(t, manager.IsLoading()) + ACTUAL: */ + assert.True(t, manager.IsLoading()) +} + +// The view is loading the content of the command task asked for last. A task +// asked for before it reaching the end of its input doesn't end that. +func TestEarlierTaskEndingLeavesALaterTaskLoading(t *testing.T) { + manager := NewViewBufferManager( + utils.NewDummyLog(), + bytes.NewBuffer(nil), + func() {}, + func() {}, + func() {}, + func() {}, + func() {}, + func() {}, + func() gocui.Task { return gocui.NewFakeTask() }, + // no UI thread in the test; run the view mutations inline + func(f func()) error { f(); return nil }, + ) + + stalled := BlockingLineReader{ + linesToYield: 3, + blocked: make(chan struct{}), + unblock: make(chan struct{}), + } + start := func() (Cmd, io.Reader) { + // not actually starting this because it's not necessary + return ExecCmd{Cmd: exec.Command("blah")}, &stalled + } + + earlierDone := make(chan struct{}) + reservation := manager.ReserveTask() + manager.StartLoading() + _ = manager.NewReservedTask(reservation, + manager.NewCmdTask(start, "", LinesToRead{100, 50, nil}, func() { close(earlierDone) }), "earlier") + <-stalled.blocked + + // The later task is asked for, but not created yet, as for a render whose task + // is created after the layout. + manager.ReserveTask() + manager.StartLoading() + + close(stalled.unblock) + <-earlierDone + /* EXPECTED: + assert.True(t, manager.IsLoading()) + ACTUAL: */ + assert.False(t, manager.IsLoading()) +}