Files
SDR-Rover/tests/test_rf001_launcher_health_diagnostic.py
2026-09-30 11:42:19 +03:00

236 lines
7.3 KiB
Python

"""Regression tests for strict launcher/health process identity."""
from __future__ import annotations
from dataclasses import replace
from datetime import datetime, timedelta, timezone
import json
from pathlib import Path
from tools.rf001_launcher_health_diagnostic import (
HandshakeExpectation,
ProcessRecord,
_terminate_owned_processes,
atomic_write_json,
evaluate_identity,
strict_owned_process_ids,
)
NOW = datetime(2026, 9, 29, 13, 0, tzinfo=timezone.utc)
SCRIPT = r"C:\diagnostic\rf001_launcher_health_diagnostic.py"
def _expectation(*, popen_pid: int = 200) -> HandshakeExpectation:
return HandshakeExpectation(
release_id="rf001-agent-2026.09.29.1",
agent_version="2026.09.29.1",
launch_nonce="fixed-unique-nonce",
state="RUNNING",
launcher_pid=100,
popen_pid=popen_pid,
launched_at=NOW.isoformat(),
diagnostic_script=SCRIPT,
)
def _record(pid: int, parent: int, *, seconds: float = 1.0) -> ProcessRecord:
return ProcessRecord(
pid=pid,
parent_pid=parent,
name="python.exe",
executable=r"C:\Python313\python.exe",
command_line=f'python "{SCRIPT}" --launch-nonce fixed-unique-nonce',
created_at=(NOW + timedelta(seconds=seconds)).isoformat(),
)
def _health(*, pid: int = 200) -> dict[str, object]:
return {
"release_id": "rf001-agent-2026.09.29.1",
"agent_version": "2026.09.29.1",
"launch_nonce": "fixed-unique-nonce",
"state": "RUNNING",
"pid": pid,
"updated_at": (NOW + timedelta(seconds=2)).isoformat(),
}
def _evaluate(
health: dict[str, object] | None,
records: list[ProcessRecord],
*,
expectation: HandshakeExpectation | None = None,
popen_alive: bool = True,
health_process_alive: bool = True,
) -> dict[str, object]:
return evaluate_identity(
health,
expectation or _expectation(),
records,
popen_alive=popen_alive,
health_process_alive=health_process_alive,
)
def test_exact_process_identity_passes() -> None:
result = _evaluate(_health(), [_record(200, 100)])
assert result["identity_valid"] is True
assert result["identity_model"] == "EXACT_POPEN_PROCESS"
def test_wrong_nonce_fails() -> None:
health = _health()
health["launch_nonce"] = "wrong"
assert _evaluate(health, [_record(200, 100)])["reason"] == "HEALTH_FIELDS_MISMATCH"
def test_wrong_release_fails() -> None:
health = _health()
health["release_id"] = "rf001-agent-elsewhere"
assert _evaluate(health, [_record(200, 100)])["reason"] == "HEALTH_FIELDS_MISMATCH"
def test_wrong_version_fails() -> None:
health = _health()
health["agent_version"] = "2026.09.29.2"
assert _evaluate(health, [_record(200, 100)])["reason"] == "HEALTH_FIELDS_MISMATCH"
def test_health_before_running_state_fails() -> None:
health = _health()
health["state"] = "STARTING"
assert _evaluate(health, [_record(200, 100)])["reason"] == "HEALTH_FIELDS_MISMATCH"
def test_stale_health_fails() -> None:
health = _health()
health["updated_at"] = (NOW - timedelta(seconds=1)).isoformat()
assert _evaluate(health, [_record(200, 100)])["reason"] == "STALE_HEALTH"
def test_rollback_does_not_accept_previous_launch_health() -> None:
previous_health = _health()
previous_health["launch_nonce"] = "previous-launch-nonce"
assert (
_evaluate(previous_health, [_record(200, 100)])["reason"]
== "HEALTH_FIELDS_MISMATCH"
)
def test_unrelated_pid_fails() -> None:
result = _evaluate(
_health(pid=300),
[_record(200, 100), _record(300, 999)],
)
assert result["reason"] == "UNRELATED_PID"
def test_unique_expected_wrapper_child_passes_strict_lineage() -> None:
result = _evaluate(
_health(pid=201),
[_record(200, 100), _record(201, 200)],
)
assert result["identity_valid"] is True
assert (
result["identity_model"]
== "UNIQUE_DIRECT_CHILD_WITH_NONCE_AND_CREATION_TIME"
)
assert result["pid_match"] is False
def test_unexpected_grandchild_fails() -> None:
result = _evaluate(
_health(pid=202),
[_record(200, 100), _record(201, 200), _record(202, 201)],
)
assert result["reason"] == "UNEXPECTED_GRANDCHILD"
def test_pid_reuse_creation_time_mismatch_fails() -> None:
reused = _record(200, 100, seconds=-30)
assert _evaluate(_health(), [reused])["reason"] == "PROCESS_CREATION_TIME_MISMATCH"
def test_target_dies_before_health_fails() -> None:
result = _evaluate(None, [_record(200, 100)], popen_alive=False)
assert result["reason"] == "HEALTH_ABSENT"
def test_target_writes_health_then_dies_immediately_fails() -> None:
result = _evaluate(
_health(),
[_record(200, 100)],
health_process_alive=False,
)
assert result["reason"] == "HEALTH_PROCESS_DIED"
def test_orphan_selection_never_includes_unrelated_process() -> None:
expectation = _expectation()
related = _record(201, 200)
unrelated = replace(_record(300, 999), command_line="python unrelated.py")
assert strict_owned_process_ids(
expectation,
[_record(200, 100), related, unrelated],
) == [201, 200]
def test_orphan_cleanup_terminates_only_strictly_owned_processes(monkeypatch) -> None:
expectation = _expectation()
wrapper = _record(200, 100)
child = _record(201, 200)
unrelated = replace(_record(300, 999), command_line="python unrelated.py")
records = [wrapper, child, unrelated]
terminated: list[int] = []
monkeypatch.setattr(
"tools.rf001_launcher_health_diagnostic.capture_windows_processes",
lambda: records,
)
monkeypatch.setattr(
"tools.rf001_launcher_health_diagnostic.os.kill",
lambda pid, _signal: terminated.append(pid),
)
actions = _terminate_owned_processes(expectation, records)
assert terminated == [201, 200]
assert actions == [
{"pid": 201, "status": "TERMINATED"},
{"pid": 200, "status": "TERMINATED"},
]
def test_repeated_success_is_deterministic_with_distinct_identity() -> None:
first = _expectation(popen_pid=200)
second = replace(
first,
popen_pid=300,
launch_nonce="second-unique-nonce",
launched_at=(NOW + timedelta(seconds=10)).isoformat(),
)
first_result = _evaluate(_health(pid=200), [_record(200, 100)], expectation=first)
second_record = replace(
_record(300, 100, seconds=11),
command_line=f'python "{SCRIPT}" --launch-nonce second-unique-nonce',
)
second_health = {
**_health(pid=300),
"launch_nonce": "second-unique-nonce",
"updated_at": (NOW + timedelta(seconds=12)).isoformat(),
}
second_result = _evaluate(second_health, [second_record], expectation=second)
assert first_result["identity_valid"] is True
assert second_result["identity_valid"] is True
assert first.launch_nonce != second.launch_nonce
assert first.popen_pid != second.popen_pid
def test_diagnostic_json_replacement_is_atomic(tmp_path: Path) -> None:
path = tmp_path / "health.json"
for sequence in range(20):
atomic_write_json(path, {"sequence": sequence})
assert json.loads(path.read_text(encoding="utf-8")) == {"sequence": sequence}
assert not path.with_suffix(".json.tmp").exists()