106 lines
4.5 KiB
Python
106 lines
4.5 KiB
Python
"""Exercise the real periodic maintenance loop with isolated on-disk state."""
|
|
import asyncio
|
|
import json
|
|
from contextlib import suppress
|
|
from queue import Queue
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_removed_sessions_keep_profile_idle_watermark(tmp_path, monkeypatch):
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import tui_gateway.server as gateway
|
|
from agent.curator import load_state, save_state
|
|
from hermes_cli.web_server_sessions import _auto_archive_ticker_loop, _skill_maintenance_idle_for
|
|
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
(tmp_path / "skills").mkdir()
|
|
(tmp_path / "config.yaml").write_text(
|
|
"curator:\n enabled: true\n interval_hours: 168\n"
|
|
" min_idle_hours: 0.002\n prune_builtins: false\n", encoding="utf-8")
|
|
save_state({"last_run_at": "2020-01-01T00:00:00+00:00", "run_count": 0})
|
|
# Let the actual timer age past its idle threshold before recent activity.
|
|
task = asyncio.create_task(_auto_archive_ticker_loop(interval_s=.05, initial_delay_s=8))
|
|
try:
|
|
await asyncio.sleep(7.5)
|
|
for reason in ("tui_close", "ws_orphan_reap"):
|
|
recent = time.time()
|
|
with gateway._sessions_lock:
|
|
gateway._sessions["watermark"] = {
|
|
"last_active": recent, "running": False, "profile_home": str(tmp_path)}
|
|
assert gateway._close_session_by_id("watermark", end_reason=reason)
|
|
assert _skill_maintenance_idle_for(recent - 3600) < 2
|
|
# An active sibling profile must not hold this profile's idle clock.
|
|
with gateway._sessions_lock:
|
|
gateway._sessions["other-profile"] = {
|
|
"running": True, "profile_home": str(tmp_path / "other")}
|
|
assert _skill_maintenance_idle_for(recent - 3600) is not None
|
|
await asyncio.sleep(2)
|
|
assert load_state()["run_count"] == 0
|
|
async with asyncio.timeout(12):
|
|
while "consolidation off" not in (load_state().get("last_run_summary") or ""):
|
|
await asyncio.sleep(.05)
|
|
assert "consolidation off" in load_state()["last_run_summary"]
|
|
finally:
|
|
task.cancel()
|
|
with suppress(asyncio.CancelledError):
|
|
await task
|
|
with gateway._sessions_lock:
|
|
gateway._sessions.pop("other-profile", None)
|
|
gateway._sessions.pop("watermark", None)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_serve_timer_runs_due_curator_once_and_honors_pause(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
|
(tmp_path / "skills").mkdir()
|
|
(tmp_path / "config.yaml").write_text(
|
|
"curator:\n enabled: true\n consolidate: false\n interval_hours: 168\n"
|
|
" min_idle_hours: 0\n prune_builtins: false\n", encoding="utf-8")
|
|
from agent.curator import load_state, save_state, set_paused
|
|
import hermes_cli.web_server_sessions as web_server_sessions
|
|
|
|
ticks = Queue()
|
|
original_maintenance = web_server_sessions._maybe_run_skill_maintenance
|
|
|
|
def observed_maintenance(started_at):
|
|
try:
|
|
return original_maintenance(started_at)
|
|
finally:
|
|
ticks.put(None)
|
|
|
|
monkeypatch.setattr(web_server_sessions, "_maybe_run_skill_maintenance", observed_maintenance)
|
|
|
|
save_state({"last_run_at": "2020-01-01T00:00:00+00:00", "run_count": 0, "paused": True})
|
|
task = asyncio.create_task(web_server_sessions._auto_archive_ticker_loop(
|
|
interval_s=.02, initial_delay_s=0))
|
|
try:
|
|
await asyncio.to_thread(ticks.get, True, 8)
|
|
assert load_state()["run_count"] == 0
|
|
# Active turns must suppress maintenance even with a zero idle threshold.
|
|
import tui_gateway.server as gateway
|
|
with gateway._sessions_lock:
|
|
gateway._sessions['maintenance-test'] = {"running": True}
|
|
set_paused(False)
|
|
try:
|
|
await asyncio.to_thread(ticks.get, True, 8)
|
|
assert load_state()["run_count"] == 0
|
|
finally:
|
|
with gateway._sessions_lock:
|
|
gateway._sessions.pop('maintenance-test', None)
|
|
async with asyncio.timeout(8):
|
|
while load_state()["run_count"] == 0:
|
|
await asyncio.sleep(.02)
|
|
await asyncio.sleep(.15)
|
|
state = json.loads((tmp_path / "skills" / ".curator_state").read_text())
|
|
assert state["run_count"] == 1
|
|
assert "consolidation off" in state["last_run_summary"]
|
|
finally:
|
|
task.cancel()
|
|
with suppress(asyncio.CancelledError):
|
|
await task
|