Commit Graph

9 Commits

Author SHA1 Message Date
a3bcd014c0 T22/T24/T26: uploader, integration tests, and release readiness
T22 -- TelemetryUploader on package:http with MockClient standing in for
MockWebServer, tested against a real in-memory Drift database rather than a fake
DAO. Upload runs on its own timer, injected as a callback so the engine has no
opinion about HTTP and tests need no network. Config on shared_preferences with
a hand-rolled UUID v4. Still no UI for the endpoint, exactly as in the native
app. 7 tests.

T24 -- integration_test/app_test.dart, 4 tests passing on the iOS simulator.
These cover what widget tests cannot: Drift opening against real platform
storage, plugin registration, go_router driving a real Navigator, cold start.

T26 -- RELEASE-IOS.md. Usage strings are specific rather than generic, which is
the leading Guideline 5.1.1 rejection cause; privacy-label answers decided; a
pre-submission list covering the bundle-id switch back to com.rippr, the still
default app icon, a release build, and a demo video for review notes.

T25 written up as REAL-RIDE-CHECKLIST.md but outstanding by nature. Two items
decide real things: force-stopping mid-ride on Android confirms the 111 km
crash-gap bug is fixed, and parking 15 minutes mid-recording on iOS decides
whether geolocator is sufficient or the paid engine is needed.

Two prefer_initializing_formals lints suppressed with a reason: Dart forbids a
named parameter beginning with an underscore, so the suggested fix will not
compile.

178 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 22:20:39 -05:00
d501528e69 T19/T21: map and export, completing Phase 4
flutter_map with the same OSM tiles and no-API-key reasoning that chose osmdroid.
All four load-bearing behaviours ported and, unlike the native app's map, tested:
one polyline per segment so a pause is a real gap, render-only decimation, the
zoom clamp at OSM's max tile zoom with a short-ride fallback, and a real user
agent. Speed colouring is bucketed per run rather than per-vertex, since neither
osmdroid nor flutter_map makes per-vertex paint reasonable.

Export via share_plus, which also handles the iPad popover anchor a naive port
forgets. It passes the raw stored points, never the map's decimated path.

164 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 16:27:42 -05:00
ee3ef74331 T12-T14: platform liveness via geolocator; drop flutter_foreground_task
geolocator_android's ForegroundNotificationConfig raises a service with
foregroundServiceType=location and holds a wake lock for as long as the position
stream is subscribed, and the plugin declares that service in its own manifest.
That covers everything the native TrackingService used a foreground service and
a PARTIAL_WAKE_LOCK for, so flutter_foreground_task is gone -- and with it both
deprecation warnings that were recorded under T01.

Known parity gap for T27: geolocator's notification cannot carry actions, so the
native Pause/Resume buttons in the shade are not reproduced.

iOS: UIBackgroundModes plus allowBackgroundLocationUpdates keep the isolate and
the writer timer alive while updates flow. Two settings matter more than they
look -- pauseLocationUpdatesAutomatically is false because CoreLocation does not
reliably restart, and activityType is otherNavigation rather than
automotiveNavigation, which would snap fixes to the road network and silently
falsify the recorded path. Usage strings are specific, not generic.

Android deliberately does not request ACCESS_BACKGROUND_LOCATION: a foreground
service with a visible notification is exactly the supported case, and asking
would trigger the harder "Allow all the time" flow for no benefit.

T14 process-death resume is implemented and called at startup, covered by four
tests including the 111 km crash-gap guard.

Verified by building both platforms and launching on the iOS simulator: Drift
opened, Riverpod resolved, restore found no active trip, controls correct.
lib/main.dart is a harness so the pipeline can be exercised on hardware before
Phase 4 builds screens; T15 replaces it.

Phase 3 done: 145 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:14:37 -05:00
5a7c598610 T10/T11: location seam and recording pipeline; fix a native crash-recovery bug
T10 -- LocationSource abstraction plus FakeLocationSource, so the riskiest code
in the app is testable with no device. Also found that geolocator ships its own
Android foreground service with enableWakeLock, which could drop
flutter_foreground_task and both its deprecation paths; deferred to T12 since it
costs notification actions.

T11 -- the pipeline. Kotlin's unbounded Channel plus blocking-receive writer
becomes a List buffer plus a periodic Timer; Dart's event loop makes a blocking
receive unnecessary and the guarantee is unchanged, since the fix callback only
appends and returns. 24 tests.

Found a real bug in the native app while porting. restoreAfterProcessDeath says
it resumes into a new segment because the dead time is a real gap, but it calls
resumeTrip, which adopts the segment a crash left open. After a pause that is
right; after a crash nothing closed it. Points either side of the dead time then
share a segment, and computeSummary -- the authoritative pass that overwrites the
live estimate on completion -- measures straight through the gap. Reverting the
fix and running the guard shows 111,217 m of phantom distance.

Fixed via TripRepository.resumeIntoNewSegment, which closes the stale segment at
its last recorded point rather than at now, so the dead time is not billed as
ride time either. A deliberate, documented departure from parity: the code
contradicted its own comment and produced silently wrong data.

The first version of that guard passed with the bug still present -- the fixture
used pause(), which closes the segment and stops reproducing the crash. Caught
only by reverting the fix and checking the test failed.

145 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:19:21 -05:00
3fd93cf8a6 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>
2026-08-15 10:59:02 -05:00
d7d9854dc9 T08: Drift schema mirroring the Room baseline
Three tables with CASCADE foreign keys, indices, WAL, and a real MigrationStrategy
from schema version 1. No destructive fallback, ever.

Two things needed deliberate handling. SQLite defaults foreign_keys to OFF and
Drift, unlike Room, does not enable it -- without the pragma every CASCADE is
decorative, so there is now a test that reads the pragma back. And Drift both
snake_cases columns and names row classes after tables; build.yaml sets
case_from_dart_to_sql: preserve so the schema stays column-for-column identical
to Room's, and @DataClassName keeps row types from colliding with the domain
models.

SchemaTest was instrumented and needed a device; the Drift version is a plain
unit test that runs in under a second with no emulator. 16 tests.

95 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:13:45 -05:00
398157b1f5 Complete Phase 1: accumulator, exports, and full parity coverage
T05 — ride_accumulator.dart with the cross-batch anchor intact (12 tests).
T06 — ride_export.dart, GPX 1.1 and GeoJSON (15 tests), parsed with real
parsers rather than substring matching.
T07 — parity harness extended to cover export byte output.

GPX and GeoJSON are byte-identical across Kotlin and Dart: same length, same
FNV hash, including the escaped hostile name and all %.7f/%.1f formatting.

Two harness bugs found and fixed while building it. String.hashCode is not
comparable across Java and Dart, so text comparison used FNV-1a instead. And a
missing jar let a failed Kotlin compile pass as a green run -- run.sh now checks
for the artifact and exits non-zero, the same failure mode as the v2 sweep that
hid a compile error behind /dev/null.

Phase 1 done: 79 tests passing, analyze clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 01:08:30 -05:00
f0f9ed8c34 Verify builds on both platforms; pin Android SDK levels
compileSdk 37 (a dependency requires it; the build fails on 36), minSdk 26 for
parity with the native app, targetSdk 36. AGP auto-installed platform 37, CMake
and a 2.8GB NDK during the build since licences were already accepted.

Both artifacts confirmed: Runner.app and app-debug.apk.

Recorded two deprecation warnings against flutter_foreground_task, the plugin
chosen for Android background liveness in T12: no Swift Package Manager support
on iOS, and it applies KGP on Android. Both are future hard errors, so the
LocationSource seam in T10 matters more than assumed.

Also recorded the disk episode: the plan braced for SDK installs, but builds
were what filled the disk (22 GiB to 3.9 GiB). Recovered to 11 GiB.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 20:28:12 -05:00
725e6f63b7 Scaffold Flutter port: toolchain, project, and Geo
T00 — toolchain green on both platforms. flutter doctor reports no issues.
Pinned Flutter to Temurin 21 (AGP rejects the default JDK 25, same constraint
as the native build) and installed Android cmdline-tools with an explicit
--sdk_root, the trap already documented in the native DEVELOPMENT.md.
No caches were deleted: the 16 GiB disk reading that drove the cleanup plan
re-measured at 36 GiB before anything was removed.

T01 — flutter create for android+ios. applicationId is com.rippr.port, not
com.rippr, so the native app stays installable alongside it during the port;
T27 switches it at cutover.

T02 — geo/geo.dart ported from com.rippr.geo.Geo with all 23 tests, tolerances
and comments carried over unchanged. Kotlin's object namespace became top-level
functions; LatLon stays our own type so the pure layer never depends on
flutter_map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 20:13:15 -05:00