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) +}