from __future__ import annotations from datetime import datetime, timezone import subprocess from govoplan_core.commands import fenced_run from govoplan_core.core.runtime_coordination import LeaseClaim class _IgnoringProcess: def __init__(self) -> None: self.terminated = False self.killed = False self.wait_count = 0 def poll(self): return None if not self.killed else -9 def terminate(self) -> None: self.terminated = True def kill(self) -> None: self.killed = True def wait(self, *, timeout: float): self.wait_count += 1 if not self.killed: raise subprocess.TimeoutExpired("command", timeout) return -9 def test_fenced_process_is_killed_when_it_ignores_termination() -> None: process = _IgnoringProcess() fenced_run._terminate_process(process, timeout_seconds=0.01) assert process.terminated is True assert process.killed is True assert process.wait_count == 2 def test_lease_release_is_best_effort_after_database_loss(monkeypatch) -> None: claim = LeaseClaim( installation_id="installation-1", resource_key="scheduler", holder_node_id="scheduler-1", holder_incarnation="incarnation-1", fencing_token=1, expires_at=datetime(2026, 8, 1, 12, 0, tzinfo=timezone.utc), ) monkeypatch.setattr( fenced_run, "get_database", lambda: (_ for _ in ()).throw(RuntimeError("database unavailable")), ) fenced_run._release(claim)