diff --git a/Makefile b/Makefile index c189fffc..d2319283 100644 --- a/Makefile +++ b/Makefile @@ -156,7 +156,7 @@ restart: stop start-example lint: @echo 'Formatting w/ Ruff...' ; echo '' ; uv run ruff format . @echo '' ; echo '' ; echo 'Linting w/ Ruff...' ; echo '' ; uv run ruff check . - @echo '' ; echo '' ; echo 'Type checking w/ Mypy...' ; echo '' ; uv run mypy . + @echo '' ; echo '' ; echo 'Type checking w/ Mypy...' ; echo '' ; uv run mypy roboco/ @echo '' ; echo '' ; echo 'Finding dead code w/ Vulture...' ; echo '' ; uv run vulture vulture_whitelist.py # Fix code @@ -241,7 +241,7 @@ quality: @echo "==> ruff check" @uv run ruff check . @echo "==> mypy" - @uv run mypy roboco/ + @uv run mypy roboco/ tests/ @echo "==> pytest with coverage" @uv run pytest -q --cov=roboco --cov-report=term-missing --cov-fail-under=80 @echo "==> xenon (cyclomatic complexity)" @@ -274,7 +274,7 @@ quality: quality-fast: @uv run ruff format --check . @uv run ruff check . - @uv run mypy roboco/ + @uv run mypy roboco/ tests/ @uv run pytest -q -x --no-cov # Fast pre-submit gate: format-check + lint + types + complexity, NO tests. diff --git a/pyproject.toml b/pyproject.toml index 39791029..e8321256 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -188,6 +188,10 @@ select = [ "roboco/foundation/_validate_lifecycle.py" = ["PLC0415"] # Test fixtures that reload modules to test env-var-at-import-time behavior "tests/unit/mcp_servers/*.py" = ["PLC0415"] +# Abstract-method stubs in test helpers: parameters must match the superclass +# signature for keyword-argument compatibility (mypy override check), but the +# stub bodies are empty — ARG002 would require renaming them, which breaks mypy. +"tests/unit/services/test_optimal_grounding.py" = ["ARG002"] # ============================================================================= # MyPy Configuration diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/foundation/test_lifecycle_consumer_parity.py b/tests/foundation/test_lifecycle_consumer_parity.py index 5f2ebfc0..b554102e 100644 --- a/tests/foundation/test_lifecycle_consumer_parity.py +++ b/tests/foundation/test_lifecycle_consumer_parity.py @@ -9,6 +9,7 @@ from __future__ import annotations from datetime import UTC, datetime from itertools import product +from typing import Any from unittest.mock import AsyncMock, MagicMock from uuid import uuid4 @@ -21,7 +22,7 @@ from roboco.services.gateway.choreographer import ( from roboco.services.gateway.choreographer._impl import DelegateInputs -def _make_deps(task_svc=None) -> ChoreographerDeps: +def _make_deps(task_svc: Any = None) -> ChoreographerDeps: base = { "task": task_svc or AsyncMock(), "work_session": AsyncMock(), diff --git a/tests/foundation/test_lifecycle_smoke_replay.py b/tests/foundation/test_lifecycle_smoke_replay.py index 9416ba49..36d8a5e2 100644 --- a/tests/foundation/test_lifecycle_smoke_replay.py +++ b/tests/foundation/test_lifecycle_smoke_replay.py @@ -53,7 +53,7 @@ from __future__ import annotations import json from pathlib import Path from types import SimpleNamespace -from typing import Any +from typing import Any, cast from uuid import uuid4 import pytest @@ -66,7 +66,7 @@ _EXPECTED_BUG_COUNT = 11 def _load_fixture() -> dict[str, Any]: - return json.loads(_FIXTURE.read_text()) + return cast("dict[str, Any]", json.loads(_FIXTURE.read_text())) def _bug_records() -> list[dict[str, Any]]: diff --git a/tests/foundation/test_lifecycle_spec.py b/tests/foundation/test_lifecycle_spec.py index be2c406a..271fdf1a 100644 --- a/tests/foundation/test_lifecycle_spec.py +++ b/tests/foundation/test_lifecycle_spec.py @@ -3,6 +3,7 @@ from __future__ import annotations from types import SimpleNamespace +from typing import Any from uuid import uuid4 import pytest @@ -486,7 +487,7 @@ def test_delegate_composes_create_subtask() -> None: assert iv.allowed_roles == frozenset({spec.Role.CELL_PM, spec.Role.MAIN_PM}) -_STUB_TASK_DEFAULTS = { +_STUB_TASK_DEFAULTS: dict[str, Any] = { "status": "pending", "task_type": "code", "commits": [], @@ -496,7 +497,7 @@ _STUB_TASK_DEFAULTS = { } -def _stub_task(**overrides): +def _stub_task(**overrides: Any) -> SimpleNamespace: fields = {**_STUB_TASK_DEFAULTS, **overrides} fields["commits"] = fields["commits"] or [] return SimpleNamespace(**fields) diff --git a/tests/foundation/test_task_completeness.py b/tests/foundation/test_task_completeness.py index 9372cd52..368881cd 100644 --- a/tests/foundation/test_task_completeness.py +++ b/tests/foundation/test_task_completeness.py @@ -3,6 +3,7 @@ from __future__ import annotations from types import SimpleNamespace +from typing import Any from roboco.foundation.policy import task_completeness as tc @@ -11,7 +12,7 @@ PARENT_PRIORITY_HIGH = 4 # parent task priority used for inheritance assertions DEFAULT_PRIORITY_MEDIUM = 2 # fill_priority_from_parent default when no parent -def _task(**fields): +def _task(**fields: Any) -> SimpleNamespace: """Build a SimpleNamespace mimicking a Task with the given fields.""" defaults = { "title": "ok", @@ -119,7 +120,7 @@ def test_fill_team_from_assignee_unknown_slug_returns_unchanged() -> None: def test_fill_priority_from_parent_inherits() -> None: - payload = {} + payload: dict[str, Any] = {} parent = SimpleNamespace(priority=PARENT_PRIORITY_HIGH) result = tc.fill_priority_from_parent(payload, parent) assert result["priority"] == PARENT_PRIORITY_HIGH @@ -135,14 +136,14 @@ def test_fill_priority_from_parent_does_not_overwrite_explicit() -> None: def test_fill_priority_from_parent_no_parent_uses_medium_default() -> None: - payload = {} + payload: dict[str, Any] = {} result = tc.fill_priority_from_parent(payload, None) assert result["priority"] == DEFAULT_PRIORITY_MEDIUM assert result["__priority_inherited"] is True def test_fill_parent_from_active_task_sets_id() -> None: - payload = {} + payload: dict[str, Any] = {} result = tc.fill_parent_from_active_task(payload, "task-id-123") assert result["parent_task_id"] == "task-id-123" diff --git a/tests/integration/test_a2a_priority_tristate.py b/tests/integration/test_a2a_priority_tristate.py index c6dba5c0..d67e309a 100644 --- a/tests/integration/test_a2a_priority_tristate.py +++ b/tests/integration/test_a2a_priority_tristate.py @@ -22,7 +22,7 @@ These tests pin that contract on both layers: from __future__ import annotations from contextlib import asynccontextmanager -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, patch from uuid import UUID, uuid4 @@ -37,7 +37,7 @@ from roboco.services.a2a import A2AService from roboco.services.notification import NotificationService if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -68,7 +68,7 @@ class _FakeDb: self.added: list = [] self._agent_uuid = agent_uuid - def add(self, obj) -> None: + def add(self, obj: Any) -> None: self.added.append(obj) obj.id = uuid4() @@ -78,7 +78,7 @@ class _FakeDb: async def commit(self) -> None: return None - async def execute(self, *_args, **_kwargs): + async def execute(self, *_args: Any, **_kwargs: Any) -> Any: result = MagicMock() agent = MagicMock() agent.id = self._agent_uuid @@ -87,13 +87,13 @@ class _FakeDb: result.scalars.return_value.all.return_value = [] return result - async def scalar(self, *_args, **_kwargs): + async def scalar(self, *_args: Any, **_kwargs: Any) -> None: # _create_notification's purpose-dedup lookup — no existing duplicate. return None @asynccontextmanager -async def _fake_ctx(db: _FakeDb): +async def _fake_ctx(db: _FakeDb) -> AsyncGenerator[_FakeDb]: yield db @@ -101,7 +101,7 @@ class _PatchDbContext: def __init__(self, db: _FakeDb) -> None: delivery_mock = MagicMock() delivery_mock.deliver = AsyncMock(return_value=None) - self._patches = [ + self._patches: list[Any] = [ patch( "roboco.services.notification.get_db_context", lambda: _fake_ctx(db), @@ -117,7 +117,7 @@ class _PatchDbContext: for p in self._patches: p.start() - def __exit__(self, *_args) -> None: + def __exit__(self, *_args: Any) -> None: for p in self._patches: p.stop() diff --git a/tests/integration/test_a2a_routes.py b/tests/integration/test_a2a_routes.py index b79b7d6a..79a63737 100644 --- a/tests/integration/test_a2a_routes.py +++ b/tests/integration/test_a2a_routes.py @@ -27,7 +27,7 @@ from roboco.models.base import ( ) if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -85,7 +85,7 @@ async def a2a_route_client( app.include_router(a2a_router, prefix="/api/a2a") app.include_router(wellknown_router) - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent_slug() -> str: diff --git a/tests/integration/test_a2a_service.py b/tests/integration/test_a2a_service.py index 95647538..67bb4cd4 100644 --- a/tests/integration/test_a2a_service.py +++ b/tests/integration/test_a2a_service.py @@ -4,7 +4,7 @@ from __future__ import annotations from datetime import UTC, datetime from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock, patch from unittest.mock import MagicMock as _MM from uuid import UUID, uuid4 @@ -809,7 +809,7 @@ async def test_cancel_task_status_no_value_attr(a2a_setup: dict) -> None: class _FakeStatus: # No .value attribute - def __str__(self): + def __str__(self) -> str: return "pending" fake_task = type( @@ -824,7 +824,7 @@ async def test_cancel_task_status_no_value_attr(a2a_setup: dict) -> None: seen = {"hit": False} - async def _intercepting_execute(stmt, *args, **kwargs): + async def _intercepting_execute(stmt: Any, *args: Any, **kwargs: Any) -> Any: if not seen["hit"]: seen["hit"] = True stub = _MM() @@ -905,7 +905,7 @@ def test_extract_message_text_no_text_attr() -> None: """text_part missing `text` attr → returns defaults.""" fake_part = SimpleNamespace(type="text") fake_msg = SimpleNamespace(parts=[fake_part]) - title, desc, full = A2AService.extract_message_text(fake_msg) + title, desc, full = A2AService.extract_message_text(cast("A2AMessage", fake_msg)) assert title == "A2A Task" assert desc == "" assert full == "" @@ -921,7 +921,9 @@ def test_update_task_with_message_no_text_attr() -> None: fake_part = SimpleNamespace(type="text") fake_msg = SimpleNamespace(parts=[fake_part]) fake_task = SimpleNamespace(dev_notes="orig") - A2AService.update_task_with_message(fake_task, fake_msg) + A2AService.update_task_with_message( + cast("TaskTable", fake_task), cast("A2AMessage", fake_msg) + ) assert fake_task.dev_notes == "orig" @@ -1137,7 +1139,7 @@ async def test_publish_a2a_response_event_no_bus() -> None: mock_bus = type("B", (), {"is_connected": lambda _self: False})() with patch("roboco.services.a2a.get_event_bus", return_value=mock_bus): await A2AService._publish_a2a_response_event( - fake_task, "creator", "requester", "responder" + cast("TaskTable", fake_task), "creator", "requester", "responder" ) @@ -1150,7 +1152,7 @@ async def test_publish_a2a_response_event_bus_exception_swallowed() -> None: ): # Exception swallowed. await A2AService._publish_a2a_response_event( - fake_task, "creator", "requester", "responder" + cast("TaskTable", fake_task), "creator", "requester", "responder" ) diff --git a/tests/integration/test_agents_routes.py b/tests/integration/test_agents_routes.py index 60a7f8c5..41763892 100644 --- a/tests/integration/test_agents_routes.py +++ b/tests/integration/test_agents_routes.py @@ -16,7 +16,7 @@ from roboco.db.tables import AgentTable from roboco.models import AgentRole, AgentStatus, Team if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -44,7 +44,7 @@ async def agents_client( app = FastAPI() app.include_router(agents_router, prefix="/api/agents") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_api_resources.py b/tests/integration/test_api_resources.py index c6d9be3c..d7cecc33 100644 --- a/tests/integration/test_api_resources.py +++ b/tests/integration/test_api_resources.py @@ -2,8 +2,9 @@ from __future__ import annotations +import uuid from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from uuid import uuid4 import pytest @@ -40,7 +41,7 @@ async def test_get_or_404_finds_existing(db_session: AsyncSession) -> None: db_session.add(agent) await db_session.flush() - fetched = await get_or_404(db_session, AgentTable, agent.id) + fetched = await get_or_404(db_session, AgentTable, cast("uuid.UUID", agent.id)) assert fetched.id == agent.id diff --git a/tests/integration/test_board_review_brief.py b/tests/integration/test_board_review_brief.py index 19b5466e..eab72c8a 100644 --- a/tests/integration/test_board_review_brief.py +++ b/tests/integration/test_board_review_brief.py @@ -2,8 +2,8 @@ from __future__ import annotations from datetime import UTC, datetime from http import HTTPStatus -from typing import TYPE_CHECKING -from uuid import uuid4 +from typing import TYPE_CHECKING, cast +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -95,7 +95,7 @@ async def brief_setup(db_session: AsyncSession) -> AsyncIterator[dict]: agent: AgentTable, *, entry_type: JournalEntryType, - task_id, + task_id: UUID, title: str, when: datetime, ) -> None: @@ -116,14 +116,14 @@ async def brief_setup(db_session: AsyncSession) -> AsyncIterator[dict]: _entry( po, entry_type=JournalEntryType.DECISION_LOG, - task_id=task.id, + task_id=cast("UUID", task.id), title="PO review", when=datetime(2026, 1, 1, tzinfo=UTC), ) _entry( hom, entry_type=JournalEntryType.DECISION_LOG, - task_id=task.id, + task_id=cast("UUID", task.id), title="HoM review", when=datetime(2026, 1, 2, tzinfo=UTC), ) @@ -131,21 +131,21 @@ async def brief_setup(db_session: AsyncSession) -> AsyncIterator[dict]: _entry( # non-board author, decision log dev, entry_type=JournalEntryType.DECISION_LOG, - task_id=task.id, + task_id=cast("UUID", task.id), title="Dev decision", when=datetime(2026, 1, 3, tzinfo=UTC), ) _entry( # board author, but not a decision log po, entry_type=JournalEntryType.TASK_REFLECTION, - task_id=task.id, + task_id=cast("UUID", task.id), title="PO reflection", when=datetime(2026, 1, 4, tzinfo=UTC), ) _entry( # board decision log, but on a different task po, entry_type=JournalEntryType.DECISION_LOG, - task_id=other.id, + task_id=cast("UUID", other.id), title="PO review of other task", when=datetime(2026, 1, 5, tzinfo=UTC), ) @@ -191,7 +191,7 @@ def _board_review_app(db_session: AsyncSession) -> FastAPI: app = FastAPI() app.include_router(tasks_router, prefix="/api/tasks") - async def _db(): + async def _db() -> AsyncIterator[AsyncSession]: yield db_session async def _agent() -> AgentContext: diff --git a/tests/integration/test_branch_name_builder.py b/tests/integration/test_branch_name_builder.py index 8ed079cd..629b12bf 100644 --- a/tests/integration/test_branch_name_builder.py +++ b/tests/integration/test_branch_name_builder.py @@ -5,7 +5,7 @@ from __future__ import annotations from types import SimpleNamespace from typing import TYPE_CHECKING from unittest.mock import AsyncMock -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -59,7 +59,7 @@ async def branch_setup( db_session.add(project) await db_session.flush() - def _make_task(parent_id=None) -> TaskTable: + def _make_task(parent_id: UUID | None = None) -> TaskTable: task = TaskTable( id=uuid4(), title="t", diff --git a/tests/integration/test_channels_routes.py b/tests/integration/test_channels_routes.py index 3c15e48a..28ffbdb2 100644 --- a/tests/integration/test_channels_routes.py +++ b/tests/integration/test_channels_routes.py @@ -2,9 +2,10 @@ from __future__ import annotations +import uuid from http import HTTPStatus from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import patch from uuid import uuid4 from uuid import uuid4 as _uuid4 @@ -24,7 +25,7 @@ from roboco.models.permissions import AgentContext from roboco.services.messaging import get_messaging_service if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -61,11 +62,13 @@ async def channels_client( app = FastAPI() app.include_router(channels_router, prefix="/api/channels") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=main_pm.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("uuid.UUID", main_pm.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -145,12 +148,14 @@ async def test_create_channel_dev_forbidden(db_session: AsyncSession) -> None: app = FastAPI() app.include_router(channels_router, prefix="/api/channels") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=dev.id, role=AgentRole.DEVELOPER, team=Team.BACKEND + agent_id=cast("uuid.UUID", dev.id), + role=AgentRole.DEVELOPER, + team=Team.BACKEND, ) app.dependency_overrides[get_db] = _override_db @@ -301,11 +306,13 @@ async def test_list_channels_filter_by_accessible_slug( app = FastAPI() app.include_router(channels_router, prefix="/api/channels") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=main_pm.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("uuid.UUID", main_pm.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -351,12 +358,14 @@ async def test_get_channel_forbidden_for_unprivileged( app = FastAPI() app.include_router(channels_router, prefix="/api/channels") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=dev.id, role=AgentRole.DEVELOPER, team=Team.FRONTEND + agent_id=cast("uuid.UUID", dev.id), + role=AgentRole.DEVELOPER, + team=Team.FRONTEND, ) app.dependency_overrides[get_db] = _override_db @@ -405,12 +414,14 @@ async def test_get_channel_groups_forbidden_for_unprivileged( app = FastAPI() app.include_router(channels_router, prefix="/api/channels") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=dev.id, role=AgentRole.DEVELOPER, team=Team.FRONTEND + agent_id=cast("uuid.UUID", dev.id), + role=AgentRole.DEVELOPER, + team=Team.FRONTEND, ) app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_dashboard_routes.py b/tests/integration/test_dashboard_routes.py index c79d62d6..85bfe1e8 100644 --- a/tests/integration/test_dashboard_routes.py +++ b/tests/integration/test_dashboard_routes.py @@ -2,8 +2,9 @@ from __future__ import annotations +import uuid from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from uuid import uuid4 import pytest @@ -19,7 +20,7 @@ from roboco.models.permissions import AgentContext from roboco.services.dashboard import reset_storage if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -48,11 +49,13 @@ async def dashboard_client( app = FastAPI() app.include_router(dashboard_router, prefix="/api/dashboard") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=agent.id, role=AgentRole.CEO, team=None) + return AgentContext( + agent_id=cast("uuid.UUID", agent.id), role=AgentRole.CEO, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent diff --git a/tests/integration/test_db_base.py b/tests/integration/test_db_base.py index dc22029a..d1f4a7ce 100644 --- a/tests/integration/test_db_base.py +++ b/tests/integration/test_db_base.py @@ -8,6 +8,7 @@ and drop/close. from __future__ import annotations +from typing import TYPE_CHECKING, cast from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -25,9 +26,14 @@ from roboco.db.base import ( run_migrations, ) +if TYPE_CHECKING: + from collections.abc import Generator + + from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker + @pytest.fixture(autouse=True) -def _reset_holder(): +def _reset_holder() -> Generator[None]: """Snapshot/restore the singleton so tests don't poison the live engine.""" saved_engine = _DbHolder.engine saved_factory = _DbHolder.session_factory @@ -465,8 +471,12 @@ async def test_close_db_disposes_and_clears_singletons() -> None: await close_db() fake_engine.dispose.assert_awaited_once() - assert _DbHolder.engine is None - assert _DbHolder.session_factory is None + engine_after = cast("AsyncEngine | None", _DbHolder.engine) + factory_after = cast( + "async_sessionmaker[AsyncSession] | None", _DbHolder.session_factory + ) + assert engine_after is None + assert factory_after is None @pytest.mark.asyncio diff --git a/tests/integration/test_db_seed.py b/tests/integration/test_db_seed.py index 1c43437e..6b21f512 100644 --- a/tests/integration/test_db_seed.py +++ b/tests/integration/test_db_seed.py @@ -4,7 +4,7 @@ from __future__ import annotations import contextlib from contextlib import asynccontextmanager -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, patch from uuid import uuid4 @@ -18,6 +18,8 @@ from roboco.db.seed import ( ) if TYPE_CHECKING: + from collections.abc import AsyncGenerator + from sqlalchemy.ext.asyncio import AsyncSession @@ -130,7 +132,7 @@ async def test_bootstrap_database_invokes_full_pipeline() -> None: fake_session.commit = AsyncMock() @asynccontextmanager - async def _ctx(): + async def _ctx() -> AsyncGenerator[Any]: yield fake_session with ( diff --git a/tests/integration/test_default_branch_master.py b/tests/integration/test_default_branch_master.py index c93fa769..13ff6b05 100644 --- a/tests/integration/test_default_branch_master.py +++ b/tests/integration/test_default_branch_master.py @@ -7,11 +7,11 @@ is exercised by the same SQLAlchemy flush path production uses. from __future__ import annotations from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from uuid import uuid4 import pytest -from roboco.db.tables import AgentTable, ProjectTable +from roboco.db.tables import AgentTable, ProjectTable, TaskTable from roboco.models import AgentRole, AgentStatus, Team from roboco.services.git import GitService from roboco.services.task import TaskService @@ -70,7 +70,7 @@ async def test_resolve_parent_branch_falls_back_to_master( task = SimpleNamespace(id=uuid4(), parent_task_id=None) project = SimpleNamespace(default_branch="") - branch = await svc._resolve_parent_branch(task, project) + branch = await svc._resolve_parent_branch(cast("TaskTable", task), project) assert branch == "master" diff --git a/tests/integration/test_dev_subtask_inherits_cross_cell_dependency.py b/tests/integration/test_dev_subtask_inherits_cross_cell_dependency.py index e590a109..52e7b4fb 100644 --- a/tests/integration/test_dev_subtask_inherits_cross_cell_dependency.py +++ b/tests/integration/test_dev_subtask_inherits_cross_cell_dependency.py @@ -10,8 +10,8 @@ must become dispatchable once the UX task reaches a terminal state. from __future__ import annotations -from typing import TYPE_CHECKING -from uuid import uuid4 +from typing import TYPE_CHECKING, cast +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -146,7 +146,7 @@ async def _build_product_fanout(setup: dict) -> dict: created_by=setup["creator"], project_id=setup["ux_project_id"], product_id=setup["product_id"], - parent_task_id=root.id, + parent_task_id=cast("UUID", root.id), task_type=TaskType.DESIGN, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, @@ -161,7 +161,7 @@ async def _build_product_fanout(setup: dict) -> dict: created_by=setup["creator"], project_id=setup["fe_project_id"], product_id=setup["product_id"], - parent_task_id=root.id, + parent_task_id=cast("UUID", root.id), task_type=TaskType.CODE, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, @@ -171,7 +171,7 @@ async def _build_product_fanout(setup: dict) -> dict: # exactly as the product fan-out does on delegate. await choreo._wire_ux_frontend_dependency(fe_cell, root) await svc.session.flush() - refreshed_fe = await svc.get(fe_cell.id) + refreshed_fe = await svc.get(cast("UUID", fe_cell.id)) assert refreshed_fe is not None assert ux_cell.id in refreshed_fe.dependency_ids, ( "precondition: frontend cell task must depend on the UX cell task" @@ -199,7 +199,7 @@ async def test_dev_subtask_held_until_ux_dependency_resolves( created_by=fanout_setup["creator"], project_id=fanout_setup["fe_project_id"], product_id=fanout_setup["product_id"], - parent_task_id=fe_cell.id, + parent_task_id=cast("UUID", fe_cell.id), assigned_to=fanout_setup["fe_dev_id"], task_type=TaskType.CODE, nature=TaskNature.TECHNICAL, @@ -282,8 +282,8 @@ async def test_backend_cell_also_depends_on_ux(fanout_setup: dict) -> None: await choreo._wire_ux_frontend_dependency(be_cell, root) await svc.session.flush() - be_row = await svc.get(be_cell.id) - ux_row = await svc.get(ux_cell.id) + be_row = await svc.get(cast("UUID", be_cell.id)) + ux_row = await svc.get(cast("UUID", ux_cell.id)) assert be_row is not None and ux_row is not None assert ux_cell.id in be_row.dependency_ids, ( "backend cell task must depend on the UX cell task" @@ -326,7 +326,7 @@ async def test_pending_impl_cells_retrowired_when_ux_arrives_later( created_by=fanout_setup["creator"], project_id=fanout_setup["fe_project_id"], product_id=fanout_setup["product_id"], - parent_task_id=root.id, + parent_task_id=cast("UUID", root.id), task_type=TaskType.CODE, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, @@ -341,7 +341,7 @@ async def test_pending_impl_cells_retrowired_when_ux_arrives_later( created_by=fanout_setup["creator"], project_id=fanout_setup["be_project_id"], product_id=fanout_setup["product_id"], - parent_task_id=root.id, + parent_task_id=cast("UUID", root.id), task_type=TaskType.CODE, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, @@ -357,7 +357,7 @@ async def test_pending_impl_cells_retrowired_when_ux_arrives_later( created_by=fanout_setup["creator"], project_id=fanout_setup["ux_project_id"], product_id=fanout_setup["product_id"], - parent_task_id=root.id, + parent_task_id=cast("UUID", root.id), task_type=TaskType.DESIGN, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, @@ -366,9 +366,9 @@ async def test_pending_impl_cells_retrowired_when_ux_arrives_later( await choreo._wire_ux_frontend_dependency(ux_cell, root) await svc.session.flush() - fe_row = await svc.get(fe_cell.id) - be_row = await svc.get(be_cell.id) - ux_row = await svc.get(ux_cell.id) + fe_row = await svc.get(cast("UUID", fe_cell.id)) + be_row = await svc.get(cast("UUID", be_cell.id)) + ux_row = await svc.get(cast("UUID", ux_cell.id)) assert fe_row is not None and be_row is not None and ux_row is not None assert ux_cell.id in fe_row.dependency_ids, "frontend must retro-wire onto UX" assert ux_cell.id in be_row.dependency_ids, "backend must retro-wire onto UX" diff --git a/tests/integration/test_docs_routes.py b/tests/integration/test_docs_routes.py index dff94bdc..a0da95b2 100644 --- a/tests/integration/test_docs_routes.py +++ b/tests/integration/test_docs_routes.py @@ -2,8 +2,9 @@ from __future__ import annotations +import uuid from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import AsyncMock, patch from uuid import uuid4 @@ -20,7 +21,7 @@ from roboco.models.task import DocRef from roboco.services.base import NotFoundError, UnauthorizedError, ValidationError if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -48,12 +49,14 @@ async def docs_client( app = FastAPI() app.include_router(docs_router, prefix="/api/docs") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=agent.id, role=AgentRole.DOCUMENTER, team=Team.BACKEND + agent_id=cast("uuid.UUID", agent.id), + role=AgentRole.DOCUMENTER, + team=Team.BACKEND, ) app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_foundation_phase2_smoke.py b/tests/integration/test_foundation_phase2_smoke.py index f451b3b4..4d4efb17 100644 --- a/tests/integration/test_foundation_phase2_smoke.py +++ b/tests/integration/test_foundation_phase2_smoke.py @@ -27,7 +27,7 @@ def _enclosing_function(tree: ast.AST, lineno: int) -> str | None: return candidate -def test_no_inline_has_decision_for_task_remains_in_choreographer(): +def test_no_inline_has_decision_for_task_remains_in_choreographer() -> None: """All journal:decision checks must use tracing.check_requirements via the unified helpers (_check_pm_decision_required, _check_complete_gates, _check_submit_up_gates, _check_tracing_gates, _check_claim_journal_at_claim, @@ -63,12 +63,12 @@ def test_no_inline_has_decision_for_task_remains_in_choreographer(): assert suspicious == [], f"inline has_decision_for_task remains: {suspicious}" -def test_tracing_gate_module_removed(): +def test_tracing_gate_module_removed() -> None: with pytest.raises(ModuleNotFoundError): importlib.import_module("roboco.services.gateway.tracing_gate") -def test_every_intent_verb_has_tracing_decision(): +def test_every_intent_verb_has_tracing_decision() -> None: """Mirror of the foundation parity test, as a smoke-gate.""" intent_verbs = set(spec._INTENT_VERBS.keys()) in_table = set(tracing.VERB_REQUIREMENTS) @@ -79,9 +79,9 @@ def test_every_intent_verb_has_tracing_decision(): ) -def test_no_dangling_requirements(): +def test_no_dangling_requirements() -> None: """Every Requirement value is referenced by at least one verb.""" - used = set() + used: set[tracing.Requirement] = set() for reqs in tracing.VERB_REQUIREMENTS.values(): used.update(reqs) assert set(tracing.Requirement) - used == set() diff --git a/tests/integration/test_foundation_phase3_smoke.py b/tests/integration/test_foundation_phase3_smoke.py index b26bc0e9..2434b3cc 100644 --- a/tests/integration/test_foundation_phase3_smoke.py +++ b/tests/integration/test_foundation_phase3_smoke.py @@ -27,42 +27,42 @@ from roboco.services.gateway.envelope import Envelope _EXPECTED_VERB_RETRY_LIMIT = 3 -def test_notification_perms_module_removed(): +def test_notification_perms_module_removed() -> None: """services/enforcement/notification_perms.py is gone.""" with pytest.raises(ModuleNotFoundError): importlib.import_module("roboco.enforcement.notification_perms") -def test_agents_config_notification_permissions_removed(): +def test_agents_config_notification_permissions_removed() -> None: """The contradictory NOTIFICATION_PERMISSIONS dict is gone.""" assert not hasattr(agents_config, "NOTIFICATION_PERMISSIONS") -def test_channel_access_derives_from_foundation(): +def test_channel_access_derives_from_foundation() -> None: """agents_config.CHANNEL_ACCESS keys match foundation.CHANNELS exactly.""" assert set(CHANNEL_ACCESS.keys()) == set(communications.CHANNELS.keys()) -def test_seed_default_channels_derive_from_foundation(): +def test_seed_default_channels_derive_from_foundation() -> None: """seeds.DEFAULT_CHANNELS slugs match foundation.CHANNELS.""" seed_slugs = {ch["slug"] for ch in DEFAULT_CHANNELS} foundation_slugs = set(communications.CHANNELS.keys()) assert seed_slugs == foundation_slugs -def test_notify_sender_roles_includes_ceo_excludes_auditor(): +def test_notify_sender_roles_includes_ceo_excludes_auditor() -> None: """Spec §5.5 contradiction closed.""" assert identity.Role.CEO in NOTIFY_SENDER_ROLES assert identity.Role.AUDITOR not in NOTIFY_SENDER_ROLES -def test_ack_required_table_covers_every_notification_type(): +def test_ack_required_table_covers_every_notification_type() -> None: """Spec §5.5 ACK_REQUIRED_BY_TYPE covers the full enum.""" for nt in NotificationType: assert nt in ACK_REQUIRED_BY_TYPE -def test_a2a_priority_high_reachable(): +def test_a2a_priority_high_reachable() -> None: """A2A urgency tristate end-to-end (was reduced to boolean pre-Phase-3).""" # Confirm the foundation enum has all three values. values = {p.value for p in Priority} @@ -71,19 +71,19 @@ def test_a2a_priority_high_reachable(): assert "urgent" in values -def test_loop_action_default_is_halt(): +def test_loop_action_default_is_halt() -> None: """Spec §5.7: BudgetPolicy.loop_action default is 'halt' (was 'warn').""" assert DEFAULT_BUDGET.loop_action == "halt" -def test_verb_retry_limits_cover_critical_handoff_verbs(): +def test_verb_retry_limits_cover_critical_handoff_verbs() -> None: """Spec §5.7: per-verb circuit breaker has caps for the handoff verbs.""" for verb in ("i_am_done", "complete", "submit_up", "delegate"): assert verb in VERB_RETRY_LIMITS assert VERB_RETRY_LIMITS[verb] == _EXPECTED_VERB_RETRY_LIMIT -def test_envelope_circuit_open_kind_distinct_from_tracing_gap(): +def test_envelope_circuit_open_kind_distinct_from_tracing_gap() -> None: """Spec §5.7: circuit_open envelope is its own kind.""" env_co = Envelope.circuit_open( verb="i_am_done", attempts=4, window_seconds=60, remediate="x" @@ -94,7 +94,7 @@ def test_envelope_circuit_open_kind_distinct_from_tracing_gap(): assert env_co.as_dict()["error"] != env_tg.as_dict()["error"] -def test_auditor_silent_runtime_guard_in_say_dm(): +def test_auditor_silent_runtime_guard_in_say_dm() -> None: """Spec §5.5: auditor say/dm refused at runtime (defense in depth).""" # The actual guard test lives in tests/unit/gateway/test_auditor_silent_guard.py. # Smoke gate verifies the guard exists by checking the source for the diff --git a/tests/integration/test_foundation_phase4_smoke.py b/tests/integration/test_foundation_phase4_smoke.py index d77a24e2..fe5e7b16 100644 --- a/tests/integration/test_foundation_phase4_smoke.py +++ b/tests/integration/test_foundation_phase4_smoke.py @@ -12,7 +12,7 @@ from roboco.api import deps as api_deps from roboco.api.routes.v1 import _role_dep as v1_role_dep -def test_lifecycle_module_lives_in_foundation(): +def test_lifecycle_module_lives_in_foundation() -> None: """Canonical import path is foundation.policy.lifecycle.""" lifecycle = importlib.import_module("roboco.foundation.policy.lifecycle") assert hasattr(lifecycle, "Role") @@ -20,18 +20,18 @@ def test_lifecycle_module_lives_in_foundation(): assert hasattr(lifecycle, "_INTENT_VERBS") -def test_legacy_lifecycle_package_removed(): +def test_legacy_lifecycle_package_removed() -> None: """Legacy roboco.lifecycle package is gone.""" with pytest.raises(ModuleNotFoundError): importlib.import_module("roboco.lifecycle") -def test_legacy_lifecycle_spec_module_removed(): +def test_legacy_lifecycle_spec_module_removed() -> None: with pytest.raises(ModuleNotFoundError): importlib.import_module("roboco.lifecycle.spec") -def test_foundation_policy_complete(): +def test_foundation_policy_complete() -> None: """All 6 policy domains exist in foundation.""" for mod in ( "lifecycle", @@ -44,7 +44,7 @@ def test_foundation_policy_complete(): importlib.import_module(f"roboco.foundation.policy.{mod}") -def test_no_lifecycle_imports_in_production(): +def test_no_lifecycle_imports_in_production() -> None: """Production code (roboco/) imports from foundation directly, no legacy paths.""" proc = subprocess.run( [ @@ -68,7 +68,7 @@ def test_no_lifecycle_imports_in_production(): assert suspicious == [], f"legacy lifecycle imports remain: {suspicious}" -def test_route_guard_role_sets_derive_from_foundation(): +def test_route_guard_role_sets_derive_from_foundation() -> None: """api/deps.py + v1/_role_dep.py use foundation Role-set composition.""" deps_src = inspect.getsource(api_deps) role_dep_src = inspect.getsource(v1_role_dep) @@ -79,7 +79,7 @@ def test_route_guard_role_sets_derive_from_foundation(): assert "Role." in role_dep_src # uses Role enum members, not raw strings -def test_make_foundation_check_target_exists(): +def test_make_foundation_check_target_exists() -> None: """Drift gate target is present in Makefile.""" makefile = Path("Makefile").read_text(encoding="utf-8") assert "foundation-check:" in makefile diff --git a/tests/integration/test_full_lifecycle_real_db.py b/tests/integration/test_full_lifecycle_real_db.py index 99bb5979..c0e25897 100644 --- a/tests/integration/test_full_lifecycle_real_db.py +++ b/tests/integration/test_full_lifecycle_real_db.py @@ -104,7 +104,7 @@ class _StubGit: sha = uuid4().hex[:40] commits = list(self._task.commits or []) commits.append({"sha": sha, "message": message, "task_id": str(task_id)}) - self._task.commits = commits # type: ignore[assignment] + self._task.commits = commits await self._session.flush() return { "sha": sha, diff --git a/tests/integration/test_git_routes.py b/tests/integration/test_git_routes.py index 02660842..660be2e3 100644 --- a/tests/integration/test_git_routes.py +++ b/tests/integration/test_git_routes.py @@ -2,9 +2,10 @@ from __future__ import annotations +import uuid from http import HTTPStatus from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 @@ -27,7 +28,7 @@ from roboco.services.base import ( ) if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -65,12 +66,14 @@ async def git_client( app = FastAPI() app.include_router(git_router, prefix="/api/git") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=agent.id, role=AgentRole.DEVELOPER, team=Team.BACKEND + agent_id=cast("uuid.UUID", agent.id), + role=AgentRole.DEVELOPER, + team=Team.BACKEND, ) app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_groups_routes.py b/tests/integration/test_groups_routes.py index 2c99ddd5..695d626e 100644 --- a/tests/integration/test_groups_routes.py +++ b/tests/integration/test_groups_routes.py @@ -2,8 +2,9 @@ from __future__ import annotations +import uuid from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import patch from uuid import uuid4 @@ -19,7 +20,7 @@ from roboco.models.base import ChannelType from roboco.models.permissions import AgentContext if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -56,11 +57,13 @@ async def groups_client( app = FastAPI() app.include_router(groups_router, prefix="/api/groups") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=pm.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("uuid.UUID", pm.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -173,12 +176,14 @@ async def test_create_group_developer_forbidden( app = FastAPI() app.include_router(groups_router, prefix="/api/groups") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=dev.id, role=AgentRole.DEVELOPER, team=Team.BACKEND + agent_id=cast("uuid.UUID", dev.id), + role=AgentRole.DEVELOPER, + team=Team.BACKEND, ) app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_indexed_document_repo.py b/tests/integration/test_indexed_document_repo.py index 3f3e28e6..5194a6d8 100644 --- a/tests/integration/test_indexed_document_repo.py +++ b/tests/integration/test_indexed_document_repo.py @@ -93,7 +93,9 @@ async def test_upsert_batch_truncates_long_preview( rows = await repo.get_by_index_type("code") matching = [r for r in rows if r.source == "long.md"] assert matching - assert len(matching[0].preview) <= _PREVIEW_MAX + preview = matching[0].preview + assert preview is not None + assert len(preview) <= _PREVIEW_MAX @pytest.mark.asyncio diff --git a/tests/integration/test_journal_routes.py b/tests/integration/test_journal_routes.py index f1758e65..a3a58f05 100644 --- a/tests/integration/test_journal_routes.py +++ b/tests/integration/test_journal_routes.py @@ -8,7 +8,7 @@ mapping) is exercised end-to-end. from __future__ import annotations from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import patch from uuid import UUID, uuid4 @@ -30,7 +30,7 @@ from roboco.models.base import JournalEntryType, TaskNature, TaskStatus, TaskTyp from roboco.models.permissions import AgentContext if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncGenerator, AsyncIterator from sqlalchemy.ext.asyncio import AsyncSession @@ -58,12 +58,12 @@ async def journal_client( app = FastAPI() app.include_router(journals_router, prefix="/api/journals") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=agent.id, + agent_id=cast("UUID", agent.id), role=AgentRole.DEVELOPER, team=Team.BACKEND, slug=agent.slug, @@ -264,12 +264,12 @@ async def journal_setup_with_task( app = FastAPI() app.include_router(journals_router, prefix="/api/journals") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=agent.id, + agent_id=cast("UUID", agent.id), role=AgentRole.DEVELOPER, team=Team.BACKEND, slug=agent.slug, @@ -280,12 +280,14 @@ async def journal_setup_with_task( transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: - yield client, agent, task.id + yield client, agent, cast("UUID", task.id) app.dependency_overrides.clear() @pytest.mark.asyncio -async def test_add_task_reflection(journal_setup_with_task) -> None: +async def test_add_task_reflection( + journal_setup_with_task: tuple[AsyncClient, AgentTable, UUID], +) -> None: client, _, task_id = journal_setup_with_task response = await client.post( "/api/journals/me/reflections", @@ -303,7 +305,9 @@ async def test_add_task_reflection(journal_setup_with_task) -> None: @pytest.mark.asyncio -async def test_add_decision_log(journal_setup_with_task) -> None: +async def test_add_decision_log( + journal_setup_with_task: tuple[AsyncClient, AgentTable, UUID], +) -> None: client, _, task_id = journal_setup_with_task response = await client.post( "/api/journals/me/decisions", @@ -325,7 +329,9 @@ async def test_add_decision_log(journal_setup_with_task) -> None: @pytest.mark.asyncio -async def test_add_learning(journal_setup_with_task) -> None: +async def test_add_learning( + journal_setup_with_task: tuple[AsyncClient, AgentTable, UUID], +) -> None: client, _, task_id = journal_setup_with_task response = await client.post( "/api/journals/me/learnings", @@ -340,7 +346,9 @@ async def test_add_learning(journal_setup_with_task) -> None: @pytest.mark.asyncio -async def test_add_struggle(journal_setup_with_task) -> None: +async def test_add_struggle( + journal_setup_with_task: tuple[AsyncClient, AgentTable, UUID], +) -> None: client, _, task_id = journal_setup_with_task response = await client.post( "/api/journals/me/struggles", @@ -418,7 +426,7 @@ async def test_list_agent_entries_for_self( @pytest.mark.asyncio async def test_search_my_entries_returns_list( - journal_setup_with_task, + journal_setup_with_task: tuple[AsyncClient, AgentTable, UUID], ) -> None: """Search route — may 200 with empty list or 500 if RAG isn't configured.""" client, _, _ = journal_setup_with_task @@ -438,7 +446,7 @@ async def test_search_my_entries_returns_list( @pytest.mark.asyncio async def test_add_general_entry( - journal_setup_with_task, + journal_setup_with_task: tuple[AsyncClient, AgentTable, UUID], ) -> None: client, _, task_id = journal_setup_with_task response = await client.post( @@ -678,12 +686,12 @@ async def test_get_entry_full_success_with_slug(db_session: AsyncSession) -> Non app = FastAPI() app.include_router(journals_router, prefix="/api/journals") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=agent.id, + agent_id=cast("UUID", agent.id), role=AgentRole.DEVELOPER, team=Team.BACKEND, slug=agent.slug, @@ -754,12 +762,12 @@ async def test_delete_entry_other_agent_forbidden( app = FastAPI() app.include_router(journals_router, prefix="/api/journals") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent_owner() -> AgentContext: return AgentContext( - agent_id=owner.id, + agent_id=cast("UUID", owner.id), role=AgentRole.DEVELOPER, team=Team.BACKEND, slug=owner.slug, @@ -785,7 +793,7 @@ async def test_delete_entry_other_agent_forbidden( # Switch to the intruder. async def _override_agent_intruder() -> AgentContext: return AgentContext( - agent_id=intruder.id, + agent_id=cast("UUID", intruder.id), role=AgentRole.DEVELOPER, team=Team.BACKEND, slug=intruder.slug, @@ -907,12 +915,12 @@ async def _make_cross_agent_app( app = FastAPI() app.include_router(journals_router, prefix="/api/journals") - async def _override_db(): + async def _override_db() -> AsyncGenerator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=reader.id, + agent_id=cast("UUID", reader.id), role=reader.role, team=reader.team, slug=reader.slug, diff --git a/tests/integration/test_journal_service.py b/tests/integration/test_journal_service.py index 3c80e772..861837ac 100644 --- a/tests/integration/test_journal_service.py +++ b/tests/integration/test_journal_service.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import uuid +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock as _AsyncMock from unittest.mock import MagicMock as _MagicMock from uuid import uuid4 @@ -268,7 +269,7 @@ async def test_get_agent_slug_returns_none_for_unknown( assert await svc.get_agent_slug(uuid4()) is None -def _reflection(tid) -> TaskReflectionParams: +def _reflection(tid: uuid.UUID) -> TaskReflectionParams: return TaskReflectionParams( task_id=tid, title="r", @@ -279,7 +280,7 @@ def _reflection(tid) -> TaskReflectionParams: ) -def _decision(tid) -> DecisionLogParams: +def _decision(tid: uuid.UUID) -> DecisionLogParams: return DecisionLogParams( title="d", context="ctx", @@ -291,11 +292,11 @@ def _decision(tid) -> DecisionLogParams: ) -def _learning(tid) -> LearningEntryParams: +def _learning(tid: uuid.UUID) -> LearningEntryParams: return LearningEntryParams(title="l", what_learned="x", task_id=tid) -def _struggle(tid) -> StruggleEntryParams: +def _struggle(tid: uuid.UUID) -> StruggleEntryParams: return StruggleEntryParams( title="s", what_struggled="x", @@ -453,7 +454,7 @@ async def test_create_entry_integrity_error_returns_none( err = _IE("insert", {}, Exception("FK violation")) original_commit = svc.session.commit - async def _raise_once(*_args, **_kwargs): + async def _raise_once(*_args: Any, **_kwargs: Any) -> None: # Restore for cleanup paths. svc.session.commit = original_commit raise err diff --git a/tests/integration/test_kanban_routes.py b/tests/integration/test_kanban_routes.py index d8533434..62764d58 100644 --- a/tests/integration/test_kanban_routes.py +++ b/tests/integration/test_kanban_routes.py @@ -25,7 +25,7 @@ async def kanban_client( app = FastAPI() app.include_router(kanban_router, prefix="/api/kanban") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_kanban_service.py b/tests/integration/test_kanban_service.py index c6b296c1..22ac1b91 100644 --- a/tests/integration/test_kanban_service.py +++ b/tests/integration/test_kanban_service.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from uuid import uuid4 import pytest @@ -59,7 +59,7 @@ async def kanban_setup( } -def _seed(setup: dict, *, status: TaskStatus, **kw) -> TaskTable: +def _seed(setup: dict, *, status: TaskStatus, **kw: Any) -> TaskTable: return TaskTable( id=uuid4(), title=kw.pop("title", "t"), diff --git a/tests/integration/test_learning_service.py b/tests/integration/test_learning_service.py index 0a9123c7..138e4b2a 100644 --- a/tests/integration/test_learning_service.py +++ b/tests/integration/test_learning_service.py @@ -15,8 +15,9 @@ shared DB for later tests (broke test_qa_agent_for_team_returns_none). from __future__ import annotations +import uuid from contextlib import asynccontextmanager -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from uuid import uuid4 import pytest @@ -103,7 +104,7 @@ async def test_team_scope_role_uppercase_via_agent_role_enum( await svc.initialize(_StubOptimal()) learning = await svc.record_learning( RecordLearningParams( - agent_id=author.id, + agent_id=cast("uuid.UUID", author.id), agent_role="developer", content="A useful pattern for batch updates", learning_type=LearningType.PATTERN, @@ -133,7 +134,7 @@ async def test_team_scope_invalid_role_skips_filter( await svc.initialize(_StubOptimal()) await svc.record_learning( RecordLearningParams( - agent_id=a1.id, + agent_id=cast("uuid.UUID", a1.id), agent_role="not_a_real_role", content="content", learning_type=LearningType.SOLUTION, @@ -158,7 +159,7 @@ async def test_cell_scope_runs_without_role_filter( await svc.initialize(_StubOptimal()) await svc.record_learning( RecordLearningParams( - agent_id=a1.id, + agent_id=cast("uuid.UUID", a1.id), agent_role="developer", content="cell-scope lesson", learning_type=LearningType.INSIGHT, @@ -184,7 +185,7 @@ async def test_org_scope_notifies_all_other_agents( await svc.initialize(_StubOptimal()) await svc.record_learning( RecordLearningParams( - agent_id=author.id, + agent_id=cast("uuid.UUID", author.id), agent_role="developer", content="x" * 250, # >200 chars to exercise the truncation branch learning_type=LearningType.SOLUTION, @@ -218,7 +219,7 @@ async def test_no_other_agents_logs_and_returns( await svc.initialize(_StubOptimal()) learning = await svc.record_learning( RecordLearningParams( - agent_id=author.id, + agent_id=cast("uuid.UUID", author.id), agent_role="developer", content="solo agent learning", learning_type=LearningType.SOLUTION, @@ -249,8 +250,8 @@ async def test_no_recipients_after_role_filter_hits_empty_branch( real_role = AgentRole class _PermissiveRole: - def __new__(cls, value: str) -> object: - return real_role(value.lower()) + def __new__(cls, value: str) -> _PermissiveRole: + return cast("_PermissiveRole", real_role(value.lower())) monkeypatch.setattr("roboco.models.base.AgentRole", _PermissiveRole) @@ -269,7 +270,7 @@ async def test_no_recipients_after_role_filter_hits_empty_branch( await svc.initialize(_StubOptimal()) learning = await svc.record_learning( RecordLearningParams( - agent_id=author.id, + agent_id=cast("uuid.UUID", author.id), agent_role="developer", content="alone with the role filter", learning_type=LearningType.SOLUTION, @@ -299,8 +300,8 @@ async def test_team_scope_with_patched_agent_role_hits_filter( class _PermissiveRole: """Stand-in: maps both lower and upper case strings to the real enum.""" - def __new__(cls, value: str) -> object: - return real_role(value.lower()) + def __new__(cls, value: str) -> _PermissiveRole: + return cast("_PermissiveRole", real_role(value.lower())) # AgentRole is imported inside the function body (`from roboco.models.base # import AgentRole`), so patching `roboco.models.base.AgentRole` is what @@ -317,7 +318,7 @@ async def test_team_scope_with_patched_agent_role_hits_filter( await svc.initialize(_StubOptimal()) await svc.record_learning( RecordLearningParams( - agent_id=author.id, + agent_id=cast("uuid.UUID", author.id), agent_role="developer", content="role-filter learning", learning_type=LearningType.PATTERN, diff --git a/tests/integration/test_messages_routes.py b/tests/integration/test_messages_routes.py index fab88274..4bae797a 100644 --- a/tests/integration/test_messages_routes.py +++ b/tests/integration/test_messages_routes.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import UTC, datetime from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import AsyncMock, patch -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -49,11 +49,11 @@ async def messages_client( app = FastAPI() app.include_router(messages_router, prefix="/api/messages") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session - async def _override_agent_id(): - return agent.id + async def _override_agent_id() -> UUID: + return cast("UUID", agent.id) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_current_agent_id] = _override_agent_id diff --git a/tests/integration/test_messaging_service.py b/tests/integration/test_messaging_service.py index a5c73355..6f20dc68 100644 --- a/tests/integration/test_messaging_service.py +++ b/tests/integration/test_messaging_service.py @@ -2,9 +2,10 @@ from __future__ import annotations +import uuid from datetime import UTC, datetime, timedelta from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock, patch from uuid import uuid4 from uuid import uuid4 as _u @@ -44,7 +45,6 @@ from sqlalchemy import select if TYPE_CHECKING: from collections.abc import AsyncIterator - from uuid import UUID from sqlalchemy.ext.asyncio import AsyncSession @@ -1422,7 +1422,7 @@ async def test_resolve_group_for_session_returns_inherited_group( await svc.link_session_to_task(parent_sess.id, parent.id, aid, is_primary=True) req = SessionForTasksCreate( - task_ids=[child.id], + task_ids=[cast("uuid.UUID", child.id)], channel_slug=channel.slug, scope=SessionScope.TASK, ) @@ -1606,7 +1606,7 @@ async def test_walk_task_ancestors_orphan_parent_breaks( def scalar_one_or_none(self) -> None: return None - async def _fake_execute(stmt, *args, **kwargs): + async def _fake_execute(stmt: Any, *args: Any, **kwargs: Any) -> Any: call_count["n"] += 1 result = await real_execute(stmt, *args, **kwargs) if call_count["n"] == _PARENT_LOOKUP_CALL: @@ -2126,7 +2126,7 @@ async def test_send_message_with_mentions_triggers_delivery( agent_id=aid, session_id=sess.id, content="hi @other", - mentions=[other.id], + mentions=[cast("uuid.UUID", other.id)], ) ) mock_delivery.deliver.assert_awaited() @@ -2523,7 +2523,7 @@ async def _seed_backend_dev(db_session: AsyncSession, slug: str) -> AgentTable: async def _seed_task( - db_session: AsyncSession, created_by: UUID + db_session: AsyncSession, created_by: Any ) -> tuple[ProjectTable, TaskTable]: project = ProjectTable( id=uuid4(), @@ -2575,16 +2575,16 @@ async def test_post_to_channel_threads_messages_under_one_task_group( await svc.create_channel(_real_channel_req("backend-cell")) first = await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="backend-cell", content="first update", - task_id=task.id, + task_id=cast("uuid.UUID", task.id), ) second = await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="backend-cell", content="second update", - task_id=task.id, + task_id=cast("uuid.UUID", task.id), ) assert first.group_id == second.group_id @@ -2604,15 +2604,15 @@ async def test_post_to_channel_task_group_is_distinct_from_default( await svc.create_channel(_real_channel_req("backend-cell")) untasked = await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="backend-cell", content="no task here", ) tasked = await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="backend-cell", content="task scoped", - task_id=task.id, + task_id=cast("uuid.UUID", task.id), ) assert tasked.group_id != untasked.group_id @@ -2630,16 +2630,16 @@ async def test_post_to_channel_separate_tasks_get_separate_groups( await svc.create_channel(_real_channel_req("backend-cell")) msg_a = await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="backend-cell", content="task a", - task_id=task_a.id, + task_id=cast("uuid.UUID", task_a.id), ) msg_b = await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="backend-cell", content="task b", - task_id=task_b.id, + task_id=cast("uuid.UUID", task_b.id), ) assert msg_a.group_id != msg_b.group_id @@ -2658,10 +2658,10 @@ async def test_post_to_channel_rejects_agent_without_channel_access( with pytest.raises(ChannelAccessDeniedError): await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="announcements", content="should be blocked", - task_id=task.id, + task_id=cast("uuid.UUID", task.id), ) @@ -2678,13 +2678,13 @@ async def test_post_to_channel_access_denied_creates_no_task_group( with pytest.raises(ChannelAccessDeniedError): await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="announcements", content="should be blocked", - task_id=task.id, + task_id=cast("uuid.UUID", task.id), ) - groups = await svc.list_groups_in_channel(ch.id) + groups = await svc.list_groups_in_channel(cast("uuid.UUID", ch.id)) assert groups == [] @@ -2699,10 +2699,10 @@ async def test_post_to_channel_permitted_agent_succeeds( await svc.create_channel(_real_channel_req("backend-cell")) msg = await svc.post_to_channel( - agent_id=agent.id, + agent_id=cast("uuid.UUID", agent.id), channel_slug="backend-cell", content="hello cell", - task_id=task.id, + task_id=cast("uuid.UUID", task.id), ) assert msg.content == "hello cell" assert msg.task_id == task.id diff --git a/tests/integration/test_notifications_routes.py b/tests/integration/test_notifications_routes.py index cedb6713..b628566f 100644 --- a/tests/integration/test_notifications_routes.py +++ b/tests/integration/test_notifications_routes.py @@ -4,9 +4,9 @@ from __future__ import annotations from datetime import UTC, datetime from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import AsyncMock, patch -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -49,16 +49,16 @@ async def notif_client( app = FastAPI() app.include_router(notifications_router, prefix="/api/notifications") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=agent.id, role=AgentRole.DEVELOPER, team=Team.BACKEND + agent_id=cast("UUID", agent.id), role=AgentRole.DEVELOPER, team=Team.BACKEND ) - async def _override_agent_id(): - return agent.id + async def _override_agent_id() -> UUID: + return cast("UUID", agent.id) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -119,7 +119,7 @@ async def test_list_with_filters(notif_client: dict) -> None: @pytest.mark.asyncio -async def test_list_notifications_system_role(db_session) -> None: +async def test_list_notifications_system_role(db_session: AsyncSession) -> None: """System role triggers list_system_notifications branch.""" sys_agent = AgentTable( id=uuid4(), @@ -140,14 +140,16 @@ async def test_list_notifications_system_role(db_session) -> None: app = FastAPI() app.include_router(notifications_router, prefix="/api/notifications") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=sys_agent.id, role=AgentRole.SYSTEM, team=None) + return AgentContext( + agent_id=cast("UUID", sys_agent.id), role=AgentRole.SYSTEM, team=None + ) - async def _override_agent_id(): - return sys_agent.id + async def _override_agent_id() -> UUID: + return cast("UUID", sys_agent.id) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent diff --git a/tests/integration/test_optimal_routes.py b/tests/integration/test_optimal_routes.py index 337d81b7..589d4366 100644 --- a/tests/integration/test_optimal_routes.py +++ b/tests/integration/test_optimal_routes.py @@ -5,7 +5,7 @@ from __future__ import annotations import json from http import HTTPStatus from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 @@ -591,7 +591,7 @@ async def test_create_prompt_template(optimal_client: AsyncClient) -> None: ) # get_optimal_service is async — wrap in async return - async def _ret(): + async def _ret() -> Any: return mock_service mock_get.side_effect = _ret @@ -609,7 +609,7 @@ async def test_list_prompt_templates(optimal_client: AsyncClient) -> None: mock_service = MagicMock() mock_service.list_prompt_templates = MagicMock(return_value=[]) - async def _ret(): + async def _ret() -> Any: return mock_service mock_get.side_effect = _ret diff --git a/tests/integration/test_permissions_db_helpers.py b/tests/integration/test_permissions_db_helpers.py index 0d4d81c4..5517c105 100644 --- a/tests/integration/test_permissions_db_helpers.py +++ b/tests/integration/test_permissions_db_helpers.py @@ -2,7 +2,8 @@ from __future__ import annotations -from typing import TYPE_CHECKING +import uuid +from typing import TYPE_CHECKING, cast from uuid import uuid4 import pytest @@ -40,7 +41,7 @@ async def test_has_privileged_access_ceo_true(db_session: AsyncSession) -> None: agent = _make_agent(AgentRole.CEO) db_session.add(agent) await db_session.flush() - assert await has_privileged_access(db_session, agent.id) is True + assert await has_privileged_access(db_session, cast("uuid.UUID", agent.id)) is True @pytest.mark.asyncio @@ -50,7 +51,7 @@ async def test_has_privileged_access_developer_false( agent = _make_agent(AgentRole.DEVELOPER, Team.BACKEND) db_session.add(agent) await db_session.flush() - assert await has_privileged_access(db_session, agent.id) is False + assert await has_privileged_access(db_session, cast("uuid.UUID", agent.id)) is False @pytest.mark.asyncio @@ -71,7 +72,7 @@ async def test_is_pm_role_main_pm_true(db_session: AsyncSession) -> None: agent = _make_agent(AgentRole.MAIN_PM) db_session.add(agent) await db_session.flush() - assert await is_pm_role(db_session, agent.id) is True + assert await is_pm_role(db_session, cast("uuid.UUID", agent.id)) is True @pytest.mark.asyncio @@ -79,7 +80,7 @@ async def test_is_pm_role_developer_false(db_session: AsyncSession) -> None: agent = _make_agent(AgentRole.DEVELOPER, Team.BACKEND) db_session.add(agent) await db_session.flush() - assert await is_pm_role(db_session, agent.id) is False + assert await is_pm_role(db_session, cast("uuid.UUID", agent.id)) is False @pytest.mark.asyncio @@ -87,7 +88,7 @@ async def test_is_pm_role_ceo_true(db_session: AsyncSession) -> None: agent = _make_agent(AgentRole.CEO) db_session.add(agent) await db_session.flush() - assert await is_pm_role(db_session, agent.id) is True + assert await is_pm_role(db_session, cast("uuid.UUID", agent.id)) is True @pytest.mark.asyncio diff --git a/tests/integration/test_post_tasks_completeness.py b/tests/integration/test_post_tasks_completeness.py index 26ab50d8..51fcad3c 100644 --- a/tests/integration/test_post_tasks_completeness.py +++ b/tests/integration/test_post_tasks_completeness.py @@ -10,8 +10,8 @@ completeness checker. from __future__ import annotations from http import HTTPStatus -from typing import TYPE_CHECKING -from uuid import uuid4 +from typing import TYPE_CHECKING, cast +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -62,11 +62,13 @@ async def post_tasks_client( app = FastAPI() app.include_router(tasks_router, prefix="/api/tasks") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=main_pm.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("UUID", main_pm.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent diff --git a/tests/integration/test_pre_assigned_dev_held_on_unmet_dependency.py b/tests/integration/test_pre_assigned_dev_held_on_unmet_dependency.py index 3b4f7734..86058d91 100644 --- a/tests/integration/test_pre_assigned_dev_held_on_unmet_dependency.py +++ b/tests/integration/test_pre_assigned_dev_held_on_unmet_dependency.py @@ -20,9 +20,9 @@ unassigned claim pool, not the pre-assigned dev. from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import PropertyMock, patch -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -185,7 +185,7 @@ async def _seed_dev_subtask_with_unmet_dep(setup: dict) -> dict: created_by=setup["creator"], project_id=setup["ux_project_id"], product_id=setup["product_id"], - parent_task_id=root.id, + parent_task_id=cast("UUID", root.id), task_type=TaskType.DESIGN, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, @@ -200,7 +200,7 @@ async def _seed_dev_subtask_with_unmet_dep(setup: dict) -> dict: created_by=setup["creator"], project_id=setup["fe_project_id"], product_id=setup["product_id"], - parent_task_id=root.id, + parent_task_id=cast("UUID", root.id), task_type=TaskType.CODE, nature=TaskNature.TECHNICAL, estimated_complexity=Complexity.MEDIUM, @@ -221,7 +221,7 @@ async def _seed_dev_subtask_with_unmet_dep(setup: dict) -> dict: created_by=setup["creator"], project_id=setup["fe_project_id"], product_id=setup["product_id"], - parent_task_id=fe_cell.id, + parent_task_id=cast("UUID", fe_cell.id), assigned_to=setup["fe_dev_db_id"], task_type=TaskType.CODE, nature=TaskNature.TECHNICAL, @@ -230,9 +230,9 @@ async def _seed_dev_subtask_with_unmet_dep(setup: dict) -> dict: ) assert dev_subtask.status == TaskStatus.PENDING # The dev subtask depends on the UX cell task (cross-cell sequencing). - await svc.add_dependency(dev_subtask.id, ux_cell.id) + await svc.add_dependency(cast("UUID", dev_subtask.id), cast("UUID", ux_cell.id)) await svc.session.flush() - refreshed = await svc.get(dev_subtask.id) + refreshed = await svc.get(cast("UUID", dev_subtask.id)) assert refreshed is not None assert ux_cell.id in refreshed.dependency_ids, ( "precondition: dev subtask must depend on the UX cell task" @@ -350,7 +350,8 @@ async def test_claimed_dependency_blocked_task_is_released_to_pending( dev_subtask.branch_name = "feature/frontend/DEVLEAF01" await svc.session.flush() - held = await svc.get(dev_subtask.id) + held = await svc.get(cast("UUID", dev_subtask.id)) + assert held is not None owner = held.assigned_to # capture before the guard releases the claim assert owner is not None guard = await choreo._run_claim_guards(agent_id=fe_dev_db_id, task=held) diff --git a/tests/integration/test_product_routes.py b/tests/integration/test_product_routes.py index c9b316a2..6a3dd186 100644 --- a/tests/integration/test_product_routes.py +++ b/tests/integration/test_product_routes.py @@ -1,7 +1,8 @@ from __future__ import annotations from http import HTTPStatus -from uuid import uuid4 +from typing import TYPE_CHECKING, cast +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -14,11 +15,16 @@ from roboco.models import AgentRole, AgentStatus, Team from roboco.models.base import TaskNature, TaskStatus, TaskType from roboco.models.permissions import AgentContext +if TYPE_CHECKING: + from collections.abc import AsyncIterator + + from sqlalchemy.ext.asyncio import AsyncSession + _HDR = {"X-Agent-ID": str(uuid4()), "X-Agent-Role": "main_pm"} @pytest_asyncio.fixture -async def product_client(db_session): # -> AsyncIterator[dict] +async def product_client(db_session: AsyncSession) -> AsyncIterator[dict]: pm = AgentTable( id=uuid4(), name="PM", @@ -48,11 +54,13 @@ async def product_client(db_session): # -> AsyncIterator[dict] app = FastAPI() app.include_router(product_router, prefix="/api/products") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=pm.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("UUID", pm.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent diff --git a/tests/integration/test_project_routes.py b/tests/integration/test_project_routes.py index 97f45b67..689de3a4 100644 --- a/tests/integration/test_project_routes.py +++ b/tests/integration/test_project_routes.py @@ -3,9 +3,9 @@ from __future__ import annotations from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import patch -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -46,11 +46,13 @@ async def project_client( app = FastAPI() app.include_router(project_router, prefix="/api/projects") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=agent.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("UUID", agent.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -337,7 +339,7 @@ async def test_remove_agent_access_via_slug_not_found( @pytest.mark.asyncio async def test_create_project_developer_forbidden( - db_session, + db_session: AsyncSession, ) -> None: """Developers cannot create projects.""" dev = AgentTable( @@ -359,12 +361,12 @@ async def test_create_project_developer_forbidden( app = FastAPI() app.include_router(project_router, prefix="/api/projects") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=dev.id, role=AgentRole.DEVELOPER, team=Team.BACKEND + agent_id=cast("UUID", dev.id), role=AgentRole.DEVELOPER, team=Team.BACKEND ) app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_project_service.py b/tests/integration/test_project_service.py index 61098131..c7df0483 100644 --- a/tests/integration/test_project_service.py +++ b/tests/integration/test_project_service.py @@ -6,7 +6,7 @@ the same SQLAlchemy paths the production code does. from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 @@ -23,6 +23,7 @@ from roboco.utils.crypto import EncryptionError if TYPE_CHECKING: from collections.abc import AsyncIterator + from pathlib import Path from sqlalchemy.ext.asyncio import AsyncSession @@ -381,7 +382,7 @@ async def test_delete_project_with_active_work_session( real_execute = svc.session.execute - async def _intercepting_execute(stmt, *args, **kwargs): + async def _intercepting_execute(stmt: Any, *args: Any, **kwargs: Any) -> Any: # When the active-sessions select runs, return a stub with our fake ws. # Identify by substring in the SQL — the only WorkSession query in # delete() filters by project_id and status. @@ -408,7 +409,7 @@ async def test_delete_project_with_active_work_session( @pytest.mark.asyncio async def test_delete_project_with_workspace_cleanup( - project_setup: dict, tmp_path + project_setup: dict, tmp_path: Path ) -> None: """delete_workspaces=True triggers filesystem cleanup branch.""" diff --git a/tests/integration/test_prompter_redraft.py b/tests/integration/test_prompter_redraft.py index 253c0e3f..16a753e0 100644 --- a/tests/integration/test_prompter_redraft.py +++ b/tests/integration/test_prompter_redraft.py @@ -1,7 +1,7 @@ from __future__ import annotations from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from uuid import uuid4 import pytest @@ -55,7 +55,7 @@ def test_compose_redraft_message_includes_draft_and_brief() -> None: entries = [ {"author_role": "product_owner", "author": "po", "title": "PO", "content": "z"} ] - msg = compose_redraft_message(task, entries) + msg = compose_redraft_message(cast("TaskTable", task), entries) assert "My Task" in msg assert "The current description." in msg assert "- does X" in msg diff --git a/tests/integration/test_provider_routes.py b/tests/integration/test_provider_routes.py index 559b51c5..d3aeb286 100644 --- a/tests/integration/test_provider_routes.py +++ b/tests/integration/test_provider_routes.py @@ -25,11 +25,15 @@ if TYPE_CHECKING: from sqlalchemy.ext.asyncio import AsyncSession -def _make_app(db_session, role: AgentRole = AgentRole.MAIN_PM, team=None) -> FastAPI: +def _make_app( + db_session: AsyncSession, + role: AgentRole = AgentRole.MAIN_PM, + team: Team | None = None, +) -> FastAPI: app = FastAPI() app.include_router(provider_router, prefix="/api/providers") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: diff --git a/tests/integration/test_provider_service.py b/tests/integration/test_provider_service.py index a58175f6..3e7b004b 100644 --- a/tests/integration/test_provider_service.py +++ b/tests/integration/test_provider_service.py @@ -8,7 +8,8 @@ exercise the tri-state semantics of ``ProviderUpdate.auth_token``. from __future__ import annotations -from typing import TYPE_CHECKING +import uuid +from typing import TYPE_CHECKING, cast from uuid import uuid4 import pytest @@ -80,7 +81,7 @@ async def test_get_provider_returns_row(provider_svc: ProviderService) -> None: row = await provider_svc.create_provider( ProviderCreate(name=f"g-{uuid4().hex[:6]}", type=ModelProvider.OPENAI) ) - fetched = await provider_svc.get_provider(row.id) + fetched = await provider_svc.get_provider(cast("uuid.UUID", row.id)) assert fetched is not None assert fetched.id == row.id @@ -146,7 +147,9 @@ async def test_update_provider_changes_name(provider_svc: ProviderService) -> No ProviderCreate(name=f"old-{uuid4().hex[:6]}", type=ModelProvider.OPENAI) ) new_name = f"new-{uuid4().hex[:6]}" - updated = await provider_svc.update_provider(row.id, ProviderUpdate(name=new_name)) + updated = await provider_svc.update_provider( + cast("uuid.UUID", row.id), ProviderUpdate(name=new_name) + ) assert updated is not None assert updated.name == new_name @@ -162,7 +165,9 @@ async def test_update_provider_duplicate_name_raises( ProviderCreate(name=f"b-{uuid4().hex[:6]}", type=ModelProvider.OPENAI) ) with pytest.raises(ConflictError): - await provider_svc.update_provider(b.id, ProviderUpdate(name=a.name)) + await provider_svc.update_provider( + cast("uuid.UUID", b.id), ProviderUpdate(name=a.name) + ) @pytest.mark.asyncio @@ -176,7 +181,9 @@ async def test_update_provider_clears_base_url_with_empty_string( base_url="https://example.com", ) ) - updated = await provider_svc.update_provider(row.id, ProviderUpdate(base_url="")) + updated = await provider_svc.update_provider( + cast("uuid.UUID", row.id), ProviderUpdate(base_url="") + ) assert updated is not None assert updated.base_url is None @@ -193,7 +200,7 @@ async def test_update_provider_token_tristate_clear( ) ) updated = await provider_svc.update_provider( - row.id, ProviderUpdate(clear_auth_token=True) + cast("uuid.UUID", row.id), ProviderUpdate(clear_auth_token=True) ) assert updated is not None assert updated.auth_token_encrypted is None @@ -207,7 +214,7 @@ async def test_update_provider_token_tristate_set( ProviderCreate(name=f"s-{uuid4().hex[:6]}", type=ModelProvider.ANTHROPIC) ) updated = await provider_svc.update_provider( - row.id, ProviderUpdate(auth_token="new-secret") + cast("uuid.UUID", row.id), ProviderUpdate(auth_token="new-secret") ) assert updated is not None assert updated.auth_token_encrypted is not None @@ -226,7 +233,7 @@ async def test_update_provider_token_tristate_unchanged( ) original_token = row.auth_token_encrypted updated = await provider_svc.update_provider( - row.id, + cast("uuid.UUID", row.id), ProviderUpdate(enabled=False), # no auth_token field ) assert updated is not None @@ -249,8 +256,8 @@ async def test_delete_provider(provider_svc: ProviderService) -> None: row = await provider_svc.create_provider( ProviderCreate(name=f"d-{uuid4().hex[:6]}", type=ModelProvider.OPENAI) ) - await provider_svc.delete_provider(row.id) - assert await provider_svc.get_provider(row.id) is None + await provider_svc.delete_provider(cast("uuid.UUID", row.id)) + assert await provider_svc.get_provider(cast("uuid.UUID", row.id)) is None @pytest.mark.asyncio @@ -272,7 +279,7 @@ async def test_delete_provider_raises_when_referenced( await db_session.flush() with pytest.raises(ConflictError): - await provider_svc.delete_provider(row.id) + await provider_svc.delete_provider(cast("uuid.UUID", row.id)) @pytest.mark.asyncio @@ -285,7 +292,7 @@ async def test_get_decrypted_token_round_trip(provider_svc: ProviderService) -> auth_token=plaintext, ) ) - decrypted = await provider_svc.get_decrypted_token(row.id) + decrypted = await provider_svc.get_decrypted_token(cast("uuid.UUID", row.id)) assert decrypted == plaintext @@ -296,7 +303,7 @@ async def test_get_decrypted_token_returns_none_when_unset( row = await provider_svc.create_provider( ProviderCreate(name=f"nt-{uuid4().hex[:6]}", type=ModelProvider.LOCAL) ) - assert await provider_svc.get_decrypted_token(row.id) is None + assert await provider_svc.get_decrypted_token(cast("uuid.UUID", row.id)) is None @pytest.mark.asyncio @@ -349,7 +356,7 @@ async def test_update_provider_encrypt_failure_propagates( monkeypatch.setattr(provider_module, "encrypt_token", _boom) with pytest.raises(EncryptionError): await provider_svc.update_provider( - row.id, ProviderUpdate(auth_token="new-secret") + cast("uuid.UUID", row.id), ProviderUpdate(auth_token="new-secret") ) @@ -378,7 +385,7 @@ async def test_get_decrypted_token_decrypt_failure_propagates( monkeypatch.setattr(provider_module, "decrypt_token", _boom) with pytest.raises(EncryptionError): - await provider_svc.get_decrypted_token(row.id) + await provider_svc.get_decrypted_token(cast("uuid.UUID", row.id)) # --------------------------------------------------------------------------- @@ -395,7 +402,9 @@ async def test_update_provider_same_name_is_noop( row = await provider_svc.create_provider( ProviderCreate(name=name, type=ModelProvider.ANTHROPIC) ) - updated = await provider_svc.update_provider(row.id, ProviderUpdate(name=name)) + updated = await provider_svc.update_provider( + cast("uuid.UUID", row.id), ProviderUpdate(name=name) + ) assert updated is not None assert updated.name == name diff --git a/tests/integration/test_query_helpers.py b/tests/integration/test_query_helpers.py index ce21c40f..ea673afe 100644 --- a/tests/integration/test_query_helpers.py +++ b/tests/integration/test_query_helpers.py @@ -2,8 +2,9 @@ from __future__ import annotations +import uuid from datetime import UTC, datetime, timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from uuid import uuid4 import pytest @@ -221,7 +222,7 @@ async def test_get_agent_slug_known(db_session: AsyncSession) -> None: ) db_session.add(agent) await db_session.flush() - slug = await get_agent_slug(db_session, agent.id) + slug = await get_agent_slug(db_session, cast("uuid.UUID", agent.id)) assert slug == agent.slug diff --git a/tests/integration/test_sessions_routes.py b/tests/integration/test_sessions_routes.py index 97c40253..ede826c9 100644 --- a/tests/integration/test_sessions_routes.py +++ b/tests/integration/test_sessions_routes.py @@ -3,7 +3,7 @@ from __future__ import annotations from http import HTTPStatus -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from unittest.mock import patch from uuid import UUID, uuid4 @@ -108,11 +108,11 @@ async def session_client( app = FastAPI() app.include_router(sessions_router, prefix="/api/sessions") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent_id() -> UUID: - return pm.id + return cast("UUID", pm.id) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_current_agent_id] = _override_agent_id @@ -282,15 +282,17 @@ async def test_get_tasks_for_unknown_session(session_client: dict) -> None: # --------------------------------------------------------------------------- -async def _make_dev_sessions_app(db_session, dev_agent: AgentTable): +async def _make_dev_sessions_app( + db_session: AsyncSession, dev_agent: AgentTable +) -> FastAPI: """Build a FastAPI app for sessions where the agent is a developer.""" app = FastAPI() app.include_router(sessions_router, prefix="/api/sessions") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session - async def _override_agent_id(): + async def _override_agent_id() -> Any: return dev_agent.id app.dependency_overrides[get_db] = _override_db @@ -300,7 +302,7 @@ async def _make_dev_sessions_app(db_session, dev_agent: AgentTable): @pytest.mark.asyncio async def test_link_task_developer_forbidden( - db_session, + db_session: AsyncSession, ) -> None: dev = AgentTable( id=uuid4(), @@ -335,7 +337,7 @@ async def test_link_task_developer_forbidden( @pytest.mark.asyncio async def test_link_task_to_session_developer_forbidden( - db_session, + db_session: AsyncSession, ) -> None: """Developer cannot link tasks to sessions via /sessions/{id}/tasks.""" dev = AgentTable( @@ -531,10 +533,10 @@ async def _make_outsider_sessions_app( app = FastAPI() app.include_router(sessions_router, prefix="/api/sessions") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session - async def _override_agent_id(): + async def _override_agent_id() -> Any: return outsider.id app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_stream_routes.py b/tests/integration/test_stream_routes.py index e062037e..a69c7e42 100644 --- a/tests/integration/test_stream_routes.py +++ b/tests/integration/test_stream_routes.py @@ -4,9 +4,9 @@ from __future__ import annotations from http import HTTPStatus from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from unittest.mock import AsyncMock, MagicMock -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -50,15 +50,15 @@ async def stream_client( app.state.extraction = None app.include_router(stream_router, prefix="/api/stream") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session - async def _override_agent_id(): - return agent.id + async def _override_agent_id() -> UUID: + return cast("UUID", agent.id) async def _override_agent() -> AgentContext: return AgentContext( - agent_id=agent.id, role=AgentRole.DEVELOPER, team=Team.BACKEND + agent_id=cast("UUID", agent.id), role=AgentRole.DEVELOPER, team=Team.BACKEND ) app.dependency_overrides[get_db] = _override_db diff --git a/tests/integration/test_task_product_id.py b/tests/integration/test_task_product_id.py index 0822144b..4ead4e59 100644 --- a/tests/integration/test_task_product_id.py +++ b/tests/integration/test_task_product_id.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING -from uuid import uuid4 +from typing import TYPE_CHECKING, Any, cast +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -59,7 +59,7 @@ async def svc_setup(db_session: AsyncSession) -> AsyncIterator[dict]: } -def _req(setup: dict, **kw) -> TaskCreateRequest: +def _req(setup: dict, **kw: Any) -> TaskCreateRequest: return TaskCreateRequest( title="t", description="a real description over twenty chars", @@ -157,7 +157,7 @@ async def test_coordination_task_claims_plans_and_starts_without_branch( created_by=svc_setup["creator"], project_id=None, product_id=svc_setup["product_id"], - assigned_to=pm.id, + assigned_to=cast("UUID", pm.id), task_type=TaskType.CODE, nature=TaskNature.NON_TECHNICAL, estimated_complexity=Complexity.HIGH, @@ -166,10 +166,10 @@ async def test_coordination_task_claims_plans_and_starts_without_branch( assert task.project_id is None assert task.product_id == svc_setup["product_id"] - claimed = await svc.claim(task.id, pm.id) + claimed = await svc.claim(cast("UUID", task.id), cast("UUID", pm.id)) assert claimed is not None, "coordination task could not be claimed" await svc.set_plan( - task.id, + cast("UUID", task.id), { "approach": "delegate to the three cells", "sub_tasks": [{"title": "backend"}], @@ -178,12 +178,14 @@ async def test_coordination_task_claims_plans_and_starts_without_branch( # The fix: claimed->in_progress no longer requires a branch for a coordination # task, so start() succeeds instead of raising GitRequirementError. - started = await svc.start(task.id, pm.id, agent_role=None) + started = await svc.start( + cast("UUID", task.id), cast("UUID", pm.id), agent_role=None + ) assert started is not None, ( "start() returned None — coordination task failed to reach in_progress" ) - refreshed = await svc.get(task.id) + refreshed = await svc.get(cast("UUID", task.id)) assert refreshed is not None assert refreshed.status == TaskStatus.IN_PROGRESS assert not refreshed.branch_name # coordination task does no git, has no branch diff --git a/tests/integration/test_task_service_background.py b/tests/integration/test_task_service_background.py index daa403ab..21ff10b0 100644 --- a/tests/integration/test_task_service_background.py +++ b/tests/integration/test_task_service_background.py @@ -79,7 +79,7 @@ async def task_setup( } -def _req(setup: dict, **overrides) -> TaskCreateRequest: +def _req(setup: dict[str, Any], **overrides: Any) -> TaskCreateRequest: return TaskCreateRequest( title=overrides.pop("title", "t"), description=overrides.pop("description", "d"), @@ -808,7 +808,7 @@ async def test_delete_task_branch_no_project_returns( real_execute = db_session.execute - async def _execute_stub(stmt, *args, **kwargs): + async def _execute_stub(stmt: Any, *args: Any, **kwargs: Any) -> Any: # Return None-result for ProjectTable.slug lookup (the SELECT in # _delete_task_branch_best_effort), forward everything else. compiled = str(stmt) diff --git a/tests/integration/test_task_service_basics.py b/tests/integration/test_task_service_basics.py index ec5e9a1e..8928ff81 100644 --- a/tests/integration/test_task_service_basics.py +++ b/tests/integration/test_task_service_basics.py @@ -8,7 +8,7 @@ existing v1-flow integration tests. from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from uuid import uuid4 import pytest @@ -68,7 +68,7 @@ async def task_setup( } -def _req(setup: dict, **overrides) -> TaskCreateRequest: +def _req(setup: dict[str, Any], **overrides: Any) -> TaskCreateRequest: return TaskCreateRequest( title=overrides.pop("title", "t"), description=overrides.pop("description", "d"), diff --git a/tests/integration/test_task_service_lifecycle_misc.py b/tests/integration/test_task_service_lifecycle_misc.py index 2f9abdd8..c2fa2dac 100644 --- a/tests/integration/test_task_service_lifecycle_misc.py +++ b/tests/integration/test_task_service_lifecycle_misc.py @@ -8,7 +8,8 @@ from __future__ import annotations import asyncio import contextlib -from typing import TYPE_CHECKING, Any +import uuid +from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock, MagicMock from uuid import uuid4 @@ -88,7 +89,7 @@ async def task_setup( } -def _req(setup: dict, **overrides) -> TaskCreateRequest: +def _req(setup: dict, **overrides: Any) -> TaskCreateRequest: return TaskCreateRequest( title=overrides.pop("title", "t"), description=overrides.pop("description", "d"), @@ -221,14 +222,14 @@ async def test_inject_proactive_context_skips_when_claim_rolled_back( assert task.assigned_to is None @asynccontextmanager_async # Helper below - async def _factory(): - return None # type: ignore[no-any-return] + async def _factory() -> None: + return None # Build a fake session_factory whose context returns the test session db = task_setup["db"] class _SessionFactory: - def __call__(self): + def __call__(self) -> _Ctx: return _Ctx(db) class _Ctx: @@ -238,7 +239,7 @@ async def test_inject_proactive_context_skips_when_claim_rolled_back( async def __aenter__(self) -> Any: return self._session - async def __aexit__(self, exc_type, exc, _tb) -> None: + async def __aexit__(self, exc_type: Any, exc: Any, _tb: Any) -> None: return None factory_instance = _SessionFactory() @@ -284,14 +285,14 @@ async def test_inject_proactive_context_writes_when_context_nonempty( async def __aenter__(self) -> Any: return self._session - async def __aexit__(self, exc_type, exc, _tb) -> None: + async def __aexit__(self, exc_type: Any, exc: Any, _tb: Any) -> None: return None class _Factory: def __init__(self, session: Any) -> None: self._session = session - def __call__(self): + def __call__(self) -> _Ctx: return _Ctx(self._session) factory = _Factory(db_session) @@ -434,7 +435,7 @@ async def test_create_work_session_no_project_returns_none( real_execute = db_session.execute - async def _exec_stub(stmt, *a, **kw): + async def _exec_stub(stmt: Any, *a: Any, **kw: Any) -> Any: compiled = str(stmt) if "FROM projects" in compiled: return fake_result @@ -806,7 +807,7 @@ async def test_cancel_descendants_cascades_for_authorized_pm( # --------------------------------------------------------------------------- -def asynccontextmanager_async(func): +def asynccontextmanager_async(func: Any) -> Any: """Stub decorator — actual implementation lives in std lib.""" return contextlib.asynccontextmanager(func) @@ -892,7 +893,7 @@ async def test_docs_complete_for_task_invokes_notification( task.pr_created = True await db_session.flush() agent_ctx = AgentContext( - agent_id=doc.id, + agent_id=cast("uuid.UUID", doc.id), role=AgentRole.DOCUMENTER, team=Team.BACKEND, slug=doc.slug, @@ -951,14 +952,14 @@ async def test_escalate_to_ceo_for_agent_invokes_notification( task.docs_complete = True await db_session.flush() agent_ctx = AgentContext( - agent_id=pm.id, + agent_id=cast("uuid.UUID", pm.id), role=AgentRole.MAIN_PM, team=Team.MAIN_PM, slug=pm.slug, ) class _P: - def can_perform_task_action(self, *a, **kw) -> bool: + def can_perform_task_action(self, *a: Any, **kw: Any) -> bool: del a, kw return True @@ -1005,7 +1006,7 @@ async def test_claim_task_for_agent_commits_and_returns( ) class _P: - def can_perform_task_action(self, *a, **kw) -> bool: + def can_perform_task_action(self, *a: Any, **kw: Any) -> bool: del a, kw return True @@ -1048,14 +1049,14 @@ async def test_complete_task_for_agent_commits( task.assigned_to = pm.id await db_session.flush() agent_ctx = AgentContext( - agent_id=pm.id, + agent_id=cast("uuid.UUID", pm.id), role=AgentRole.CELL_PM, team=Team.BACKEND, slug=pm.slug, ) class _P: - def can_perform_task_action(self, *a, **kw) -> bool: + def can_perform_task_action(self, *a: Any, **kw: Any) -> bool: del a, kw return True diff --git a/tests/integration/test_task_service_misc.py b/tests/integration/test_task_service_misc.py index fdc22f96..29afbbad 100644 --- a/tests/integration/test_task_service_misc.py +++ b/tests/integration/test_task_service_misc.py @@ -25,7 +25,7 @@ Targets: from __future__ import annotations import asyncio -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock, MagicMock from uuid import UUID, uuid4 @@ -51,6 +51,7 @@ from roboco.templates.git.constants import MAX_TASK_DEPTH if TYPE_CHECKING: from collections.abc import AsyncIterator + from sqlalchemy import Table from sqlalchemy.ext.asyncio import AsyncSession @@ -93,7 +94,7 @@ async def task_setup( } -def _req(setup: dict, **overrides) -> TaskCreateRequest: +def _req(setup: dict, **overrides: Any) -> TaskCreateRequest: return TaskCreateRequest( title=overrides.pop("title", "t"), description=overrides.pop("description", "d"), @@ -239,7 +240,7 @@ async def test_activate_without_project_or_product_raises( fake_task.project_id = None fake_task.product_id = None - async def _stub_get(tid): + async def _stub_get(tid: Any) -> Any: del tid return fake_task @@ -253,7 +254,7 @@ async def test_activate_without_project_or_product_raises( db = task_setup["db"] real_execute = db.execute - async def _exec_stub(stmt, *a, **kw): + async def _exec_stub(stmt: Any, *a: Any, **kw: Any) -> Any: compiled = str(stmt) if "session_tasks" in compiled.lower(): return fake_result @@ -600,7 +601,7 @@ async def test_unclaim_for_reaper_lifecycle_error_returns( task.assigned_to = task_setup["agent_id"] await db_session.flush() - def _raise(*_args, **_kwargs) -> None: + def _raise(*_args: Any, **_kwargs: Any) -> None: raise TaskLifecycleError( current_status="claimed", target_status="pending", @@ -624,7 +625,7 @@ async def test_unclaim_for_agent_lifecycle_error_returns_none( task.assigned_to = task_setup["agent_id"] await db_session.flush() - def _raise(*_args, **_kwargs) -> None: + def _raise(*_args: Any, **_kwargs: Any) -> None: raise TaskLifecycleError( current_status="claimed", target_status="pending", @@ -648,7 +649,7 @@ async def test_resume_for_agent_lifecycle_error_returns_none( task.assigned_to = task_setup["agent_id"] await db_session.flush() - async def _bad_resume(*_a, **_kw) -> None: + async def _bad_resume(*_a: Any, **_kw: Any) -> None: raise TaskLifecycleError( current_status="paused", target_status="in_progress", @@ -900,7 +901,8 @@ async def test_complete_returns_none_when_pr_not_merged( """ svc = task_setup["svc"] await db_session.execute( - AgentTable.__table__.update() + cast("Table", AgentTable.__table__) + .update() .where(AgentTable.role == AgentRole.MAIN_PM) .values(role=AgentRole.SYSTEM) ) @@ -989,7 +991,7 @@ async def test_escalate_to_ceo_for_agent_inner_returns_none( ) class _P: - def can_perform_task_action(self, *a, **kw) -> bool: + def can_perform_task_action(self, *a: Any, **kw: Any) -> bool: del a, kw return True @@ -1105,7 +1107,7 @@ async def test_ensure_branch_calls_auto_create( task = await svc.create(_req(task_setup)) # No branch_name set → falls through to auto-create - async def _stub(_t, _a) -> str: + async def _stub(_t: Any, _a: Any) -> str: return "feature/backend/MOCK" monkeypatch.setattr(svc, "_auto_create_branch", _stub) @@ -1125,7 +1127,7 @@ async def test_find_ancestor_branch_break_when_parent_missing( real_get = svc.get - async def _stub_get(tid): + async def _stub_get(tid: Any) -> Any: # Return None when looking up the parent if tid == parent.id: return None @@ -1179,7 +1181,7 @@ async def test_finalize_claim_refreshes_after_branch_creation( task = await svc.create(_req(task_setup)) await db_session.flush() - async def _ensure_branch(_t, _a) -> str: + async def _ensure_branch(_t: Any, _a: Any) -> str: _t.branch_name = "feature/backend/X" return "feature/backend/X" @@ -1205,7 +1207,7 @@ async def test_resolve_pm_for_review_returns_none_when_chain_broken( real_get = svc.get - async def _stub_get(tid): + async def _stub_get(tid: Any) -> Any: # Return None when looking up the parent if tid == parent.id: return None diff --git a/tests/integration/test_task_service_no_silent_fallback.py b/tests/integration/test_task_service_no_silent_fallback.py index f1675d07..65a1c960 100644 --- a/tests/integration/test_task_service_no_silent_fallback.py +++ b/tests/integration/test_task_service_no_silent_fallback.py @@ -10,7 +10,7 @@ either. from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from uuid import uuid4 import pytest @@ -65,7 +65,7 @@ async def task_setup( } -def _request(setup: dict, **overrides: object) -> TaskCreateRequest: +def _request(setup: dict, **overrides: Any) -> TaskCreateRequest: """Build a TaskCreateRequest with sensible defaults; overrides win. `TaskCreateRequest` is a plain dataclass — no Pydantic validation — @@ -88,7 +88,7 @@ def _request(setup: dict, **overrides: object) -> TaskCreateRequest: task_type=overrides.pop("task_type", TaskType.CODE), nature=overrides.pop("nature", TaskNature.TECHNICAL), estimated_complexity=overrides.pop("estimated_complexity", Complexity.MEDIUM), - **overrides, # type: ignore[arg-type] + **overrides, ) diff --git a/tests/integration/test_task_service_route_orchestration.py b/tests/integration/test_task_service_route_orchestration.py index 7f6393e7..0b92b302 100644 --- a/tests/integration/test_task_service_route_orchestration.py +++ b/tests/integration/test_task_service_route_orchestration.py @@ -10,9 +10,9 @@ service primitives + permission checks + notification delivery. from __future__ import annotations from contextlib import asynccontextmanager -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -96,7 +96,7 @@ async def task_setup( } -def _req(setup: dict, **overrides) -> TaskCreateRequest: +def _req(setup: dict, **overrides: Any) -> TaskCreateRequest: return TaskCreateRequest( title=overrides.pop("title", "t"), description=overrides.pop("description", "d"), @@ -111,7 +111,7 @@ def _req(setup: dict, **overrides) -> TaskCreateRequest: ) -def _ctx(agent_id, role: AgentRole, team: Team = Team.BACKEND) -> AgentContext: +def _ctx(agent_id: UUID, role: AgentRole, team: Team = Team.BACKEND) -> AgentContext: return AgentContext( agent_id=agent_id, role=role, @@ -134,7 +134,7 @@ class _Permissions: self, agent: AgentContext, action: str, - team=None, # noqa: ARG002 + team: Team | None = None, # noqa: ARG002 ) -> bool: del agent if action == "claim": @@ -355,7 +355,7 @@ async def test_soft_block_task_for_agent_pm_blocks_other_task( task.assigned_to = task_setup["agent_id"] task.status = TaskStatus.IN_PROGRESS await db_session.flush() - agent_ctx = _ctx(pm.id, AgentRole.CELL_PM) + agent_ctx = _ctx(cast("UUID", pm.id), AgentRole.CELL_PM) monkeypatch.setattr( "roboco.services.notification_delivery.get_notification_delivery_service", @@ -429,7 +429,7 @@ async def test_docs_complete_for_task_self_documentation_blocked( audit_mock = AsyncMock() class _Audit: - async def log_task_action_denial(self, **_kwargs) -> None: + async def log_task_action_denial(self, **_kwargs: Any) -> None: await audit_mock(_kwargs) audit_instance = _Audit() @@ -507,7 +507,7 @@ async def test_docs_complete_for_task_succeeds( task.pr_url = "u" task.pr_created = True await db_session.flush() - agent_ctx = _ctx(doc.id, AgentRole.DOCUMENTER) + agent_ctx = _ctx(cast("UUID", doc.id), AgentRole.DOCUMENTER) fake_delivery = AsyncMock() fake_delivery.notify_pm_of_docs_complete = AsyncMock() monkeypatch.setattr( @@ -617,7 +617,7 @@ async def test_complete_task_for_agent_validation_when_not_completable( await db_session.flush() task = await svc.create(_req(task_setup)) # PENDING — cannot complete await db_session.flush() - agent_ctx = _ctx(pm.id, AgentRole.CELL_PM) + agent_ctx = _ctx(cast("UUID", pm.id), AgentRole.CELL_PM) perms = _Permissions(can_close=True) with pytest.raises(ValidationError): await svc.complete_task_for_agent(task.id, agent_ctx, perms) @@ -647,7 +647,7 @@ async def test_complete_task_for_agent_succeeds_for_pm( task.status = TaskStatus.IN_PROGRESS task.assigned_to = pm.id await db_session.flush() - agent_ctx = _ctx(pm.id, AgentRole.CELL_PM) + agent_ctx = _ctx(cast("UUID", pm.id), AgentRole.CELL_PM) perms = _Permissions(can_close=True) out = await svc.complete_task_for_agent(task.id, agent_ctx, perms) assert out.status == TaskStatus.COMPLETED @@ -764,7 +764,7 @@ async def test_escalate_to_ceo_for_agent_succeeds( task.pr_created = True task.docs_complete = True await db_session.flush() - agent_ctx = _ctx(pm.id, AgentRole.MAIN_PM, Team.MAIN_PM) + agent_ctx = _ctx(cast("UUID", pm.id), AgentRole.MAIN_PM, Team.MAIN_PM) perms = _Permissions(can_close=True) fake_delivery = AsyncMock() fake_delivery.notify_ceo_of_escalation = AsyncMock() @@ -896,11 +896,11 @@ async def test_substitute_task_for_agent_qa_task_complete_routes_to_pm_review( task = await svc.create(_req(task_setup)) task.assigned_to = qa.id await db_session.flush() - agent_ctx = _ctx(qa.id, AgentRole.QA) + agent_ctx = _ctx(cast("UUID", qa.id), AgentRole.QA) monkeypatch.setattr("roboco.agents_config.get_pm_for_agent", lambda _slug: pm.slug) # Patch notify_pm_for_substitute so we don't hit the notification stack - async def _fake_notify(*_args, **_kwargs) -> None: + async def _fake_notify(*_args: Any, **_kwargs: Any) -> None: return None monkeypatch.setattr("roboco.services.task.notify_pm_for_substitute", _fake_notify) diff --git a/tests/integration/test_task_service_transitions.py b/tests/integration/test_task_service_transitions.py index 2e3d8efa..f3736679 100644 --- a/tests/integration/test_task_service_transitions.py +++ b/tests/integration/test_task_service_transitions.py @@ -7,7 +7,7 @@ reassignment, ceo_approve/ceo_reject, escalation chains). from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock from uuid import UUID, uuid4 @@ -33,7 +33,7 @@ from roboco.models.task import TaskCreateRequest from roboco.seeds.initial_data import AGENT_UUIDS from roboco.services.base import NotFoundError from roboco.services.task import SoftBlockInfo, TaskService -from sqlalchemy import select +from sqlalchemy import Table, select if TYPE_CHECKING: from collections.abc import AsyncIterator @@ -78,7 +78,7 @@ async def task_setup( } -def _req(setup: dict, **overrides) -> TaskCreateRequest: +def _req(setup: dict, **overrides: Any) -> TaskCreateRequest: return TaskCreateRequest( title=overrides.pop("title", "t"), description=overrides.pop("description", "d"), @@ -1336,7 +1336,8 @@ async def test_complete_with_force_with_cancelled_succeeds( """ svc = task_setup["svc"] await db_session.execute( - AgentTable.__table__.update() + cast("Table", AgentTable.__table__) + .update() .where(AgentTable.role == AgentRole.MAIN_PM) .values(role=AgentRole.SYSTEM) ) @@ -1378,7 +1379,8 @@ async def test_complete_without_force_with_cancelled_blocks( """Without force_with_cancelled, cancelled descendants block completion.""" svc = task_setup["svc"] await db_session.execute( - AgentTable.__table__.update() + cast("Table", AgentTable.__table__) + .update() .where(AgentTable.role == AgentRole.MAIN_PM) .values(role=AgentRole.SYSTEM) ) @@ -1627,7 +1629,8 @@ async def test_escalate_up_to_role_returns_none_when_no_target_role( """ svc = task_setup["svc"] await db_session.execute( - AgentTable.__table__.update() + cast("Table", AgentTable.__table__) + .update() .where(AgentTable.role == AgentRole.MAIN_PM) .values(role=AgentRole.SYSTEM) ) @@ -2159,7 +2162,7 @@ async def test_emit_task_event_publishes_when_connected( def is_connected(self) -> bool: return True - async def publish(self, event) -> None: + async def publish(self, event: Any) -> None: await publish_mock(event) def _bus_factory() -> _Bus: diff --git a/tests/integration/test_tasks_routes.py b/tests/integration/test_tasks_routes.py index 018ca842..c68b2aad 100644 --- a/tests/integration/test_tasks_routes.py +++ b/tests/integration/test_tasks_routes.py @@ -5,9 +5,9 @@ from __future__ import annotations from http import HTTPStatus from pathlib import Path from types import SimpleNamespace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock, patch -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -84,11 +84,13 @@ async def task_client( app = FastAPI() app.include_router(tasks_router, prefix="/api/tasks") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=main_pm.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("UUID", main_pm.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -108,7 +110,7 @@ _HDR = {"X-Agent-ID": str(uuid4()), "X-Agent-Role": "main_pm"} def _seed_task( - setup: dict, *, status: TaskStatus = TaskStatus.PENDING, **kw + setup: dict, *, status: TaskStatus = TaskStatus.PENDING, **kw: Any ) -> TaskTable: task = TaskTable( id=uuid4(), @@ -2001,11 +2003,13 @@ async def qa_client(db_session: AsyncSession) -> AsyncIterator[dict]: app = FastAPI() app.include_router(tasks_router, prefix="/api/tasks") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=qa.id, role=AgentRole.QA, team=Team.BACKEND) + return AgentContext( + agent_id=cast("UUID", qa.id), role=AgentRole.QA, team=Team.BACKEND + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -2021,7 +2025,7 @@ async def qa_client(db_session: AsyncSession) -> AsyncIterator[dict]: app.dependency_overrides.clear() -def _seed_task_qa(setup: dict, **kw) -> TaskTable: +def _seed_task_qa(setup: dict, **kw: Any) -> TaskTable: task = TaskTable( id=uuid4(), title="t", @@ -2365,11 +2369,13 @@ async def ceo_client(db_session: AsyncSession) -> AsyncIterator[dict]: app = FastAPI() app.include_router(tasks_router, prefix="/api/tasks") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=ceo.id, role=AgentRole.CEO, team=None) + return AgentContext( + agent_id=cast("UUID", ceo.id), role=AgentRole.CEO, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -2385,7 +2391,7 @@ async def ceo_client(db_session: AsyncSession) -> AsyncIterator[dict]: app.dependency_overrides.clear() -def _seed_task_ceo(setup: dict, **kw) -> TaskTable: +def _seed_task_ceo(setup: dict, **kw: Any) -> TaskTable: task = TaskTable( id=uuid4(), title="t", diff --git a/tests/integration/test_work_session_routes.py b/tests/integration/test_work_session_routes.py index 8c5a2011..5e41a411 100644 --- a/tests/integration/test_work_session_routes.py +++ b/tests/integration/test_work_session_routes.py @@ -3,8 +3,8 @@ from __future__ import annotations from http import HTTPStatus -from typing import TYPE_CHECKING -from uuid import uuid4 +from typing import TYPE_CHECKING, Any, cast +from uuid import UUID, uuid4 import pytest import pytest_asyncio @@ -77,12 +77,12 @@ async def ws_client( app = FastAPI() app.include_router(ws_router, prefix="/api/work-sessions") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: return AgentContext( - agent_id=agent.id, role=AgentRole.DEVELOPER, team=Team.BACKEND + agent_id=cast("UUID", agent.id), role=AgentRole.DEVELOPER, team=Team.BACKEND ) app.dependency_overrides[get_db] = _override_db @@ -423,7 +423,7 @@ async def test_abandon_session_not_found(ws_client: dict) -> None: # --------------------------------------------------------------------------- -def _seed_ws(setup: dict, **kwargs) -> WorkSessionTable: +def _seed_ws(setup: dict, **kwargs: Any) -> WorkSessionTable: """Insert a WorkSessionTable row directly via the session fixture.""" return WorkSessionTable( id=uuid4(), @@ -657,11 +657,13 @@ async def test_merge_pr_pm_succeeds(db_session: AsyncSession) -> None: app = FastAPI() app.include_router(ws_router, prefix="/api/work-sessions") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=pm.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("UUID", pm.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -699,11 +701,13 @@ async def test_merge_pr_unknown_session_pm(db_session: AsyncSession) -> None: app = FastAPI() app.include_router(ws_router, prefix="/api/work-sessions") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=pm.id, role=AgentRole.MAIN_PM, team=None) + return AgentContext( + agent_id=cast("UUID", pm.id), role=AgentRole.MAIN_PM, team=None + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent @@ -768,11 +772,13 @@ async def test_create_session_non_developer_forbidden( app = FastAPI() app.include_router(ws_router, prefix="/api/work-sessions") - async def _override_db(): + async def _override_db() -> AsyncIterator[AsyncSession]: yield db_session async def _override_agent() -> AgentContext: - return AgentContext(agent_id=qa.id, role=AgentRole.QA, team=Team.BACKEND) + return AgentContext( + agent_id=cast("UUID", qa.id), role=AgentRole.QA, team=Team.BACKEND + ) app.dependency_overrides[get_db] = _override_db app.dependency_overrides[get_agent_context] = _override_agent diff --git a/tests/unit/agent_sdk/test_intake_driver.py b/tests/unit/agent_sdk/test_intake_driver.py index 6265132b..db271d60 100644 --- a/tests/unit/agent_sdk/test_intake_driver.py +++ b/tests/unit/agent_sdk/test_intake_driver.py @@ -20,7 +20,7 @@ from roboco.agent_sdk.intake_driver import ( ) if TYPE_CHECKING: - from collections.abc import AsyncIterator + from collections.abc import AsyncIterator, Awaitable, Callable # --------------------------------------------------------------------------- # Fakes mirroring the claude-agent-sdk message/block shapes @@ -209,7 +209,7 @@ class _RaisingSession: raise RuntimeError("boom") -def _source(messages: list[str | None]): +def _source(messages: list[str | None]) -> Callable[[], Awaitable[str | None]]: queue = list(messages) async def _next() -> str | None: @@ -231,7 +231,7 @@ async def test_driver_streams_turns_until_shutdown() -> None: ) @asynccontextmanager - async def factory(): + async def factory() -> AsyncIterator[_FakeSession]: yield session collected: list[StreamChunk] = [] @@ -250,7 +250,7 @@ async def test_driver_streams_turns_until_shutdown() -> None: @pytest.mark.asyncio async def test_driver_turn_failure_emits_error_and_continues() -> None: @asynccontextmanager - async def factory(): + async def factory() -> AsyncIterator[_RaisingSession]: yield _RaisingSession() collected: list[StreamChunk] = [] diff --git a/tests/unit/api/test_app.py b/tests/unit/api/test_app.py index 26b1f19c..6077f7d9 100644 --- a/tests/unit/api/test_app.py +++ b/tests/unit/api/test_app.py @@ -9,6 +9,7 @@ extraction, optimal-service). from __future__ import annotations from contextlib import ExitStack, asynccontextmanager +from typing import TYPE_CHECKING from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -16,6 +17,9 @@ from fastapi import FastAPI from roboco.api.app import app as default_app from roboco.api.app import create_app, lifespan +if TYPE_CHECKING: + from collections.abc import AsyncIterator + def test_default_app_is_a_fastapi_instance() -> None: """Importing the module yields a configured FastAPI instance.""" @@ -74,7 +78,7 @@ def test_create_app_includes_v1_flow_routes() -> None: def test_create_app_attaches_cors_middleware() -> None: app = create_app() - middleware_classes = [m.cls.__name__ for m in app.user_middleware] + middleware_classes = [getattr(m.cls, "__name__", "") for m in app.user_middleware] assert "CORSMiddleware" in middleware_classes @@ -84,7 +88,7 @@ def test_create_app_attaches_cors_middleware() -> None: @asynccontextmanager -async def _stub_get_optimal(): +async def _stub_get_optimal() -> AsyncIterator[MagicMock]: """Stand-in for the optimal-service factory.""" yield MagicMock() diff --git a/tests/unit/api/test_middleware.py b/tests/unit/api/test_middleware.py index c0d4b0c7..9efcd134 100644 --- a/tests/unit/api/test_middleware.py +++ b/tests/unit/api/test_middleware.py @@ -3,6 +3,7 @@ from __future__ import annotations from http import HTTPStatus +from typing import Any # UUID annotates a Pydantic model field below, so it must stay a runtime import # (Pydantic resolves the annotation when building the model) despite `from @@ -79,36 +80,36 @@ def _make_app() -> FastAPI: app = FastAPI() @app.get("/ok") - async def _ok(): + async def _ok() -> Any: return {"status": "ok"} @app.get("/raise") - async def _raise(): + async def _raise() -> Any: raise RuntimeError("boom") @app.get("/notfound") - async def _nf(): + async def _nf() -> Any: raise NotFoundError("Resource", "abc") @app.get("/http-error") - async def _he(): + async def _he() -> Any: raise HTTPException(status_code=403, detail="nope") # service-layer errors (parallel hierarchy from roboco.services.base) @app.get("/svc-notfound") - async def _svc_nf(): + async def _svc_nf() -> Any: raise ServiceNotFoundError("Channel", "main-pm") @app.get("/svc-validation") - async def _svc_v(): + async def _svc_v() -> Any: raise ServiceValidationError("invalid input", field="title") @app.get("/svc-conflict") - async def _svc_c(): + async def _svc_c() -> Any: raise ServiceConflictError("duplicate", resource_type="task") @app.get("/svc-unauth") - async def _svc_u(): + async def _svc_u() -> Any: raise ServiceUnauthorizedError("merge_pr", reason="not your PR") setup_middleware(app) @@ -265,7 +266,7 @@ def test_request_validation_handler_returns_422_with_details() -> None: setup_middleware(app) @app.post("/validate") - async def _v(_data: _Body): + async def _v(_data: _Body) -> Any: return {"ok": True} client = TestClient(app, raise_server_exceptions=False) diff --git a/tests/unit/api/test_middleware_docs.py b/tests/unit/api/test_middleware_docs.py index 18035cc1..0bbdb2d2 100644 --- a/tests/unit/api/test_middleware_docs.py +++ b/tests/unit/api/test_middleware_docs.py @@ -208,7 +208,10 @@ def test_check_permission_match_with_string_permission() -> None: def test_path_allowed_for_agent_read_all_role() -> None: """Line 297-298: READ_ALL_ROLES gets read access automatically.""" role = next(iter(READ_ALL_ROLES)) - rule = {"read": [], "write": []} # empty perms — but read-all role bypasses + rule: dict[str, list[str]] = { + "read": [], + "write": [], + } # empty perms — but read-all role bypasses assert _path_allowed_for_agent(rule, "x", role, None, "read") is True diff --git a/tests/unit/api/test_schemas_messages.py b/tests/unit/api/test_schemas_messages.py index 3d1b581a..9e47b334 100644 --- a/tests/unit/api/test_schemas_messages.py +++ b/tests/unit/api/test_schemas_messages.py @@ -4,14 +4,20 @@ from __future__ import annotations from datetime import UTC, datetime from types import SimpleNamespace +from typing import TYPE_CHECKING, Any, cast from uuid import uuid4 from roboco.api.schemas.messages import message_to_response +if TYPE_CHECKING: + from roboco.db.tables import MessageTable -def _stub_message(*, edit_history: list | None = None, edited_at=None): + +def _stub_message( + *, edit_history: list[Any] | None = None, edited_at: datetime | None = None +) -> MessageTable: """Build a MessageTable-shaped object.""" - return SimpleNamespace( + obj = SimpleNamespace( id=uuid4(), agent_id=uuid4(), channel_id=uuid4(), @@ -29,6 +35,7 @@ def _stub_message(*, edit_history: list | None = None, edited_at=None): edited_at=edited_at, edit_history=edit_history, ) + return cast("MessageTable", obj) def test_message_to_response_was_edited_true_when_history_present() -> None: diff --git a/tests/unit/api/test_schemas_provider.py b/tests/unit/api/test_schemas_provider.py index 7942fb64..743ffeac 100644 --- a/tests/unit/api/test_schemas_provider.py +++ b/tests/unit/api/test_schemas_provider.py @@ -3,20 +3,27 @@ from __future__ import annotations from types import SimpleNamespace +from typing import TYPE_CHECKING, cast from uuid import uuid4 from roboco.api.schemas.provider import assignment_to_response from roboco.models.base import AssignmentScope, ModelProvider +if TYPE_CHECKING: + from roboco.db.tables import ModelAssignmentTable + def test_assignment_to_response_round_trip() -> None: provider = SimpleNamespace(type=ModelProvider.ANTHROPIC) - row = SimpleNamespace( - id=uuid4(), - scope=AssignmentScope.GLOBAL, - scope_value=None, - provider=provider, - model_name="opus", + row = cast( + "ModelAssignmentTable", + SimpleNamespace( + id=uuid4(), + scope=AssignmentScope.GLOBAL, + scope_value=None, + provider=provider, + model_name="opus", + ), ) response = assignment_to_response(row) assert response.scope == AssignmentScope.GLOBAL diff --git a/tests/unit/api/test_schemas_tasks.py b/tests/unit/api/test_schemas_tasks.py index e7069b37..c9823a79 100644 --- a/tests/unit/api/test_schemas_tasks.py +++ b/tests/unit/api/test_schemas_tasks.py @@ -341,7 +341,7 @@ def test_task_list_to_response_returns_list() -> None: # --------------------------------------------------------------------------- -def _stub_response(): +def _stub_response() -> Any: """Build a TaskResponse-like object that supports model_dump.""" fake_inspector = MagicMock() fake_inspector.unloaded = {"project"} diff --git a/tests/unit/api/test_schemas_v1_flow.py b/tests/unit/api/test_schemas_v1_flow.py index cfe779a3..60dcbf02 100644 --- a/tests/unit/api/test_schemas_v1_flow.py +++ b/tests/unit/api/test_schemas_v1_flow.py @@ -19,16 +19,18 @@ def test_delegate_request_requires_task_type() -> None: HTTP boundary with a clear 422. """ with pytest.raises(ValidationError) as exc: - DelegateRequest( - parent_task_id=uuid4(), - title="t", - description="add the new endpoint plus tests", - assigned_to="be-dev-1", - team="backend", - nature="technical", - estimated_complexity="medium", - acceptance_criteria=["returns 200"], - # task_type intentionally omitted + DelegateRequest.model_validate( + { + "parent_task_id": uuid4(), + "title": "t", + "description": "add the new endpoint plus tests", + "assigned_to": "be-dev-1", + "team": "backend", + "nature": "technical", + "estimated_complexity": "medium", + "acceptance_criteria": ["returns 200"], + # task_type intentionally omitted + } ) assert "task_type" in str(exc.value) diff --git a/tests/unit/api/test_websocket_bridge.py b/tests/unit/api/test_websocket_bridge.py index f00fdfb2..4d786493 100644 --- a/tests/unit/api/test_websocket_bridge.py +++ b/tests/unit/api/test_websocket_bridge.py @@ -101,6 +101,7 @@ async def test_handle_notification_sent_broadcasts_when_connected() -> None: mgr.notification_connections = {rid: {"socket-1"}} # Has a connection. await _handle_notification_sent(event) bcast.assert_awaited_once() + assert bcast.await_args is not None call_kwargs = bcast.await_args.kwargs assert call_kwargs["notification_id"] == nid assert call_kwargs["agent_ids"] == [rid] @@ -125,6 +126,7 @@ async def test_handle_notification_acked_broadcasts_using_agent_id() -> None: mgr.notification_connections = {aid: {"socket-1"}} await _handle_notification_sent(event) bcast.assert_awaited_once() + assert bcast.await_args is not None call_kwargs = bcast.await_args.kwargs assert call_kwargs["notification_id"] == nid assert call_kwargs["agent_ids"] == [aid] diff --git a/tests/unit/events/test_handlers.py b/tests/unit/events/test_handlers.py index 24766c15..d80471b2 100644 --- a/tests/unit/events/test_handlers.py +++ b/tests/unit/events/test_handlers.py @@ -3,7 +3,11 @@ from __future__ import annotations from dataclasses import dataclass +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, patch + +if TYPE_CHECKING: + from collections.abc import Iterator from uuid import uuid4 import pytest @@ -24,7 +28,7 @@ from roboco.events.handlers import ( ) -def _make_event(event_type: EventType, **data) -> Event: +def _make_event(event_type: EventType, **data: Any) -> Event: return Event( type=event_type, data=data, @@ -33,7 +37,7 @@ def _make_event(event_type: EventType, **data) -> Event: @pytest.fixture(autouse=True) -def reset_context(): +def reset_context() -> Iterator[None]: """Reset event context after each test. set_event_context only updates attrs when truthy, so we need to diff --git a/tests/unit/gateway/test_choreographer_impl_branches.py b/tests/unit/gateway/test_choreographer_impl_branches.py index 88c17c91..d907f45a 100644 --- a/tests/unit/gateway/test_choreographer_impl_branches.py +++ b/tests/unit/gateway/test_choreographer_impl_branches.py @@ -48,8 +48,13 @@ _GOOD_RISKS = [ def _wire_dev_task_svc( - task_id, *, status: str, assigned_to=None, plan=None, parent_task_id=None -): + task_id: Any, + *, + status: str, + assigned_to: Any = None, + plan: Any = None, + parent_task_id: Any = None, +) -> AsyncMock: """Build a TaskService AsyncMock pre-wired with claim-guard side effects. Defaults `agent_for` → developer/backend and the three list-* methods to diff --git a/tests/unit/gateway/test_choreographer_pm_extras.py b/tests/unit/gateway/test_choreographer_pm_extras.py index 12e10e10..24e0c0e1 100644 --- a/tests/unit/gateway/test_choreographer_pm_extras.py +++ b/tests/unit/gateway/test_choreographer_pm_extras.py @@ -1030,6 +1030,7 @@ async def test_pm_give_me_work_returns_first_assigned() -> None: env = await c.pm_give_me_work(pm_id) assert env.error is None assert env.task_id == str(t.id) + assert env.next is not None assert "i_will_plan" in env.next @@ -1057,6 +1058,7 @@ async def test_pm_give_me_work_paused_hint_mentions_subtasks() -> None: c = Choreographer(deps) env = await c.pm_give_me_work(pm_id) + assert env.next is not None assert "subtasks" in env.next or "complete" in env.next diff --git a/tests/unit/gateway/test_commit_validator.py b/tests/unit/gateway/test_commit_validator.py index 734b6da0..5c80fcad 100644 --- a/tests/unit/gateway/test_commit_validator.py +++ b/tests/unit/gateway/test_commit_validator.py @@ -75,4 +75,5 @@ def test_banned_word_long_enough_to_pass_min_chars() -> None: # Use min_chars=2 so 'wip' (3 chars) passes length but hits banned-word. r = validate_commit_message("wip", min_chars=2) assert r.ok is False + assert r.reason is not None assert "banned single-word" in r.reason diff --git a/tests/unit/gateway/test_delegate_project_routing.py b/tests/unit/gateway/test_delegate_project_routing.py index 9527d549..0ac972b0 100644 --- a/tests/unit/gateway/test_delegate_project_routing.py +++ b/tests/unit/gateway/test_delegate_project_routing.py @@ -41,7 +41,7 @@ def _make_deps(**overrides: Any) -> ChoreographerDeps: return ChoreographerDeps(**base) -def _parent(pm_id, product_id=None, project_id=None): +def _parent(pm_id: Any, product_id: Any = None, project_id: Any = None) -> MagicMock: return MagicMock( id=uuid4(), project_id=project_id or uuid4(), @@ -65,7 +65,7 @@ def _inputs(**kw: Any) -> DelegateInputs: return DelegateInputs(**base) -async def _run(parent, inputs, product=None): +async def _run(parent: Any, inputs: DelegateInputs, product: Any = None) -> Any: pm_id = parent.assigned_to task_svc = AsyncMock() task_svc.get.return_value = parent diff --git a/tests/unit/gateway/test_dm_a2a_denied.py b/tests/unit/gateway/test_dm_a2a_denied.py index 1efc92a4..17c147d9 100644 --- a/tests/unit/gateway/test_dm_a2a_denied.py +++ b/tests/unit/gateway/test_dm_a2a_denied.py @@ -75,5 +75,6 @@ async def test_dm_a2a_denied_returns_envelope_not_authorized() -> None: "If it escapes to FastAPI middleware, RobocoError.to_dict() renders the " "error as a dict and the do_server circuit breaker crashes." ) + assert env.message is not None assert "be-qa" in env.message assert env.remediate is not None diff --git a/tests/unit/gateway/test_envelope_from_decision.py b/tests/unit/gateway/test_envelope_from_decision.py index 11f41785..d4b2d260 100644 --- a/tests/unit/gateway/test_envelope_from_decision.py +++ b/tests/unit/gateway/test_envelope_from_decision.py @@ -48,6 +48,7 @@ def test_from_decision_self_review_maps_to_not_authorized_with_hint() -> None: ) env = Envelope.from_decision(d, briefing={}) assert env.error == "not_authorized" + assert env.message is not None assert "self-review" in env.message.lower() diff --git a/tests/unit/gateway/test_quality_gate.py b/tests/unit/gateway/test_quality_gate.py index d3e4d035..5aafbc91 100644 --- a/tests/unit/gateway/test_quality_gate.py +++ b/tests/unit/gateway/test_quality_gate.py @@ -5,7 +5,11 @@ i_am_done, blocking a red submit before it reaches QA. Full tests stay on CI. from __future__ import annotations from types import SimpleNamespace +from typing import TYPE_CHECKING from unittest.mock import AsyncMock, MagicMock + +if TYPE_CHECKING: + import pathlib from uuid import uuid4 import pytest @@ -18,14 +22,14 @@ from roboco.services.git import GitService @pytest.mark.asyncio -async def test_no_commands_is_a_skipped_pass(tmp_path) -> None: +async def test_no_commands_is_a_skipped_pass(tmp_path: pathlib.Path) -> None: result = await run_quality_commands(tmp_path, []) assert result.passed is True assert result.skipped is True @pytest.mark.asyncio -async def test_all_commands_pass(tmp_path) -> None: +async def test_all_commands_pass(tmp_path: pathlib.Path) -> None: result = await run_quality_commands( tmp_path, [("lint", "echo lint-ok"), ("typecheck", "true")] ) @@ -34,7 +38,7 @@ async def test_all_commands_pass(tmp_path) -> None: @pytest.mark.asyncio -async def test_a_failing_command_blocks_and_is_named(tmp_path) -> None: +async def test_a_failing_command_blocks_and_is_named(tmp_path: pathlib.Path) -> None: result = await run_quality_commands( tmp_path, [("lint", "echo problem-here; exit 1"), ("typecheck", "true")] ) @@ -44,7 +48,7 @@ async def test_a_failing_command_blocks_and_is_named(tmp_path) -> None: @pytest.mark.asyncio -async def test_every_command_runs_even_after_a_failure(tmp_path) -> None: +async def test_every_command_runs_even_after_a_failure(tmp_path: pathlib.Path) -> None: result = await run_quality_commands( tmp_path, [("lint", "echo AAA; exit 1"), ("typecheck", "echo BBB; exit 2")] ) diff --git a/tests/unit/gateway/test_work_session_auto_create.py b/tests/unit/gateway/test_work_session_auto_create.py index 7345cd11..c9d1578c 100644 --- a/tests/unit/gateway/test_work_session_auto_create.py +++ b/tests/unit/gateway/test_work_session_auto_create.py @@ -14,7 +14,7 @@ its id on the task. from __future__ import annotations from unittest.mock import AsyncMock, MagicMock -from uuid import uuid4 +from uuid import UUID, uuid4 import pytest from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps @@ -47,7 +47,7 @@ _GOOD_RISKS = [ ] -def _make_task_svc(agent_id, task_id, *, status: str): +def _make_task_svc(agent_id: UUID, task_id: UUID, *, status: str) -> AsyncMock: """Build a TaskService AsyncMock that completes the (claim, set_plan, start) sequence and returns a task with branch_name set (as the real service does after auto-creating the branch during claim side-effects). @@ -109,7 +109,7 @@ def _make_task_svc(agent_id, task_id, *, status: str): return task_svc -def _make_deps(task_svc) -> ChoreographerDeps: +def _make_deps(task_svc: AsyncMock) -> ChoreographerDeps: evidence_repo = AsyncMock() for method in ( "list_unread_a2a", diff --git a/tests/unit/mcp_servers/test_do_server_circuit_breaker.py b/tests/unit/mcp_servers/test_do_server_circuit_breaker.py index 5e80683e..03af60e3 100644 --- a/tests/unit/mcp_servers/test_do_server_circuit_breaker.py +++ b/tests/unit/mcp_servers/test_do_server_circuit_breaker.py @@ -77,7 +77,7 @@ def do_module(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> types.ModuleTy def _make_client( orchestrator_response: dict[str, Any], sdk_response: dict[str, Any] | None -): +) -> Any: """Build an httpx.Client mock that dispatches by destination URL.""" captured: list[tuple[str, dict[str, Any] | None]] = [] diff --git a/tests/unit/mcp_servers/test_flow_server_circuit_breaker.py b/tests/unit/mcp_servers/test_flow_server_circuit_breaker.py index 7b6e0e64..2eaa6a91 100644 --- a/tests/unit/mcp_servers/test_flow_server_circuit_breaker.py +++ b/tests/unit/mcp_servers/test_flow_server_circuit_breaker.py @@ -85,7 +85,7 @@ def flow_module(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> types.Module def _make_client( orchestrator_response: dict[str, Any], sdk_response: dict[str, Any] | None -): +) -> Any: """Build an httpx.Client mock that dispatches by destination URL. Calls hitting ``test-orchestrator`` return ``orchestrator_response``; @@ -239,7 +239,7 @@ def test_other_error_kinds_do_not_touch_sdk(flow_module: types.ModuleType) -> No def test_breaker_open_substitutes_envelope(flow_module: types.ModuleType) -> None: """When SDK reports open=true, the agent gets the circuit_open envelope.""" - circuit_env = { + circuit_env: dict[str, Any] = { "error": "circuit_open", "message": ("verb 'i_am_done' rejected 3 times in last 60s — breaker open"), "remediate": "call i_am_blocked or i_am_idle", @@ -281,7 +281,7 @@ def test_fourth_rejection_returns_circuit_open(flow_module: types.ModuleType) -> trips the breaker is also the call that sees the substitution. """ # On the trip call the SDK reports open=true with the envelope. - circuit_env = { + circuit_env: dict[str, Any] = { "error": "circuit_open", "message": ("verb 'i_am_done' rejected 3 times in last 60s — breaker open"), "remediate": "call i_am_blocked(reason='...') or i_am_idle()", diff --git a/tests/unit/mcp_servers/test_flow_server_intent_public_mapping.py b/tests/unit/mcp_servers/test_flow_server_intent_public_mapping.py index 301906cc..4deefb67 100644 --- a/tests/unit/mcp_servers/test_flow_server_intent_public_mapping.py +++ b/tests/unit/mcp_servers/test_flow_server_intent_public_mapping.py @@ -14,7 +14,7 @@ from __future__ import annotations import importlib import json -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from unittest.mock import MagicMock, patch import pytest @@ -100,7 +100,7 @@ def test_post_to_correct_orchestrator_path( flow_module_qa: types.ModuleType, ) -> None: """Calling the registered 'pass' tool POSTs to /api/v1/flow/qa/pass.""" - captured: list[tuple[str, dict]] = [] + captured: list[tuple[str, Any]] = [] def _client_factory(*_a: object, **_kw: object) -> MagicMock: client = MagicMock() diff --git a/tests/unit/runtime/test_blocker_and_claimed_dispatch.py b/tests/unit/runtime/test_blocker_and_claimed_dispatch.py index d3e1ce46..d8d20d7f 100644 --- a/tests/unit/runtime/test_blocker_and_claimed_dispatch.py +++ b/tests/unit/runtime/test_blocker_and_claimed_dispatch.py @@ -14,7 +14,7 @@ from __future__ import annotations from datetime import UTC, datetime, timedelta from typing import Any -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch import pytest from roboco.models.runtime import AgentInstance @@ -300,17 +300,23 @@ async def test_handle_dev_existing_owner_skips_blocked() -> None: """A blocked task's owner is not respawned — it has no legal move from blocked, so respawning it only churns; it waits for unblock or release.""" orch = _orch() - orch._respawn_dev_if_inactive = AsyncMock() - orch._is_agent_active = MagicMock(return_value=False) - await orch._handle_dev_existing_owner({"id": "t1"}, "blocked", "be-dev-1") - orch._respawn_dev_if_inactive.assert_not_called() + respawn_mock = AsyncMock() + with ( + patch.object(orch, "_respawn_dev_if_inactive", new=respawn_mock), + patch.object(orch, "_is_agent_active", new=MagicMock(return_value=False)), + ): + await orch._handle_dev_existing_owner({"id": "t1"}, "blocked", "be-dev-1") + respawn_mock.assert_not_called() @pytest.mark.asyncio async def test_handle_dev_existing_owner_respawns_in_progress() -> None: """An in_progress task whose owner is inactive is still respawned.""" orch = _orch() - orch._respawn_dev_if_inactive = AsyncMock() - orch._is_agent_active = MagicMock(return_value=False) - await orch._handle_dev_existing_owner({"id": "t1"}, "in_progress", "be-dev-1") - orch._respawn_dev_if_inactive.assert_awaited_once() + respawn_mock = AsyncMock() + with ( + patch.object(orch, "_respawn_dev_if_inactive", new=respawn_mock), + patch.object(orch, "_is_agent_active", new=MagicMock(return_value=False)), + ): + await orch._handle_dev_existing_owner({"id": "t1"}, "in_progress", "be-dev-1") + respawn_mock.assert_awaited_once() diff --git a/tests/unit/runtime/test_board_dispatch.py b/tests/unit/runtime/test_board_dispatch.py index aceb4aba..ae38f9be 100644 --- a/tests/unit/runtime/test_board_dispatch.py +++ b/tests/unit/runtime/test_board_dispatch.py @@ -13,11 +13,16 @@ from __future__ import annotations from contextlib import asynccontextmanager -from typing import Any +from typing import TYPE_CHECKING, Any, cast from unittest.mock import AsyncMock, patch from uuid import uuid4 import pytest + +if TYPE_CHECKING: + from collections.abc import AsyncIterator + + import httpx from roboco.runtime.orchestrator import AgentOrchestrator @@ -40,7 +45,7 @@ def _board_task(assigned_to: str) -> dict[str, Any]: } -def _patch_handoff_db(task_svc: AsyncMock): +def _patch_handoff_db(task_svc: AsyncMock) -> tuple[Any, Any]: """Patch the DB context + TaskService the handoff opens to flag the task. Returns a tuple of context managers for the caller's ``with`` block so the @@ -48,7 +53,7 @@ def _patch_handoff_db(task_svc: AsyncMock): """ @asynccontextmanager - async def _fake_ctx(): + async def _fake_ctx() -> AsyncIterator[AsyncMock]: yield AsyncMock() return ( @@ -156,7 +161,7 @@ async def test_unassigned_board_task_dispatches_both_via_board_handler() -> None "description": "A board-level task to review and shape.", "assigned_to": None, } - client = object() + client = cast("httpx.AsyncClient", object()) with ( patch.object(orch, "_is_agent_active", return_value=False), patch.object(orch, "_task_git_context", return_value=None), diff --git a/tests/unit/runtime/test_briefing_tool_load_block.py b/tests/unit/runtime/test_briefing_tool_load_block.py index 2495f7e8..a1e3a49f 100644 --- a/tests/unit/runtime/test_briefing_tool_load_block.py +++ b/tests/unit/runtime/test_briefing_tool_load_block.py @@ -22,7 +22,7 @@ from roboco.runtime.orchestrator import AgentOrchestrator def _orch() -> AgentOrchestrator: with patch.object(AgentOrchestrator, "__init__", return_value=None): orch = AgentOrchestrator.__new__(AgentOrchestrator) - orch._TOOL_LOAD_CACHE = {} + object.__setattr__(orch, "_TOOL_LOAD_CACHE", {}) return orch diff --git a/tests/unit/runtime/test_check_health_graceful_exit.py b/tests/unit/runtime/test_check_health_graceful_exit.py index 454ed299..ee9f230b 100644 --- a/tests/unit/runtime/test_check_health_graceful_exit.py +++ b/tests/unit/runtime/test_check_health_graceful_exit.py @@ -52,9 +52,11 @@ async def test_graceful_exit_does_not_respawn() -> None: proc = MagicMock() proc.communicate = AsyncMock(return_value=(b"false 0\n", b"")) spawn = AsyncMock() - orch.spawn_agent = spawn - with patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)): + with ( + patch.object(orch, "spawn_agent", new=spawn), + patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)), + ): await orch._check_health() spawn.assert_not_awaited() @@ -77,12 +79,15 @@ async def test_crash_exit_triggers_restart() -> None: proc = MagicMock() proc.communicate = AsyncMock(return_value=(b"false 137\n", b"")) spawn = AsyncMock() - orch.spawn_agent = spawn - with patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)): + with ( + patch.object(orch, "spawn_agent", new=spawn), + patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)), + ): await orch._check_health() spawn.assert_awaited_once() + assert spawn.await_args is not None args = spawn.await_args.kwargs assert args["agent_id"] == "be-dev-1" assert args["task_id"] == task_id @@ -99,9 +104,11 @@ async def test_still_running_no_action() -> None: proc = MagicMock() proc.communicate = AsyncMock(return_value=(b"true 0\n", b"")) spawn = AsyncMock() - orch.spawn_agent = spawn - with patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)): + with ( + patch.object(orch, "spawn_agent", new=spawn), + patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)), + ): await orch._check_health() spawn.assert_not_awaited() @@ -122,10 +129,13 @@ async def test_crash_max_retries_does_not_restart() -> None: proc = MagicMock() proc.communicate = AsyncMock(return_value=(b"false 1\n", b"")) spawn = AsyncMock() - orch.spawn_agent = spawn - orch._notify_agent_stranded = AsyncMock() + notify_stranded = AsyncMock() - with patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)): + with ( + patch.object(orch, "spawn_agent", new=spawn), + patch.object(orch, "_notify_agent_stranded", new=notify_stranded), + patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)), + ): await orch._check_health() spawn.assert_not_awaited() @@ -143,9 +153,11 @@ async def test_malformed_inspect_treated_as_crash() -> None: # No exit code field at all. proc.communicate = AsyncMock(return_value=(b"false\n", b"")) spawn = AsyncMock() - orch.spawn_agent = spawn - with patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)): + with ( + patch.object(orch, "spawn_agent", new=spawn), + patch("asyncio.create_subprocess_exec", AsyncMock(return_value=proc)), + ): await orch._check_health() # exit_code is None → not graceful → counts as crash. diff --git a/tests/unit/runtime/test_orchestrator_write_hooks.py b/tests/unit/runtime/test_orchestrator_write_hooks.py index 77071eec..a0b1fa83 100644 --- a/tests/unit/runtime/test_orchestrator_write_hooks.py +++ b/tests/unit/runtime/test_orchestrator_write_hooks.py @@ -22,7 +22,7 @@ from __future__ import annotations import json from contextlib import asynccontextmanager from pathlib import Path -from typing import Any +from typing import Any, cast from unittest.mock import AsyncMock, MagicMock, patch from uuid import UUID, uuid4 @@ -621,7 +621,9 @@ async def test_resolve_active_tokens_falls_back_to_transcript() -> None: client = _FakeHTTPClient(_handler) with patch.object(orch, "_usage_from_transcript", return_value=(6, 514, 100, 50)): - tokens = await orch._resolve_active_tokens(client, _AGENT_ID) + tokens = await orch._resolve_active_tokens( + cast("httpx.AsyncClient", client), _AGENT_ID + ) assert tokens == (6, 514, 100, 50) @@ -645,7 +647,9 @@ async def test_resolve_active_tokens_prefers_sdk() -> None: with patch.object( orch, "_usage_from_transcript", return_value=(999, 999, 999, 999) ) as mock_tx: - tokens = await orch._resolve_active_tokens(client, _AGENT_ID) + tokens = await orch._resolve_active_tokens( + cast("httpx.AsyncClient", client), _AGENT_ID + ) assert tokens == (10, 20, 0, 0) mock_tx.assert_not_called() diff --git a/tests/unit/runtime/test_pm_closure_auto_resume.py b/tests/unit/runtime/test_pm_closure_auto_resume.py index 56eba508..088e14e6 100644 --- a/tests/unit/runtime/test_pm_closure_auto_resume.py +++ b/tests/unit/runtime/test_pm_closure_auto_resume.py @@ -18,6 +18,7 @@ triggers only its own recovery helper. from __future__ import annotations +from typing import cast from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -58,9 +59,11 @@ async def test_paused_parent_is_resumed_before_spawn() -> None: await orch._maybe_spawn_pm_closure(client, task) - orch._auto_resume_paused_parent.assert_awaited_once_with(client, "parent-1") - orch._auto_recover_blocked_parent.assert_not_awaited() - orch.spawn_agent.assert_awaited_once() + cast("AsyncMock", orch._auto_resume_paused_parent).assert_awaited_once_with( + client, "parent-1" + ) + cast("AsyncMock", orch._auto_recover_blocked_parent).assert_not_awaited() + cast("AsyncMock", orch.spawn_agent).assert_awaited_once() @pytest.mark.asyncio @@ -72,9 +75,11 @@ async def test_blocked_parent_is_recovered_before_spawn() -> None: await orch._maybe_spawn_pm_closure(client, task) - orch._auto_recover_blocked_parent.assert_awaited_once_with(client, "parent-2") - orch._auto_resume_paused_parent.assert_not_awaited() - orch.spawn_agent.assert_awaited_once() + cast("AsyncMock", orch._auto_recover_blocked_parent).assert_awaited_once_with( + client, "parent-2" + ) + cast("AsyncMock", orch._auto_resume_paused_parent).assert_not_awaited() + cast("AsyncMock", orch.spawn_agent).assert_awaited_once() @pytest.mark.asyncio @@ -88,9 +93,9 @@ async def test_non_paused_parent_is_not_resumed() -> None: await orch._maybe_spawn_pm_closure(client, task) - orch._auto_resume_paused_parent.assert_not_awaited() - orch._auto_recover_blocked_parent.assert_not_awaited() - orch.spawn_agent.assert_awaited_once() + cast("AsyncMock", orch._auto_resume_paused_parent).assert_not_awaited() + cast("AsyncMock", orch._auto_recover_blocked_parent).assert_not_awaited() + cast("AsyncMock", orch.spawn_agent).assert_awaited_once() @pytest.mark.asyncio @@ -104,8 +109,8 @@ async def test_resume_skipped_when_closure_gate_blocks_spawn() -> None: client, {"id": "p", "status": "paused", "team": "backend"} ) - orch._auto_resume_paused_parent.assert_not_awaited() - orch.spawn_agent.assert_not_awaited() + cast("AsyncMock", orch._auto_resume_paused_parent).assert_not_awaited() + cast("AsyncMock", orch.spawn_agent).assert_not_awaited() @pytest.mark.asyncio diff --git a/tests/unit/runtime/test_rate_limit_sweep.py b/tests/unit/runtime/test_rate_limit_sweep.py index 9040d056..d86eb52f 100644 --- a/tests/unit/runtime/test_rate_limit_sweep.py +++ b/tests/unit/runtime/test_rate_limit_sweep.py @@ -13,7 +13,7 @@ import fnmatch import json from datetime import UTC, datetime from typing import Any -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch from uuid import uuid4 from httpx import ASGITransport, AsyncClient @@ -77,9 +77,9 @@ def _make_orchestrator() -> AgentOrchestrator: """Build a minimal orchestrator via __new__ (no __init__ side-effects).""" orch = AgentOrchestrator.__new__(AgentOrchestrator) orch._running = True - orch._waiting_records: dict[str, WaitingRecord] = {} - orch._instances: dict[str, Any] = {} - orch._rate_limit_ceo_notified: set[str] = set() + orch._waiting_records = {} + orch._instances = {} + orch._rate_limit_ceo_notified = set() return orch @@ -139,15 +139,13 @@ class TestProbeSuccessPath: state = _make_active_state(provider, retry_after=None) tracker_mock = _make_tracker_mock() - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - orch.resolve_wait = AsyncMock(return_value=None) - async def fake_do_probe(_p: str) -> bool: - return True - - orch._do_probe = fake_do_probe # type: ignore[method-assign] - - with patch("roboco.events.get_event_bus") as mock_bus_fn: + with ( + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "resolve_wait", new=AsyncMock(return_value=None)), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=True)), + patch("roboco.events.get_event_bus") as mock_bus_fn, + ): bus_mock = AsyncMock() bus_mock.publish = AsyncMock() mock_bus_fn.return_value = bus_mock @@ -172,17 +170,15 @@ class TestProbeSuccessPath: ), # different provider } - orch.resolve_wait = AsyncMock(return_value=None) - + resolve_mock = AsyncMock(return_value=None) tracker_mock = _make_tracker_mock() - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - async def fake_do_probe(_p: str) -> bool: - return True - - orch._do_probe = fake_do_probe # type: ignore[method-assign] - - with patch("roboco.events.get_event_bus") as mock_bus_fn: + with ( + patch.object(orch, "resolve_wait", new=resolve_mock), + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=True)), + patch("roboco.events.get_event_bus") as mock_bus_fn, + ): bus_mock = AsyncMock() bus_mock.publish = AsyncMock() mock_bus_fn.return_value = bus_mock @@ -190,8 +186,8 @@ class TestProbeSuccessPath: await orch._probe_one_provider(provider, state) # Only the two anthropic-parked agents should be resolved - assert orch.resolve_wait.await_count == 2 # noqa: PLR2004 - resolved_ids = {call.args[0] for call in orch.resolve_wait.call_args_list} + assert resolve_mock.await_count == 2 # noqa: PLR2004 + resolved_ids = {call.args[0] for call in resolve_mock.call_args_list} assert agent1 in resolved_ids assert agent2 in resolved_ids assert "be-qa-1" not in resolved_ids @@ -202,19 +198,15 @@ class TestProbeSuccessPath: provider = "anthropic" state = _make_active_state(provider, retry_after=None) - orch.resolve_wait = AsyncMock(return_value=None) - tracker_mock = _make_tracker_mock() - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - published_events: list[Any] = [] - async def fake_do_probe(_p: str) -> bool: - return True - - orch._do_probe = fake_do_probe # type: ignore[method-assign] - - with patch("roboco.events.get_event_bus") as mock_bus_fn: + with ( + patch.object(orch, "resolve_wait", new=AsyncMock(return_value=None)), + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=True)), + patch("roboco.events.get_event_bus") as mock_bus_fn, + ): bus_mock = AsyncMock() bus_mock.publish = AsyncMock(side_effect=published_events.append) mock_bus_fn.return_value = bus_mock @@ -233,17 +225,14 @@ class TestProbeSuccessPath: orch._rate_limit_ceo_notified.add(provider) # simulates prior episode state = _make_active_state(provider, retry_after=None) - orch.resolve_wait = AsyncMock(return_value=None) - tracker_mock = _make_tracker_mock() - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - async def fake_do_probe(_p: str) -> bool: - return True - - orch._do_probe = fake_do_probe # type: ignore[method-assign] - - with patch("roboco.events.get_event_bus") as mock_bus_fn: + with ( + patch.object(orch, "resolve_wait", new=AsyncMock(return_value=None)), + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=True)), + patch("roboco.events.get_event_bus") as mock_bus_fn, + ): bus_mock = AsyncMock() bus_mock.publish = AsyncMock() mock_bus_fn.return_value = bus_mock @@ -263,15 +252,14 @@ class TestProbeSuccessPath: activated_at=datetime.now(UTC), ) - probe_called = [] + probe_called: list[str] = [] async def fake_do_probe(_p: str) -> bool: probe_called.append(_p) return True - orch._do_probe = fake_do_probe # type: ignore[method-assign] - - await orch._probe_one_provider(provider, state) + with patch.object(orch, "_do_probe", new=fake_do_probe): + await orch._probe_one_provider(provider, state) assert probe_called == [] # probe was gated by time @@ -291,15 +279,13 @@ class TestProbeFailurePath: state = _make_active_state(provider, retry_after=None) tracker_mock = _make_tracker_mock(failure_return=1) - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - async def fake_do_probe(_p: str) -> bool: - return False - - orch._do_probe = fake_do_probe # type: ignore[method-assign] - orch._notify_rate_limit_ceo = AsyncMock() - - await orch._probe_one_provider(provider, state) + with ( + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=False)), + patch.object(orch, "_notify_rate_limit_ceo", new=AsyncMock()), + ): + await orch._probe_one_provider(provider, state) tracker_mock.increment_probe_failures.assert_awaited_once() @@ -310,15 +296,13 @@ class TestProbeFailurePath: state = _make_active_state(provider, retry_after=None) tracker_mock = _make_tracker_mock(failure_return=1) - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - async def fake_do_probe(_p: str) -> bool: - return False - - orch._do_probe = fake_do_probe # type: ignore[method-assign] - orch._notify_rate_limit_ceo = AsyncMock() - - await orch._probe_one_provider(provider, state) + with ( + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=False)), + patch.object(orch, "_notify_rate_limit_ceo", new=AsyncMock()), + ): + await orch._probe_one_provider(provider, state) tracker_mock.clear.assert_not_awaited() @@ -339,17 +323,16 @@ class TestCEONotificationThreshold: # simulate already at 9 failures; next increment returns 10 tracker_mock = _make_tracker_mock(failure_return=10) - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - orch._notify_rate_limit_ceo = AsyncMock() + notify_mock = AsyncMock() - async def fake_do_probe(_p: str) -> bool: - return False + with ( + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_notify_rate_limit_ceo", new=notify_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=False)), + ): + await orch._probe_one_provider(provider, state) - orch._do_probe = fake_do_probe # type: ignore[method-assign] - - await orch._probe_one_provider(provider, state) - - orch._notify_rate_limit_ceo.assert_awaited_once() + notify_mock.assert_awaited_once() async def test_notification_not_fired_before_threshold(self) -> None: """No CEO notification below threshold 10.""" @@ -358,17 +341,16 @@ class TestCEONotificationThreshold: state = _make_active_state(provider, retry_after=None) tracker_mock = _make_tracker_mock(failure_return=9) - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - orch._notify_rate_limit_ceo = AsyncMock() + notify_mock = AsyncMock() - async def fake_do_probe(_p: str) -> bool: - return False + with ( + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_notify_rate_limit_ceo", new=notify_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=False)), + ): + await orch._probe_one_provider(provider, state) - orch._do_probe = fake_do_probe # type: ignore[method-assign] - - await orch._probe_one_provider(provider, state) - - orch._notify_rate_limit_ceo.assert_not_awaited() + notify_mock.assert_not_awaited() async def test_notification_sent_only_once_per_episode(self) -> None: """Even if failures keep accumulating, the CEO is notified only once.""" @@ -380,17 +362,16 @@ class TestCEONotificationThreshold: orch._rate_limit_ceo_notified.add(provider) tracker_mock = _make_tracker_mock(failure_return=15) - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] - orch._notify_rate_limit_ceo = AsyncMock() + notify_mock = AsyncMock() - async def fake_do_probe(_p: str) -> bool: - return False + with ( + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_notify_rate_limit_ceo", new=notify_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=False)), + ): + await orch._probe_one_provider(provider, state) - orch._do_probe = fake_do_probe # type: ignore[method-assign] - - await orch._probe_one_provider(provider, state) - - orch._notify_rate_limit_ceo.assert_not_awaited() + notify_mock.assert_not_awaited() async def test_new_episode_allows_new_notification(self) -> None: """After a rate-limit clears (success) a new episode starts fresh.""" @@ -400,19 +381,16 @@ class TestCEONotificationThreshold: orch._rate_limit_ceo_notified.add(provider) success_state = _make_active_state(provider, retry_after=None) - orch.resolve_wait = AsyncMock(return_value=None) - tracker_mock = _make_tracker_mock(failure_return=10) - orch._make_tracker = MagicMock(return_value=tracker_mock) # type: ignore[method-assign] notify_mock = AsyncMock() - orch._notify_rate_limit_ceo = notify_mock - async def fake_do_probe_success(_p: str) -> bool: - return True - - orch._do_probe = fake_do_probe_success # type: ignore[method-assign] - - with patch("roboco.events.get_event_bus") as mock_bus_fn: + with ( + patch.object(orch, "resolve_wait", new=AsyncMock(return_value=None)), + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_notify_rate_limit_ceo", new=notify_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=True)), + patch("roboco.events.get_event_bus") as mock_bus_fn, + ): bus_mock = AsyncMock() bus_mock.publish = AsyncMock() mock_bus_fn.return_value = bus_mock @@ -423,13 +401,14 @@ class TestCEONotificationThreshold: assert provider not in orch._rate_limit_ceo_notified # Episode 2: simulate a new failure reaching threshold 10 - async def fake_do_probe_fail(_p: str) -> bool: - return False - - orch._do_probe = fake_do_probe_fail # type: ignore[method-assign] - failure_state = _make_active_state(provider, retry_after=None) - await orch._probe_one_provider(provider, failure_state) + + with ( + patch.object(orch, "_make_tracker", return_value=tracker_mock), + patch.object(orch, "_notify_rate_limit_ceo", new=notify_mock), + patch.object(orch, "_do_probe", new=AsyncMock(return_value=False)), + ): + await orch._probe_one_provider(provider, failure_state) # Notification SHOULD fire for the new episode notify_mock.assert_awaited_once() diff --git a/tests/unit/runtime/test_streaming.py b/tests/unit/runtime/test_streaming.py index b8c2470d..bedef3b4 100644 --- a/tests/unit/runtime/test_streaming.py +++ b/tests/unit/runtime/test_streaming.py @@ -2,6 +2,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Iterator + import pytest from roboco.runtime.streaming import ( get_reasoning_stream_callback, @@ -11,7 +16,7 @@ from roboco.runtime.streaming import ( @pytest.fixture(autouse=True) -def reset_callback(): +def reset_callback() -> Iterator[None]: """Reset the global callback after each test.""" yield set_reasoning_stream_callback(None) @@ -23,7 +28,7 @@ def test_get_callback_returns_none_initially() -> None: def test_set_and_get_callback() -> None: - async def cb(agent_id, chunk, metadata): + async def cb(agent_id: str, chunk: str, metadata: dict[str, Any]) -> None: pass set_reasoning_stream_callback(cb) diff --git a/tests/unit/services/test_choreographer_protocol.py b/tests/unit/services/test_choreographer_protocol.py index 1ccc9ee6..2485de34 100644 --- a/tests/unit/services/test_choreographer_protocol.py +++ b/tests/unit/services/test_choreographer_protocol.py @@ -9,17 +9,14 @@ file shows up in coverage. from __future__ import annotations -from typing import TYPE_CHECKING +from typing import Any from uuid import uuid4 import pytest from roboco.services.gateway.choreographer._protocol import ChoreographerHelpers -if TYPE_CHECKING: - from roboco.services.gateway.envelope import Envelope - -def _empty_env() -> Envelope: +def _empty_env() -> Any: return { "status": "ok", "task_id": None, diff --git a/tests/unit/services/test_escalation_board_guard.py b/tests/unit/services/test_escalation_board_guard.py index 0c865355..b04a04ba 100644 --- a/tests/unit/services/test_escalation_board_guard.py +++ b/tests/unit/services/test_escalation_board_guard.py @@ -581,7 +581,7 @@ async def test_unblock_dependents_rehomes_board_owned_cell_task() -> None: active_claimant_id=board_owner, dev_notes="prior", ) - svc.session.execute = _blocked_dependent(task) + object.__setattr__(svc.session, "execute", _blocked_dependent(task)) _bind(svc, "_is_board_advisory_agent", AsyncMock(return_value=True)) await svc._unblock_dependents(completed_id) @@ -608,7 +608,7 @@ async def test_unblock_dependents_rehomes_ownerless_cell_task() -> None: active_claimant_id=None, dev_notes="", ) - svc.session.execute = _blocked_dependent(task) + object.__setattr__(svc.session, "execute", _blocked_dependent(task)) board_check = AsyncMock(return_value=False) _bind(svc, "_is_board_advisory_agent", board_check) @@ -634,7 +634,7 @@ async def test_unblock_dependents_resumes_dev_owned_cell_task() -> None: assigned_to=dev_owner, claimed_by=dev_owner, ) - svc.session.execute = _blocked_dependent(task) + object.__setattr__(svc.session, "execute", _blocked_dependent(task)) _bind(svc, "_is_board_advisory_agent", AsyncMock(return_value=False)) validate_mock = MagicMock() _bind(svc, "_validate_and_set_status", validate_mock) @@ -663,7 +663,7 @@ async def test_unblock_dependents_resumes_board_owned_root_task() -> None: assigned_to=uuid4(), claimed_by=uuid4(), ) - svc.session.execute = _blocked_dependent(task) + object.__setattr__(svc.session, "execute", _blocked_dependent(task)) _bind(svc, "_is_board_advisory_agent", AsyncMock(return_value=True)) validate_mock = MagicMock() _bind(svc, "_validate_and_set_status", validate_mock) diff --git a/tests/unit/services/test_extraction.py b/tests/unit/services/test_extraction.py index 183e9c4f..2872ff9e 100644 --- a/tests/unit/services/test_extraction.py +++ b/tests/unit/services/test_extraction.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import ClassVar +from typing import Any, ClassVar from uuid import uuid4 import anthropic as anthropic_mod @@ -194,7 +194,7 @@ async def test_pipeline_invokes_callback() -> None: pipeline = ExtractionPipeline() received: list = [] - async def on_message(msg) -> None: + async def on_message(msg: Any) -> None: received.append(msg) pipeline.on_message(on_message) @@ -210,7 +210,7 @@ async def test_pipeline_swallows_callback_errors() -> None: """Callback failure should not abort the pipeline.""" pipeline = ExtractionPipeline() - async def bad_callback(_msg) -> None: + async def bad_callback(_msg: Any) -> None: raise RuntimeError("boom") pipeline.on_message(bad_callback) diff --git a/tests/unit/services/test_git_diff_base_fallback.py b/tests/unit/services/test_git_diff_base_fallback.py index 4cbb95eb..48140171 100644 --- a/tests/unit/services/test_git_diff_base_fallback.py +++ b/tests/unit/services/test_git_diff_base_fallback.py @@ -12,7 +12,7 @@ from __future__ import annotations from pathlib import Path from typing import Any -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch import pytest from roboco.services.git import GitService @@ -69,8 +69,8 @@ async def test_default_branch_ref_prefers_origin_head() -> None: )() return type("R", (), {"returncode": 1, "stdout": ""})() - svc._run_git = fake_run # type: ignore[method-assign] - ref = await svc._default_branch_ref(Path("/tmp/ws")) + with patch.object(svc, "_run_git", new=fake_run): + ref = await svc._default_branch_ref(Path("/tmp/ws")) assert ref == "origin/main" @@ -84,9 +84,9 @@ async def test_default_branch_ref_fallback_when_no_head() -> None: # symbolic-ref fails; fetches succeed but ref never verifies. return type("R", (), {"returncode": 1, "stdout": ""})() - svc._run_git = fake_run # type: ignore[method-assign] svc._ref_exists = AsyncMock(return_value=False) # type: ignore[method-assign] - ref = await svc._default_branch_ref(Path("/tmp/ws")) + with patch.object(svc, "_run_git", new=fake_run): + ref = await svc._default_branch_ref(Path("/tmp/ws")) assert ref == "origin/master" @@ -125,8 +125,8 @@ async def test_resolve_head_ref_falls_back_to_origin_in_foreign_clone() -> None: # local branch absent; only the remote-tracking ref resolves. return ref == f"origin/{_BR}" - svc._ref_exists = ref_exists # type: ignore[method-assign] - head = await svc._resolve_head_ref(Path("/tmp/ws"), _BR) + with patch.object(svc, "_ref_exists", new=ref_exists): + head = await svc._resolve_head_ref(Path("/tmp/ws"), _BR) assert head == f"origin/{_BR}" @@ -141,9 +141,9 @@ async def test_resolve_head_ref_fetches_branch_before_resolving() -> None: calls.append(args) return type("R", (), {"returncode": 0, "stdout": ""})() - svc._run_git = fake_run # type: ignore[method-assign] svc._ref_exists = AsyncMock(return_value=True) # type: ignore[method-assign] - await svc._resolve_head_ref(Path("/tmp/ws"), _BR) + with patch.object(svc, "_run_git", new=fake_run): + await svc._resolve_head_ref(Path("/tmp/ws"), _BR) assert ["fetch", "origin", _BR] in calls @@ -171,9 +171,8 @@ async def test_diff_targets_origin_head_in_foreign_clone() -> None: captured.append(args) return type("R", (), {"returncode": 0, "stdout": "diff body"})() - svc._run_git = fake_run # type: ignore[method-assign] - - out = await svc.diff(branch_name=_BR) + with patch.object(svc, "_run_git", new=fake_run): + out = await svc.diff(branch_name=_BR) assert out == "diff body" assert captured == [["diff", f"origin/master...origin/{_BR}"]] # #168: the resolved project token is threaded into ref resolution so @@ -206,9 +205,8 @@ async def test_list_changed_files_targets_origin_head_in_foreign_clone() -> None captured.append(args) return type("R", (), {"returncode": 0, "stdout": "README.md\nsrc/app.py\n"})() - svc._run_git = fake_run # type: ignore[method-assign] - - files = await svc.list_changed_files(branch_name=_BR) + with patch.object(svc, "_run_git", new=fake_run): + files = await svc.list_changed_files(branch_name=_BR) assert files == ["README.md", "src/app.py"] assert captured == [["diff", "--name-only", f"origin/master...origin/{_BR}"]] svc._resolve_diff_base.assert_awaited_once_with( @@ -239,8 +237,8 @@ async def test_diff_honours_explicit_base_with_resolved_head() -> None: captured.append(args) return type("R", (), {"returncode": 0, "stdout": ""})() - svc._run_git = fake_run # type: ignore[method-assign] - await svc.diff(branch_name=_BR, base="HEAD~1") + with patch.object(svc, "_run_git", new=fake_run): + await svc.diff(branch_name=_BR, base="HEAD~1") assert captured == [["diff", f"HEAD~1...{_BR}"]] svc._resolve_diff_base.assert_not_awaited() @@ -267,14 +265,14 @@ async def test_resolve_diff_base_refetches_default_branch_with_token() -> None: calls.append((args, kw.get("token"))) return type("R", (), {"returncode": 0, "stdout": ""})() - svc._run_git = fake_run # type: ignore[method-assign] # parent ref never exists → fall back to default branch. svc._ref_exists = AsyncMock(return_value=False) # type: ignore[method-assign] svc._default_branch_ref = AsyncMock( # type: ignore[method-assign] return_value="origin/master" ) - base = await svc._resolve_diff_base(Path("/tmp/ws"), _BR, token="tok") + with patch.object(svc, "_run_git", new=fake_run): + base = await svc._resolve_diff_base(Path("/tmp/ws"), _BR, token="tok") assert base == "origin/master" # The resolved default branch ('master') was fetched, authenticated. assert (["fetch", "origin", "master"], "tok") in calls @@ -293,9 +291,9 @@ async def test_resolve_head_ref_fetch_is_authenticated() -> None: seen.append((args, kw.get("token"))) return type("R", (), {"returncode": 0, "stdout": ""})() - svc._run_git = fake_run # type: ignore[method-assign] svc._ref_exists = AsyncMock(return_value=True) # type: ignore[method-assign] - await svc._resolve_head_ref(Path("/tmp/ws"), _BR, token="tok") + with patch.object(svc, "_run_git", new=fake_run): + await svc._resolve_head_ref(Path("/tmp/ws"), _BR, token="tok") assert (["fetch", "origin", _BR], "tok") in seen diff --git a/tests/unit/services/test_git_pr_update.py b/tests/unit/services/test_git_pr_update.py index 1fe6ff26..453c7fb8 100644 --- a/tests/unit/services/test_git_pr_update.py +++ b/tests/unit/services/test_git_pr_update.py @@ -90,7 +90,7 @@ async def _stub_task_get(svc: GitService, task: object | None) -> None: _bind(svc, "_task_service_for_pr_update", task_service) -def _wire_service(svc: GitService, task: MagicMock) -> None: +def _wire_service(svc: GitService, task: MagicMock) -> MagicMock: """Apply common bindings: workspace, remote parse, token resolution.""" _bind(svc, "get_workspace", AsyncMock(return_value=Path("/tmp/ws"))) _bind(svc, "_parse_github_remote", MagicMock(return_value=("acme", "repo"))) @@ -100,7 +100,7 @@ def _wire_service(svc: GitService, task: MagicMock) -> None: # at the module level. fake_task_service = MagicMock() fake_task_service.get = AsyncMock(return_value=task) - return fake_task_service # type: ignore[return-value] + return fake_task_service @pytest.mark.asyncio diff --git a/tests/unit/services/test_notification.py b/tests/unit/services/test_notification.py index 87304a2a..4fdafa42 100644 --- a/tests/unit/services/test_notification.py +++ b/tests/unit/services/test_notification.py @@ -9,6 +9,10 @@ without spinning up a Postgres + Redis stack. from __future__ import annotations from contextlib import asynccontextmanager +from typing import TYPE_CHECKING, Any, cast + +if TYPE_CHECKING: + from collections.abc import AsyncIterator from unittest.mock import AsyncMock, MagicMock, patch from uuid import UUID, uuid4 @@ -29,7 +33,7 @@ class _FakeDb: self.committed = False self._agent_uuid = agent_uuid - def add(self, obj) -> None: + def add(self, obj: Any) -> None: self.added.append(obj) # The notification row needs an `id` for delivery_service.deliver(). obj.id = uuid4() @@ -40,7 +44,7 @@ class _FakeDb: async def commit(self) -> None: self.committed = True - async def execute(self, *_args, **_kwargs): + async def execute(self, *_args: Any, **_kwargs: Any) -> Any: # Two paths use this: agent slug→UUID resolution and the # notification_delivery service's own DB queries. We return a # MagicMock that supports `scalar_one_or_none()` returning either @@ -56,14 +60,14 @@ class _FakeDb: result.scalars.return_value.all.return_value = [] return result - async def scalar(self, *_args, **_kwargs): + async def scalar(self, *_args: Any, **_kwargs: Any) -> Any: # _create_notification's purpose-dedup lookup runs db.scalar(); model # "no existing duplicate" so creation proceeds. return None @asynccontextmanager -async def _fake_ctx(db: _FakeDb): +async def _fake_ctx(db: _FakeDb) -> AsyncIterator[_FakeDb]: yield db @@ -75,36 +79,36 @@ def svc() -> NotificationService: @pytest.mark.asyncio async def test_resolve_agent_uuid_returns_none_for_blank() -> None: db = _FakeDb() - assert await _resolve_agent_uuid(db, None) is None - assert await _resolve_agent_uuid(db, "") is None + assert await _resolve_agent_uuid(cast("Any", db), None) is None + assert await _resolve_agent_uuid(cast("Any", db), "") is None @pytest.mark.asyncio async def test_resolve_agent_uuid_passes_through_uuid() -> None: aid = uuid4() db = _FakeDb() - assert await _resolve_agent_uuid(db, aid) == aid + assert await _resolve_agent_uuid(cast("Any", db), aid) == aid @pytest.mark.asyncio async def test_resolve_agent_uuid_parses_uuid_string() -> None: aid = uuid4() db = _FakeDb() - assert await _resolve_agent_uuid(db, str(aid)) == aid + assert await _resolve_agent_uuid(cast("Any", db), str(aid)) == aid @pytest.mark.asyncio async def test_resolve_agent_uuid_resolves_slug() -> None: expected = uuid4() db = _FakeDb(agent_uuid=expected) - resolved = await _resolve_agent_uuid(db, "be-dev-1") + resolved = await _resolve_agent_uuid(cast("Any", db), "be-dev-1") assert resolved == expected @pytest.mark.asyncio async def test_resolve_agent_uuid_returns_none_for_unknown_slug() -> None: db = _FakeDb(agent_uuid=None) - assert await _resolve_agent_uuid(db, "ghost") is None + assert await _resolve_agent_uuid(cast("Any", db), "ghost") is None class _PatchDbContext: @@ -114,7 +118,7 @@ class _PatchDbContext: self.db = db delivery_mock = MagicMock() delivery_mock.deliver = AsyncMock(return_value=None) - self._patches = [ + self._patches: list[Any] = [ patch( "roboco.services.notification.get_db_context", lambda: _fake_ctx(db), @@ -129,7 +133,7 @@ class _PatchDbContext: for p in self._patches: p.start() - def __exit__(self, *_args) -> None: + def __exit__(self, *_args: Any) -> None: for p in self._patches: p.stop() @@ -291,7 +295,7 @@ async def test_create_notification_skips_when_no_resolvable_recipients( super().__init__(agent_uuid=aid) self._calls = 0 - async def execute(self, *_args, **_kwargs): + async def execute(self, *_args: Any, **_kwargs: Any) -> Any: self._calls += 1 result = MagicMock() if self._calls == 1: diff --git a/tests/unit/services/test_optimal_grounding.py b/tests/unit/services/test_optimal_grounding.py index 6af37928..880e7f1f 100644 --- a/tests/unit/services/test_optimal_grounding.py +++ b/tests/unit/services/test_optimal_grounding.py @@ -15,6 +15,7 @@ Covers three failure modes that surfaced at runtime: from __future__ import annotations import asyncio +from typing import Any import pytest from roboco.mcp.optimal_server import normalize_index_types @@ -25,6 +26,7 @@ from roboco.services.optimal import ( close_optimal_service, get_optimal_service, ) +from roboco.services.optimal_brain.indexes.base import BaseIndexPlugin # --------------------------------------------------------------------------- # Sub-issue 2: kb_search must not forward the invalid 'docs' alias @@ -39,7 +41,9 @@ def test_normalize_index_types_maps_docs_alias_to_documentation() -> None: """ assert normalize_index_types(["docs"]) == ["documentation"] # Every normalized value must be a constructible IndexType. - for value in normalize_index_types(["docs"]): + normalized = normalize_index_types(["docs"]) + assert normalized is not None + for value in normalized: IndexType(value) @@ -93,7 +97,7 @@ async def test_get_optimal_service_never_returns_uninitialized( monkeypatch.setattr(optimal_module, "OptimalService", _SlowInitService) try: - results: list[OptimalService] = await asyncio.gather( + results: Any = await asyncio.gather( get_optimal_service(), get_optimal_service(), get_optimal_service(), @@ -130,8 +134,18 @@ async def test_indexing_entrypoint_does_not_raise_not_initialized( await close_optimal_service() -class _FakePlugin: +class _FakePlugin(BaseIndexPlugin): """Minimal stand-in so _get_plugin returns without a real plugin.""" + @property + def index_type(self) -> IndexType: + return IndexType.DOCUMENTATION + + def prepare_metadata(self, content: str, **kwargs: Any) -> dict[str, Any]: + return {} + + def build_source_uri(self, doc_id: str | None = None, **kwargs: Any) -> str | None: + return None + async def close(self) -> None: # pragma: no cover - never awaited here return None diff --git a/tests/unit/services/test_prompter.py b/tests/unit/services/test_prompter.py index 840f799e..bedf0103 100644 --- a/tests/unit/services/test_prompter.py +++ b/tests/unit/services/test_prompter.py @@ -8,7 +8,7 @@ conftest fixtures. from __future__ import annotations -from typing import Any +from typing import Any, cast from uuid import UUID, uuid4 import pytest @@ -259,9 +259,9 @@ async def test_assignee_is_board_distinguishes_roles(db_session: Any) -> None: db_session.add_all([po, hom, dev]) await db_session.flush() - assert await service._assignee_is_board(po.id) is True - assert await service._assignee_is_board(hom.id) is True - assert await service._assignee_is_board(dev.id) is False + assert await service._assignee_is_board(cast("UUID", po.id)) is True + assert await service._assignee_is_board(cast("UUID", hom.id)) is True + assert await service._assignee_is_board(cast("UUID", dev.id)) is False # Unknown id is not a board agent — defensive, must not raise. assert await service._assignee_is_board(uuid4()) is False diff --git a/tests/unit/services/test_propagate_sessions_to_subtask.py b/tests/unit/services/test_propagate_sessions_to_subtask.py index 456ae7ac..8750acaa 100644 --- a/tests/unit/services/test_propagate_sessions_to_subtask.py +++ b/tests/unit/services/test_propagate_sessions_to_subtask.py @@ -8,7 +8,7 @@ through the DB. Integration coverage (real DB, real linking) lives in from __future__ import annotations from typing import Any -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 import pytest @@ -45,13 +45,12 @@ async def test_propagate_links_every_parent_session_to_subtask() -> None: link.task_id = kwargs["task_id"] return link - svc.link_session_to_task = fake_link # type: ignore[method-assign] - parent_id = uuid4() subtask_id = uuid4() added_by = uuid4() + with patch.object(svc, "link_session_to_task", new=fake_link): + out = await svc.propagate_sessions_to_subtask(parent_id, subtask_id, added_by) expected_sessions = {parent_session, review_session} - out = await svc.propagate_sessions_to_subtask(parent_id, subtask_id, added_by) assert len(out) == len(expected_sessions) assert {c["session_id"] for c in calls} == expected_sessions # Every propagated link must be non-primary — primary is the subtask's @@ -91,8 +90,7 @@ async def test_propagate_unknown_relationship_type_defaults_to_discussion() -> N calls.append(kwargs) return MagicMock() - svc.link_session_to_task = fake_link # type: ignore[method-assign] - - await svc.propagate_sessions_to_subtask(uuid4(), uuid4(), uuid4()) + with patch.object(svc, "link_session_to_task", new=fake_link): + await svc.propagate_sessions_to_subtask(uuid4(), uuid4(), uuid4()) assert len(calls) == 1 assert calls[0]["relationship_type"] == SessionTaskRelationshipType.DISCUSSION diff --git a/tests/unit/services/test_rate_limit_tracker.py b/tests/unit/services/test_rate_limit_tracker.py index 582a9f54..83531cbf 100644 --- a/tests/unit/services/test_rate_limit_tracker.py +++ b/tests/unit/services/test_rate_limit_tracker.py @@ -60,7 +60,7 @@ def _make_tracker( """Build a tracker with an injected mock Redis client.""" tracker = RateLimitStateTracker(provider=provider, redis_url="redis://unused") if redis_mock is not None: - tracker._redis = redis_mock # type: ignore[assignment] + tracker._redis = redis_mock return tracker @@ -178,7 +178,7 @@ class TestStatePersistsAcrossReconnection: tracker_b = RateLimitStateTracker( provider="anthropic", redis_url="redis://unused" ) - tracker_b._redis = mock_b # type: ignore[assignment] + tracker_b._redis = mock_b assert await tracker_b.is_rate_limited() is True @@ -194,7 +194,7 @@ class TestStatePersistsAcrossReconnection: tracker_b = RateLimitStateTracker( provider="anthropic", redis_url="redis://unused" ) - tracker_b._redis = mock_b # type: ignore[assignment] + tracker_b._redis = mock_b state = await tracker_b.get_state() assert state["rate_limited"] is True @@ -213,7 +213,7 @@ class TestStatePersistsAcrossReconnection: tracker_b = RateLimitStateTracker( provider="anthropic", redis_url="redis://unused" ) - tracker_b._redis = mock_b # type: ignore[assignment] + tracker_b._redis = mock_b # Write clear via tracker_a await tracker_a.clear() diff --git a/tests/unit/services/test_record_plan_progress.py b/tests/unit/services/test_record_plan_progress.py index 76660e6d..3bf1e4cc 100644 --- a/tests/unit/services/test_record_plan_progress.py +++ b/tests/unit/services/test_record_plan_progress.py @@ -69,6 +69,7 @@ async def test_marking_step_by_one_based_order() -> None: ) svc = _svc_with_task(task) res = await svc.record_plan_progress(task.id, uuid4(), "did B", plan_step="2") + assert res is not None assert res["step_resolved"] is True assert res["percentage"] == _PCT_FULL # both now complete assert task.plan["sub_tasks"][1]["completed"] is True @@ -79,6 +80,7 @@ async def test_unknown_step_is_not_resolved_and_lists_valid() -> None: task = _task_with_plan([{"id": "s1", "title": "A", "completed": False}]) svc = _svc_with_task(task) res = await svc.record_plan_progress(task.id, uuid4(), "?", plan_step="nope") + assert res is not None assert res["step_resolved"] is False assert res["valid_steps"] == ["s1"] # Nothing marked; % still derived from (unchanged) checklist = 0. @@ -96,6 +98,7 @@ async def test_narrative_entry_carries_current_derived_pct() -> None: ) svc = _svc_with_task(task) res = await svc.record_plan_progress(task.id, uuid4(), "midway note") + assert res is not None assert res["step_resolved"] is None # no plan_step requested assert res["percentage"] == _PCT_HALF # current checklist state assert task.progress_updates[-1]["message"] == "midway note" @@ -108,6 +111,7 @@ async def test_no_checklist_falls_back_to_supplied_percentage() -> None: res = await svc.record_plan_progress( task.id, uuid4(), "legacy", fallback_percentage=_PCT_FALLBACK ) + assert res is not None assert res["percentage"] == _PCT_FALLBACK assert res["valid_steps"] == [] assert task.progress_updates[-1]["percentage"] == _PCT_FALLBACK diff --git a/tests/unit/services/test_task.py b/tests/unit/services/test_task.py index 12b18438..047d817f 100644 --- a/tests/unit/services/test_task.py +++ b/tests/unit/services/test_task.py @@ -9,6 +9,7 @@ from __future__ import annotations from datetime import datetime from types import SimpleNamespace +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 @@ -746,9 +747,10 @@ async def test_update_skips_none_to_protect_partial_callers() -> None: """ task = SimpleNamespace(title="original", acceptance_criteria=["keep me"]) svc = TaskService(MagicMock(flush=AsyncMock())) - svc.get = AsyncMock(return_value=task) - - result = await svc.update(uuid4(), title="updated", acceptance_criteria=None) + with patch.object(svc, "get", AsyncMock(return_value=task)): + result: Any = await svc.update( + uuid4(), title="updated", acceptance_criteria=None + ) assert result is task assert task.title == "updated" # explicit, non-None value is applied diff --git a/tests/unit/test_bootstrap.py b/tests/unit/test_bootstrap.py index a6e4d9f6..0001a130 100644 --- a/tests/unit/test_bootstrap.py +++ b/tests/unit/test_bootstrap.py @@ -12,6 +12,7 @@ from __future__ import annotations import asyncio import runpy from http import HTTPStatus +from typing import Any from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -98,7 +99,7 @@ async def test_wait_for_api_ready_keeps_polling_on_non_200() -> None: # --------------------------------------------------------------------------- -def _make_main_patches(*, raise_on_spawn: bool = False): +def _make_main_patches(*, raise_on_spawn: bool = False) -> tuple[Any, Any, Any]: """Bundle the heavy IO mocks main() needs. Returns a context manager and the orchestrator mock so the test can