- Add reset_running_to_pending() to PersistentTaskQueue for recovery - Implement recover_on_startup() to reset interrupted tasks and clean zombies - Add ContainerCleaner for periodic removal of old sandbox containers - Add 4 tests covering recovery scenarios and container cleanup logic
80 lines
2.5 KiB
Python
80 lines
2.5 KiB
Python
import pytest
|
|
import os
|
|
import tempfile
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from agent.task_queue import PersistentTaskQueue
|
|
from agent.recovery import recover_on_startup, ContainerCleaner
|
|
|
|
|
|
@pytest.fixture
|
|
async def task_queue():
|
|
fd, db_path = tempfile.mkstemp(suffix=".db")
|
|
os.close(fd)
|
|
queue = PersistentTaskQueue(db_path=db_path)
|
|
await queue.initialize()
|
|
yield queue
|
|
await queue.close()
|
|
os.unlink(db_path)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recover_resets_running_to_pending(task_queue):
|
|
await task_queue.enqueue("thread-1", "gitea", {"msg": "interrupted"})
|
|
await task_queue.dequeue() # → running
|
|
assert await task_queue.has_running_task("thread-1") is True
|
|
|
|
with patch("agent.recovery._cleanup_zombie_containers", new_callable=AsyncMock):
|
|
await recover_on_startup(task_queue)
|
|
|
|
assert await task_queue.has_running_task("thread-1") is False
|
|
pending = await task_queue.get_pending()
|
|
assert len(pending) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recover_no_running_tasks(task_queue):
|
|
with patch("agent.recovery._cleanup_zombie_containers", new_callable=AsyncMock):
|
|
await recover_on_startup(task_queue)
|
|
pending = await task_queue.get_pending()
|
|
assert len(pending) == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_container_cleaner_removes_old():
|
|
mock_container = MagicMock()
|
|
mock_container.name = "galaxis-sandbox-old"
|
|
mock_container.labels = {"galaxis-agent-sandbox": "true"}
|
|
mock_container.attrs = {"Created": "2026-03-19T00:00:00Z"}
|
|
mock_container.stop = MagicMock()
|
|
mock_container.remove = MagicMock()
|
|
|
|
mock_docker = MagicMock()
|
|
mock_docker.containers.list.return_value = [mock_container]
|
|
|
|
cleaner = ContainerCleaner(docker_client=mock_docker, max_age_seconds=600)
|
|
removed = await cleaner.cleanup_once()
|
|
|
|
assert removed == 1
|
|
mock_container.stop.assert_called_once()
|
|
mock_container.remove.assert_called_once()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_container_cleaner_keeps_recent():
|
|
from datetime import datetime, timezone
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
|
|
mock_container = MagicMock()
|
|
mock_container.labels = {"galaxis-agent-sandbox": "true"}
|
|
mock_container.attrs = {"Created": now}
|
|
|
|
mock_docker = MagicMock()
|
|
mock_docker.containers.list.return_value = [mock_container]
|
|
|
|
cleaner = ContainerCleaner(docker_client=mock_docker, max_age_seconds=3600)
|
|
removed = await cleaner.cleanup_once()
|
|
|
|
assert removed == 0
|
|
mock_container.stop.assert_not_called()
|