[AZ-650] mission_executor pre-flight BIT (F9) gate (batch 8)

AZ-650 (mission_executor pre-flight Built-In Test):
- BitEvaluator trait + BitItemStatus { Pass, Degraded, Fail, Skipped }
  + BitReport + BitOverall fusion. Pluggable per-item evaluators so
  the composition root decides which dependencies are wired today.
- BitController owns evaluator list + mpsc ack channel + sticky-pass
  + ack deadline. Publishes bit_ok via tokio watch — composition root
  pipes it into the telemetry projection where the existing FSM
  bit_ok guard already consumes it (no FSM changes needed).
- BitState { Idle, Pass, AwaitingAck { report_id }, Failed { reason } }
  with broadcast::Sender<BitEvent> for operator-side observability.
  Sticky-pass semantics: once Pass is reached (directly or via signed
  ack on a Degraded report), the controller stops re-evaluating —
  BIT is a one-shot pre-flight gate, not a continuous monitor.
- BitDegradedAck arrives pre-validated by operator_bridge; the
  controller only matches report_id and applies the operator id to
  the audit log.
- Concrete evaluators landed today (3 of 12 spec items, the rest
  depend on components still in todo/):
  - StateDirFreeSpaceEvaluator (dir creatable/readable; statvfs is
    documented follow-up).
  - WallClockBoundEvaluator (chrono::Utc::now vs configurable bound).
  - MissionLoadedEvaluator (waypoint count via Arc<Mutex<usize>>).
  - MapObjectsSyncedEvaluator (maps SyncState -> BIT status per Q9).

Tests:
- ac1_all_pass_proceeds, ac2_fail_blocks_transition,
  ac3_degraded_requires_signed_ack (+ mismatched_ack supplement),
  ac4_degraded_ack_timeout_fails_the_bit — all 4 ACs green.
- Pure next_state table covered by lib unit tests.
- Per-evaluator unit tests for Pass/Fail/Degraded branches.

Quality gates:
- cargo fmt: clean.
- cargo clippy -p mission_executor --tests -- -D warnings: 0 warns.
- cargo test --workspace: all green.
- Pre-existing flake in state_machine::ac3_bounded_retry_then_success
  (batch 7 report) remains pre-existing — passes on rerun.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-19 19:12:48 +03:00
parent 2bcd4a8059
commit 8a4bd00526
15 changed files with 1373 additions and 47 deletions
@@ -208,7 +208,10 @@ fn ac4_mavlink_loss_does_not_trigger_autopilot_rtl() {
target_follow_active: false,
});
// Assert — never fire while mavlink is down
assert!(!out.rtl_should_fire, "rtl fired at +{ms} ms with mavlink down");
assert!(
!out.rtl_should_fire,
"rtl fired at +{ms} ms with mavlink down"
);
last_state = out.state;
}
// Assert
@@ -328,10 +331,7 @@ impl MissionDriver for AutoDriver {
/// Drive the executor through telemetry until it reaches `FlyMission`.
/// Uses real time with a short tick interval so the test finishes in
/// well under a second.
async fn drive_to_fly_mission(
handle: &MissionExecutorHandle,
tel_tx: &watch::Sender<Telemetry>,
) {
async fn drive_to_fly_mission(handle: &MissionExecutorHandle, tel_tx: &watch::Sender<Telemetry>) {
// mission_reached_final stays false so the FSM idles in FlyMission.
let t = Telemetry {
link_up: true,
@@ -408,8 +408,7 @@ async fn ac2_driver_issues_rtl_once_and_transitions_fsm() {
// Arrange — spawn the lost-link driver with fast thresholds
let spy = Arc::new(SpyCommandIssuer::default());
let (op_tx, op_rx) = watch::channel(true);
let (mavlink_events_tx, mavlink_events_rx) =
broadcast::channel::<mavlink_layer::LinkEvent>(8);
let (mavlink_events_tx, mavlink_events_rx) = broadcast::channel::<mavlink_layer::LinkEvent>(8);
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let driver = LostLinkDriver::new(
@@ -432,7 +431,10 @@ async fn ac2_driver_issues_rtl_once_and_transitions_fsm() {
break;
}
if StdInstant::now() >= deadline {
panic!("RTL never fired within 2 s; ladder state={:?}", ladder_handle.state().await);
panic!(
"RTL never fired within 2 s; ladder state={:?}",
ladder_handle.state().await
);
}
tokio::time::sleep(Duration::from_millis(5)).await;
}