"""Reboot evidence/state tests without restarting or changing a real box.""" import importlib.util from importlib.machinery import SourceFileLoader import json from pathlib import Path import tempfile import unittest from unittest.mock import patch script = Path(__file__).resolve().parents[2]/"terminal/kiosk-shell/scripts/vdi-overnight-observer" spec = importlib.util.spec_from_loader("observer", SourceFileLoader("observer", str(script))) m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) class RebootEvidence(unittest.TestCase): def baseline(self): return m.transition(None, "boot-a", "start")[0] def test_baseline_zero_reboots(self): state,event=m.transition(None,"boot-a","start") self.assertEqual(event,"boot_baseline"); self.assertEqual(state["reboots"],0) def test_service_restart_is_not_box_reboot(self): before=self.baseline(); after,event=m.transition(before,"boot-a","later") self.assertEqual(event,"observer_restart_same_boot"); self.assertEqual(after["reboots"],0) self.assertEqual(after["started_at"],"start") def test_boot_change_preserves_last_heartbeat(self): before=self.baseline(); before.update(last_seen="last",uptime_seconds=100) after,event=m.transition(before,"boot-b","next") self.assertEqual(event,"boot_change"); self.assertEqual(after["reboots"],1) self.assertEqual(after["history"][-1]["previous_last_seen"],"last") self.assertEqual(before["reboots"],0) def test_unknown_reason_is_not_reported_as_power_loss(self): text=m.classify({}) self.assertIn("未确认",text); self.assertIn("不能区分",text) def test_orderly_shutdown_requires_real_markers(self): self.assertIn("正常关机",m.classify({"tail":{"text":"systemd-shutdown[1]: Syncing filesystems"}})) self.assertIn("未确认",m.classify({"tail":{"text":"USB power management enabled; watchdog capability present"}})) def test_oom_is_a_clue_not_a_proven_reboot_cause(self): text=m.classify({"warnings":{"text":"Out of memory: Killed process 123"}}) self.assertIn("不能单独认定",text) def test_only_changed_pstore_is_a_new_clue(self): archive={"text":"Kernel panic - not syncing: example"} self.assertIn("未确认",m.classify({"old_pstore":archive})) self.assertIn("不能单独认定",m.classify({"pstore_new":archive})) def test_invalid_state_preserved(self): for value in ({"version":2,"reboots":0},{"version":1,"reboots":True}): with self.assertRaises(ValueError): m.transition(value,"new","now") def test_multiple_boots_and_history_bound(self): state=self.baseline() for i in range(40): state,event=m.transition(state,"next-"+str(i),"now") self.assertEqual(state["reboots"],40); self.assertEqual(len(state["history"]),32) def test_durable_state_and_chinese_report(self): with tempfile.TemporaryDirectory() as d: o=m.Observer(Path(d)/"logs",Path(d)/"state"); o.state=self.baseline(); o.save() self.assertEqual(json.loads(o.state_file.read_text())["boot_id"],"boot-a") self.assertIn("重启次数:0",(o.logs/"overnight-summary.txt").read_text(encoding="utf-8")) def test_daily_rollover_uses_each_event_date(self): from datetime import datetime,timezone with tempfile.TemporaryDirectory() as d: o=m.Observer(Path(d)/"logs",Path(d)/"state") for day in (4,5): with patch.object(m,"datetime") as clock: clock.now.return_value=datetime(2026,11,day,0,0,tzinfo=timezone.utc) o.log("sample") self.assertEqual(len(list(o.logs.glob("*.log"))),2) if __name__=="__main__": unittest.main()