atlas-iac/testing/tests/test_hermes_cli_retention.py

326 lines
10 KiB
Python

"""Bounded reads, artifact retention, and recovery cadence."""
from __future__ import annotations
from testing.tests.test_hermes_cli_support import (
Path,
SimpleNamespace,
_completed_result,
json,
lanes,
nullcontext,
os,
pytest,
sys,
)
def test_invalid_utf8_journal_quarantines_and_does_not_stop_later_replay(
tmp_path: Path,
monkeypatch,
):
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
bad = lanes._terminal_path(lanes.state_path("cassandra", "a_bad_utf8"), 31)
bad.parent.mkdir(parents=True)
bad.write_bytes(b"\xff\xfe\x80not-json")
good, _record = lanes._write_terminal_record(
lanes.state_path("cassandra", "z_good"),
board="cassandra",
task_id="z_good",
run_id=32,
structured=_completed_result("valid after invalid UTF-8"),
summary="valid after invalid UTF-8",
metadata={},
)
tasks = {
"a_bad_utf8": SimpleNamespace(
id="a_bad_utf8",
status="running",
current_run_id=31,
result=None,
assignee="cli-auto",
),
"z_good": SimpleNamespace(
id="z_good",
status="running",
current_run_id=32,
result=None,
assignee="cli-auto",
),
}
class Connection:
def close(self):
return None
def reclaim_task(_conn, task_id, **_kwargs):
tasks[task_id].status = "ready"
tasks[task_id].current_run_id = None
return True
def complete_task(_conn, task_id, **kwargs):
tasks[task_id].status = "done"
tasks[task_id].result = kwargs["result"]
tasks[task_id].current_run_id = None
return True
fake_db = SimpleNamespace(
scoped_current_board=lambda _board: nullcontext(),
connect=lambda board: Connection(),
get_task=lambda _conn, task_id: tasks[task_id],
reclaim_task=reclaim_task,
complete_task=complete_task,
)
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
assert lanes.recover_pending_finalizations() == 1
assert not bad.exists()
assert not good.exists()
assert tasks["a_bad_utf8"].status == "ready"
assert tasks["z_good"].status == "done"
diagnostic_path = next((bad.parent / "quarantine").glob("*.quarantine"))
diagnostic = json.loads(diagnostic_path.read_text(encoding="utf-8"))
assert diagnostic["size"] == len(b"\xff\xfe\x80not-json")
assert diagnostic_path.stat().st_size < 4096
@pytest.mark.parametrize(
"size",
[lanes.MAX_TERMINAL_RECORD_BYTES + 1, 16 * 1024 * 1024],
)
def test_oversized_sparse_journal_has_bounded_read_and_quarantine(
tmp_path: Path,
monkeypatch,
size: int,
):
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
huge = lanes._terminal_path(lanes.state_path("cassandra", "a_huge"), 41)
huge.parent.mkdir(parents=True)
with huge.open("wb") as stream:
stream.seek(size - 1)
stream.write(b"\0")
good, _record = lanes._write_terminal_record(
lanes.state_path("cassandra", "z_after_huge"),
board="cassandra",
task_id="z_after_huge",
run_id=42,
structured=_completed_result("valid after oversized journal"),
summary="valid after oversized journal",
metadata={},
)
tasks = {
"a_huge": SimpleNamespace(
id="a_huge",
status="running",
current_run_id=41,
result=None,
assignee="cli-auto",
),
"z_after_huge": SimpleNamespace(
id="z_after_huge",
status="running",
current_run_id=42,
result=None,
assignee="cli-auto",
),
}
reads = []
real_read_bounded = lanes._read_bounded
def observe_read_limit(descriptor, limit):
reads.append((os.fstat(descriptor).st_size, limit))
return real_read_bounded(descriptor, limit)
monkeypatch.setattr(lanes, "_read_bounded", observe_read_limit)
class Connection:
def close(self):
return None
def reclaim_task(_conn, task_id, **_kwargs):
tasks[task_id].status = "ready"
tasks[task_id].current_run_id = None
return True
def complete_task(_conn, task_id, **kwargs):
tasks[task_id].status = "done"
tasks[task_id].result = kwargs["result"]
tasks[task_id].current_run_id = None
return True
fake_db = SimpleNamespace(
scoped_current_board=lambda _board: nullcontext(),
connect=lambda board: Connection(),
get_task=lambda _conn, task_id: tasks[task_id],
reclaim_task=reclaim_task,
complete_task=complete_task,
)
monkeypatch.setitem(sys.modules, "hermes_cli", SimpleNamespace(kanban_db=fake_db))
assert lanes.recover_pending_finalizations() == 1
assert not huge.exists()
assert not good.exists()
assert tasks["a_huge"].status == "ready"
assert tasks["z_after_huge"].status == "done"
diagnostics = list((huge.parent / "quarantine").glob("*.quarantine"))
assert len(diagnostics) == 1
assert diagnostics[0].stat().st_size < 4096
diagnostic = json.loads(diagnostics[0].read_text(encoding="utf-8"))
assert diagnostic["size"] == size
assert diagnostic["hashed_bytes"] == lanes.QUARANTINE_HASH_BYTES
assert diagnostic["hash_complete"] is False
assert (size, lanes.QUARANTINE_HASH_BYTES) in reads
assert (size, lanes.MAX_TERMINAL_RECORD_BYTES + 1) not in reads
assert max(limit for _file_size, limit in reads) <= (
lanes.MAX_TERMINAL_RECORD_BYTES + 1
)
def test_recovery_does_not_parse_committed_journals_every_tick(
tmp_path: Path,
monkeypatch,
):
state_root = tmp_path / "cli-lanes"
monkeypatch.setattr(lanes, "STATE_ROOT", state_root)
committed = lanes._terminal_path(
lanes.state_path("cassandra", "t_done"), 1, "committed"
)
lanes.atomic_json(committed, {"kanban_state": "committed"})
reads = []
monkeypatch.setattr(lanes, "load_json", lambda path: (reads.append(path) or {}))
monkeypatch.setitem(
sys.modules,
"hermes_cli",
SimpleNamespace(kanban_db=SimpleNamespace()),
)
assert lanes.recover_pending_finalizations() == 0
assert reads == []
def test_atomic_json_fsyncs_file_and_directory_and_uses_private_mode(
tmp_path: Path,
monkeypatch,
):
calls = []
real_fsync = lanes.os.fsync
monkeypatch.setattr(
lanes.os,
"fsync",
lambda descriptor: (calls.append(descriptor), real_fsync(descriptor))[1],
)
path = tmp_path / "state.json"
lanes.atomic_json(path, {"safe": True})
assert json.loads(path.read_text()) == {"safe": True}
assert path.stat().st_mode & 0o777 == 0o600
assert len(calls) >= 2
def test_atomic_json_refuses_a_precreated_temp_symlink(
tmp_path: Path,
monkeypatch,
):
path = tmp_path / "state.json"
victim = tmp_path / "credential"
victim.write_text("do not overwrite", encoding="utf-8")
temporary = tmp_path / ".state.json.fixed.tmp"
temporary.symlink_to(victim)
monkeypatch.setattr(lanes.uuid, "uuid4", lambda: SimpleNamespace(hex="fixed"))
with pytest.raises(FileExistsError):
lanes.atomic_json(path, {"unsafe": True})
assert victim.read_text(encoding="utf-8") == "do not overwrite"
assert not path.exists()
def test_artifact_gc_prunes_by_age_without_touching_pending_journals(
tmp_path: Path,
monkeypatch,
):
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
board = lanes.STATE_ROOT / "cassandra"
board.mkdir(parents=True)
quarantine = board / "quarantine"
quarantine.mkdir()
old_artifacts = [
board / "t.run-1.candidate-1.json",
board / "t.run-1.provider-1.result.json",
board / "t.run-1.terminal.committed.json",
board / f"t.run-1.terminal.conflict-{'a' * 32}.json",
quarantine / "t.invalid.1234.quarantine",
]
for artifact in old_artifacts:
artifact.write_text("{}", encoding="utf-8")
quarantine_symlink = quarantine / "attacker.quarantine"
quarantine_symlink.symlink_to(tmp_path / "missing-target")
pending = board / "t.run-1.terminal.pending.json"
pending.write_text("{}", encoding="utf-8")
prepared = board / f"t.run-1.terminal.prepared-{'b' * 32}.json"
prepared.write_text("{}", encoding="utf-8")
old_time = 100.0
for artifact in old_artifacts:
os.utime(artifact, (old_time, old_time))
os.utime(pending, (old_time, old_time))
os.utime(prepared, (old_time, old_time))
assert lanes.gc_lane_artifacts(
now=1000.0, max_age_seconds=10, max_count=100, max_bytes=10000
) == len(old_artifacts) + 1
assert not any(artifact.exists() for artifact in old_artifacts)
assert not quarantine_symlink.is_symlink()
assert pending.exists()
assert prepared.exists()
def test_artifact_gc_prunes_oldest_by_count_and_total_bytes(
tmp_path: Path,
monkeypatch,
):
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
board = lanes.STATE_ROOT / "cassandra"
board.mkdir(parents=True)
files = []
for sequence in range(3):
path = board / f"t.run-1.provider-{sequence}.result.json"
path.write_text("x" * 40, encoding="utf-8")
os.utime(path, (100 + sequence, 100 + sequence))
files.append(path)
assert lanes.gc_lane_artifacts(
now=200.0, max_age_seconds=1000, max_count=2, max_bytes=45
) == 2
assert [path.exists() for path in files] == [False, False, True]
def test_artifact_gc_preserves_a_concurrent_replacement(
tmp_path: Path,
monkeypatch,
):
monkeypatch.setattr(lanes, "STATE_ROOT", tmp_path / "cli-lanes")
board = lanes.STATE_ROOT / "cassandra"
board.mkdir(parents=True)
path = board / "t.run-1.provider-1.result.json"
path.write_text("old", encoding="utf-8")
os.utime(path, (100, 100))
replacement = board / "replacement.tmp"
replacement.write_text("new", encoding="utf-8")
real_unlink = lanes._unlink_artifact_if_same
swapped = {"value": False}
def swap_before_identity_checked(candidate, observed):
if not swapped["value"]:
os.replace(replacement, candidate)
swapped["value"] = True
return real_unlink(candidate, observed)
monkeypatch.setattr(lanes, "_unlink_artifact_if_same", swap_before_identity_checked)
assert lanes.gc_lane_artifacts(
now=1000.0, max_age_seconds=10, max_count=100, max_bytes=10000
) == 0
assert swapped["value"] is True
assert path.read_text(encoding="utf-8") == "new"