From 51f789bf5cf8b2753652125277498b19b254a937 Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 2 Mar 2026 21:53:00 +0100 Subject: [PATCH] fix(e2e): drain event handlers and stop browser between tests Async event handlers (via Watermill) from the previous test can hold SQLite connections, starving the next test's fixture setup PATCH request. Three changes fix this: 1. Track in-flight event handler goroutines with a WaitGroup. 2. Call WaitForPendingHandlers() in the test endpoint before truncating/inserting data. 3. Navigate the browser to about:blank in fixture teardown to stop notification polling and other frontend requests between tests. --- frontend/tests/support/fixtures.ts | 5 +++++ pkg/events/events.go | 26 ++++++++++++++++++++++++++ pkg/routes/api/v1/testing.go | 6 ++++++ 3 files changed, 37 insertions(+) diff --git a/frontend/tests/support/fixtures.ts b/frontend/tests/support/fixtures.ts index cebf92f5a..f04fe8c0b 100644 --- a/frontend/tests/support/fixtures.ts +++ b/frontend/tests/support/fixtures.ts @@ -32,6 +32,11 @@ export const test = base.extend<{ authenticatedPage: async ({page, apiContext, currentUser}, use) => { const {token} = await login(page, apiContext, currentUser) await use(page) + // Navigate away to stop all frontend requests (notification polling, token + // refresh, etc.) before the next test's fixture setup seeds the database. + // Without this, the previous test's page can hold DB connections via API + // requests, starving the next test's Factory.seed() PATCH call. + await page.goto('about:blank').catch(() => {}) }, }) diff --git a/pkg/events/events.go b/pkg/events/events.go index 00599f9d9..4ca7be72d 100644 --- a/pkg/events/events.go +++ b/pkg/events/events.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "sync" "time" "github.com/getsentry/sentry-go" @@ -36,6 +37,19 @@ import ( var pubsub *gochannel.GoChannel +// activeHandlers tracks in-flight event handler goroutines so the test +// endpoint can wait for them to finish before truncating tables. +var activeHandlers sync.WaitGroup + +// WaitForPendingHandlers blocks until all currently in-flight event handler +// goroutines have completed (including retries). This is intended for the +// testing endpoint to avoid connection starvation: async handlers from the +// previous test can hold SQLite connections, starving the next test's seed +// request. +func WaitForPendingHandlers() { + activeHandlers.Wait() +} + // Event represents the event interface used by all events type Event interface { Name() string @@ -90,7 +104,19 @@ func InitEvents() (err error) { return nil }) + // handlerTracker is a middleware that tracks in-flight handlers via the + // activeHandlers WaitGroup. It wraps the entire processing chain + // (including retries) so WaitForPendingHandlers() can drain all work. + handlerTracker := func(h message.HandlerFunc) message.HandlerFunc { + return func(msg *message.Message) ([]*message.Message, error) { + activeHandlers.Add(1) + defer activeHandlers.Done() + return h(msg) + } + } + router.AddMiddleware( + handlerTracker, poison, middleware.Retry{ MaxRetries: 5, diff --git a/pkg/routes/api/v1/testing.go b/pkg/routes/api/v1/testing.go index 6f32d2395..53631845b 100644 --- a/pkg/routes/api/v1/testing.go +++ b/pkg/routes/api/v1/testing.go @@ -23,6 +23,7 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/log" "github.com/labstack/echo/v5" @@ -61,6 +62,11 @@ func HandleTesting(c *echo.Context) error { }) } + // Wait for all async event handlers from the previous test to complete + // before modifying the database. Without this, handlers hold SQLite + // connections and starve this request's truncate/insert operations. + events.WaitForPendingHandlers() + truncate := c.QueryParam("truncate") if truncate == "true" || truncate == "" { err = db.RestoreAndTruncate(table, content)