docs(v0.3): план реализации power-safe (План 7)

P7.1 чистый PowerFsm (TDD, все переходы) → P7.2 Power-сервис на FSM
(dev-mock кормит события, grace-таймер + durable-barrier sync, integration abort) →
P7.3 watchdog-конфиг + save-time timer → P7.4 lima/E2E блок (N=3 цикла + abort +
power-cut-сим) → P7.5 verify Lima + acceptance + швы. Полный код, без плейсхолдеров.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
2026-06-24 20:58:38 +03:00
parent 4fe5103e88
commit 598070de96
+643
View File
@@ -0,0 +1,643 @@
# План 7 — v0.3 Power-safe ядро (FSM + graceful shutdown)
> REQUIRED SUB-SKILL: `executing-plans` (или `subagent-driven-development`) + **TDD**. Спека: `docs/specs/v0.3-power-safe.md`.
> Шаги — чекбоксы `- [ ]`. P7.4/P7.5 — тяжёлые (Lima); VM уже поднята.
**Goal:** стаб Power → реальный lifecycle-FSM: ACC → graceful shutdown с durable-write до PONR → переживание срыва питания.
**Architecture:** чистый `PowerFsm` (состояния/события/действия, юнит-тестируемый) в `shturman-power`; сервис оборачивает его
(D-Bus state/signals из FSM, dev-mock кормит события, grace-таймер на монотонике, durable-barrier `sync` на commit).
Teardown/unmount — через systemd (реальный poweroff) / харнесс (in-VM-цикл). Подход A спеки §6.
**Tech Stack:** Rust, zbus 4 (signals/properties), tokio (grace-таймер), systemd (watchdog/savetime), Lima E2E (bash).
---
## File Structure
- **Create** `crates/core/shturman-power/src/fsm.rs``PowerFsm` (State/Event/Action/step) + проекции в `PowerState`/`IgnitionState`/`PowerSource`.
- **Modify** `crates/core/shturman-power/src/lib.rs``pub mod fsm;`.
- **Modify** `crates/core/shturman-power/src/service.rs` — обернуть FSM: D-Bus из FSM; dev-mock кормит события; `apply_event` (grace-таймер + durable-barrier).
- **Create** `systemd/watchdog-shturman.conf``RuntimeWatchdogSec`/`RebootWatchdogSec` (system.conf.d).
- **Create** `systemd/shturman-savetime.service` + `systemd/shturman-savetime.timer` (B07 periodic save).
- **Modify** `lima/shturman.yaml` (разложить watchdog/savetime), `tests/e2e/run.sh` (блок power-safe).
- **Modify (P7.5)** `docs/domains/b-power-lifecycle.md`, `docs/contracts/ipc.md`, `docs/specs/v0.1-v0.6-foundation.md` §5.2, `CLAUDE.md`.
---
## P7.1: `PowerFsm` — чистый FSM питания (B03)
**Files:** Create `crates/core/shturman-power/src/fsm.rs`; Modify `lib.rs`.
- [ ] **Шаг 1 — реализация** `crates/core/shturman-power/src/fsm.rs` (тесты — в этом же файле, шаг 2):
```rust
//! Чистый FSM питания (B03, спека v0.3 §5). Без D-Bus/async/I/O — сервис исполняет `Action`.
use shturman_ipc::types::{IgnitionState, PowerSource, PowerState, ShutdownReason};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Phase {
Abortable,
Committed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum State {
Off,
Accessory,
Running,
ShuttingDown { phase: Phase, reason: ShutdownReason },
Sleep, // зарезервировано (полные sleep/wake — v1/v2)
BatteryCutoff, // зарезервировано (long-park — v1/v2)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Event {
AccOn,
AccOff,
EngineOn,
EngineOff,
UnderVoltage,
ThermalTrip,
GraceExpired,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
ShutdownImminent(ShutdownReason),
ShutdownAborted,
AccChanged(bool),
StartGrace, // сервис запускает grace-таймер (длительность — конфиг сервиса)
Commit, // durable-barrier (sync) → PONR
}
/// FSM питания. v0: старт в `Running` (как стаб v0.1). Чистый: `step` без I/O.
pub struct PowerFsm {
state: State,
}
impl Default for PowerFsm {
fn default() -> Self {
Self { state: State::Running }
}
}
impl PowerFsm {
pub fn new() -> Self {
Self::default()
}
pub fn state(&self) -> State {
self.state
}
pub fn power_state(&self) -> PowerState {
match self.state {
State::Off => PowerState::Off,
State::Accessory => PowerState::Accessory,
State::Running => PowerState::Running,
State::ShuttingDown { .. } => PowerState::ShuttingDown,
State::Sleep => PowerState::Sleep,
State::BatteryCutoff => PowerState::BatteryCutoff,
}
}
pub fn ignition(&self) -> IgnitionState {
match self.state {
State::Running => IgnitionState::Running,
State::Accessory => IgnitionState::Accessory,
_ => IgnitionState::Off,
}
}
pub fn source(&self) -> PowerSource {
match self.state {
State::ShuttingDown { reason: ShutdownReason::UnderVoltage, .. } => PowerSource::LowBattery,
State::ShuttingDown { .. } => PowerSource::HoldupCap,
_ => PowerSource::Vehicle12v,
}
}
/// Шаг FSM. Возвращает действия для исполнения сервисом (спека §5).
pub fn step(&mut self, ev: Event) -> Vec<Action> {
use Event as E;
use Phase::*;
use State::*;
match (self.state, ev) {
(Off, E::AccOn) => {
self.state = Accessory;
vec![Action::AccChanged(true)]
}
(Accessory, E::EngineOn) => {
self.state = Running;
vec![]
}
(Running, E::EngineOff) => {
self.state = Accessory;
vec![]
}
(Accessory | Running, E::AccOff) => self.begin_shutdown(ShutdownReason::AccOff),
(Accessory | Running, E::UnderVoltage) => self.begin_shutdown(ShutdownReason::UnderVoltage),
(Accessory | Running, E::ThermalTrip) => self.begin_shutdown(ShutdownReason::Thermal),
(ShuttingDown { phase: Abortable, .. }, E::AccOn) => {
self.state = Running;
vec![Action::ShutdownAborted, Action::AccChanged(true)]
}
(ShuttingDown { phase: Abortable, reason }, E::GraceExpired) => {
self.state = ShuttingDown { phase: Committed, reason };
vec![Action::Commit]
}
// committed/off/sleep/battery_cutoff + всё прочее — no-op (инвариант: committed не abort-ится)
_ => vec![],
}
}
fn begin_shutdown(&mut self, reason: ShutdownReason) -> Vec<Action> {
self.state = State::ShuttingDown { phase: Phase::Abortable, reason };
vec![Action::ShutdownImminent(reason), Action::StartGrace]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn off_acc_on_to_accessory() {
let mut f = PowerFsm { state: State::Off };
assert_eq!(f.step(Event::AccOn), vec![Action::AccChanged(true)]);
assert_eq!(f.state(), State::Accessory);
}
#[test]
fn accessory_engine_on_to_running_and_back() {
let mut f = PowerFsm { state: State::Accessory };
assert_eq!(f.step(Event::EngineOn), vec![]);
assert_eq!(f.state(), State::Running);
assert_eq!(f.step(Event::EngineOff), vec![]);
assert_eq!(f.state(), State::Accessory);
}
#[test]
fn acc_off_begins_abortable_shutdown() {
let mut f = PowerFsm::new(); // Running
assert_eq!(
f.step(Event::AccOff),
vec![Action::ShutdownImminent(ShutdownReason::AccOff), Action::StartGrace]
);
assert_eq!(f.power_state(), PowerState::ShuttingDown);
assert_eq!(f.source(), PowerSource::HoldupCap);
}
#[test]
fn under_voltage_reason_and_source() {
let mut f = PowerFsm::new();
let a = f.step(Event::UnderVoltage);
assert_eq!(a[0], Action::ShutdownImminent(ShutdownReason::UnderVoltage));
assert_eq!(f.source(), PowerSource::LowBattery);
}
#[test]
fn abort_before_ponr() {
let mut f = PowerFsm::new();
f.step(Event::AccOff);
assert_eq!(
f.step(Event::AccOn),
vec![Action::ShutdownAborted, Action::AccChanged(true)]
);
assert_eq!(f.state(), State::Running);
}
#[test]
fn grace_expired_commits_and_is_irreversible() {
let mut f = PowerFsm::new();
f.step(Event::AccOff);
assert_eq!(f.step(Event::GraceExpired), vec![Action::Commit]);
// committed: abort игнорируется
assert_eq!(f.step(Event::AccOn), vec![]);
assert!(matches!(f.state(), State::ShuttingDown { phase: Phase::Committed, .. }));
}
#[test]
fn reserved_states_noop() {
let mut f = PowerFsm { state: State::Sleep };
assert_eq!(f.step(Event::AccOn), vec![]);
assert_eq!(f.state(), State::Sleep);
}
}
```
- [ ] **Шаг 2 — `lib.rs`:** добавить `pub mod fsm;`. (Прочитать `crates/core/shturman-power/src/lib.rs`, добавить строку рядом с другими `mod`.)
- [ ] **Шаг 3 — прогон.** Run: `cargo test -p shturman-power fsm`. Expected: PASS (7 тестов).
- [ ] **Шаг 4 — commit.**
```bash
git add crates/core/shturman-power/src/fsm.rs crates/core/shturman-power/src/lib.rs
git commit -s -m "feat(v0.3): чистый PowerFsm (состояния/переходы B03)"
```
---
## P7.2: обернуть FSM в сервис (D-Bus из FSM + dev-mock кормит события + grace + barrier)
**Files:** Modify `crates/core/shturman-power/src/service.rs`. Test: `crates/core/shturman-power/tests/integration.rs` (расширить).
- [ ] **Шаг 1 — переписать `service.rs`** (полностью — заменяет плоский `State` на FSM):
```rust
//! Server `ru.shturman.Power1` + (feature `dev-mocks`) `ru.shturman.dev.PowerMock1` (fake-ACC).
//! v0.3: оборачивает чистый `PowerFsm` (спека §5–§7). dev-mock кормит входы FSM (не флипает состояние).
use crate::fsm::{Action, Event, PowerFsm};
use shturman_common::monotonic_secs;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use zbus::interface;
use zbus::object_server::SignalContext;
/// Grace-окно (сек): и поле сигнала `ShutdownImminent`, и длительность таймера. v0.3 — фикс. (конфиг — позже).
const GRACE_SECS: u32 = 2;
pub struct PowerService {
fsm: Arc<Mutex<PowerFsm>>,
}
impl Default for PowerService {
fn default() -> Self {
Self { fsm: Arc::new(Mutex::new(PowerFsm::new())) }
}
}
impl PowerService {
pub fn new() -> Self {
Self::default()
}
pub fn power_state(&self) -> shturman_ipc::types::PowerState {
self.fsm.lock().unwrap().power_state()
}
pub fn ignition(&self) -> shturman_ipc::types::IgnitionState {
self.fsm.lock().unwrap().ignition()
}
pub fn source(&self) -> shturman_ipc::types::PowerSource {
self.fsm.lock().unwrap().source()
}
#[cfg(feature = "dev-mocks")]
pub fn mock(&self) -> PowerMock {
PowerMock { fsm: Arc::clone(&self.fsm) }
}
}
/// Шагнуть FSM и исполнить действия (эмит сигналов, grace-таймер, durable-barrier). Свободная функция —
/// чтобы её мог звать и dev-mock, и фоновый grace-таймер (с owned-контекстом).
async fn apply_event(
fsm: &Arc<Mutex<PowerFsm>>,
ev: Event,
ctx: &SignalContext<'_>,
) -> zbus::Result<()> {
let actions = fsm.lock().unwrap().step(ev);
for a in actions {
match a {
Action::ShutdownImminent(r) => {
PowerService::shutdown_imminent(ctx, GRACE_SECS, r.as_str()).await?
}
Action::ShutdownAborted => PowerService::shutdown_aborted(ctx).await?,
Action::AccChanged(on) => PowerService::acc_changed(ctx, on).await?,
Action::StartGrace => {
let fsm = Arc::clone(fsm);
let octx = ctx.to_owned();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(GRACE_SECS as u64)).await;
let _ = apply_event(&fsm, Event::GraceExpired, &octx).await;
});
}
Action::Commit => {
// Durable-write barrier (#5): сбросить грязные страницы /data ДО PONR. Settings уже синхронен.
let _ = std::process::Command::new("sync").status();
tracing::info!("power: commit (PONR) — durable barrier sync; load-shed: amp/backlight/modem (нет реальных нагрузок в v0)");
}
}
}
Ok(())
}
#[interface(name = "ru.shturman.Power1")]
impl PowerService {
async fn get_power_state(&self) -> String {
self.power_state().as_str().to_string()
}
/// Внутренний; sleep/wake — v1/v2 (B §7). В v0.3 — no-op.
async fn request_sleep(&self) {}
#[zbus(property)]
async fn ignition_state(&self) -> String {
self.ignition().as_str().to_string()
}
#[zbus(property)]
async fn uptime(&self) -> u64 {
monotonic_secs()
}
#[zbus(property)]
async fn power_source(&self) -> String {
self.source().as_str().to_string()
}
#[zbus(signal)]
async fn acc_changed(ctx: &SignalContext<'_>, on: bool) -> zbus::Result<()>;
#[zbus(signal)]
async fn shutdown_imminent(ctx: &SignalContext<'_>, seconds: u32, reason: &str) -> zbus::Result<()>;
#[zbus(signal)]
async fn shutdown_aborted(ctx: &SignalContext<'_>) -> zbus::Result<()>;
#[zbus(signal)]
async fn sleep(ctx: &SignalContext<'_>) -> zbus::Result<()>;
#[zbus(signal)]
async fn wake(ctx: &SignalContext<'_>) -> zbus::Result<()>;
}
/// dev-mock «fake-ACC/voltage/thermal» — кормит входы FSM. Прод (`--no-default-features`) не регистрирует.
#[cfg(feature = "dev-mocks")]
pub struct PowerMock {
fsm: Arc<Mutex<PowerFsm>>,
}
#[cfg(feature = "dev-mocks")]
#[interface(name = "ru.shturman.dev.PowerMock1")]
impl PowerMock {
async fn set_acc(&self, on: bool, #[zbus(signal_context)] ctx: SignalContext<'_>) {
let ev = if on { Event::AccOn } else { Event::AccOff };
let _ = apply_event(&self.fsm, ev, &ctx).await;
}
async fn set_ignition(&self, state: String, #[zbus(signal_context)] ctx: SignalContext<'_>) {
// accessory↔running — через EngineOn/Off; off — AccOff.
let ev = match state.as_str() {
"running" => Event::EngineOn,
"accessory" => Event::EngineOff,
_ => Event::AccOff,
};
let _ = apply_event(&self.fsm, ev, &ctx).await;
}
async fn trigger_shutdown(
&self,
_seconds: u32,
reason: String,
#[zbus(signal_context)] ctx: SignalContext<'_>,
) {
let ev = match reason.as_str() {
"thermal" => Event::ThermalTrip,
"under_voltage" => Event::UnderVoltage,
_ => Event::AccOff,
};
let _ = apply_event(&self.fsm, ev, &ctx).await;
}
async fn abort_shutdown(&self, #[zbus(signal_context)] ctx: SignalContext<'_>) {
let _ = apply_event(&self.fsm, Event::AccOn, &ctx).await;
}
}
#[cfg(test)]
mod tests {
use super::*;
use shturman_ipc::types::{IgnitionState, PowerState, PowerSource};
#[test]
fn defaults_running() {
let svc = PowerService::new();
assert_eq!(svc.power_state(), PowerState::Running);
assert_eq!(svc.ignition(), IgnitionState::Running);
assert_eq!(svc.source(), PowerSource::Vehicle12v);
}
}
```
- [ ] **Шаг 2 — прогон unit.** Run: `cargo test -p shturman-power`. Expected: PASS (fsm 7 + service 1).
- [ ] **Шаг 3 — расширить integration-тест** `crates/core/shturman-power/tests/integration.rs` — добавить тест abort (после существующего `power_state_and_fake_acc`):
```rust
#[tokio::test]
#[ignore = "нужна session-шина: just test-integration"]
async fn shutdown_imminent_then_abort() {
use futures_util::StreamExt;
let svc = PowerService::new();
let mock = svc.mock();
let server = zbus::Connection::session().await.unwrap();
server.object_server().at(names::power::PATH, svc).await.unwrap();
server.object_server().at(names::power::PATH, mock).await.unwrap();
server.request_name(names::power::NAME).await.unwrap();
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();
// ACC-off → ShutdownImminent(acc_off)
client.call_method(Some(names::power::NAME), names::power::PATH, Some(names::power::MOCK_IFACE), "SetAcc", &(false,)).await.unwrap();
let sig = imminent.next().await.unwrap();
assert_eq!(sig.args().unwrap().reason(), "acc_off");
assert_eq!(power.power_state().await.unwrap(), PowerState::ShuttingDown);
// re-power до grace → ShutdownAborted + running
client.call_method(Some(names::power::NAME), names::power::PATH, Some(names::power::MOCK_IFACE), "SetAcc", &(true,)).await.unwrap();
aborted.next().await.unwrap();
assert_eq!(power.power_state().await.unwrap(), PowerState::Running);
}
```
(`Power1Proxy` уже объявляет сигналы `shutdown_imminent`/`shutdown_aborted``crates/shturman-ipc/src/proxy.rs:29,31`; zbus генерит `receive_shutdown_imminent()`/`receive_shutdown_aborted()`. `sig.args().unwrap().reason()``&str`.)
- [ ] **Шаг 4 — прогон integration.** Run: `just test-integration` (или `dbus-run-session -- cargo test -p shturman-power -- --ignored`). Expected: PASS (оба теста).
- [ ] **Шаг 5 — lint + commit.**
```bash
cargo clippy -p shturman-power --all-targets -- -D warnings
git add crates/core/shturman-power/
git commit -s -m "feat(v0.3): Power-сервис на FSM — dev-mock кормит события, grace+durable-barrier"
```
---
## P7.3: systemd watchdog drop-in + save-time timer (B05/A14/B07)
**Files:** Create `systemd/watchdog-shturman.conf`, `systemd/shturman-savetime.service`, `systemd/shturman-savetime.timer`.
- [ ] **Шаг 1 — `systemd/watchdog-shturman.conf`** (system.conf.d; реальный `/dev/watchdog` — HW, в VM no-op):
```ini
# Watchdog (B05/A14): systemd пингует HW-watchdog в runtime + дедлайн на shutdown-фазу.
# Установка: /etc/systemd/system.conf.d/shturman-watchdog.conf. В VM /dev/watchdog нет → дисциплина (HW-арминг — v0.4).
[Manager]
RuntimeWatchdogSec=30s
RebootWatchdogSec=60s
```
- [ ] **Шаг 2 — `systemd/shturman-savetime.service`** (B07 periodic save last-known-time):
```ini
[Unit]
Description=Штурман save last-known-time (fake-hwclock → /data, B07)
After=data.mount
Requires=data.mount
[Service]
Type=oneshot
# FILE из /etc/default/fake-hwclock (→ /data; v0.6). Сервис в Lima masked → зовём напрямую с env.
ExecStart=/bin/sh -c '. /etc/default/fake-hwclock 2>/dev/null; FILE="${FILE:-/data/state/fake-hwclock.data}" fake-hwclock save'
```
- [ ] **Шаг 3 — `systemd/shturman-savetime.timer`** (периодика ~5 мин):
```ini
[Unit]
Description=Штурман periodic save-time (B07)
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
[Install]
WantedBy=shturman-stage2.target
```
- [ ] **Шаг 4 — commit** (проверка — P7.4/Lima).
```bash
git add systemd/watchdog-shturman.conf systemd/shturman-savetime.service systemd/shturman-savetime.timer
git commit -s -m "feat(v0.3): watchdog-конфиг (B05/A14) + save-time timer (B07)"
```
---
## P7.4: lima + E2E-блок power-safe (гибрид §9.3)
**Files:** Modify `lima/shturman.yaml`, `tests/e2e/run.sh`.
- [ ] **Шаг 1 — `lima/shturman.yaml`:** в блоке раскладки юнитов добавить watchdog + savetime:
```bash
install -d /etc/systemd/system.conf.d
install -m644 /shturman/systemd/watchdog-shturman.conf /etc/systemd/system.conf.d/shturman-watchdog.conf
# savetime.service/.timer ловит glob shturman-*.service/.timer? Нет — .timer не под *.service. Ставим явно:
install -m644 /shturman/systemd/shturman-savetime.service /shturman/systemd/shturman-savetime.timer /etc/systemd/system/
```
(Существующий `install /shturman/systemd/shturman-*.service` НЕ ловит `.timer` — ставим явно выше.)
- [ ] **Шаг 2 — `tests/e2e/run.sh`: установка savetime + watchdog в раскладке.** К блоку install (рядом с tmpfiles) добавить:
```bash
sudo install -d /etc/systemd/system.conf.d
sudo install -m644 systemd/watchdog-shturman.conf /etc/systemd/system.conf.d/shturman-watchdog.conf
sudo install -m644 systemd/shturman-savetime.service systemd/shturman-savetime.timer /etc/systemd/system/
```
- [ ] **Шаг 3 — `tests/e2e/run.sh`: блок power-safe.** Вставить после блока «Stage 0/1/2», до §8:
```bash
# ---- power-safe (v0.3): FSM + N циклов зажигания + abort + power-cut ----
info "power-safe: ShutdownImminent + N=3 цикла зажигания + abort + power-cut"
P_CALL() { busctl --system call "$P_NAME" "$P_PATH" "$P_MOCK" "$@"; }
busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Set sv ui.theme s night >/dev/null
echo 0 | sudo tee /data/state/power-cycles >/dev/null
observe_imminent() { # SetAcc(false) → ждём ShutdownImminent
local mon; mon=$(mktemp)
# shellcheck disable=SC2024
sudo busctl --system monitor "$P_NAME" >"$mon" 2>&1 & local M=$!
sleep 0.7; P_CALL SetAcc b false; sleep 0.7
sudo kill "$M" 2>/dev/null; wait "$M" 2>/dev/null
grep -q ShutdownImminent "$mon" || { echo "--- mon ---"; cat "$mon"; rm -f "$mon"; return 1; }
rm -f "$mon"
}
for i in 1 2 3; do
observe_imminent || fail "цикл $i: ShutdownImminent не наблюдаем"
n=$(($(sudo cat /data/state/power-cycles) + 1))
sudo systemctl stop shturman-stage1.target # освобождает /data
sync; sudo umount /data || fail "цикл $i: umount /data"
sudo mount /data || fail "цикл $i: mount /data"
echo "$n" | sudo tee /data/state/power-cycles >/dev/null
sudo systemctl start shturman.target
for _ in $(seq 1 15); do systemctl is-active --quiet shturman-settings && break; sleep 1; done
pass "цикл зажигания $i: stop→umount→remount→restart"
done
got=$(busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Get s ui.theme 2>/dev/null)
echo "$got" | grep -q '"night"' || fail "ui.theme потерян после циклов"
[ "$(sudo cat /data/state/power-cycles)" = 3 ] || fail "счётчик циклов != 3"
pass "N=3 цикла: /data + счётчик целы (нет потери)"
# abort до PONR
mon=$(mktemp)
# shellcheck disable=SC2024
sudo busctl --system monitor "$P_NAME" >"$mon" 2>&1 & M=$!
sleep 0.7; P_CALL SetAcc b false; sleep 0.3; P_CALL SetAcc b true; sleep 0.7
sudo kill "$M" 2>/dev/null; wait "$M" 2>/dev/null
grep -q ShutdownAborted "$mon" || { cat "$mon"; rm -f "$mon"; fail "ShutdownAborted не наблюдаем"; }
rm -f "$mon"
findmnt /data >/dev/null || fail "/data не смонтирован после abort"
busctl --system call "$P_NAME" "$P_PATH" "$P_IFACE" GetPowerState | grep -q running || fail "не running после abort"
pass "abort до PONR: ShutdownAborted + /data RW + running"
# power-cut-сим: SIGKILL во время shutdown → /data консистентен
P_CALL SetAcc b false; sleep 0.3
sudo systemctl kill -s KILL shturman-power.service shturman-settings.service 2>/dev/null || true
sudo systemctl stop shturman-stage1.target 2>/dev/null || true
sudo umount /data 2>/dev/null || true
sudo fsck.ext4 -n /var/lib/shturman/data.img >/dev/null 2>&1 || fail "fsck /data не clean после power-cut"
sudo mount /data
sudo grep -q night /data/settings/settings.json || fail "last durable value потерян после power-cut"
pass "power-cut-сим: /data консистентен (fsck clean, night present)"
sudo systemctl start shturman.target
for _ in $(seq 1 15); do systemctl is-active --quiet shturman-settings && break; sleep 1; done
# watchdog/save-time конфиг присутствует
test -f /etc/systemd/system.conf.d/shturman-watchdog.conf || fail "нет watchdog-конфига"
systemctl is-active --quiet shturman-savetime.timer && pass "savetime.timer активен" || echo " WARN: savetime.timer не активен"
pass "watchdog-конфиг (RuntimeWatchdogSec) на месте"
```
- [ ] **Шаг 4 — shellcheck + commit.**
```bash
shellcheck -S warning tests/e2e/run.sh
git add lima/shturman.yaml tests/e2e/run.sh
git commit -s -m "feat(v0.3): lima/E2E блок power-safe (N циклов + abort + power-cut)"
```
---
## P7.5: verify в Lima + acceptance + синхронизация доков
- [ ] **Шаг 1 — host-гейт.** Run: `just ci`. Expected: exit 0 (fsm-юниты + service + integration `#[ignore]`; clippy; deny). Плюс `just test-integration` (session-шина) — оба Power-теста зелёные.
- [ ] **Шаг 2 — чистый E2E.** Run: `just vm-reset && just e2e`. Expected: exit 0; power-safe-блок зелёный (N=3 цикла /data цел, abort→ShutdownAborted, power-cut fsck clean); **регресс v0.1/v0.2 зелёный**; `E2E OK ✅`.
- [ ] **Шаг 3 — итерации** по реальным ошибкам (grace-таймауты, umount-holders, fsck, proxy-сигналы) — систематически, один симптом → одна правка, повтор P7.5 шаг 2.
- [ ] **Шаг 4 — prod-build-gate.** Run: `cargo build -p shturman-power --no-default-features && ! strings target/debug/shturman-power | grep -q PowerMock1`. Expected: сборка ок, `PowerMock1` отсутствует.
- [ ] **Шаг 5 — синхронизация доков (швы §10):** `docs/domains/b-power-lifecycle.md` (реализованные срезы B01B07 в v0.3; abort/PONR-модель VM; HW/MCU/B08-B09 → v0.4); `docs/contracts/ipc.md` §3 (Power оживлён из FSM); `docs/specs/v0.1-v0.6-foundation.md` §5.2 («стаб» → реальный FSM); `CLAUDE.md` (статус v0.3 готово → v0.4/v0.5).
- [ ] **Шаг 6 — commit доков.**
```bash
git add docs/ CLAUDE.md
git commit -s -m "docs(v0.3): синхронизация швов power-safe + статус"
```
- [ ] **Шаг 7 — finishing-a-development-branch** (merge/PR — спросить пользователя; в `main` без явного «ок» не мержить).
---
## Acceptance (спека v0.3 §9.4)
- [ ] FSM: все переходы §5 — unit-тесты; sleep/battery_cutoff — no-op.
- [ ] `ShutdownImminent` на ACC-off; **abort до PONR → `ShutdownAborted`**; commit после grace + durable-barrier.
- [ ] **N=3 цикла зажигания — `/data` + счётчик целы**.
- [ ] power-cut-сим — `/data` консистентен (`fsck -n` clean, last value present).
- [ ] `Uptime` монотонен; watchdog/save-time конфиг на месте.
- [ ] Регресс v0.1/v0.2 зелёный; `just ci` зелёный; prod-build-gate (нет `PowerMock1`); красные линии целы (нет CAN/actuator).