diff --git a/AGENTS.md b/AGENTS.md index af7adc1..fafe92a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,6 +154,7 @@ Styles from theme config. Format helpers (timestamps, durations). `RunProgram` r - Unit tests per package - E2E tests in `internal/app/e2e_test.go` + `e2e_helpers_test.go` use mock HTTP server + mock player - E2E tests drive the bubbletea Update loop programmatically (no real TUI) +- `feedCmd` in `e2e_helpers_test.go` skips a command that needs more than 200 ms, so that a test does not wait for a `tea.Tick`. A request to an address with no server, or a load that writes to a database file, can need more on Windows in CI. For such a command, use `feedCmdWithin` with `slowCmdWait`. - `internal/app/model_test.go`, `playback_test.go`, `bookmarks_test.go`, `render_test.go` for app-level tests - `internal/ui/program_unix_test.go` starts the test program again as a child process, sends SIGHUP to it, and checks that it gets to the place where a `main` calls `Cleanup`. The child runs its program with `ui.RunProgram`, as each `main` does. It does not run on Windows. - `internal/cache/*_test.go` for cache store and helper behavior diff --git a/internal/app/e2e_helpers_test.go b/internal/app/e2e_helpers_test.go index 152fbd3..6a77916 100644 --- a/internal/app/e2e_helpers_test.go +++ b/internal/app/e2e_helpers_test.go @@ -515,10 +515,27 @@ func newE2EModelUnauthWithDB(t *testing.T, srv *httptest.Server, mp *mockPlayer) // E2E helpers — driving the Update→Cmd→Msg loop // --------------------------------------------------------------------------- +// feedCmdWait is the time that feedCmd waits for a command. +const feedCmdWait = 200 * time.Millisecond + +// slowCmdWait is the time that feedCmdWithin waits for a command that a test +// needs and that can be slow. It is longer than the timeout of one request of +// abs.Client, which is 10 seconds. +const slowCmdWait = 15 * time.Second + // feedCmd executes a tea.Cmd, feeds the resulting message back into Update, // and returns the new model + the cmd returned by Update. -// Commands that block longer than 200ms (e.g. tea.Tick) are skipped. +// Commands that block longer than feedCmdWait (e.g. tea.Tick) are skipped. func feedCmd(m Model, cmd tea.Cmd) (Model, tea.Cmd) { + return feedCmdWithin(m, cmd, feedCmdWait) +} + +// feedCmdWithin is feedCmd with a wait of its own. Use slowCmdWait for a +// command that the test must not skip and that can need more than feedCmdWait +// on a slow machine, for example on Windows in CI: a request to an address +// with no server, or a load that writes to a database file. A tea.Tick that +// is shorter than the wait is not skipped. +func feedCmdWithin(m Model, cmd tea.Cmd, wait time.Duration) (Model, tea.Cmd) { if cmd == nil { return m, nil } @@ -543,13 +560,13 @@ func feedCmd(m Model, cmd tea.Cmd) (Model, tea.Cmd) { if batch, ok := r.msg.(tea.BatchMsg); ok { var lastCmd tea.Cmd for _, c := range batch { - m, lastCmd = feedCmd(m, c) + m, lastCmd = feedCmdWithin(m, c, wait) } return m, lastCmd } res, nextCmd := m.Update(r.msg) return res.(Model), nextCmd - case <-time.After(200 * time.Millisecond): + case <-time.After(wait): // Drain the channel in case the goroutine completes after timeout go func() { <-ch; <-done }() return m, nil diff --git a/internal/app/e2e_test.go b/internal/app/e2e_test.go index fe66a86..c538ddf 100644 --- a/internal/app/e2e_test.go +++ b/internal/app/e2e_test.go @@ -103,8 +103,9 @@ func TestE2E_LoginFailure(t *testing.T) { // Press enter m, cmd := e2ePressSpecial(m, tea.KeyEnter) - // Execute login command — should fail (unreachable) - m, _ = feedCmd(m, cmd) + // Execute login command — should fail (unreachable). A refused connection + // can need more than feedCmdWait on Windows. + m, _ = feedCmdWithin(m, cmd, slowCmdWait) // Should still be on login with error assertScreen(t, m, ScreenLogin) @@ -2264,7 +2265,10 @@ func TestE2E_CacheSurvivesRestart(t *testing.T) { m1 := NewWithPlayer(cfg, store, cachedClient, cacheStore, mp) m1 = e2eSetSize(m1, 120, 40) - m1 = feedCmdChain(m1, m1.Init(), 5) + // The load of the home screen sends its requests and writes to the + // database file in one command, which can need more than feedCmdWait. + m1, cmd := feedCmdWithin(m1, m1.Init(), slowCmdWait) + m1 = feedCmdChain(m1, cmd, 4) if m1.home.Loading() { t.Fatal("home should not be loading after first init") @@ -2279,7 +2283,8 @@ func TestE2E_CacheSurvivesRestart(t *testing.T) { m2 := NewWithPlayer(cfg, store, cachedClient, cacheStore, mp) m2 = e2eSetSize(m2, 120, 40) - m2 = feedCmdChain(m2, m2.Init(), 5) + m2, cmd = feedCmdWithin(m2, m2.Init(), slowCmdWait) + m2 = feedCmdChain(m2, cmd, 4) if m2.home.Loading() { t.Fatal("home should not be loading after second init (cache should serve data)")