import 'dart:async'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:motorcycle_dash/database/database.dart' hide TelemetrySample; import 'package:motorcycle_dash/features/performance/models/performance_launch_models.dart'; import 'package:motorcycle_dash/features/performance/services/monotonic_clock.dart'; import 'package:motorcycle_dash/features/performance/services/performance_launch_state_machine.dart'; import 'package:motorcycle_dash/features/records/records_service.dart'; import 'package:motorcycle_dash/models/telemetry_sample.dart'; import 'package:motorcycle_dash/services/validation/telemetry_validator.dart'; import 'package:shared_preferences/shared_preferences.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); late AppDatabase db; late StreamController telemetryController; late FakeMonotonicClock clock; late RecordsService recordsService; late SharedPreferences prefs; PerformanceLaunchStateMachine? stateMachine; setUp(() async { SharedPreferences.setMockInitialValues({}); prefs = await SharedPreferences.getInstance(); recordsService = RecordsService(prefs); db = AppDatabase.forTesting(NativeDatabase.memory()); telemetryController = StreamController.broadcast(); clock = FakeMonotonicClock( initialElapsedMs: 10000, initialDateTime: DateTime.utc(2026, 9, 5, 12, 0, 0), ); }); tearDown(() async { stateMachine?.dispose(); await telemetryController.close(); await db.close(); }); TelemetrySample makeSample({ required double speed, int? monotonicMs, DateTime? timestamp, int? seq, ValidationStatus status = ValidationStatus.valid, double confidence = 1.0, }) { final ms = monotonicMs ?? clock.elapsedMilliseconds; final dt = timestamp ?? clock.now; return TelemetrySample( timestamp: dt, monotonicTimestampMs: ms, sequenceNumber: seq, speed: speed, quality: TelemetryQuality( status: status, overallConfidence: confidence, speedConfidence: confidence, gpsConfidence: confidence, ), ); } group('Performance Launch Mode — Auto Launch Tests', () { test('1. IDLE -> READY after valid stationary condition', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); // Initially idle expect(stateMachine!.state.state, PerformanceLaunchState.idle); // Arm auto launch stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); expect(stateMachine!.state.state, PerformanceLaunchState.idle); // Emit stationary valid sample (< 1.0 MPH) telemetryController.add(makeSample(speed: 0.0)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.ready); }); test('2. READY does not occur with invalid telemetry', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); // Emit rejected sample telemetryController.add(makeSample(speed: 0.0, status: ValidationStatus.rejected, confidence: 0.0)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.idle); }); test('3. movement starts timer (READY -> LAUNCHED)', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.ready); // Vehicle launches past 1.0 MPH threshold telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.launched); expect(stateMachine!.state.movementDetectedAtMonotonicMs, isNotNull); expect(stateMachine!.state.movementDetectedAtMonotonicMs, greaterThanOrEqualTo(1000)); expect(stateMachine!.state.movementDetectedAtMonotonicMs, lessThanOrEqualTo(1050)); }); test('4. 60 MPH threshold completes 0-60', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, recordsService: recordsService, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); // Launch at t=1050ms telemetryController.add(makeSample(speed: 4.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); // Mid run at t=2500ms (speed 45) telemetryController.add(makeSample(speed: 45.0, monotonicMs: 2500)); await Future.delayed(const Duration(milliseconds: 10)); // Cross 60 MPH at t=3800ms (speed 62.0) telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3800)); await Future.delayed(const Duration(milliseconds: 20)); expect(stateMachine!.state.state, PerformanceLaunchState.completed); expect(stateMachine!.state.lastResult, isNotNull); expect(stateMachine!.state.lastResult!.accelerationTimeSec, greaterThan(2.0)); expect(stateMachine!.state.lastResult!.accelerationTimeSec, lessThan(3.5)); expect(stateMachine!.state.lastResult!.target, PerformanceLaunchTarget.sixty); expect(stateMachine!.state.lastResult!.isFalseStart, isFalse); // Verify persisted in database final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); expect(runs.single.type, '0-60'); }); test('5. 100 MPH target captures intermediate 60 split', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, recordsService: recordsService, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.hundred); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); // Launch at t=1050ms telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); // 60 MPH split crossing at t=3500ms telemetryController.add(makeSample(speed: 63.0, monotonicMs: 3500)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.splits, hasLength(1)); expect(stateMachine!.state.splits.single.targetSpeedMph, 60.0); expect(stateMachine!.state.splits.single.elapsedFromMovementSec, greaterThan(2.0)); // 100 MPH target crossing at t=6500ms telemetryController.add(makeSample(speed: 102.0, monotonicMs: 6500)); await Future.delayed(const Duration(milliseconds: 20)); expect(stateMachine!.state.state, PerformanceLaunchState.completed); expect(stateMachine!.state.lastResult!.target, PerformanceLaunchTarget.hundred); expect(stateMachine!.state.lastResult!.splits, hasLength(1)); expect(stateMachine!.state.lastResult!.split60TimeSec, isNotNull); expect(stateMachine!.state.lastResult!.accelerationTimeSec, greaterThan(5.0)); final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); expect(runs.single.type, '0-100'); }); test('6. target crossing only completes once', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 61.0, monotonicMs: 3500)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.completed); // Additional high speed samples after completion telemetryController.add(makeSample(speed: 70.0, monotonicMs: 3800)); telemetryController.add(makeSample(speed: 80.0, monotonicMs: 4000)); await Future.delayed(const Duration(milliseconds: 20)); final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); }); }); group('Performance Launch Mode — Countdown Tests', () { test('7. READY -> COUNTDOWN', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.ready); stateMachine!.startCountdown(); expect(stateMachine!.state.state, PerformanceLaunchState.countdown); expect(stateMachine!.state.scheduledGoAtMonotonicMs, isNotNull); }); test('8. countdown uses monotonic schedule & 9. GO timestamp is deterministic', () async { clock.setElapsedMs(2000); stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, countdownDurationMs: 3000, ); stateMachine!.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 2000)); await Future.delayed(const Duration(milliseconds: 10)); stateMachine!.startCountdown(); expect(stateMachine!.state.countdownStartedAtMonotonicMs, 2000); expect(stateMachine!.state.scheduledGoAtMonotonicMs, 5000); }); test('10. movement after GO produces positive Launch Reaction', () async { clock.setElapsedMs(1000); stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, countdownDurationMs: 3000, ); stateMachine!.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); stateMachine!.startCountdown(); // scheduledGo = 4000 // Sample at GO (t=4000, stationary) telemetryController.add(makeSample(speed: 0.0, monotonicMs: 4000)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.goAtMonotonicMs, 4000); // Rider launches at t=4250 (250ms reaction time) telemetryController.add(makeSample(speed: 5.0, monotonicMs: 4250)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.launched); expect(stateMachine!.state.isFalseStart, isFalse); // Finish 0-60 at t=7000 (speed 61) telemetryController.add(makeSample(speed: 61.0, monotonicMs: 7000)); await Future.delayed(const Duration(milliseconds: 20)); expect(stateMachine!.state.state, PerformanceLaunchState.completed); expect(stateMachine!.state.lastResult!.reactionTimeSec, isNotNull); expect(stateMachine!.state.lastResult!.reactionTimeSec!, closeTo(0.25, 0.01)); expect(stateMachine!.state.lastResult!.totalTimeFromGoSec, greaterThan(2.5)); }); test('11. movement exactly at GO produces zero Launch Reaction', () async { clock.setElapsedMs(1000); stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, countdownDurationMs: 3000, ); stateMachine!.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); stateMachine!.startCountdown(); // scheduledGo = 4000 // Vehicle moves right across 1.0 MPH exactly at t=4000 telemetryController.add(makeSample(speed: 1.0, monotonicMs: 4000)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.isFalseStart, isFalse); }); test('12. movement before GO records FALSE START & 13. false start not discarded & 14. acceleration timing still works', () async { clock.setElapsedMs(1000); stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, recordsService: recordsService, clock: clock, countdownDurationMs: 3000, ); stateMachine!.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); stateMachine!.startCountdown(); // scheduledGo = 4000 // Vehicle jumps before GO at t=3500ms (500ms early) telemetryController.add(makeSample(speed: 6.0, monotonicMs: 3500)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.launched); expect(stateMachine!.state.isFalseStart, isTrue); // Rider continues accelerating and crosses 60 MPH at t=6000ms telemetryController.add(makeSample(speed: 62.0, monotonicMs: 6000)); await Future.delayed(const Duration(milliseconds: 20)); expect(stateMachine!.state.state, PerformanceLaunchState.completed); expect(stateMachine!.state.lastResult!.isFalseStart, isTrue); expect(stateMachine!.state.lastResult!.reactionTimeSec, isNotNull); expect(stateMachine!.state.lastResult!.reactionTimeSec!, lessThan(0.0)); // Negative reaction expect(stateMachine!.state.lastResult!.accelerationTimeSec, greaterThan(2.0)); // Persisted in DB as record of run final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); // But personal records service NOT updated because it was a false start expect(recordsService.state.fastest0To60, double.infinity); }); }); group('Performance Launch Mode — Monotonic Timing & Resiliency Tests', () { test('15. wall-clock change does not affect elapsed timing', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000, timestamp: DateTime.utc(2026, 9, 5, 12, 0, 0))); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050, timestamp: DateTime.utc(2026, 9, 5, 12, 0, 0, 50))); await Future.delayed(const Duration(milliseconds: 10)); // Simulate abrupt wall clock shift (e.g. NTP backwards adjustment by 1 hour) clock.setWallClock(DateTime.utc(2026, 9, 5, 11, 0, 0)); // Finish run at monotonicMs = 3800 with shifted wall-clock timestamp telemetryController.add(makeSample(speed: 61.0, monotonicMs: 3800, timestamp: DateTime.utc(2026, 9, 5, 11, 0, 2))); await Future.delayed(const Duration(milliseconds: 20)); expect(stateMachine!.state.state, PerformanceLaunchState.completed); // Measured elapsed time should be purely derived from monotonic clock (~2.75s) expect(stateMachine!.state.lastResult!.accelerationTimeSec, greaterThan(2.5)); expect(stateMachine!.state.lastResult!.accelerationTimeSec, lessThan(3.0)); }); test('16. monotonic timestamp ordering enforced & 17. stale/out-of-order samples ignored', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000, seq: 10)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 10.0, monotonicMs: 1100, seq: 11)); await Future.delayed(const Duration(milliseconds: 10)); // Stale out-of-order sample (older monotonic ms and seq) telemetryController.add(makeSample(speed: 65.0, monotonicMs: 900, seq: 8)); await Future.delayed(const Duration(milliseconds: 10)); // State machine should NOT complete from stale out-of-order sample expect(stateMachine!.state.state, PerformanceLaunchState.launched); }); test('18. duplicate samples cannot complete twice', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000, seq: 1)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050, seq: 2)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3500, seq: 3)); await Future.delayed(const Duration(milliseconds: 10)); // Duplicate sample replay with same seq telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3500, seq: 3)); await Future.delayed(const Duration(milliseconds: 20)); final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); }); }); group('Performance Launch Mode — Lifecycle & Integration Tests', () { test('19. cancel before movement', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.ready); stateMachine!.cancel(); expect(stateMachine!.state.state, PerformanceLaunchState.aborted); expect(stateMachine!.state.abortReason, PerformanceAbortReason.userCancelled); }); test('20. cancel after movement', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 20.0, monotonicMs: 1200)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.launched); stateMachine!.cancel(); expect(stateMachine!.state.state, PerformanceLaunchState.aborted); expect(stateMachine!.state.abortReason, PerformanceAbortReason.userCancelled); }); test('21. telemetry invalidation aborts active timing', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 20.0, monotonicMs: 1200)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.launched); // Corrupted / rejected telemetry sample received telemetryController.add(makeSample(speed: 25.0, monotonicMs: 1300, status: ValidationStatus.rejected)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.aborted); expect(stateMachine!.state.abortReason, PerformanceAbortReason.telemetryRejected); }); test('22. reset after completed attempt', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3500)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.completed); stateMachine!.reset(); expect(stateMachine!.state.state, PerformanceLaunchState.idle); expect(stateMachine!.state.lastResult, isNull); }); test('23. cannot arm a second concurrent attempt while launched', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 25.0, monotonicMs: 1200)); await Future.delayed(const Duration(milliseconds: 10)); expect(stateMachine!.state.state, PerformanceLaunchState.launched); // Attempt to re-arm while timing stateMachine!.arm(mode: PerformanceLaunchMode.countdown, target: PerformanceLaunchTarget.hundred); // State should remain launched with original 0-60 target expect(stateMachine!.state.state, PerformanceLaunchState.launched); expect(stateMachine!.state.target, PerformanceLaunchTarget.sixty); }); test('24. active ride identity carried into result', () async { final rideId = await db.startRide(startTime: DateTime.utc(2026, 9, 5, 12)); stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, activeRideIdReader: () => rideId, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3500)); await Future.delayed(const Duration(milliseconds: 20)); expect(stateMachine!.state.lastResult!.rideId, rideId); final runs = await db.select(db.performanceRuns).get(); expect(runs.single.rideId, rideId); }); test('25. no duplicate PerformanceRun persistence on multiple queries', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 61.0, monotonicMs: 3500)); await Future.delayed(const Duration(milliseconds: 20)); final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); }); test('26. existing PR validation remains authoritative on clean runs', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, recordsService: recordsService, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.sixty); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 61.0, monotonicMs: 3500)); await Future.delayed(const Duration(milliseconds: 20)); expect(recordsService.state.fastest0To60, isNot(double.infinity)); expect(recordsService.state.fastest0To60, greaterThan(1.8)); // within PR sanity limit }); test('27. 0-100 attempt uses ONE timeline with 60 MPH split', () async { stateMachine = PerformanceLaunchStateMachine( db: db, telemetryStream: telemetryController.stream, clock: clock, ); stateMachine!.arm(mode: PerformanceLaunchMode.auto, target: PerformanceLaunchTarget.hundred); telemetryController.add(makeSample(speed: 0.0, monotonicMs: 1000)); await Future.delayed(const Duration(milliseconds: 10)); telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050)); await Future.delayed(const Duration(milliseconds: 10)); // 60 MPH intermediate crossing telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3600)); await Future.delayed(const Duration(milliseconds: 10)); // 100 MPH target crossing telemetryController.add(makeSample(speed: 101.0, monotonicMs: 6800)); await Future.delayed(const Duration(milliseconds: 20)); // Only ONE PerformanceRun row created for 0-100 attempt final runs = await db.select(db.performanceRuns).get(); expect(runs, hasLength(1)); expect(runs.single.type, '0-100'); // Result contains both total time and split final result = stateMachine!.state.lastResult!; expect(result.target, PerformanceLaunchTarget.hundred); expect(result.splits, hasLength(1)); expect(result.split60TimeSec, isNotNull); expect(result.accelerationTimeSec, greaterThan(result.split60TimeSec!)); }); }); }