feat(ground-suite): add replay observability console
This commit is contained in:
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;
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user