docs(claude): record the widget-test hang gotcha + OS timeout wrapper

This commit is contained in:
vjrj 2026-07-15 01:28:20 +02:00
parent 7a82505a4e
commit bbf2e97027

View file

@ -48,6 +48,8 @@ Dependency direction: **`app_seeds` → `commons_core`, never the reverse.** Bou
- `dart test packages/commons_core` — pure Dart, ~1s. The TDD workhorse. (`dart test` can't run `app_seeds` — it needs `flutter`.) - `dart test packages/commons_core` — pure Dart, ~1s. The TDD workhorse. (`dart test` can't run `app_seeds` — it needs `flutter`.)
- `flutter test test/services` — app logic + in-memory DB, no widgets, ~10s, reliable. Put new app logic in plain `test()` here. - `flutter test test/services` — app logic + in-memory DB, no widgets, ~10s, reliable. Put new app logic in plain `test()` here.
- Widget tests: run targeted (`flutter test test/ui/<file>`), always with a timeout so a hang FAILS fast — a global `apps/app_seeds/dart_test.yaml` sets `timeout: 90s`; add `--timeout 30s` when in doubt. Never `pumpAndSettle` a live screen; use bounded `pump(Duration)`. See the testing-gotchas memory + [testing.md](docs/design/testing.md). - Widget tests: run targeted (`flutter test test/ui/<file>`), always with a timeout so a hang FAILS fast — a global `apps/app_seeds/dart_test.yaml` sets `timeout: 90s`; add `--timeout 30s` when in doubt. Never `pumpAndSettle` a live screen; use bounded `pump(Duration)`. See the testing-gotchas memory + [testing.md](docs/design/testing.md).
- **ALWAYS wrap a widget-test run in an OS hard-kill: `timeout -k 10 200 flutter test test/ui/<file> --timeout 45s`.** Recurring pain: some hangs sit for 3040 min because neither `dart_test.yaml`'s `timeout` nor `--timeout` can bound them — a bare `await someStream.first` (outside the per-test clock) or the slow whole-app compile. The OS `timeout` is the only thing that reliably cuts them; without it a hung run wastes half an hour.
- **The #1 hang cause: a live Drift stream where a one-shot Future belongs.** Never `await watchX().first` — not in a `testWidgets` body AND not in production widget code (e.g. a transient sheet's `initState` filling a picker). A guard (`test/no_stream_first_in_widget_tests_test.dart`) scans `test/ui/` for it, but NOT `lib/ui/`. When a widget test hangs, grep BOTH `test/ui` and the `lib/ui` widget under test for `watch*().first` / a `StreamBuilder` on a never-closing source; replace with a repo one-shot (`…get()` Future). A transient sheet/dialog must never hold a live subscription for one-time data.
## Data model (decided, `schemaVersion = 1`) ## Data model (decided, `schemaVersion = 1`)