import 'dart:convert'; import 'package:flutter_test/flutter_test.dart'; import 'package:drift/native.dart'; import 'package:motorcycle_dash/core/version/app_identity.dart'; import 'package:motorcycle_dash/core/app_info.dart'; import 'package:motorcycle_dash/database/database.dart' hide FieldTestEvent, FieldTestSummary, TelemetrySample; 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 FieldTestExportService exportService; late FieldTestReplayService replayService; setUp(() { db = AppDatabase.forTesting(NativeDatabase.memory()); logger = FieldTestLogger(db); exportService = FieldTestExportService(db); replayService = FieldTestReplayService(validator: TelemetryValidator()); }); tearDown(() async { logger.dispose(); await db.close(); }); group('Production Versioning & Identity System Tests', () { // 1. Semantic version parsing test('1. Semantic version parsing conforms to MAJOR.MINOR.PATCH format', () { final identity = AppIdentity.current(); final parts = identity.semanticVersion.split('.'); expect(parts.length, 3); expect(int.tryParse(parts[0]), isNotNull); expect(int.tryParse(parts[1]), isNotNull); expect(int.tryParse(parts[2]), isNotNull); }); // 2. Build number handling test('2. Build number is a valid positive integer', () { final identity = AppIdentity.current(); expect(identity.buildNumber, isPositive); expect(identity.buildNumber, greaterThanOrEqualTo(100)); }); // 3. Git SHA handling test('3. Git SHA is captured with valid 40-char full hash representation', () { final identity = AppIdentity.current(); expect(identity.gitCommit, isNotEmpty); expect(identity.displayString.contains(identity.gitCommit.substring(0, 8)), isTrue); }); // 4. Missing Git metadata handling test('4. Deserialization with missing Git metadata gracefully falls back to unknown', () { final fallbackJson = { 'version': '1.0.0', 'build': 100, }; final fallback = AppIdentity.fromJson(fallbackJson); expect(fallback.gitCommit, 'unknown'); expect(fallback.gitBranch, 'unknown'); expect(fallback.releaseChannel, 'release'); expect(fallback.telemetrySchemaVersion, 5); expect(fallback.exportSchemaVersion, 1); }); // 5. Release-channel handling test('5. Release channel is recognized from environment or runtime mode', () { final identity = AppIdentity.current(); expect(['debug', 'profile', 'release', 'beta', 'production'].contains(identity.releaseChannel), isTrue); }); // 6. Schema-version separation test('6. Database schema version, telemetry schema, and export schema are independent', () { final identity = AppIdentity.current(); expect(identity.telemetrySchemaVersion, 5); expect(identity.exportSchemaVersion, 1); expect(identity.semanticVersion, '1.0.0'); // Verify changing app version does not change schema versions const customIdentity = AppIdentity( semanticVersion: '2.0.0', buildNumber: 200, gitCommit: 'abc123456789', gitBranch: 'feature', releaseChannel: 'beta', telemetrySchemaVersion: 5, exportSchemaVersion: 1, buildTimestamp: '2026-08-28T05:00:00Z', ); expect(customIdentity.telemetrySchemaVersion, 5); expect(customIdentity.exportSchemaVersion, 1); }); // 7. Field-test session identity capture test('7. Field test session captures immutable software identity on startSession', () async { const rideId = 777; logger.startSession(rideId, notes: 'Field test readiness session'); expect(logger.sessionIdentity.semanticVersion, '1.0.0'); expect(logger.sessionIdentity.buildNumber, 100); expect(logger.sessionIdentity.telemetrySchemaVersion, 5); await logger.flush(); final events = await (db.select(db.fieldTestEvents)..where((e) => e.rideId.equals(rideId))).get(); final startEvent = events.firstWhere((e) => e.eventType == 'RIDE_STARTED'); expect(startEvent.message.contains('1.0.0+100'), isTrue); final meta = jsonDecode(startEvent.metadata!); expect(meta['software']['version'], '1.0.0'); expect(meta['software']['build'], 100); }); // 8. Export metadata verification test('8. Forensic export JSON contains comprehensive software identity object', () async { const rideId = 888; logger.startSession(rideId); final sample = TelemetrySample( timestamp: DateTime(2026, 8, 28, 9, 0, 0), speed: 45.0, latitude: 37.7749, longitude: -122.4194, gpsAccuracy: 3.0, ); logger.processTelemetrySample(sample, TelemetryValidator().validate(sample)); await logger.flush(); final jsonString = await exportService.generateFieldTestSessionJson(rideId); final Map data = jsonDecode(jsonString); expect(data['software'], isNotNull); expect(data['software']['version'], '1.0.0'); expect(data['software']['build'], 100); expect(data['software']['telemetrySchemaVersion'], 5); expect(data['software']['exportSchemaVersion'], 1); }); // 9. Replay compatibility test('9. Replay engine evaluates export schema compatibility safely', () { expect(AppIdentity.checkExportCompatibility(1), ExportCompatibility.supported); expect(AppIdentity.checkExportCompatibility(2), ExportCompatibility.unsupported); expect(AppIdentity.checkExportCompatibility(0), ExportCompatibility.unknown); }); // 10. Version metadata consistency test('10. AppInfo and AppIdentity maintain consistent fallback and single source of truth', () { final identity = AppInfo.identity; expect(AppInfo.fallbackVersion, '1.0.0+100'); expect(identity.semanticVersion, '1.0.0'); expect(identity.buildNumber, 100); }); // 11. Release manifest generation test('11. Release manifest contains all required metadata attributes', () { final manifest = AppIdentity.current().generateReleaseManifest(); expect(manifest['application'], 'MotorSignal'); expect(manifest['version'], '1.0.0'); expect(manifest['build'], 100); expect(manifest['gitCommit'], isNotEmpty); expect(manifest['telemetrySchema'], 5); expect(manifest['exportSchema'], 1); expect(manifest['buildTimestamp'], isNotEmpty); }); // 12. Existing exports remaining readable test('12. Legacy export payloads without explicit software block remain readable', () async { final legacyExportJson = jsonEncode({ 'exportVersion': '1.0.0', 'generatedAt': '2026-08-28T04:00:00Z', 'rideMetadata': {'id': 1}, 'events': [ { 'timestamp': '2026-08-28T04:00:00.000Z', 'latitude': 37.7749, 'longitude': -122.4194, 'speed': 35.0, 'gpsAccuracy': 3.0, } ] }); final result = await replayService.replaySessionJson(legacyExportJson); expect(result.totalSamples, 1); expect(result.validSamples, 1); expect(result.exportCompatibility, ExportCompatibility.supported); }); }); }