"""The orchestrator dep-update loop: dormant when off, runs the engine when on. Dormant unless ``dep_update_enabled``; loads the eligible set (projects with a dep_update_command, one per repo), warns when enabled-but-empty, and runs DepUpdateEngine.run_cycle each interval. Separate from self-heal/CI-watch loops. """ from __future__ import annotations from contextlib import asynccontextmanager from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest from roboco.config import settings from roboco.runtime.orchestrator import AgentOrchestrator def _orch() -> AgentOrchestrator: return AgentOrchestrator.__new__(AgentOrchestrator) @pytest.mark.asyncio async def test_loop_noop_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(settings, "dep_update_enabled", False) orch = _orch() cycle = AsyncMock() orch._run_dep_update_cycle = cycle # type: ignore[method-assign] await orch._dep_update_loop() cycle.assert_not_awaited() @pytest.mark.asyncio async def test_load_set_filters_command_one_per_repo() -> None: orch = _orch() on_a = MagicMock(slug="be", git_url="https://x/a.git", dep_update_command="uv -U") on_a2 = MagicMock(slug="fe", git_url="https://x/a.git", dep_update_command="uv -U") off = MagicMock(slug="c", git_url="https://x/c.git", dep_update_command=None) svc = MagicMock() svc.list_all = AsyncMock(return_value=[on_a, on_a2, off]) with patch("roboco.services.project.get_project_service", return_value=svc): eligible = await orch._load_dep_update_set(MagicMock()) assert len(eligible) == 1 # no-command excluded; same-repo collapsed assert eligible[0].git_url == "https://x/a.git" def _db_ctx(db: Any) -> Any: @asynccontextmanager async def _ctx() -> Any: yield db return _ctx @pytest.mark.asyncio async def test_cycle_warns_and_skips_engine_when_empty() -> None: orch = _orch() orch._load_dep_update_set = AsyncMock(return_value=[]) # type: ignore[method-assign] get_eng = MagicMock() with ( patch("roboco.db.get_db_context", _db_ctx(MagicMock())), patch("roboco.services.dep_update_engine.get_dep_update_engine", get_eng), ): await orch._run_dep_update_cycle() get_eng.assert_not_called() @pytest.mark.asyncio async def test_cycle_runs_engine_when_eligible_present() -> None: orch = _orch() eligible = [MagicMock()] orch._load_dep_update_set = AsyncMock(return_value=eligible) # type: ignore[method-assign] db = MagicMock() db.commit = AsyncMock() engine = MagicMock() engine.run_cycle = AsyncMock(return_value=[]) with ( patch("roboco.db.get_db_context", _db_ctx(db)), patch( "roboco.services.dep_update_engine.get_dep_update_engine", return_value=engine, ) as get_eng, ): await orch._run_dep_update_cycle() get_eng.assert_called_once() engine.run_cycle.assert_awaited_once_with(eligible) db.commit.assert_awaited_once()