V3-01: activity type per ride
Adds Activity (motorcycle/bicycle/scooter/skateboard/running/walking/other) as a column on Trip, and threads it through as real behaviour rather than a label: ActivityProfile (domain/activity_profile.dart) carries a noise floor, accuracy gate, histogram bucket width, and elevation smoothing window/threshold per activity, consumed by sanitizeSpeedKmh, isUsableFix, computeSummary, Accumulator and speedHistogram. The motorcycle profile reproduces the exact constants the app shipped with before this existed, and a test asserts they never drift apart. This is the port's first real migration: schemaVersion 1 -> 2, m.addColumn(trips, trips.activity) with a motorcycle default so every existing row survives unmodified. Proven with a hand-built v1 SQLite file (raw sqlite3, not drift_dev's schema tooling) that a real database with real rides upgrades and keeps every trip, segment and point. No picker in front of Start: a new trip defaults to whichever activity was most recently used, derived live from the trips table rather than duplicated into a Config field. Editable afterwards on trip detail, which recomputes aggregates under the new profile immediately -- a walking pace that reads as noise under a motorcycle's floor reads as real movement once the activity is corrected, and there's a test proving exactly that transition. The widget-test pass for the activity-picker sheet caught a real overflow bug: seven options overflowed a Column-based bottom sheet the same way the record screen once did. Fixed with a scrollable ListView + isScrollControlled, same shape as that earlier fix. 188 tests (171 -> 188): 2 migration, 5 ActivityProfile, 3 profile-threading proofs in computeSummary, 5 repository, 2 widget. Analyze clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:rippr/src/data/database.dart';
|
||||
import 'package:rippr/src/data/trip_repository.dart';
|
||||
import 'package:rippr/src/domain/activity_profile.dart';
|
||||
import 'package:rippr/src/domain/models.dart';
|
||||
|
||||
/// Ported from `com.rippr.data.TripRepositoryTest` and `MergeTest`.
|
||||
@@ -187,6 +188,84 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Activity type — V3-01
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
group('activity', () {
|
||||
test('a brand new database defaults a fresh trip to motorcycle', () async {
|
||||
final h = await repo.startTrip(1000);
|
||||
expect((await repo.tripById(h.tripId))!.activity, Activity.motorcycle);
|
||||
});
|
||||
|
||||
test('starting a new trip defaults to whatever activity was last used, '
|
||||
'with no picker required', () async {
|
||||
final first = await repo.startTrip(1000);
|
||||
await repo.completeTrip(2000);
|
||||
await repo.setActivity(first.tripId, Activity.walking);
|
||||
|
||||
final second = await repo.startTrip(3000);
|
||||
|
||||
expect((await repo.tripById(second.tripId))!.activity, Activity.walking,
|
||||
reason: 'the new ride should inherit the most recently created trip\'s '
|
||||
'activity, not silently reset to motorcycle');
|
||||
});
|
||||
|
||||
test('an explicit activity overrides the last-used default', () async {
|
||||
final h = await repo.startTrip(1000, activity: Activity.bicycle);
|
||||
expect((await repo.tripById(h.tripId))!.activity, Activity.bicycle);
|
||||
});
|
||||
|
||||
test('resuming an active trip never changes its activity', () async {
|
||||
final h = await repo.startTrip(1000, activity: Activity.running);
|
||||
await repo.pauseTrip(2000);
|
||||
// Started again with no explicit activity -- must adopt the paused trip's own,
|
||||
// not fall back to whatever "last used" would otherwise resolve to.
|
||||
final resumed = await repo.startTrip(3000);
|
||||
|
||||
expect(resumed.tripId, h.tripId);
|
||||
expect((await repo.tripById(h.tripId))!.activity, Activity.running);
|
||||
});
|
||||
|
||||
test('setActivity recomputes aggregates under the new profile', () async {
|
||||
// A slow walking pace: under the motorcycle noise floor (1.5 km/h) but above
|
||||
// walking's own (0.3), so moving time genuinely differs between the two profiles.
|
||||
final h = await repo.startTrip(1000, activity: Activity.motorcycle);
|
||||
for (var i = 0; i < 10; i++) {
|
||||
await addPoint(h, 1000 + i * 1000, speed: 0.7);
|
||||
}
|
||||
await repo.completeTrip(11000);
|
||||
// completeTrip() alone does not recompute aggregates -- the recorder does that
|
||||
// separately on stop(). Mirror that here rather than relying on a leftover value.
|
||||
await repo.recomputeAggregates(h.tripId);
|
||||
|
||||
final asMotorcycle = (await repo.tripById(h.tripId))!;
|
||||
expect(asMotorcycle.movingMillis, 0,
|
||||
reason: 'a motorcycle profile must read a 0.7 km/h pace as noise');
|
||||
|
||||
await repo.setActivity(h.tripId, Activity.walking);
|
||||
|
||||
final asWalking = (await repo.tripById(h.tripId))!;
|
||||
expect(asWalking.activity, Activity.walking);
|
||||
expect(asWalking.movingMillis, greaterThan(0),
|
||||
reason: 'recomputing under the walking profile must recognise the same '
|
||||
'fixes as real movement');
|
||||
// Sanity check that this really did come from ActivityProfile.walking and not a
|
||||
// coincidence -- computing with the profile explicitly must agree.
|
||||
expect(
|
||||
asWalking.movingMillis,
|
||||
greaterThan(asMotorcycle.movingMillis),
|
||||
);
|
||||
// The bicycle/motorcycle-equivalent profile does not exist as a category here;
|
||||
// just confirm the profile object itself expresses the difference this test
|
||||
// exercises indirectly through the repository.
|
||||
expect(
|
||||
ActivityProfile.walking.noiseFloorKmh,
|
||||
lessThan(ActivityProfile.motorcycle.noiseFloorKmh),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('process death', () {
|
||||
test('a fresh repository over the same database sees the active trip',
|
||||
() async {
|
||||
|
||||
Reference in New Issue
Block a user