MotorSignal-App/test/phase11_field_test_readines...

216 lines
8.1 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:drift/drift.dart' as drift;
import 'package:drift/native.dart';
import 'package:motorcycle_dash/database/database.dart' hide FieldTestEvent, FieldTestSummary, TelemetrySample;
import 'package:motorcycle_dash/models/field_test_event.dart';
import 'package:motorcycle_dash/models/telemetry_sample.dart';
import 'package:motorcycle_dash/services/field_test/field_test_logger.dart';
import 'package:motorcycle_dash/services/field_test/field_test_export_service.dart';
import 'package:motorcycle_dash/services/field_test/field_test_replay_service.dart';
import 'package:motorcycle_dash/services/validation/telemetry_validator.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late AppDatabase db;
late FieldTestLogger logger;
late TelemetryValidator validator;
late FieldTestExportService exportService;
late FieldTestReplayService replayService;
setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory());
logger = FieldTestLogger(db);
validator = TelemetryValidator();
exportService = FieldTestExportService(db);
replayService = FieldTestReplayService(validator: validator);
});
tearDown(() async {
logger.dispose();
await db.close();
});
group('Phase 11: Requirement 5 & 6 — Timing & Queue Observability Forensics', () {
test('Detects and logs timestamp regressions when sample time reverses', () async {
logger.startSession(101);
final t1 = DateTime(2026, 8, 28, 12, 0, 10);
final t2 = DateTime(2026, 8, 28, 12, 0, 5); // 5 seconds in the past!
final s1 = TelemetrySample(timestamp: t1, speed: 45.0, gpsAccuracy: 3.0);
final s2 = TelemetrySample(timestamp: t2, speed: 45.0, gpsAccuracy: 3.0);
logger.processTelemetrySample(s1, validator.validate(s1));
logger.processTelemetrySample(s2, validator.validate(s2));
await logger.flush();
final events = logger.timelineEvents;
expect(events.any((e) => e.eventType == FieldTestEventType.timestampError), isTrue);
expect(events.firstWhere((e) => e.eventType == FieldTestEventType.timestampError).severity, EventSeverity.error);
});
test('Queue bounds at capacity and drops INFO events first while retaining CRITICAL events', () async {
logger.startSession(102);
// Enqueue 999 INFO events
for (int i = 0; i < 999; i++) {
logger.log(
eventType: FieldTestEventType.sampleAccepted,
severity: EventSeverity.info,
message: 'Info sample #$i',
);
}
expect(logger.currentQueueDepth, 1000); // 1 start event + 999 info events = 1000 (at capacity)
// Add a critical event at full queue
logger.log(
eventType: FieldTestEventType.imuSaturation,
severity: EventSeverity.critical,
message: 'Critical IMU Saturation',
);
// Queue length should remain capped at 1000
expect(logger.currentQueueDepth, 1000);
expect(logger.droppedEventsCount, 1);
expect(logger.oldestDroppedItem, isNotNull);
await logger.flush();
expect(logger.totalFlushed, 1000);
expect(logger.currentQueueDepth, 0);
});
test('Tracks live peak queue depth, flush duration, and telemetry sample rate', () async {
logger.startSession(103);
final base = DateTime(2026, 8, 28, 14, 0, 0);
for (int i = 0; i < 20; i++) {
final sample = TelemetrySample(
timestamp: base.add(Duration(milliseconds: i * 50)),
speed: 35.0,
gpsAccuracy: 2.5,
);
logger.processTelemetrySample(sample, validator.validate(sample));
}
expect(logger.peakQueueDepth, greaterThan(0));
expect(logger.queueCapacity, 1000);
expect(logger.droppedEventsCount, 0);
expect(logger.failedFlushCount, 0);
});
});
group('Phase 11: Requirement 7 & 14 — Interrupted Crash Recovery & Summary Generation', () {
test('Reconstructs complete forensic summary for unclosed interrupted ride', () async {
final rideStart = DateTime(2026, 8, 28, 10, 0, 0);
// Insert unclosed ride (endTime is null)
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
startTime: rideStart,
));
// Simulate 50 realistic telemetry points
for (int i = 0; i < 50; i++) {
await db.into(db.telemetrySamples).insert(TelemetrySamplesCompanion.insert(
rideId: rideId,
timestamp: rideStart.add(Duration(seconds: i)),
latitude: drift.Value(37.7749 + (i * 0.0001)),
longitude: const drift.Value(-122.4194),
speed: const drift.Value(55.0),
gpsAccuracy: const drift.Value(3.2),
));
}
// Generate crash-recovery forensic summary
final summary = await logger.generateAndSaveSummary(rideId, wasRecovered: true);
expect(summary.wasRecoveredAfterCrash, isTrue);
expect(summary.wasInterrupted, isTrue);
expect(summary.totalSamples, 50);
expect(summary.validPercentage, 100.0);
expect(summary.rideQualityScore, greaterThanOrEqualTo(85));
expect(summary.rideQualityRating, 'EXCELLENT');
expect(summary.exportIntegrity, 'VERIFIED_OK');
});
});
group('Phase 11: Requirement 15 — Forensic Export Package Content & Structure', () {
test('Generates complete forensic session JSON and CSV with all required diagnostic headers', () async {
final rideStart = DateTime(2026, 8, 28, 11, 0, 0);
final rideId = await db.into(db.rides).insert(RidesCompanion.insert(
startTime: rideStart,
endTime: drift.Value(rideStart.add(const Duration(minutes: 30))),
distanceMiles: const drift.Value(18.5),
topSpeedMph: const drift.Value(78.2),
));
logger.startSession(rideId);
logger.log(
eventType: FieldTestEventType.accelerationEvent,
severity: EventSeverity.info,
message: 'Hard launch 0.75g',
speed: 45.0,
longitudinalG: 0.75,
);
await logger.flush();
await logger.generateAndSaveSummary(rideId);
// Test CSV Export
final csv = await exportService.generateFieldTestEventsCsv(rideId);
expect(csv.startsWith('timestamp,ride_id,event_type,severity,message'), isTrue);
expect(csv.contains('ACCELERATION_EVENT'), isTrue);
// Test JSON Session Export
final jsonStr = await exportService.generateFieldTestSessionJson(rideId);
final Map<String, dynamic> parsed = jsonDecode(jsonStr);
expect(parsed['exportVersion'], '1.0.0');
expect(parsed['rideMetadata']['id'], rideId);
expect(parsed['summary']['totalSamples'], isNotNull);
expect(parsed['events'], isNotEmpty);
});
});
group('Phase 11: Requirement 16 — Deterministic Field-Test Replay Engine', () {
test('Offline replay of synthetic ride yields identical deterministic validity classifications', () async {
final base = DateTime(2026, 8, 28, 16, 0, 0);
final samples = <TelemetrySample>[];
// 100 normal cruising samples
for (int i = 0; i < 100; i++) {
samples.add(TelemetrySample(
timestamp: base.add(Duration(milliseconds: i * 50)),
latitude: 37.7749 + (i * 0.00005),
longitude: -122.4194 + (i * 0.00005),
speed: 45.0,
gpsAccuracy: 3.0,
leanAngle: 12.0,
longitudinalG: 0.05,
lateralG: 0.12,
));
}
// Add 1 invalid GPS teleport jump sample
samples.add(TelemetrySample(
timestamp: base.add(const Duration(milliseconds: 100 * 50)),
latitude: 38.5000, // Instant 80km jump!
longitude: -122.4194,
speed: 45.0,
gpsAccuracy: 3.0,
));
final result = await replayService.replaySamples(samples);
expect(result.totalSamples, 101);
expect(result.validSamples, 100);
expect(result.rejectedSamples, 1);
expect(result.validPercentage, closeTo(99.0, 0.1));
expect(result.rideQualityScore, greaterThanOrEqualTo(85));
expect(result.rideQualityRating, 'EXCELLENT');
expect(result.reconstructedEvents.any((e) => e.eventType == FieldTestEventType.sampleRejected), isTrue);
});
});
}