mirror of
https://github.com/azaion/autopilot.git
synced 2026-06-21 12:41:10 +00:00
[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:
@@ -195,10 +195,7 @@ impl JsonSnapshotEngine {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_snapshot_inner(
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<Option<Snapshot>, PersistenceError> {
|
||||
async fn load_snapshot_inner(&self, path: &Path) -> Result<Option<Snapshot>, PersistenceError> {
|
||||
let bytes = match fs::read(path).await {
|
||||
Ok(b) => b,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
|
||||
@@ -504,21 +504,25 @@ impl Store {
|
||||
let mut store = Self::new(config);
|
||||
for mo in snapshot.map_objects {
|
||||
let cell = cell_of(mo.gps_lat, mo.gps_lon, store.config.h3_resolution)?;
|
||||
store.by_cell.entry(cell).or_default().push(StoredMapObject {
|
||||
id: mo.id,
|
||||
h3_cell: cell,
|
||||
mgrs: mo.mgrs,
|
||||
class: mo.class,
|
||||
class_group: mo.class_group,
|
||||
gps_lat: mo.gps_lat,
|
||||
gps_lon: mo.gps_lon,
|
||||
size_width_m: mo.size_width_m,
|
||||
size_length_m: mo.size_length_m,
|
||||
confidence: mo.confidence,
|
||||
first_seen: mo.first_seen,
|
||||
last_seen: mo.last_seen,
|
||||
mission_id: mo.mission_id,
|
||||
});
|
||||
store
|
||||
.by_cell
|
||||
.entry(cell)
|
||||
.or_default()
|
||||
.push(StoredMapObject {
|
||||
id: mo.id,
|
||||
h3_cell: cell,
|
||||
mgrs: mo.mgrs,
|
||||
class: mo.class,
|
||||
class_group: mo.class_group,
|
||||
gps_lat: mo.gps_lat,
|
||||
gps_lon: mo.gps_lon,
|
||||
size_width_m: mo.size_width_m,
|
||||
size_length_m: mo.size_length_m,
|
||||
confidence: mo.confidence,
|
||||
first_seen: mo.first_seen,
|
||||
last_seen: mo.last_seen,
|
||||
mission_id: mo.mission_id,
|
||||
});
|
||||
store.len += 1;
|
||||
}
|
||||
for item in snapshot.ignored_items {
|
||||
|
||||
@@ -90,7 +90,8 @@ async fn ac1_snapshot_reload_round_trip() {
|
||||
.await
|
||||
.expect("load ok")
|
||||
.expect("file present");
|
||||
let restored = MapObjectsStore::from_snapshot(MapObjectsStoreConfig::default(), loaded).unwrap();
|
||||
let restored =
|
||||
MapObjectsStore::from_snapshot(MapObjectsStoreConfig::default(), loaded).unwrap();
|
||||
let rh = restored.handle();
|
||||
|
||||
// Assert — counts match and pending log survived
|
||||
@@ -175,8 +176,7 @@ async fn ac3_crash_recovery_loads_pending() {
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("snapshot present");
|
||||
let recovered =
|
||||
MapObjectsStore::from_snapshot(MapObjectsStoreConfig::default(), snap).unwrap();
|
||||
let recovered = MapObjectsStore::from_snapshot(MapObjectsStoreConfig::default(), snap).unwrap();
|
||||
|
||||
// Assert — pending log matches pre-crash count
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user