Files
shturman/docs/specs/plans/07-v0.3-power-safe.md
T
kk0t9 598070de96 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>
2026-06-24 20:58:38 +03:00

644 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План 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).