[AZ-680] [AZ-681] operator_bridge command dispatch + safety lane

Add the operator-command dispatcher behind a typed CommandAck:
60 s per-command-id idempotency cache, surfaced-POI registry with
unknown_poi_id + expired gates, BIT-degraded ack severity check, and
SafetyOverride forwarding to mission_executor with structured audit
log (redacts signature + session_token).

Cross-layer wiring goes through three new traits in shared::contracts
(ScanCommandRouter, MissionSafetyRouter, BitReportSeverityLookup) so
operator_bridge stays free of direct scan_controller / mission_executor
imports. scan_controller::ScanControllerHandle implements the scan
router; a new mission_executor::SafetyDispatchHandle wraps the BIT
ack channel + battery monitor handle and implements the safety router;
BitControllerHandle gains a bounded (16-entry) report-severity cache
for the lookup trait.

scan_controller also picks up ConfirmPoi handling: PoiQueue::confirm
removes the entry and SubmitOutcome::Confirmed carries the typed
(target_mgrs, target_class) hint for AZ-684/AZ-686 downstream.

Tests: 9 new integration tests in operator_bridge/tests/dispatcher.rs
cover AZ-680 AC-1..AC-5 + AZ-681 AC-1..AC-4. scan_controller adds 2
ConfirmPoi tests. All modified-crate suites green; one pre-existing
mission_executor state-machine test flake (already documented in
_docs/_process_leftovers) updated to note ac1 also affected.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-20 17:32:38 +03:00
parent aa4282f9f8
commit c4eff40dbc
24 changed files with 2017 additions and 53 deletions
+67 -1
View File
@@ -153,7 +153,73 @@ async fn decline_poi_via_operator_command_emits_action() {
assert_eq!(action.mgrs, "decline-me");
assert_eq!(action.class_group, "armor");
}
SubmitOutcome::Accepted => panic!("decline must return Declined action"),
other => panic!("decline must return Declined action, got {other:?}"),
}
assert_eq!(h.poi_queue_len().await, 0);
}
/// AZ-680 — ConfirmPoi via operator command returns
/// `SubmitOutcome::Confirmed` with the typed target hint and drains
/// the POI from the queue.
#[tokio::test]
async fn confirm_poi_via_operator_command_emits_action() {
// Arrange
let h = ScanController::new().handle();
let p = poi(0.8, "confirm-me");
let id = p.id;
let expected_class = p.class.clone();
let expected_group = p.class_group.clone();
h.submit_poi_candidate(p, 0.5).await;
let cmd = OperatorCommand {
command_id: Uuid::new_v4(),
session_token: "s".to_string(),
sequence_number: 1,
issued_at_wallclock: Utc::now(),
kind: OperatorCommandKind::ConfirmPoi,
payload: json!({ "poi_id": id.to_string() }),
signature: vec![],
};
// Act
let outcome = h.submit_operator_cmd(cmd).await.expect("confirm accepted");
// Assert
match outcome {
SubmitOutcome::Confirmed(action) => {
assert_eq!(action.poi_id, id);
assert_eq!(action.target_mgrs, "confirm-me");
assert_eq!(action.target_class, expected_class);
assert_eq!(action.class_group, expected_group);
}
other => panic!("confirm must return Confirmed action, got {other:?}"),
}
assert_eq!(h.poi_queue_len().await, 0);
}
/// AZ-680 — ConfirmPoi for an unknown poi_id must NOT silently
/// succeed. Returns a `Validation` error so `operator_bridge` can
/// surface a typed NACK to the operator UI.
#[tokio::test]
async fn confirm_poi_unknown_id_is_validation_error() {
// Arrange
let h = ScanController::new().handle();
let cmd = OperatorCommand {
command_id: Uuid::new_v4(),
session_token: "s".to_string(),
sequence_number: 1,
issued_at_wallclock: Utc::now(),
kind: OperatorCommandKind::ConfirmPoi,
payload: json!({ "poi_id": Uuid::new_v4().to_string() }),
signature: vec![],
};
// Act
let err = h
.submit_operator_cmd(cmd)
.await
.expect_err("unknown poi must error");
// Assert
assert!(matches!(err, shared::error::AutopilotError::Validation(_)));
}