213 lines
6.8 KiB
Dart
213 lines
6.8 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:motorcycle_dash/models/telemetry_sample.dart';
|
|
import 'package:motorcycle_dash/services/validation/telemetry_validator.dart';
|
|
import 'replay/telemetry_replay_engine.dart';
|
|
|
|
void main() {
|
|
group('Phase 9 Forensic Validation: GPS & Sensor Freshness', () {
|
|
late TelemetryValidator validator;
|
|
|
|
setUp(() {
|
|
validator = TelemetryValidator();
|
|
});
|
|
|
|
test('Fresh GPS position (<200ms) receives 1.0 confidence score', () {
|
|
final sample = TelemetrySample(
|
|
timestamp: DateTime(2026, 8, 28, 10, 0, 0),
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
speed: 45.0,
|
|
gpsAccuracy: 3.5,
|
|
gpsAgeMs: 80,
|
|
);
|
|
|
|
final quality = validator.validate(sample);
|
|
expect(quality.isValid, isTrue);
|
|
expect(quality.gpsConfidence, 1.0);
|
|
expect(quality.overallConfidence, greaterThanOrEqualTo(0.90));
|
|
});
|
|
|
|
test('Stale GPS fix (>2000ms) while moving marks sample suspect with low confidence', () {
|
|
final sample = TelemetrySample(
|
|
timestamp: DateTime(2026, 8, 28, 10, 0, 0),
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
speed: 55.0,
|
|
gpsAccuracy: 5.0,
|
|
gpsAgeMs: 2500, // 2.5s old fix
|
|
);
|
|
|
|
final quality = validator.validate(sample);
|
|
expect(quality.isSuspect, isTrue);
|
|
expect(quality.gpsConfidence, lessThanOrEqualTo(0.20));
|
|
expect(quality.reasons.any((r) => r.contains('Stale GPS fix')), isTrue);
|
|
});
|
|
|
|
test('GPS position teleportation (>110 m/s jump) is rejected with zero confidence', () {
|
|
final baseTime = DateTime(2026, 8, 28, 10, 0, 0);
|
|
|
|
// Sample 1: Baseline in San Francisco
|
|
final s1 = TelemetrySample(
|
|
timestamp: baseTime,
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
speed: 30.0,
|
|
gpsAccuracy: 3.0,
|
|
gpsAgeMs: 50,
|
|
);
|
|
validator.validate(s1);
|
|
|
|
// Sample 2: Sudden 500m coordinate jump in 50ms (teleportation)
|
|
final s2 = TelemetrySample(
|
|
timestamp: baseTime.add(const Duration(milliseconds: 50)),
|
|
latitude: 37.7800, // ~570 meters away
|
|
longitude: -122.4194,
|
|
speed: 30.0,
|
|
gpsAccuracy: 3.0,
|
|
gpsAgeMs: 50,
|
|
);
|
|
|
|
final quality2 = validator.validate(s2);
|
|
expect(quality2.isRejected, isTrue);
|
|
expect(quality2.gpsConfidence, 0.0);
|
|
expect(quality2.reasons.any((r) => r.contains('GPS position jump detected')), isTrue);
|
|
});
|
|
|
|
test('RideQualityReport evaluates excellent 100-point session correctly', () {
|
|
final report = RideQualityReport.evaluate(
|
|
totalSamples: 1000,
|
|
validSamples: 980,
|
|
suspectSamples: 18,
|
|
rejectedSamples: 2,
|
|
timestampGaps: 1,
|
|
gpsFixCount: 500,
|
|
totalGpsAccuracy: 1750.0, // 3.5m average
|
|
isMountCalibrated: true,
|
|
);
|
|
|
|
expect(report.overallScore, greaterThanOrEqualTo(90));
|
|
expect(report.rating, 'EXCELLENT');
|
|
expect(report.isMountCalibrated, isTrue);
|
|
});
|
|
|
|
test('RideQualityReport evaluates degraded session with poor GPS and uncalibrated mount', () {
|
|
final report = RideQualityReport.evaluate(
|
|
totalSamples: 1000,
|
|
validSamples: 450,
|
|
suspectSamples: 400,
|
|
rejectedSamples: 150,
|
|
timestampGaps: 12,
|
|
gpsFixCount: 100,
|
|
totalGpsAccuracy: 4500.0, // 45m average
|
|
isMountCalibrated: false,
|
|
);
|
|
|
|
expect(report.overallScore, lessThan(60));
|
|
expect(report.rating, anyOf('POOR', 'INVALID'));
|
|
});
|
|
});
|
|
|
|
group('Phase 9 Forensic Validation: Replay Engine & Scenario Playback', () {
|
|
late TelemetryReplayEngine engine;
|
|
|
|
setUp(() {
|
|
engine = TelemetryReplayEngine();
|
|
});
|
|
|
|
tearDown(() {
|
|
engine.dispose();
|
|
});
|
|
|
|
test('Replays straight-line cruise scenario with 100% validity', () async {
|
|
final baseTime = DateTime(2026, 8, 28, 12, 0, 0);
|
|
final cruiseSamples = List.generate(40, (i) {
|
|
return TelemetrySample(
|
|
timestamp: baseTime.add(Duration(milliseconds: i * 50)),
|
|
latitude: 37.7749 + (i * 0.0001),
|
|
longitude: -122.4194,
|
|
speed: 60.0,
|
|
heading: 0.0,
|
|
gpsAccuracy: 2.8,
|
|
gpsAgeMs: 50,
|
|
leanAngle: 0.0,
|
|
longitudinalG: 0.0,
|
|
lateralG: 0.0,
|
|
);
|
|
});
|
|
|
|
final scenario = ReplayScenario(name: 'Straight Cruise', samples: cruiseSamples);
|
|
final results = await engine.play(scenario);
|
|
|
|
expect(results.length, 40);
|
|
expect(results.every((s) => s.quality!.isValid), isTrue);
|
|
expect(engine.validator.rejectedCount, 0);
|
|
});
|
|
|
|
test('Replays aggressive cornering scenario with valid lean tracking', () async {
|
|
final baseTime = DateTime(2026, 8, 28, 12, 0, 0);
|
|
final cornerSamples = List.generate(30, (i) {
|
|
final lean = (i < 15) ? (i * 3.0) : (45.0 - (i - 15) * 3.0); // 0 -> 45° -> 0°
|
|
return TelemetrySample(
|
|
timestamp: baseTime.add(Duration(milliseconds: i * 50)),
|
|
latitude: 37.7749,
|
|
longitude: -122.4194 + (i * 0.0001),
|
|
speed: 42.0,
|
|
heading: 90.0,
|
|
gpsAccuracy: 3.2,
|
|
gpsAgeMs: 50,
|
|
leanAngle: lean,
|
|
longitudinalG: -0.05,
|
|
lateralG: lean / 45.0 * 0.85,
|
|
);
|
|
});
|
|
|
|
final scenario = ReplayScenario(name: 'Right Sweeper Corner', samples: cornerSamples);
|
|
final results = await engine.play(scenario);
|
|
|
|
expect(results.length, 30);
|
|
expect(results.every((s) => s.quality!.isValid), isTrue);
|
|
expect(results[15].leanAngle, 45.0);
|
|
});
|
|
|
|
test('Replays GPS dropout scenario and flags stale/gap samples correctly', () async {
|
|
final baseTime = DateTime(2026, 8, 28, 12, 0, 0);
|
|
final samples = <TelemetrySample>[
|
|
// Good sample
|
|
TelemetrySample(
|
|
timestamp: baseTime,
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
speed: 55.0,
|
|
gpsAccuracy: 3.0,
|
|
gpsAgeMs: 50,
|
|
),
|
|
// Stale GPS sample (>2s old)
|
|
TelemetrySample(
|
|
timestamp: baseTime.add(const Duration(milliseconds: 50)),
|
|
latitude: 37.7749,
|
|
longitude: -122.4194,
|
|
speed: 55.0,
|
|
gpsAccuracy: 3.0,
|
|
gpsAgeMs: 2200,
|
|
),
|
|
// Teleportation sample
|
|
TelemetrySample(
|
|
timestamp: baseTime.add(const Duration(milliseconds: 100)),
|
|
latitude: 37.8500, // 8km jump
|
|
longitude: -122.4194,
|
|
speed: 55.0,
|
|
gpsAccuracy: 3.0,
|
|
gpsAgeMs: 50,
|
|
),
|
|
];
|
|
|
|
final scenario = ReplayScenario(name: 'GPS Dropout & Jump', samples: samples);
|
|
final results = await engine.play(scenario);
|
|
|
|
expect(results[0].quality!.isValid, isTrue);
|
|
expect(results[1].quality!.isSuspect, isTrue);
|
|
expect(results[2].quality!.isRejected, isTrue);
|
|
});
|
|
});
|
|
}
|