MotorSignal-App/test/performance_launch_mode_tes...

632 lines
28 KiB
Dart

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<TelemetrySample> 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<TelemetrySample>.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<void>.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<void>.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<void>.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<void>.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<void>.delayed(const Duration(milliseconds: 10));
// Launch at t=1050ms
telemetryController.add(makeSample(speed: 4.0, monotonicMs: 1050));
await Future<void>.delayed(const Duration(milliseconds: 10));
// Mid run at t=2500ms (speed 45)
telemetryController.add(makeSample(speed: 45.0, monotonicMs: 2500));
await Future<void>.delayed(const Duration(milliseconds: 10));
// Cross 60 MPH at t=3800ms (speed 62.0)
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3800));
await Future<void>.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<void>.delayed(const Duration(milliseconds: 10));
// Launch at t=1050ms
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await Future<void>.delayed(const Duration(milliseconds: 10));
// 60 MPH split crossing at t=3500ms
telemetryController.add(makeSample(speed: 63.0, monotonicMs: 3500));
await Future<void>.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<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await Future<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 61.0, monotonicMs: 3500));
await Future<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 10.0, monotonicMs: 1100, seq: 11));
await Future<void>.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<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050, seq: 2));
await Future<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3500, seq: 3));
await Future<void>.delayed(const Duration(milliseconds: 10));
// Duplicate sample replay with same seq
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3500, seq: 3));
await Future<void>.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<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 20.0, monotonicMs: 1200));
await Future<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 20.0, monotonicMs: 1200));
await Future<void>.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<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await Future<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3500));
await Future<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 25.0, monotonicMs: 1200));
await Future<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await Future<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3500));
await Future<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await Future<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 61.0, monotonicMs: 3500));
await Future<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await Future<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 61.0, monotonicMs: 3500));
await Future<void>.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<void>.delayed(const Duration(milliseconds: 10));
telemetryController.add(makeSample(speed: 5.0, monotonicMs: 1050));
await Future<void>.delayed(const Duration(milliseconds: 10));
// 60 MPH intermediate crossing
telemetryController.add(makeSample(speed: 62.0, monotonicMs: 3600));
await Future<void>.delayed(const Duration(milliseconds: 10));
// 100 MPH target crossing
telemetryController.add(makeSample(speed: 101.0, monotonicMs: 6800));
await Future<void>.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!));
});
});
}