MotorSignal-App/test/versioning_test.dart

192 lines
7.6 KiB
Dart

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<String, dynamic> 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);
});
});
}