Files
PilotVault/Fly Agent App/test/session_test.dart
T
tajniak81andClaude Opus 5.5 bc41d8b128 Split the automatic logging path by the app that logged the flight
logging_path is now manual, automatic_flyapp or automatic_flyagentapp. The
Fly App's entries are backed by the aircraft's flight recorder and keep the
missing-FDR red flag; the Fly Agent App's come from Remote ID, which is not a
flight recorder, so they must carry the § 5 fields like a manual entry. The API
maps the old "automatic" onto automatic_flyapp so Fly App builds already on
devices keep filing. The migration has been applied to the remote PocketBase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 19:36:16 +02:00

98 lines
3.7 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:pilotvault_fly_agent/session/flight_session.dart';
void main() {
final DateTime t0 = DateTime(2026, 10, 22, 14, 5);
FixSample fix(int s, {bool airborne = true, double height = 30, double lat = 55.0, double lon = 12.0}) =>
FixSample(at: t0.add(Duration(seconds: s)), airborne: airborne, lat: lat, lon: lon, height: height, speed: 3);
FlightSession armed() => FlightSession()
..droneId = 'd1'
..ridSerial = 'SN1'
..arm(t0);
test('armed session starts on the first airborne fix', () {
final FlightSession s = armed();
expect(s.onFix(fix(0, airborne: false, height: 0)), isFalse);
expect(s.phase, SessionPhase.armed);
expect(s.onFix(fix(5)), isTrue);
expect(s.phase, SessionPhase.active);
expect(s.autoStarted, isTrue);
expect(s.startedAt, t0.add(const Duration(seconds: 5)));
});
test('stops at touchdown after the landed hold, not at the end of the hold', () {
final FlightSession s = armed();
s.onFix(fix(0));
s.onFix(fix(10, airborne: false, height: 0));
expect(s.onFix(fix(20, airborne: false, height: 0)), isFalse);
expect(s.onFix(fix(26, airborne: false, height: 0)), isTrue);
expect(s.phase, SessionPhase.landed);
expect(s.endedAt, t0.add(const Duration(seconds: 10)));
});
test('a brief low hover does not end the flight', () {
final FlightSession s = armed();
s.onFix(fix(0));
s.onFix(fix(10, airborne: false, height: 0.5));
s.onFix(fix(20, airborne: true));
s.onFix(fix(30, airborne: false, height: 0));
s.onFix(fix(40, airborne: false, height: 0));
expect(s.phase, SessionPhase.active);
});
test('Remote ID silence stops the flight at last contact', () {
final FlightSession s = armed();
s.onFix(fix(0));
s.onFix(fix(30));
expect(s.onTick(t0.add(const Duration(seconds: 60))), isFalse);
expect(s.onTick(t0.add(const Duration(seconds: 91))), isTrue);
expect(s.endedAt, t0.add(const Duration(seconds: 30)));
expect(s.alerts.single.code, 'rid_lost');
});
test('tracks max height, distance and a single height alert', () {
final FlightSession s = armed();
s.onFix(fix(0, height: 50, lat: 55.0));
s.onFix(fix(1, height: 125, lat: 55.001));
s.onFix(fix(2, height: 130, lat: 55.002));
expect(s.maxHeight, 130);
expect(s.distance, closeTo(222, 2));
expect(s.alerts.where((SessionAlert a) => a.code == 'height').length, 1);
});
test('flight input carries the § 5 manual fields and a GeoJSON route', () {
final FlightSession s = armed()
..purpose = 'commercial'
..category = 'open'
..weather = '12°C';
s.onFix(fix(0, lat: 55.0));
s.onFix(fix(60, lat: 55.001, height: 80));
s.stop(t0.add(const Duration(minutes: 12)));
final Map<String, dynamic> j = s.toFlightInput();
expect(j['operationDate'], '2026-10-22');
expect(j['startTime'], '14:05');
expect(j['endTime'], '14:17');
expect(j['drone'], 'd1');
expect(j['maxAltitudeAgl'], 80.0);
expect(j['loggingPath'], 'automatic_flyagentapp');
expect((j['areaRoute'] as String), contains('Remote ID track'));
expect(j['routeGeojson']['type'], 'LineString');
expect((j['routeGeojson']['coordinates'] as List).first, <double>[12.0, 55.0, 30.0]);
expect(j['notes'], contains('serial SN1'));
});
test('survives a round trip through persistence', () {
final FlightSession s = armed();
s.onFix(fix(0));
s.onFix(fix(30, height: 140, lat: 55.001));
final FlightSession r = FlightSession.decode(s.encode());
expect(r.phase, SessionPhase.active);
expect(r.track.length, 2);
expect(r.maxHeight, 140);
expect(r.alerts.single.code, 'height');
expect(r.startedAt, s.startedAt);
});
}