Files
kaptan/ground-suite/test/features/observability/observability_controller_test.dart
T

175 lines
5.4 KiB
Dart

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);
});
test(
'bounds diagnostics while telemetry stays outside the timeline',
() 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',
),
),
);
port.events.add(
ObservationReceived(
HeartbeatEvent(
receivedAt: DateTime.utc(2026, 8, 10, 0, 0, 1),
systemId: 7,
componentId: 42,
armed: false,
vehicleState: 'Standby',
),
),
);
for (var index = 0; index < 501; index++) {
port.events.add(
ObservationReceived(
TelemetryEvent(
receivedAt: DateTime.utc(2026, 8, 10, 0, 0, index % 60),
kind: TelemetryKind.rawImu,
),
),
);
}
for (var index = 0; index < 500; index++) {
port.events.add(
ObservationReceived(
LogEvent(
receivedAt: DateTime.utc(2026, 8, 10, 0, 1, index % 60),
level: MavSeverity.info,
message: 'Diagnostic $index',
),
),
);
}
final state = container.read(observabilityControllerProvider);
expect(state.events, hasLength(500));
expect(state.localTimelineEvictionCount, 1);
expect(state.events.whereType<ErrorEvent>(), hasLength(1));
expect(state.lastHeartbeat?.systemId, 7);
expect(state.observedTelemetryKinds, contains(TelemetryKind.rawImu));
},
);
}
class _ControlledDataPort implements VehicleDataPort {
final events = StreamController<VehicleDataUpdate>(sync: true);
@override
bool get isReplay => false;
@override
String get label => 'Test transport';
@override
Future<void> close() => events.close();
@override
Stream<VehicleDataUpdate> observe() => events.stream;
}