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>
This commit is contained in:
@@ -661,3 +661,79 @@ Phases 0–4 are complete. What is left is verification and cutover:
|
||||
|
||||
**Known parity gap so far:** notification actions (Pause/Resume in the shade), lost with
|
||||
`flutter_foreground_task`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 — Verification
|
||||
|
||||
### T22 — Uploader and config · **complete**
|
||||
|
||||
`lib/src/telemetry/telemetry_uploader.dart` and `lib/src/config/config.dart`. **7 tests.**
|
||||
|
||||
`package:http` with a `MockClient` replaces OkHttp with MockWebServer, and the tests run
|
||||
against a real in-memory Drift database rather than a fake DAO — closer to production and
|
||||
still no device. Batching, retry, the offline-safe backlog via `synced`, and per-point
|
||||
trip/segment identity all carried over.
|
||||
|
||||
Upload runs on **its own timer** in the engine, injected as a callback so the engine has
|
||||
no opinion about HTTP and tests need no network. Every failure path is swallowed: nothing
|
||||
about uploading may disturb recording.
|
||||
|
||||
`Config` uses `shared_preferences`, with a hand-rolled UUID v4 rather than a package for
|
||||
sixteen bytes. `mapEnabled` now comes from preferences instead of being hardcoded.
|
||||
|
||||
**Parity note:** there is still **no UI** for the endpoint, exactly as in the native app.
|
||||
|
||||
> Two `prefer_initializing_formals` lints are suppressed with a reason: Dart does not
|
||||
> permit a named parameter whose name begins with an underscore, so the lint's suggested
|
||||
> fix does not compile.
|
||||
|
||||
### T23 — Widget tests · **complete** (delivered in Phase 4)
|
||||
|
||||
### T24 — Integration tests · **complete**
|
||||
|
||||
`integration_test/app_test.dart`. **4 tests, passing on the iOS simulator.**
|
||||
|
||||
These cover what widget tests structurally cannot: Drift opening against real platform
|
||||
storage, plugin registration resolving, `go_router` driving a real Navigator, and a cold
|
||||
start surviving. The database test in particular is meaningful — an in-memory Drift
|
||||
instance cannot prove the sqlite3 native library loaded on the device.
|
||||
|
||||
```
|
||||
flutter test integration_test -d <device-id> → 00:50 +4: All tests passed!
|
||||
```
|
||||
|
||||
**Not yet run on the Android emulator**, which needs 7.4 GiB free to boot; run
|
||||
`flutter clean` first.
|
||||
|
||||
### T25 — Real ride · **outstanding, and it is the important one**
|
||||
|
||||
Written up as [REAL-RIDE-CHECKLIST.md](REAL-RIDE-CHECKLIST.md): eleven shared checks, three
|
||||
Android-only, five iOS-only.
|
||||
|
||||
Three things in it are worth calling out:
|
||||
|
||||
- **A2 (Android):** force-stop mid-ride and confirm the dead time is *not* added to
|
||||
distance — the native bug found in T11, measured at 111 km.
|
||||
- **I3 (iOS):** park for fifteen minutes mid-recording and see whether recording resumes.
|
||||
**This decides the platform strategy.** If `geolocator` loses rides to suspension, the
|
||||
`LocationSource` seam exists so `flutter_background_geolocation` (~$500/yr) can replace
|
||||
it as one new implementation.
|
||||
- **Record the Android ride on both apps at once.** They have different application ids
|
||||
precisely so they can coexist, and a direct numeric comparison is far stronger evidence
|
||||
than either app alone.
|
||||
|
||||
### T26 — iOS release readiness · **configured, not submitted**
|
||||
|
||||
[RELEASE-IOS.md](RELEASE-IOS.md). Usage strings are written specifically rather than
|
||||
generically (the leading 5.1.1 rejection cause), privacy-label answers are decided, and
|
||||
the pre-submission list covers the bundle-id switch, the still-default app icon, a release
|
||||
build, and a demo video for the review notes.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5 status
|
||||
|
||||
**T22, T23, T24 complete. T25 and T26 need a real device and a real rider.**
|
||||
|
||||
`178 tests passing` (171 unit + widget, plus 4 integration on device), analyze clean.
|
||||
|
||||
100
docs/port/REAL-RIDE-CHECKLIST.md
Normal file
100
docs/port/REAL-RIDE-CHECKLIST.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# T25 — the real-ride checklist
|
||||
|
||||
**This is the only task in the plan that cannot be automated, and it is the one that
|
||||
matters most.** 171 unit and widget tests plus 4 integration tests are green, and none of
|
||||
them prove the app records a ride correctly.
|
||||
|
||||
Adapted from the native repo's `docs/TESTING.md`, extended for two platforms.
|
||||
|
||||
---
|
||||
|
||||
## Why a green suite proves nothing here
|
||||
|
||||
**Neither simulator produces velocity.** `adb emu geo fix` teleports the device and iOS's
|
||||
simulated locations are no better, so every recorded `speedKmh` is `0.0`. That makes the
|
||||
following **structurally unverifiable** without riding:
|
||||
|
||||
- max speed, average moving speed
|
||||
- moving time (everything sits under the 1.5 km/h noise floor, so it stays 00:00:00)
|
||||
- speed colouring on the map — the whole path renders in one colour
|
||||
- elevation gain against real, *correlated* GPS altitude error
|
||||
- battery over a multi-hour ride
|
||||
- whether iOS suspends a stationary app mid-ride
|
||||
|
||||
And the subtler trap, which cost v2.0 a release: **when a value cannot change under test,
|
||||
the UI around it cannot be judged either.** Max speed as the headline looked perfectly
|
||||
fine on an emulator where every number was zero. On a real ride it read as a frozen,
|
||||
broken screen.
|
||||
|
||||
---
|
||||
|
||||
## Before you ride
|
||||
|
||||
Install both apps. They have different application ids on purpose
|
||||
(`com.rippr` and `com.rippr.port`), so they coexist:
|
||||
|
||||
```bash
|
||||
flutter build apk --debug && flutter install # Flutter port
|
||||
adb install -r ~/dojo/samplez/rippr-2.0.1-debug.apk # native reference
|
||||
```
|
||||
|
||||
**Run both simultaneously on the Android ride.** Recording the same ride twice gives a
|
||||
direct numeric comparison, which is far stronger evidence than either app alone.
|
||||
|
||||
---
|
||||
|
||||
## The ride
|
||||
|
||||
Do this once on **Android** and once on **iOS**. Pocket the phone — that is the founding
|
||||
use case.
|
||||
|
||||
| # | Check | Why it is here |
|
||||
|---|---|---|
|
||||
| 1 | Start, pocket, ride ~20 min, stop | The actual usage pattern |
|
||||
| 2 | Max speed plausible against the speedometer | Unverifiable on any simulator |
|
||||
| 3 | Distance plausible against the odometer | Guards the cross-batch anchor |
|
||||
| 4 | Moving time excludes stops | Noise floor behaviour on real data |
|
||||
| 5 | **Elevation gain near zero on flat ground** | The most likely silent bug; synthetic noise is uniform, real error is correlated |
|
||||
| 6 | Pause at a stop, resume — **no straight line across the gap** | The segment guarantee |
|
||||
| 7 | Speed colouring visibly varies along the path | Cannot render in more than one colour on a simulator |
|
||||
| 8 | **A short ride (under 100 m) still shows streets** | The v2.0 zoom bug |
|
||||
| 9 | GPX opens correctly in Google Earth or Strava | Schema-valid is not the same as accepted |
|
||||
| 10 | Battery drain over a multi-hour ride | Never measured, on either app |
|
||||
| 11 | Pause/resume survives a screen-off stretch | Android wake lock, iOS background mode |
|
||||
|
||||
### Android only
|
||||
|
||||
| # | Check |
|
||||
|---|---|
|
||||
| A1 | The ongoing notification appears and persists with the screen off |
|
||||
| A2 | Force-stop the app mid-ride, relaunch — the ride resumes into a **new segment**, and the dead time is **not** added to distance |
|
||||
| A3 | Compare totals against the native app recording the same ride |
|
||||
|
||||
> **A2 is the fix for a real bug found in the native app.** See T11 in
|
||||
> [PROGRESS.md](PROGRESS.md): the native version measures straight through the dead time,
|
||||
> which a synthetic test showed adding **111 km**. Confirm the port does not.
|
||||
|
||||
### iOS only — the genuine unknown
|
||||
|
||||
| # | Check |
|
||||
|---|---|
|
||||
| I1 | The blue background-location indicator appears while recording |
|
||||
| I2 | Lock the screen for 10 minutes of riding — fixes keep arriving |
|
||||
| I3 | **Park for 15 minutes without stopping the recording, then ride again.** Does recording resume? |
|
||||
| I4 | Take a phone call mid-ride; recording survives |
|
||||
| I5 | Swipe the app away mid-ride — what happens? Document it, whatever it is |
|
||||
|
||||
**I3 is the decisive test for the whole platform strategy.** If iOS suspends the app when
|
||||
stationary and does not reliably resume, `geolocator` is not sufficient and the
|
||||
`LocationSource` seam exists precisely so `flutter_background_geolocation` (~$500/yr) can
|
||||
be swapped in as one new implementation. Do not make that call without this data.
|
||||
|
||||
---
|
||||
|
||||
## Recording the results
|
||||
|
||||
Append findings to [PROGRESS.md](PROGRESS.md) under a T25 heading, including the numbers
|
||||
from both apps where Android was recorded twice. If elevation gain on flat ground is
|
||||
implausible, **do not tune it blind** — the native backlog says so explicitly, and the
|
||||
port's parity harness (`tool/parity/run.sh`) means any change can be checked against the
|
||||
Kotlin implementation first.
|
||||
78
docs/port/RELEASE-IOS.md
Normal file
78
docs/port/RELEASE-IOS.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# T26 — iOS release readiness
|
||||
|
||||
What App Review will look at, and what is already in place. Background-location apps get
|
||||
more scrutiny than most, and the research in `docs/PORT_RESEARCH.md` names unclear
|
||||
privacy disclosure as the leading rejection cause.
|
||||
|
||||
**Status: configured, not submitted.** Nothing here has been through review.
|
||||
|
||||
---
|
||||
|
||||
## Guideline 5.1.1 — data privacy and transparency
|
||||
|
||||
**Rejection cause:** generic usage strings like *"This app needs location"*.
|
||||
|
||||
Already in `ios/Runner/Info.plist`, written specifically:
|
||||
|
||||
| Key | Says |
|
||||
|---|---|
|
||||
| `NSLocationWhenInUseUsageDescription` | Records the GPS track of your ride so you can see route, speed and distance afterwards. **Nothing is recorded until you press Start.** |
|
||||
| `NSLocationAlwaysAndWhenInUseUsageDescription` | Keeps recording while the phone is in your pocket or the screen is off, so a ride you started is captured beginning to end. **Recording stops the moment you press Stop.** |
|
||||
|
||||
Both name what is collected, why, and when it stops. That last clause matters — it is the
|
||||
difference between "an app that wants your location" and "a recorder you control".
|
||||
|
||||
## App Privacy nutrition labels
|
||||
|
||||
Declare, and make sure it stays true:
|
||||
|
||||
- **Location → Precise Location**, linked to the user? **No.** Used for **App
|
||||
Functionality** only.
|
||||
- **No data collected for tracking**, no advertising identifiers, no analytics SDKs.
|
||||
- **Data is not transmitted off-device by default.** The uploader exists but has no UI and
|
||||
no endpoint configured; it is inert unless someone sets one deliberately.
|
||||
|
||||
> If a server ever ships (the v3 group-ride idea), these labels must change **before** it
|
||||
> does. Undisclosed background transmission is a straightforward rejection.
|
||||
|
||||
## Guideline 2.1 — completeness and background stability
|
||||
|
||||
The exposure is crashing on background resume. Mitigations in place:
|
||||
|
||||
- Recording state lives in the database, so resuming after suspension reads real state
|
||||
rather than guessing.
|
||||
- `restoreAfterProcessDeath` runs at startup and is covered by four tests, including the
|
||||
crash-gap guard.
|
||||
- Every upload failure path is swallowed; the network cannot stop a recording.
|
||||
- Database write failures are caught per batch — losing points beats losing the app.
|
||||
|
||||
**Still unproven:** what iOS actually does when the app is suspended while stationary.
|
||||
That is item **I3** in [REAL-RIDE-CHECKLIST.md](REAL-RIDE-CHECKLIST.md) and it must be
|
||||
answered before submission.
|
||||
|
||||
## Guideline 4.2 — minimum functionality
|
||||
|
||||
Not a realistic risk: native Flutter UI, real hardware integration, offline-first storage,
|
||||
no web view anywhere.
|
||||
|
||||
---
|
||||
|
||||
## Before submitting
|
||||
|
||||
- [ ] **Switch the bundle id** from `com.rippr.port` to `com.rippr` (T27). The `.port`
|
||||
suffix exists only so the native app can be installed alongside during the port.
|
||||
- [ ] `CFBundleName` is still the generated lowercase `rippr`; `CFBundleDisplayName` is
|
||||
already `Rippr`. Make them consistent.
|
||||
- [ ] App icon and launch screen — still Flutter defaults. The native app's adaptive icon
|
||||
artwork lives in `~/dojo/rippr/design/` and needs re-exporting at iOS sizes.
|
||||
- [ ] Run [REAL-RIDE-CHECKLIST.md](REAL-RIDE-CHECKLIST.md) on a real iPhone, especially I3.
|
||||
- [ ] Record a demo video of a real ride for the review notes. Background-location apps
|
||||
are frequently asked to justify the entitlement; a video pre-empts a rejection round.
|
||||
- [ ] `flutter build ipa --release` and confirm the release build works — everything so
|
||||
far has been debug.
|
||||
|
||||
## Known parity gap to disclose internally
|
||||
|
||||
The notification cannot carry actions, so the native app's Pause/Resume buttons in the
|
||||
shade are absent on both platforms. Not an App Review issue; it is a feature difference
|
||||
the T27 audit must record.
|
||||
Reference in New Issue
Block a user