"""Tests for RunManager.""" import re import pytest from deerflow.runtime import RunManager, RunStatus ISO_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}") @pytest.fixture def manager() -> RunManager: return RunManager() @pytest.mark.anyio async def test_create_and_get(manager: RunManager): """Created run should be retrievable with new fields.""" record = await manager.create( "thread-1", "lead_agent", metadata={"key": "val"}, kwargs={"input": {}}, multitask_strategy="reject", ) assert record.status == RunStatus.pending assert record.thread_id == "thread-1" assert record.assistant_id == "lead_agent" assert record.metadata == {"key": "val"} assert record.kwargs == {"input": {}} assert record.multitask_strategy == "reject" assert ISO_RE.match(record.created_at) assert ISO_RE.match(record.updated_at) fetched = manager.get(record.run_id) assert fetched is record @pytest.mark.anyio async def test_status_transitions(manager: RunManager): """Status should transition pending -> running -> success.""" record = await manager.create("thread-1") assert record.status == RunStatus.pending await manager.set_status(record.run_id, RunStatus.running) assert record.status == RunStatus.running assert ISO_RE.match(record.updated_at) await manager.set_status(record.run_id, RunStatus.success) assert record.status == RunStatus.success @pytest.mark.anyio async def test_cancel(manager: RunManager): """Cancel should set abort_event and transition to interrupted.""" record = await manager.create("thread-1") await manager.set_status(record.run_id, RunStatus.running) cancelled = await manager.cancel(record.run_id) assert cancelled is True assert record.abort_event.is_set() assert record.status == RunStatus.interrupted @pytest.mark.anyio async def test_cancel_not_inflight(manager: RunManager): """Cancelling a completed run should return False.""" record = await manager.create("thread-1") await manager.set_status(record.run_id, RunStatus.success) cancelled = await manager.cancel(record.run_id) assert cancelled is False @pytest.mark.anyio async def test_list_by_thread(manager: RunManager): """Same thread should return multiple runs, newest first.""" r1 = await manager.create("thread-1") r2 = await manager.create("thread-1") await manager.create("thread-2") runs = await manager.list_by_thread("thread-1") assert len(runs) == 2 assert runs[0].run_id == r2.run_id assert runs[1].run_id == r1.run_id @pytest.mark.anyio async def test_list_by_thread_is_stable_when_timestamps_tie(manager: RunManager, monkeypatch: pytest.MonkeyPatch): """Newest-first ordering should not depend on timestamp precision.""" monkeypatch.setattr("deerflow.runtime.runs.manager._now_iso", lambda: "2026-01-01T00:00:00+00:00") r1 = await manager.create("thread-1") r2 = await manager.create("thread-1") runs = await manager.list_by_thread("thread-1") assert [run.run_id for run in runs] == [r2.run_id, r1.run_id] @pytest.mark.anyio async def test_has_inflight(manager: RunManager): """has_inflight should be True when a run is pending or running.""" record = await manager.create("thread-1") assert await manager.has_inflight("thread-1") is True await manager.set_status(record.run_id, RunStatus.success) assert await manager.has_inflight("thread-1") is False @pytest.mark.anyio async def test_cleanup(manager: RunManager): """After cleanup, the run should be gone.""" record = await manager.create("thread-1") run_id = record.run_id await manager.cleanup(run_id, delay=0) assert manager.get(run_id) is None @pytest.mark.anyio async def test_set_status_with_error(manager: RunManager): """Error message should be stored on the record.""" record = await manager.create("thread-1") await manager.set_status(record.run_id, RunStatus.error, error="Something went wrong") assert record.status == RunStatus.error assert record.error == "Something went wrong" @pytest.mark.anyio async def test_get_nonexistent(manager: RunManager): """Getting a nonexistent run should return None.""" assert manager.get("does-not-exist") is None @pytest.mark.anyio async def test_create_defaults(manager: RunManager): """Create with no optional args should use defaults.""" record = await manager.create("thread-1") assert record.metadata == {} assert record.kwargs == {} assert record.multitask_strategy == "reject" assert record.assistant_id is None