test(v0.4): integration — thermal-trip/abort + MCU fail-safe-cut (session-шина)

+ фикс B09-таймера: last_heartbeat Option<u64> вместо сентинела 0 (monotonic_secs() стартует
с 0 → первый heartbeat попадал на 0, guard !=0 ложно трактовал как «не было» → cut не срабатывал).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
2026-06-25 15:42:20 +03:00
parent cd2442f672
commit 32ba1136c7
2 changed files with 104 additions and 6 deletions
@@ -104,3 +104,101 @@ async fn shutdown_imminent_then_abort() {
aborted.next().await.unwrap();
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
}
#[tokio::test]
#[ignore = "нужна session-шина: just test-integration"]
async fn thermal_trip_then_clear() {
let svc = PowerService::new();
let fsm = svc.fsm_handle();
let thermal_state = svc.thermal_state_handle();
let temp = Arc::new(MockTempSource::new(20));
let copro = Arc::new(MockCoprocessor::new());
let server = zbus::Connection::session().await.unwrap();
server
.object_server()
.at(names::power::PATH, svc)
.await
.unwrap();
server.request_name(names::power::NAME).await.unwrap();
let iface = server
.object_server()
.interface::<_, PowerService>(names::power::PATH)
.await
.unwrap();
let ctx = iface.signal_context().to_owned();
shturman_power::service::spawn_loops(
fsm,
thermal_state,
temp.clone() as Arc<dyn shturman_power::thermal::TempSource>,
Arc::new(shturman_power::thermal::NoopThrottler::default())
as Arc<dyn shturman_power::thermal::Throttler>,
copro as Arc<dyn shturman_power::coprocessor::Coprocessor>,
ctx,
);
let client = zbus::Connection::session().await.unwrap();
let power = PowerClient::new(&client).await.unwrap();
let mut imminent = power.proxy().receive_shutdown_imminent().await.unwrap();
let mut aborted = power.proxy().receive_shutdown_aborted().await.unwrap();
// перегрев → ShutdownImminent(thermal)
temp.set(99);
let sig = imminent.next().await.unwrap();
assert_eq!(sig.args().unwrap().reason(), "thermal");
assert_eq!(power.power_state().await.unwrap(), PowerState::ShuttingDown);
// остыло до PONR → ShutdownAborted + running
temp.set(20);
aborted.next().await.unwrap();
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
}
#[tokio::test]
#[ignore = "нужна session-шина: just test-integration"]
async fn mcu_failsafe_cuts_on_hang() {
let svc = PowerService::new();
let fsm = svc.fsm_handle();
let thermal_state = svc.thermal_state_handle();
let temp = Arc::new(MockTempSource::new(20));
let copro = Arc::new(MockCoprocessor::new());
let server = zbus::Connection::session().await.unwrap();
server
.object_server()
.at(names::power::PATH, svc)
.await
.unwrap();
server.request_name(names::power::NAME).await.unwrap();
let iface = server
.object_server()
.interface::<_, PowerService>(names::power::PATH)
.await
.unwrap();
let ctx = iface.signal_context().to_owned();
shturman_power::service::spawn_loops(
fsm,
thermal_state,
temp as Arc<dyn shturman_power::thermal::TempSource>,
Arc::new(shturman_power::thermal::NoopThrottler::default())
as Arc<dyn shturman_power::thermal::Throttler>,
copro.clone() as Arc<dyn shturman_power::coprocessor::Coprocessor>,
ctx,
);
let client = zbus::Connection::session().await.unwrap();
let power = PowerClient::new(&client).await.unwrap();
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
// дать coproc-циклу послать ≥1 heartbeat (иначе last_heartbeat=0 и guard не даст cut)
tokio::time::sleep(std::time::Duration::from_millis(1300)).await;
copro.hang(); // SoC завис → heartbeat не освежает таймер
// ждём, пока coproc-цикл (HEARTBEAT=1с) накопит > FAILSAFE_MISS окон и сделает FailsafeCut
for _ in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(700)).await;
if power.power_state().await.unwrap() == PowerState::Off {
break;
}
}
assert_eq!(power.power_state().await.unwrap(), PowerState::Off);
}