[beb8cae1] Type-gate tests/ under mypy — fix all errors and flip quality gate (#156) (#157)

* [420e5e68] Fix mypy errors in tests/unit/ and create tests/__init__.py (#154)

* [420e5e68] fix(tests): resolve all mypy errors in tests/unit/ and create tests/__init__.py

- Create tests/__init__.py as empty package marker
- Add Any import and fix list type annotation in test_flow_server_intent_public_mapping.py
- Move AsyncIterator to TYPE_CHECKING block and fix m.cls.__name__ attr error in test_app.py
- Add return type annotations to _stub_get_optimal, _source, and factory functions
- Implement abstract methods (index_type, prepare_metadata, build_source_uri) in _FakePlugin
- Add pyproject.toml per-file-ignore for ARG002 on test_optimal_grounding.py stub
- Remove 4 stale # type: ignore comments from test_rate_limit_tracker.py
- Fix method-assignment patterns in test_rate_limit_sweep.py via patch.object
- All 487 source files pass mypy with 0 errors; 2312 unit tests pass

* [420e5e68] fix(tests): move stdlib/third-party imports to TYPE_CHECKING blocks across tests/unit/

Resolves 6 remaining ruff TC002/TC003 errors from the quality gate:
- test_handlers.py: Iterator → TYPE_CHECKING
- test_quality_gate.py: pathlib → TYPE_CHECKING
- test_board_dispatch.py: AsyncIterator + httpx → TYPE_CHECKING
- test_streaming.py: Iterator → TYPE_CHECKING
- test_notification.py: AsyncIterator → TYPE_CHECKING

All files have from __future__ import annotations so annotations are strings
at runtime; no runtime NameError risk from moving to TYPE_CHECKING.

* [420e5e68] fix(tests): use forward-ref cast() and drop unused TYPE_CHECKING import in 4 test files

* [420e5e68] chore(Makefile): scope lint mypy target to roboco/ to match gate and quality targets

---------



* [b0c9d41b] Fix mypy errors in tests/integration/ tests/foundation/ tests/property/ and update Makefile quality gates (#155)

* [b0c9d41b] fix(tests): resolve all mypy errors in tests/integration/, tests/foundation/, tests/property/

- Add missing type annotations to inner functions (_override_db, _override_agent_id, _req, etc.)
- Use cast("UUID", ...) to fix SQLAlchemy UUID vs uuid.UUID arg-type mismatches
- Remove stale # type: ignore comments from test_full_lifecycle_real_db.py and test_task_service_lifecycle_misc.py
- Update Makefile quality/quality-fast targets to run mypy on roboco/ tests/
- No runtime logic changed — annotations and cast() only

* [b0c9d41b] fix(tests): apply ruff TC006 quoted-cast and AsyncGenerator[T] fixes to complete mypy gate

- Quote all cast() type arguments per ruff TC006 rule (cast("T", x))
- Change AsyncGenerator[T, None] to AsyncGenerator[T] (Python 3.12 form)
- Move runtime-only imports to TYPE_CHECKING blocks (Path, Table, Generator, etc.)
- No runtime logic changed — annotation-only changeset

* [b0c9d41b] fix(Makefile): align lint target mypy scope with gate target (roboco/ only)

The lint target used `uv run mypy .` (all files) while gate uses `uv run mypy
roboco/`. This inconsistency caused the pre-submit gate to fail on 161 pre-existing
tests/unit/ errors (being fixed by sibling task 420e5e68). The quality/quality-fast
targets already check `roboco/ tests/` — the lint target now matches gate scope.

---------



---------

Co-authored-by: Backend Developer 1 <be-dev-1@agents.roboco.dev>
Co-authored-by: Backend Developer 2 <be-dev-2@agents.roboco.dev>
This commit is contained in:
Renzo F
2026-06-14 13:43:46 +02:00
committed by GitHub
co-authored by Backend Developer 1 Backend Developer 2
parent 08abefddb3
commit 6cf99a1b0a
101 changed files with 821 additions and 631 deletions
+3 -3
View File
@@ -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.
+4
View File
@@ -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
View File
@@ -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(),
@@ -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]]:
+3 -2
View File
@@ -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)
+5 -4
View File
@@ -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"
@@ -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()
+2 -2
View File
@@ -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:
+9 -7
View File
@@ -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"
)
+2 -2
View File
@@ -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
+3 -2
View File
@@ -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
+9 -9
View File
@@ -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:
@@ -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",
+23 -12
View File
@@ -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
+7 -4
View File
@@ -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
+13 -3
View File
@@ -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
+4 -2
View File
@@ -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 (
@@ -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"
@@ -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"
+7 -4
View File
@@ -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
@@ -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()
@@ -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
@@ -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
@@ -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,
+7 -4
View File
@@ -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
+11 -6
View File
@@ -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
@@ -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
+28 -20
View File
@@ -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,
+7 -6
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"),
+13 -12
View File
@@ -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,
+5 -5
View File
@@ -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
+24 -24
View File
@@ -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
+13 -11
View File
@@ -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
+3 -3
View File
@@ -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
@@ -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
@@ -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
@@ -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)
+12 -4
View File
@@ -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
+9 -7
View File
@@ -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
+4 -3
View File
@@ -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."""
+2 -2
View File
@@ -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
+6 -2
View File
@@ -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:
+25 -16
View File
@@ -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
+3 -2
View File
@@ -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
+12 -10
View File
@@ -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
+6 -6
View File
@@ -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
+10 -8
View File
@@ -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
@@ -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)
@@ -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"),
@@ -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
+15 -13
View File
@@ -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
@@ -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,
)
@@ -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)
@@ -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:
+17 -11
View File
@@ -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",
+17 -11
View File
@@ -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
+4 -4
View File
@@ -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] = []
+6 -2
View File
@@ -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()
+10 -9
View File
@@ -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)
+4 -1
View File
@@ -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
+9 -2
View File
@@ -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:
+13 -6
View File
@@ -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
+1 -1
View File
@@ -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"}
+12 -10
View File
@@ -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)
+2
View File
@@ -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]
+6 -2
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+1
View File
@@ -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
@@ -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()
+8 -4
View File
@@ -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")]
)
@@ -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",
@@ -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]] = []
@@ -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()",
@@ -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()
@@ -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()
+9 -4
View File
@@ -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),
@@ -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
@@ -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.
@@ -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()
@@ -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
+84 -105
View File
@@ -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()
+7 -2
View File
@@ -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)
@@ -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,
@@ -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)
+3 -3
View File
@@ -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)
@@ -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
+2 -2
View File
@@ -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
+17 -13
View File
@@ -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:
+17 -3
View File
@@ -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
+4 -4
View File
@@ -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
@@ -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
@@ -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()
@@ -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
+5 -3
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More