T09: trip repository, completing Phase 2

All lifecycle transitions inside Drift transactions, each idempotent: start
(adopting an active trip rather than duplicating it), pause, resume, complete,
discard, rename, delete, merge, and the authoritative aggregate recomputation.

Room.withTransaction maps onto Drift's transaction() almost exactly, so this was
the most mechanical port so far. 26 tests, green first run.

Merge keeps its hard-won properties: segments are never joined, so the boundary
between two merged rides stays a segment boundary like a pause, and aggregates
are recomputed rather than summed because distance is not additive across the
gap. The regression test puts the two rides a degree of latitude apart and
asserts the ~111km gap never reaches distanceM.

Dropped a mergeTableUpdates helper before committing -- Drift's update() already
notifies dependent streams, so it was scaffolding that earned nothing.

TripRepositoryTest, MergeTest and SchemaTest were 666 lines of instrumented
tests needing a booted emulator. All three now run as unit tests in ~2 seconds.

Phase 2 done: 121 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 10:59:02 -05:00
parent d7d9854dc9
commit 3fd93cf8a6
3 changed files with 646 additions and 0 deletions

View File

@@ -317,3 +317,54 @@ well under a second with nothing booted. `TripRepositoryTest` and `MergeTest` (4
lines) should convert the same way in T09.
**95 tests passing, analyze clean.**
---
## T09 — Trip repository · **complete**
`lib/src/data/trip_repository.dart`. **26 tests, green on the first run.**
Every lifecycle transition ported: `startTrip` (adopting, never duplicating), `pauseTrip`,
`resumeTrip`, `completeTrip`, `discardTrip`, `renameTrip`, `deleteTrip`, `mergeTrips`,
`recomputeAggregates`. Each runs inside `_db.transaction` and each is idempotent, because
the platform can restart the recorder from any state.
`Room.withTransaction` maps onto Drift's `transaction()` almost exactly, so this was the
most mechanical port so far.
### What the tests protect
- **Adoption over rejection.** `startTrip` on an already-active trip returns the *same*
handle rather than opening a second trip — the behaviour that makes a process kill
survivable.
- **Merge never joins segments.** The boundary between two merged rides stays a segment
boundary, exactly like a pause. The regression test puts the two rides a degree of
latitude apart and asserts the ~111 km gap never reaches `distanceM`.
- **Aggregates are recomputed, not summed** — because distance is not additive across
that gap.
- **Rename collapses empty and whitespace-only input to null**, so a stored `""` can
never diverge from the UI's date-label branch.
- **Merge rejects** self-merge, an active trip, and a missing id, and leaves no orphans.
### One piece of speculative code removed
A `mergeTableUpdates` helper was written to nudge Drift's stream queries after
re-parenting, then deleted before commit: Drift's own `update()` already notifies
dependent streams, so it earned nothing and would have been misleading scaffolding.
### The instrumented-to-unit win, totalled
`TripRepositoryTest` + `MergeTest` + `SchemaTest` were **666 lines of instrumented tests
requiring a booted emulator**. All three are now plain unit tests finishing in about two
seconds with nothing running.
---
## Phase 2 complete
**121 tests passing, `flutter analyze` clean.** The data layer is done and the domain,
statistics and export layers above it are proven equivalent to the Kotlin original.
Next: **Phase 3, the recording engine** — the risky phase. T10's `LocationSource` seam
first, then the pipeline, then the two platform liveness stories. The
`flutter_foreground_task` deprecation warnings recorded under T01 become relevant at T12.