Files
odysseus/tests/test_stale_process_intersection.py
T
Alexandre Teixeira ab89e3274a fix(runtime): exclude stale process resources during authority intersection
Catch ResourceIdentityError during intersection so that normal process exit
or background job termination does not crash child authority creation.
Stale or unverifiable observations are conservatively excluded from the
resulting authority while maintaining identity verification and preventing
PID reuse or renewal.
2026-10-02 18:54:09 +01:00

125 lines
5.1 KiB
Python

"""Regression tests for stale ProcessResource during authority intersection.
Covers:
1. Parent observes process → process exits → child intersection does not crash.
2. Stale process disappears from resulting child authority.
3. Stale parent cannot be renewed by a fresh/replacement process.
4. PID reuse/replacement remains rejected.
5. Child-side stale observation is handled conservatively.
6. Valid live identical observations still intersect correctly.
"""
import pytest
from dataclasses import dataclass
from src.agent_runtime.process_resources import intersect_observed
from src.agent_runtime.resources import ResourceIdentityError
@dataclass(frozen=True)
class _FakeResource:
"""Lightweight stand-in for ProcessResource/BackgroundJobResource in
intersection tests. Equality is by (pid, token) so we can verify
identity-based matching, while ``live`` controls whether validate raises.
"""
pid: int
token: str
live: bool = True
def __eq__(self, other):
return isinstance(other, _FakeResource) and (self.pid, self.token) == (other.pid, other.token)
def __hash__(self):
return hash((self.pid, self.token))
def _validate(resource):
"""Mirrors ProcessResource.validate() semantics."""
if not resource.live:
raise ResourceIdentityError("Process resource is stale or unverifiable")
# 1. Parent observes process → process exits → child intersection does not crash.
def test_stale_parent_process_does_not_crash_intersection():
stale = _FakeResource(pid=1000, token="tok-1", live=False)
child_copy = _FakeResource(pid=1000, token="tok-1", live=False)
result = intersect_observed((stale,), (child_copy,), _validate)
# Must not raise; stale resources are conservatively excluded.
assert result == ()
# 2. Stale process disappears from resulting child authority.
def test_stale_process_excluded_from_intersection_result():
live = _FakeResource(pid=2000, token="tok-2", live=True)
stale = _FakeResource(pid=3000, token="tok-3", live=False)
child_live = _FakeResource(pid=2000, token="tok-2", live=True)
child_stale = _FakeResource(pid=3000, token="tok-3", live=False)
result = intersect_observed((live, stale), (child_live, child_stale), _validate)
assert len(result) == 1
assert result[0].pid == 2000
# 3. Stale parent cannot be renewed by a fresh/replacement process.
def test_stale_parent_not_renewed_by_fresh_child():
stale_parent = _FakeResource(pid=4000, token="tok-4", live=False)
fresh_child = _FakeResource(pid=4000, token="tok-4-new", live=True)
result = intersect_observed((stale_parent,), (fresh_child,), _validate)
# Parent is stale → excluded before equality check.
assert result == ()
# 4. PID reuse/replacement remains rejected.
def test_pid_reuse_rejected():
"""A replacement process with the same PID but different token is never equal."""
original = _FakeResource(pid=5000, token="tok-original", live=True)
replacement = _FakeResource(pid=5000, token="tok-replacement", live=True)
result = intersect_observed((original,), (replacement,), _validate)
# Different identity → not equal → not in result.
assert result == ()
# 5. Child-side stale observation is handled conservatively.
def test_child_side_stale_excluded():
live_parent = _FakeResource(pid=6000, token="tok-6", live=True)
stale_child = _FakeResource(pid=6000, token="tok-6", live=False)
result = intersect_observed((live_parent,), (stale_child,), _validate)
# Child side is stale → not in live_child set → excluded.
assert result == ()
# 6. Valid live identical observations still intersect correctly.
def test_live_identical_observations_intersect():
parent = _FakeResource(pid=7000, token="tok-7", live=True)
child = _FakeResource(pid=7000, token="tok-7", live=True)
result = intersect_observed((parent,), (child,), _validate)
assert len(result) == 1
assert result[0].pid == 7000
assert result[0].token == "tok-7"
# Additional: multiple live resources intersect correctly preserving order.
def test_multiple_live_resources_intersect():
p1 = _FakeResource(pid=8000, token="tok-8a", live=True)
p2 = _FakeResource(pid=8001, token="tok-8b", live=True)
c1 = _FakeResource(pid=8000, token="tok-8a", live=True)
c2 = _FakeResource(pid=8001, token="tok-8b", live=True)
result = intersect_observed((p1, p2), (c1, c2), _validate)
assert len(result) == 2
assert result[0].pid == 8000
assert result[1].pid == 8001
# Additional: mixed stale/live across both sides.
def test_mixed_stale_live_across_both_sides():
p_live = _FakeResource(pid=9000, token="tok-9a", live=True)
p_stale = _FakeResource(pid=9001, token="tok-9b", live=False)
c_live = _FakeResource(pid=9000, token="tok-9a", live=True)
c_stale = _FakeResource(pid=9001, token="tok-9b", live=False)
result = intersect_observed((p_live, p_stale), (c_live, c_stale), _validate)
assert len(result) == 1
assert result[0].pid == 9000
# Edge: empty inputs produce empty output.
def test_empty_intersection():
assert intersect_observed((), (), _validate) == ()