#!/usr/bin/env python3 """test_tmux_server_watchdog.py — Death-capture transition logic. Covers: steady state is quiet, pid change yields restart, alive->dead yields a death bundle, dead->alive yields started, corrupt state file is tolerated. """ import json import sys import unittest from pathlib import Path from unittest import mock REPO_ROOT = Path("/home/super/Projects/NetVM") BIN_DIR = REPO_ROOT / "bin" sys.path.insert(0, str(BIN_DIR)) import tmux_server_watchdog as w class TestEvaluate(unittest.TestCase): def test_steady_alive_is_quiet(self): prev = {"/s": {"pid": 100, "since": "t"}} new, events = w.evaluate(prev, {"/s": 100}) self.assertEqual(events, []) self.assertEqual(new["/s"]["pid"], 100) def test_pid_change_is_restart(self): prev = {"/s": {"pid": 100, "since": "t"}} new, events = w.evaluate(prev, {"/s": 200}) self.assertEqual(len(events), 1) self.assertEqual(events[0]["type"], "restart") self.assertEqual(events[0]["old_pid"], 100) def test_alive_to_dead_is_death(self): prev = {"/s": {"pid": 100, "since": "t"}} new, events = w.evaluate(prev, {"/s": None}) self.assertEqual(len(events), 1) self.assertEqual(events[0]["type"], "death") self.assertIsNone(new["/s"]["pid"]) def test_dead_stays_dead_is_quiet(self): prev = {"/s": {"pid": None, "died": "t", "last_pid": 100}} _, events = w.evaluate(prev, {"/s": None}) self.assertEqual(events, []) def test_dead_to_alive_is_started(self): prev = {"/s": {"pid": None, "died": "t", "last_pid": 100}} _, events = w.evaluate(prev, {"/s": 300}) self.assertEqual(len(events), 1) self.assertEqual(events[0]["type"], "started") def test_unknown_socket_first_seen_is_started(self): _, events = w.evaluate({}, {"/s": 300}) self.assertEqual(events[0]["type"], "started") class TestCheck(unittest.TestCase): def _iso(self, td): state = str(td / "servers.json") log = str(td / "deaths.jsonl") p1 = mock.patch.object(w, "STATE_FILE", state) p2 = mock.patch.object(w, "DEATH_LOG", log) return p1, p2, state, log def test_death_writes_bundle(self): import tempfile with tempfile.TemporaryDirectory() as td: td = Path(td) p1, p2, state, log = self._iso(td) with open(state, "w") as f: json.dump({"/s": {"pid": 100, "since": "t"}}, f) with p1, p2, \ mock.patch.object(w, "probe", return_value=None), \ mock.patch.object(w, "collect_forensics", return_value={"ts": "t", "socket": "/s", "last_pid": 100}): res = w.check(sockets=["/s"]) self.assertEqual(res["events"][0]["type"], "death") bundle = json.loads(open(log).read().strip()) self.assertEqual(bundle["event"], "death") self.assertEqual(bundle["last_pid"], 100) self.assertIsNone(json.load(open(state))["/s"]["pid"]) def test_dry_run_writes_nothing(self): import tempfile with tempfile.TemporaryDirectory() as td: td = Path(td) p1, p2, state, log = self._iso(td) with p1, p2, \ mock.patch.object(w, "probe", return_value=100): res = w.check(sockets=["/s"], dry_run=True) self.assertEqual(res["events"][0]["type"], "started") self.assertFalse(Path(state).exists()) self.assertFalse(Path(log).exists()) def test_corrupt_state_tolerated(self): import tempfile with tempfile.TemporaryDirectory() as td: td = Path(td) p1, p2, state, _ = self._iso(td) with open(state, "w") as f: f.write("{{{nope") with p1, p2, \ mock.patch.object(w, "probe", return_value=100): res = w.check(sockets=["/s"]) self.assertEqual(res["events"][0]["type"], "started") if __name__ == "__main__": unittest.main()