From caee81a4c7c8a78a803eea00431dc06a614ed540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnaud=20He=CC=81ritier?= Date: Thu, 10 Sep 2026 18:48:13 +0200 Subject: [PATCH] test(tui): stop sidebar tests from reading the checkout's git branch sidebar.New reads os.Getwd() and that directory's git branch. In CI that is the repository checkout, and in the merge queue the branch is the 60-char gh-readonly-queue/main/pr--, which wraps the collapsed sidebar and shifts every row two layout tests assume (they passed on PRs and main only because the branch there was short). SetWorkingDirectoryForTesting swaps the three process reads for fixed values and disables the branch watcher; TestMain in the three packages that build a sidebar pins /work/app with no branch. A regression test documents the pin. --- pkg/tui/components/sidebar/main_test.go | 15 ++++++++ pkg/tui/components/sidebar/sidebar.go | 34 ++++++++++++++++--- .../sidebar/working_directory_test.go | 30 ++++++++++++++++ pkg/tui/main_test.go | 18 ++++++++++ pkg/tui/page/chat/main_test.go | 18 ++++++++++ 5 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 pkg/tui/components/sidebar/main_test.go create mode 100644 pkg/tui/components/sidebar/working_directory_test.go create mode 100644 pkg/tui/main_test.go create mode 100644 pkg/tui/page/chat/main_test.go diff --git a/pkg/tui/components/sidebar/main_test.go b/pkg/tui/components/sidebar/main_test.go new file mode 100644 index 000000000..883f4f914 --- /dev/null +++ b/pkg/tui/components/sidebar/main_test.go @@ -0,0 +1,15 @@ +package sidebar + +import ( + "os" + "testing" +) + +// The test binary runs inside the repository checkout; pin the working +// directory and branch so layouts do not depend on the checkout's branch name. +func TestMain(m *testing.M) { + restore := SetWorkingDirectoryForTesting("/work/app", "") + code := m.Run() + restore() + os.Exit(code) +} diff --git a/pkg/tui/components/sidebar/sidebar.go b/pkg/tui/components/sidebar/sidebar.go index 4e6f1aee3..0392bc724 100644 --- a/pkg/tui/components/sidebar/sidebar.go +++ b/pkg/tui/components/sidebar/sidebar.go @@ -2,6 +2,7 @@ package sidebar import ( "context" + "errors" "fmt" "log/slog" "maps" @@ -161,6 +162,31 @@ type Model interface { type gitBranchChangedMsg string +// Seams for the process state the sidebar reads at construction. Unit tests +// run inside the repository checkout, so without them the checkout's branch +// name (60 characters in a merge queue) leaks into every rendered layout. +var ( + getwd = os.Getwd + currentBranch = gitbranch.Current + watchBranch = gitbranch.Watch +) + +var errBranchWatcherDisabled = errors.New("sidebar: git branch watcher disabled for testing") + +// SetWorkingDirectoryForTesting makes every sidebar created afterwards report +// dir and branch instead of the process's working directory and its git +// branch, with no branch watcher, and returns a function restoring the +// defaults. Call it from TestMain: it is not safe alongside running tests. +func SetWorkingDirectoryForTesting(dir, branch string) (restore func()) { + prevGetwd, prevCurrent, prevWatch := getwd, currentBranch, watchBranch + getwd = func() (string, error) { return dir, nil } + currentBranch = func(string) string { return branch } + watchBranch = func(context.Context, string) (*gitbranch.Watcher, error) { + return nil, errBranchWatcherDisabled + } + return func() { getwd, currentBranch, watchBranch = prevGetwd, prevCurrent, prevWatch } +} + func waitForGitBranch(watcher *gitbranch.Watcher) tea.Cmd { if watcher == nil { return nil @@ -397,9 +423,9 @@ func New(ar *animation.Runtime, ctx context.Context, sessionState *service.Sessi ti.CharLimit = 50 ti.Prompt = "" // No prompt to maximize usable width in collapsed sidebar - rawDir, _ := os.Getwd() + rawDir, _ := getwd() wd, branch := formatWorkingDirectory(rawDir) - branchWatcher, _ := gitbranch.Watch(ctx, rawDir) + branchWatcher, _ := watchBranch(ctx, rawDir) m := &model{ ctx: func() context.Context { return context.WithoutCancel(ctx) }, @@ -1024,7 +1050,7 @@ func (m *model) LoadFromSession(sess *session.Session) { if m.gitBranchWatcher != nil { m.gitBranchName = m.gitBranchWatcher.SetDir(sess.WorkingDir) } else { - m.gitBranchName = gitbranch.Current(sess.WorkingDir) + m.gitBranchName = currentBranch(sess.WorkingDir) } } @@ -1156,7 +1182,7 @@ func formatWorkingDirectory(rawDir string) (display, branch string) { if rawDir == "" { return "", "" } - return pathx.ShortenHome(rawDir), gitbranch.Current(rawDir) + return pathx.ShortenHome(rawDir), currentBranch(rawDir) } // workingDirWithBranch returns the working directory path with the git branch diff --git a/pkg/tui/components/sidebar/working_directory_test.go b/pkg/tui/components/sidebar/working_directory_test.go new file mode 100644 index 000000000..2474a42a8 --- /dev/null +++ b/pkg/tui/components/sidebar/working_directory_test.go @@ -0,0 +1,30 @@ +package sidebar + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/docker/docker-agent/pkg/session" + "github.com/docker/docker-agent/pkg/tui/animation" + "github.com/docker/docker-agent/pkg/tui/service" +) + +// TestNewIgnoresProcessGitState pins the TestMain seam: a new sidebar shows +// the injected directory with no branch and no watcher, whatever repository +// the test binary happens to run in. +func TestNewIgnoresProcessGitState(t *testing.T) { + t.Parallel() + + sess := session.New() + sb := New(animation.NewRuntime(), t.Context(), service.NewSessionState(sess)) + m := sb.(*model) + + // The display path is normalized to the OS separator. + assert.Equal(t, filepath.FromSlash("/work/app"), m.workingDirectory) + assert.Empty(t, m.gitBranchName) + require.Nil(t, m.gitBranchWatcher) + assert.Nil(t, m.Init(), "no watcher means nothing to wait for") +} diff --git a/pkg/tui/main_test.go b/pkg/tui/main_test.go new file mode 100644 index 000000000..4a8e9433e --- /dev/null +++ b/pkg/tui/main_test.go @@ -0,0 +1,18 @@ +package tui + +import ( + "os" + "testing" + + "github.com/docker/docker-agent/pkg/tui/components/sidebar" +) + +// The test binary runs inside the repository checkout; pin the sidebar's +// working directory and branch so layouts do not depend on the checkout's +// branch name. +func TestMain(m *testing.M) { + restore := sidebar.SetWorkingDirectoryForTesting("/work/app", "") + code := m.Run() + restore() + os.Exit(code) +} diff --git a/pkg/tui/page/chat/main_test.go b/pkg/tui/page/chat/main_test.go new file mode 100644 index 000000000..45fc6c302 --- /dev/null +++ b/pkg/tui/page/chat/main_test.go @@ -0,0 +1,18 @@ +package chat + +import ( + "os" + "testing" + + "github.com/docker/docker-agent/pkg/tui/components/sidebar" +) + +// The test binary runs inside the repository checkout; pin the sidebar's +// working directory and branch so layouts do not depend on the checkout's +// branch name. +func TestMain(m *testing.M) { + restore := sidebar.SetWorkingDirectoryForTesting("/work/app", "") + code := m.Run() + restore() + os.Exit(code) +}