Painful-to-rediscover gotchas, captured as we find them. Each entry is a short “symptom / cause / fix.” If a gotcha becomes a recurring source of bugs, promote it to an ADR or bake a fix into the code with a test that protects it.
Symptom. A widget test that boots an AppEngine (and therefore
opens SQLite via sqflite_ffi) hangs forever inside pumpAndSettle,
even though the same engine setup works fine in non-widget tests.
Cause. flutter_test runs the test body inside a FakeAsync zone
that intercepts every Timer / scheduleMicrotask and only fires
them when the zone advances. sqflite_ffi schedules work on a
native-bridge isolate that runs outside the FakeAsync zone — its
completion timers are intercepted but never fired, so any
pumpAndSettle waiting for “settle” waits forever.
Fix. Use the harness primitives in
test/widget/_test_harness.dart:
bootEngineFor(tester, specJson) wraps bootEngine in
tester.runAsync(...) so SQLite work happens in the real async
zone.disposeAllFor(tester, booted) does the same for teardown.pumpAndSettle(tester, widget) does fixed real-time pump rounds
(16 × 100ms by default) instead of FakeAsync-based settling.pumpUntilFound(tester, finder, timeout) rather than hoping
fixed rounds are enough.This was diagnosed and the harness rewritten on 2026-04-26 — widget tests went from “all skipped on Windows” to “42 in the gate.”
Color APIs expect floats, not intsSymptom. Color comparisons silently wrong; WCAG contrast calculations off by 255×.
Cause. Modern Flutter Color APIs use floats (0.0–1.0), not
the legacy 0–255 int range. Easy to mix up.
Fix. Always verify value ranges in contrast helpers and color
comparisons. The alpha property is deprecated — use
(color.a * 255.0).round().clamp(0, 255). Already captured in
CLAUDE.md; mentioned here for discoverability.
Symptom. getApplicationDocumentsDirectory() returns a path
under C:\Users\<name>\OneDrive - ...\Documents\, so app data
silently ends up syncing.
Cause. Windows-with-OneDrive redirects the user’s Documents folder
as a default sync target. Flutter’s path_provider honors the
system default.
Fix. ODS uses a bootstrap mechanism: a tiny ods_bootstrap.json
in getApplicationSupportDirectory() (which is under AppData, not
OneDrive-redirected) points to the user’s chosen data folder. First
run prompts; Framework Settings offers a “Move Data” flow that
copies + retargets. See
Frameworks/flutter-local/lib/engine/settings_store.dart
(getOdsDirectory, readBootstrapStorageFolder).
Symptom. User clicks “Create Account” in the ODS login screen; nothing happens. Sometimes a generic “Failed to create account” banner appears; sometimes nothing visible at all.
Cause. PocketBase’s users auth collection is not created by
pocketbase superuser upsert. On a fresh install there’s literally
nowhere for a user record to land, so the SDK call 404s and the
LoginScreen shows a generic failure message (which also happens to be
the duplicate-email case — indistinguishable).
Fix. AuthService.ensureUsersCollection creates the users collection on first admin login, wired into AdminGuard.tsx. Fresh PB → admin logs in once → users collection exists → any subsequent sign-up works.
Symptom. After clicking “Create Account” a valid user gets created and auto-logged-in per server logs, but the UI stays on the sign-up form.
Cause. LoginScreen.handleSignUp originally only cleared
needsLogin; the orthogonal needsAdminSetup gate stayed true when
no admin existed in the system. AppLoader rendered LoginScreen as
long as either gate was truthy.
Fix. Clear both gates after a successful self-registration. See LoginScreen.tsx:115-121.
Symptom. Log in via /admin → click a link → land back on the
login card. Repeat forever.
Cause. src/lib/pocketbase.ts used to call pb.authStore.clear()
at module load with a comment “Force fresh login on every page load —
no persisted sessions.” That fired on every goto() (Playwright +
real users), nuking the session that had just been established.
Fix. Removed the clear() call. Sessions now persist until
explicit logout (standard PB SDK behavior). There’s a TODO
(TODO.md → Next) for a regression test that pins
this.
Symptom. Navigate to /my-app as an unauthenticated user.
Instead of rendering, the page fails with a 401 on a request like
POST /api/collections.
Cause. When a framework loads an app with local://<table> data
sources, it calls DataService.ensureCollection on first access.
Creating a PB collection requires superadmin auth — which a guest
doesn’t have.
Fix. The seed helper used by E2E tests pre-creates data-source
collections as part of seedApp. In production, the admin loads the
app once (under admin auth) which provisions collections; subsequent
guest access only reads/writes records, which is permitted by the
collection’s rules. See
tests/e2e/helpers/app-seed.ts.
Symptom. Tests green, but the Playwright trace shows React warnings / hydration mismatches / PB request errors.
Cause. Tests often assert positive conditions (element visible, text present) without asserting the absence of console errors.
Fix. Critical-path tests already include
expect(...).toHaveCount(0) for error strings like /failed to load|
something went wrong/. Widen this on new tests when in doubt.
Symptom. 5 performance tests in Frameworks/flutter-local/test/integration/batch9_performance_test.dart occasionally exceed their budgets (e.g., “insert 10,000 rows under 150s” takes 160s).
Cause. Windows file I/O + sqflite is slower than Linux; the budgets were calibrated on a different machine.
Fix. Documented in
REGRESSION_LOG.md as a known flake, not a
correctness regression. Long-term fix in TODO.md:
tag with @slow and move to a separate CI job.
Symptom. Trying to run PB manually for debugging: pocketbase:
command not found.
Cause. ODS doesn’t require PB to be installed system-wide — the
E2E suite downloads its own binary to
Frameworks/react-web/tests/e2e/.pb-e2e/.
Fix. For manual PB runs, either: (a) reuse the E2E binary —
./tests/e2e/.pb-e2e/pocketbase.exe serve from Frameworks/react-web/,
or (b) download separately from https://pocketbase.io/ to somewhere
convenient.
Each entry uses the same shape: Symptom / Cause / Fix plus an optional “Long-term” note. Keep them terse — this is a lookup table, not an essay. When an entry becomes load-bearing enough to deserve real justification, promote it to an ADR.