feat(ground-suite): add replay observability console

This commit is contained in:
Onur Özkan committed 2026-08-10 15:24:39 +03:00
1 parent 1d28db2983
commit c06c293f49
33 files changed
+2636

No files matched your search

@@ -0,0 +1,30 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:ground_suite/core/observations/observed_event.dart';
import 'package:ground_suite/core/observations/vehicle_data_update.dart';
import 'package:ground_suite/core/transport/replay_vehicle_data_port.dart';
void main() {
test('replay covers identity, telemetry, log, error, and shutdown', () async {
const port = ReplayVehicleDataPort();
final events = [
for (final update in await port.observe().toList())
if (update case ObservationReceived(:final event)) event,
];
expect(events.whereType<HeartbeatEvent>(), hasLength(1));
expect(events.whereType<TelemetryEvent>(), hasLength(2));
final logs = events.whereType<LogEvent>().toList();
final errors = events.whereType<ErrorEvent>().toList();
expect(logs, hasLength(2));
expect(errors, hasLength(1));
expect(events.whereType<ShutdownEvent>(), hasLength(1));
expect(logs.first.isCoalesced, isTrue);
expect(errors.single.isTruncated, isTrue);
});
test('unknown wire enum values remain visible', () {
expect(const MavSeverity(99).label, 'Unknown (99)');
expect(const KaptanShutdownReason(99).label, 'Unknown (99)');
});
}
@@ -0,0 +1,112 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ground_suite/core/observations/observed_event.dart';
import 'package:ground_suite/core/observations/vehicle_data_port.dart';
import 'package:ground_suite/core/observations/vehicle_data_update.dart';
import 'package:ground_suite/features/observability/presentation/observability_controller.dart';
void main() {
test('coalesces rapid critical accessibility announcements', () async {
final port = _ControlledDataPort();
final container = ProviderContainer(
overrides: [vehicleDataPortProvider.overrideWithValue(port)],
);
addTearDown(container.dispose);
container.read(observabilityControllerProvider);
await Future<void>.delayed(Duration.zero);
port.events.add(
ObservationReceived(
ErrorEvent(
receivedAt: DateTime.utc(2026, 8, 10),
path: 'kaptan_drivers::lidar_lite_v3::error::Error::TransferFault',
),
),
);
await Future<void>.delayed(Duration.zero);
expect(
container.read(observabilityControllerProvider).accessibilityAnnouncement,
'Error received: kaptan_drivers::lidar_lite_v3::error::Error::TransferFault',
);
port.events.add(
ObservationReceived(
ShutdownEvent(
receivedAt: DateTime.utc(2026, 8, 10, 0, 0, 1),
reason: KaptanShutdownReason.safety,
),
),
);
await Future<void>.delayed(Duration.zero);
final state = container.read(observabilityControllerProvider);
expect(state.accessibilityAnnouncement, isNull);
expect(state.coalescedAlertCount, 1);
});
test(
'retains expected parser and semantic drops without ending observation',
() async {
final port = _ControlledDataPort();
final container = ProviderContainer(
overrides: [vehicleDataPortProvider.overrideWithValue(port)],
);
addTearDown(container.dispose);
container.read(observabilityControllerProvider);
await Future<void>.delayed(Duration.zero);
port.events.add(
const ObservationDropped(kind: ObservationDropKind.parser, count: 2),
);
port.events.add(
const ObservationDropped(kind: ObservationDropKind.semantic),
);
await Future<void>.delayed(Duration.zero);
final state = container.read(observabilityControllerProvider);
expect(state.status, ObservationStatus.receiving);
expect(state.parserDropCount, 2);
expect(state.semanticDropCount, 1);
},
);
test('treats a transport stream failure as a disconnected link', () async {
final port = _ControlledDataPort();
final container = ProviderContainer(
overrides: [vehicleDataPortProvider.overrideWithValue(port)],
);
addTearDown(container.dispose);
container.read(observabilityControllerProvider);
await Future<void>.delayed(Duration.zero);
port.events.addError(StateError('serial endpoint closed'));
await Future<void>.delayed(Duration.zero);
final state = container.read(observabilityControllerProvider);
expect(state.status, ObservationStatus.disconnected);
expect(state.parserDropCount, 0);
expect(state.semanticDropCount, 0);
});
}
class _ControlledDataPort implements VehicleDataPort {
final events = StreamController<VehicleDataUpdate>();
@override
bool get isReplay => false;
@override
String get label => 'Test transport';
@override
Future<void> close() => events.close();
@override
Stream<VehicleDataUpdate> observe() => events.stream;
}
+45
View File
@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:ground_suite/app/app.dart';
void main() {
testWidgets('replay workspace keeps diagnostics distinct and read-only', (
tester,
) async {
await tester.pumpWidget(const ProviderScope(child: GroundSuiteApp()));
await tester.pumpAndSettle();
expect(find.text('Observation only'), findsOneWidget);
expect(find.text('Replay fixture'), findsAtLeastNWidgets(1));
expect(find.text('Vehicle identity'), findsOneWidget);
await tester.tap(find.byIcon(Icons.receipt_long_outlined));
await tester.pumpAndSettle();
expect(find.text('Diagnostics'), findsAtLeastNWidgets(1));
expect(
find.text('kaptan_drivers::lidar_lite_v3::error::Error::TransferFault'),
findsOneWidget,
);
await tester.tap(find.text('Shutdowns'));
await tester.pumpAndSettle();
expect(find.text('Orderly shutdown observed'), findsOneWidget);
});
testWidgets(
'compact desktop layout uses bottom navigation without overflow',
(tester) async {
await tester.binding.setSurfaceSize(const Size(480, 800));
addTearDown(() => tester.binding.setSurfaceSize(null));
await tester.pumpWidget(const ProviderScope(child: GroundSuiteApp()));
await tester.pumpAndSettle();
expect(find.byType(NavigationBar), findsOneWidget);
expect(tester.takeException(), isNull);
},
);
}