mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[chore] remove all remaining type:ignore suppressions from tests/
Converts 115 `# type: ignore[...]` suppressions across 23 test files to no-suppression patterns (helper-return widening to Any, local Any aliases, cc:Any aliases, cast at narrow call sites, typed fixtures) so the hard no-type:ignore convention holds across tests/. No test logic or assertions changed — only mock-wiring mechanics and type annotations. Gate: ruff check tests/ clean; mypy tests/ (538 files) clean; 176 changed-file tests pass. Zero real suppressions remain (the 7 grep hits are 3 hygiene- checker string-literal test inputs and 4 prose mentions in comments).
This commit is contained in:
@@ -8,7 +8,7 @@ or committing/pushing. Fail-safe: a null/failing command returns False.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -36,9 +36,9 @@ def _make_read_clone(tmp_path: Path) -> Path:
|
|||||||
return repo
|
return repo
|
||||||
|
|
||||||
|
|
||||||
def _svc(read_clone: Path) -> WorkspaceService:
|
def _svc(read_clone: Path) -> Any:
|
||||||
svc = WorkspaceService.__new__(WorkspaceService)
|
svc: Any = WorkspaceService.__new__(WorkspaceService)
|
||||||
svc.ensure_read_clone = AsyncMock(return_value=read_clone) # type: ignore[method-assign]
|
svc.ensure_read_clone = AsyncMock(return_value=read_clone)
|
||||||
return svc
|
return svc
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,17 @@ class. The migration drops the unused one with a safety check.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_no_column_uses_role_type(db_session) -> None: # type: ignore[no-untyped-def]
|
async def test_no_column_uses_role_type(db_session: AsyncSession) -> None:
|
||||||
"""Before dropping, confirm no column actually uses the `role` type.
|
"""Before dropping, confirm no column actually uses the `role` type.
|
||||||
|
|
||||||
If this ever fails it means a column was added that references the
|
If this ever fails it means a column was added that references the
|
||||||
@@ -33,7 +38,7 @@ async def test_no_column_uses_role_type(db_session) -> None: # type: ignore[no-
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_role_enum_dropped_after_upgrade(db_session) -> None: # type: ignore[no-untyped-def]
|
async def test_role_enum_dropped_after_upgrade(db_session: AsyncSession) -> None:
|
||||||
"""After migration 013 runs, only `agentrole` remains; `role` is gone."""
|
"""After migration 013 runs, only `agentrole` remains; `role` is gone."""
|
||||||
# This test runs against a db where migrations have been applied to head.
|
# This test runs against a db where migrations have been applied to head.
|
||||||
# The conftest fixture should handle that — verify by reading the
|
# The conftest fixture should handle that — verify by reading the
|
||||||
|
|||||||
@@ -7,12 +7,17 @@ tracking, RAG context). Only pm_approvals is truly orphaned.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pm_approvals_dropped(db_session) -> None: # type: ignore[no-untyped-def]
|
async def test_pm_approvals_dropped(db_session: AsyncSession) -> None:
|
||||||
"""pm_approvals column is gone from the tasks table."""
|
"""pm_approvals column is gone from the tasks table."""
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
text(
|
text(
|
||||||
@@ -25,7 +30,9 @@ async def test_pm_approvals_dropped(db_session) -> None: # type: ignore[no-unty
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_quick_context_and_proactive_context_remain(db_session) -> None: # type: ignore[no-untyped-def]
|
async def test_quick_context_and_proactive_context_remain(
|
||||||
|
db_session: AsyncSession,
|
||||||
|
) -> None:
|
||||||
"""quick_context and proactive_context MUST remain — they're actively used."""
|
"""quick_context and proactive_context MUST remain — they're actively used."""
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
text(
|
text(
|
||||||
|
|||||||
@@ -10,12 +10,17 @@ discipline); these assertions guard the resulting schema shape.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_revision_count_defaults_to_zero(db_session) -> None: # type: ignore[no-untyped-def]
|
async def test_revision_count_defaults_to_zero(db_session: AsyncSession) -> None:
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
text(
|
text(
|
||||||
"SELECT column_default, is_nullable "
|
"SELECT column_default, is_nullable "
|
||||||
@@ -30,7 +35,7 @@ async def test_revision_count_defaults_to_zero(db_session) -> None: # type: ign
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_audit_log_query_index_exists(db_session) -> None: # type: ignore[no-untyped-def]
|
async def test_audit_log_query_index_exists(db_session: AsyncSession) -> None:
|
||||||
result = await db_session.execute(
|
result = await db_session.execute(
|
||||||
text(
|
text(
|
||||||
"SELECT indexname FROM pg_indexes "
|
"SELECT indexname FROM pg_indexes "
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
@@ -212,7 +212,8 @@ def test_parse_uuid_list_with_valid() -> None:
|
|||||||
|
|
||||||
def test_parse_uuid_list_skips_empty_strings() -> None:
|
def test_parse_uuid_list_skips_empty_strings() -> None:
|
||||||
raw = uuid4()
|
raw = uuid4()
|
||||||
out = _parse_uuid_list([str(raw), "", None]) # type: ignore[list-item]
|
vals: list[Any] = [str(raw), "", None]
|
||||||
|
out = _parse_uuid_list(cast("list[str]", vals))
|
||||||
assert raw in out
|
assert raw in out
|
||||||
assert len(out) == 1
|
assert len(out) == 1
|
||||||
|
|
||||||
@@ -278,7 +279,7 @@ def test_task_update_sequence_rejects_negative() -> None:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _stub_task(*, with_project: bool = False) -> SimpleNamespace:
|
def _stub_task(*, with_project: bool = False) -> Any:
|
||||||
"""Build a TaskTable stand-in that matches task_to_response's reads."""
|
"""Build a TaskTable stand-in that matches task_to_response's reads."""
|
||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
id=uuid4(),
|
id=uuid4(),
|
||||||
@@ -337,7 +338,7 @@ def test_task_to_response_omits_slug_when_project_not_loaded() -> None:
|
|||||||
fake_inspector = MagicMock()
|
fake_inspector = MagicMock()
|
||||||
fake_inspector.unloaded = {"project"}
|
fake_inspector.unloaded = {"project"}
|
||||||
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
||||||
resp = task_to_response(stub) # type: ignore[arg-type]
|
resp = task_to_response(stub)
|
||||||
assert resp.project_slug is None
|
assert resp.project_slug is None
|
||||||
|
|
||||||
|
|
||||||
@@ -346,7 +347,7 @@ def test_task_to_response_includes_slug_when_project_loaded() -> None:
|
|||||||
fake_inspector = MagicMock()
|
fake_inspector = MagicMock()
|
||||||
fake_inspector.unloaded = set() # project IS loaded
|
fake_inspector.unloaded = set() # project IS loaded
|
||||||
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
||||||
resp = task_to_response(stub) # type: ignore[arg-type]
|
resp = task_to_response(stub)
|
||||||
assert resp.project_slug == "proj-1"
|
assert resp.project_slug == "proj-1"
|
||||||
|
|
||||||
|
|
||||||
@@ -364,7 +365,7 @@ def test_task_to_response_serializes_cell_projects_when_loaded() -> None:
|
|||||||
fake_inspector = MagicMock()
|
fake_inspector = MagicMock()
|
||||||
fake_inspector.unloaded = set() # cell_projects IS loaded
|
fake_inspector.unloaded = set() # cell_projects IS loaded
|
||||||
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
||||||
resp = task_to_response(stub) # type: ignore[arg-type]
|
resp = task_to_response(stub)
|
||||||
assert resp.cell_projects == [
|
assert resp.cell_projects == [
|
||||||
ProductCellMapping(team=Team.BACKEND, project_id=be_proj),
|
ProductCellMapping(team=Team.BACKEND, project_id=be_proj),
|
||||||
ProductCellMapping(team=Team.FRONTEND, project_id=fe_proj),
|
ProductCellMapping(team=Team.FRONTEND, project_id=fe_proj),
|
||||||
@@ -378,7 +379,7 @@ def test_task_to_response_omits_cell_projects_when_unloaded() -> None:
|
|||||||
fake_inspector = MagicMock()
|
fake_inspector = MagicMock()
|
||||||
fake_inspector.unloaded = {"cell_projects"}
|
fake_inspector.unloaded = {"cell_projects"}
|
||||||
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
||||||
resp = task_to_response(stub) # type: ignore[arg-type]
|
resp = task_to_response(stub)
|
||||||
assert resp.cell_projects == []
|
assert resp.cell_projects == []
|
||||||
|
|
||||||
|
|
||||||
@@ -393,7 +394,7 @@ def test_task_to_response_serializes_all_note_sections() -> None:
|
|||||||
fake_inspector = MagicMock()
|
fake_inspector = MagicMock()
|
||||||
fake_inspector.unloaded = {"project"}
|
fake_inspector.unloaded = {"project"}
|
||||||
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
||||||
resp = task_to_response(stub) # type: ignore[arg-type]
|
resp = task_to_response(stub)
|
||||||
assert resp.pr_reviewer_notes == "## Findings\n- looks good"
|
assert resp.pr_reviewer_notes == "## Findings\n- looks good"
|
||||||
assert resp.doc_notes == "Updated the README"
|
assert resp.doc_notes == "Updated the README"
|
||||||
assert resp.notes_structured == {"pr_review": {"verdict": "passed"}}
|
assert resp.notes_structured == {"pr_review": {"verdict": "passed"}}
|
||||||
@@ -404,7 +405,7 @@ def test_task_list_to_response_returns_list() -> None:
|
|||||||
fake_inspector = MagicMock()
|
fake_inspector = MagicMock()
|
||||||
fake_inspector.unloaded = {"project"}
|
fake_inspector.unloaded = {"project"}
|
||||||
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
||||||
out = task_list_to_response(stubs) # type: ignore[arg-type]
|
out = task_list_to_response(stubs)
|
||||||
assert len(out) == len(stubs)
|
assert len(out) == len(stubs)
|
||||||
|
|
||||||
|
|
||||||
@@ -418,7 +419,7 @@ def _stub_response() -> Any:
|
|||||||
fake_inspector = MagicMock()
|
fake_inspector = MagicMock()
|
||||||
fake_inspector.unloaded = {"project"}
|
fake_inspector.unloaded = {"project"}
|
||||||
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
with patch("roboco.api.schemas.tasks.sa_inspect", return_value=fake_inspector):
|
||||||
resp = task_to_response(_stub_task()) # type: ignore[arg-type]
|
resp = task_to_response(_stub_task())
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import pytest
|
|||||||
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
|
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
|
||||||
|
|
||||||
|
|
||||||
def _make_choreographer(git: AsyncMock) -> Choreographer:
|
def _make_choreographer(git: AsyncMock) -> Any:
|
||||||
base: dict[str, Any] = {
|
base: dict[str, Any] = {
|
||||||
"task": AsyncMock(),
|
"task": AsyncMock(),
|
||||||
"work_session": AsyncMock(),
|
"work_session": AsyncMock(),
|
||||||
@@ -25,9 +25,9 @@ def _make_choreographer(git: AsyncMock) -> Choreographer:
|
|||||||
"audit": AsyncMock(),
|
"audit": AsyncMock(),
|
||||||
"evidence_repo": AsyncMock(),
|
"evidence_repo": AsyncMock(),
|
||||||
}
|
}
|
||||||
c = Choreographer(ChoreographerDeps(**base))
|
c: Any = Choreographer(ChoreographerDeps(**base))
|
||||||
# _project_slug_for hits the project service; stub it for the unit.
|
# _project_slug_for hits the project service; stub it for the unit.
|
||||||
c._project_slug_for = AsyncMock(return_value="proj") # type: ignore[method-assign]
|
c._project_slug_for = AsyncMock(return_value="proj")
|
||||||
return c
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from roboco.foundation.policy import lifecycle as spec_module
|
|||||||
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
|
from roboco.services.gateway.choreographer import Choreographer, ChoreographerDeps
|
||||||
|
|
||||||
|
|
||||||
def _make_choreographer() -> Choreographer:
|
def _make_choreographer() -> Any:
|
||||||
base: dict[str, Any] = {
|
base: dict[str, Any] = {
|
||||||
"task": AsyncMock(),
|
"task": AsyncMock(),
|
||||||
"work_session": AsyncMock(),
|
"work_session": AsyncMock(),
|
||||||
@@ -45,12 +45,12 @@ def _make_choreographer() -> Choreographer:
|
|||||||
return Choreographer(ChoreographerDeps(**base))
|
return Choreographer(ChoreographerDeps(**base))
|
||||||
|
|
||||||
|
|
||||||
def _stub_post_path(c: Choreographer, *, reviewer_id: Any, t: Any) -> None:
|
def _stub_post_path(c: Any, *, reviewer_id: Any, t: Any) -> None:
|
||||||
"""Drive ``post_pr_review`` past preflight + the verdict-consistency gate so
|
"""Drive ``post_pr_review`` past preflight + the verdict-consistency gate so
|
||||||
the hand-format guard is the thing under test. The runner / side-effects are
|
the hand-format guard is the thing under test. The runner / side-effects are
|
||||||
stubbed so a passing case does not hit GitHub or the DB transition."""
|
stubbed so a passing case does not hit GitHub or the DB transition."""
|
||||||
agent = MagicMock(role="pr_reviewer", slug="be-pr-reviewer")
|
agent = MagicMock(role="pr_reviewer", slug="be-pr-reviewer")
|
||||||
c._post_pr_review_preflight = AsyncMock( # type: ignore[method-assign]
|
c._post_pr_review_preflight = AsyncMock(
|
||||||
return_value=(
|
return_value=(
|
||||||
agent,
|
agent,
|
||||||
"pr_reviewer",
|
"pr_reviewer",
|
||||||
@@ -58,13 +58,13 @@ def _stub_post_path(c: Choreographer, *, reviewer_id: Any, t: Any) -> None:
|
|||||||
spec_module.Context(actor_id=reviewer_id),
|
spec_module.Context(actor_id=reviewer_id),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
c._verdict_consistency_gate = AsyncMock(return_value=None) # type: ignore[method-assign]
|
c._verdict_consistency_gate = AsyncMock(return_value=None)
|
||||||
c._project_slug_for = AsyncMock(return_value="proj") # type: ignore[method-assign]
|
c._project_slug_for = AsyncMock(return_value="proj")
|
||||||
c._resolve_post_body = MagicMock(return_value="generated body") # type: ignore[method-assign]
|
c._resolve_post_body = MagicMock(return_value="generated body")
|
||||||
runner = MagicMock()
|
runner = MagicMock()
|
||||||
runner.run_intent = AsyncMock(return_value=t)
|
runner.run_intent = AsyncMock(return_value=t)
|
||||||
c._verb_runner = MagicMock(return_value=runner) # type: ignore[method-assign]
|
c._verb_runner = MagicMock(return_value=runner)
|
||||||
c._post_review_side_effects = AsyncMock() # type: ignore[method-assign]
|
c._post_review_side_effects = AsyncMock()
|
||||||
|
|
||||||
|
|
||||||
def _task() -> Any:
|
def _task() -> Any:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ safety properties the Grok provider must hold:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -303,7 +304,8 @@ async def test_claude_spawn_delegates_to_host() -> None:
|
|||||||
|
|
||||||
async def test_claude_spawn_wraps_host_error() -> None:
|
async def test_claude_spawn_wraps_host_error() -> None:
|
||||||
host = _FakeHost()
|
host = _FakeHost()
|
||||||
host._spawn_container = AsyncMock(side_effect=RuntimeError("docker down")) # type: ignore[method-assign]
|
cc: Any = host
|
||||||
|
cc._spawn_container = AsyncMock(side_effect=RuntimeError("docker down"))
|
||||||
provider = ClaudeCodeProvider(host)
|
provider = ClaudeCodeProvider(host)
|
||||||
with pytest.raises(ProviderError, match="docker down"):
|
with pytest.raises(ProviderError, match="docker down"):
|
||||||
await provider.spawn(_config())
|
await provider.spawn(_config())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -29,7 +30,7 @@ _DO_TEST_MANIFEST = {
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def do_module(monkeypatch): # type: ignore[no-untyped-def]
|
def do_module(monkeypatch: pytest.MonkeyPatch) -> Any:
|
||||||
monkeypatch.setenv("ROBOCO_AGENT_ID", "00000000-0000-0000-0000-000000000001")
|
monkeypatch.setenv("ROBOCO_AGENT_ID", "00000000-0000-0000-0000-000000000001")
|
||||||
monkeypatch.setenv("ROBOCO_AGENT_ROLE", "developer")
|
monkeypatch.setenv("ROBOCO_AGENT_ROLE", "developer")
|
||||||
monkeypatch.setenv("ROBOCO_ORCHESTRATOR_URL", "http://test-orchestrator:8000")
|
monkeypatch.setenv("ROBOCO_ORCHESTRATOR_URL", "http://test-orchestrator:8000")
|
||||||
@@ -44,7 +45,7 @@ def do_module(monkeypatch): # type: ignore[no-untyped-def]
|
|||||||
return srv
|
return srv
|
||||||
|
|
||||||
|
|
||||||
def test_commit_posts_message_and_files(do_module): # type: ignore[no-untyped-def]
|
def test_commit_posts_message_and_files(do_module: Any) -> None:
|
||||||
fake_client = MagicMock()
|
fake_client = MagicMock()
|
||||||
fake_client.__enter__.return_value = fake_client
|
fake_client.__enter__.return_value = fake_client
|
||||||
fake_response = MagicMock()
|
fake_response = MagicMock()
|
||||||
@@ -60,7 +61,7 @@ def test_commit_posts_message_and_files(do_module): # type: ignore[no-untyped-d
|
|||||||
assert kwargs["json"] == {"message": "feat(api): add /healthz", "files": ["foo.py"]}
|
assert kwargs["json"] == {"message": "feat(api): add /healthz", "files": ["foo.py"]}
|
||||||
|
|
||||||
|
|
||||||
def test_note_default_scope_note(do_module): # type: ignore[no-untyped-def]
|
def test_note_default_scope_note(do_module: Any) -> None:
|
||||||
fake_client = MagicMock()
|
fake_client = MagicMock()
|
||||||
fake_client.__enter__.return_value = fake_client
|
fake_client.__enter__.return_value = fake_client
|
||||||
fake_response = MagicMock()
|
fake_response = MagicMock()
|
||||||
@@ -74,7 +75,7 @@ def test_note_default_scope_note(do_module): # type: ignore[no-untyped-def]
|
|||||||
assert kwargs["json"]["scope"] == "note"
|
assert kwargs["json"]["scope"] == "note"
|
||||||
|
|
||||||
|
|
||||||
def test_note_with_scope_reflect(do_module): # type: ignore[no-untyped-def]
|
def test_note_with_scope_reflect(do_module: Any) -> None:
|
||||||
fake_client = MagicMock()
|
fake_client = MagicMock()
|
||||||
fake_client.__enter__.return_value = fake_client
|
fake_client.__enter__.return_value = fake_client
|
||||||
fake_response = MagicMock()
|
fake_response = MagicMock()
|
||||||
@@ -88,7 +89,7 @@ def test_note_with_scope_reflect(do_module): # type: ignore[no-untyped-def]
|
|||||||
assert kwargs["json"]["scope"] == "reflect"
|
assert kwargs["json"]["scope"] == "reflect"
|
||||||
|
|
||||||
|
|
||||||
def test_say_posts_channel_and_text(do_module): # type: ignore[no-untyped-def]
|
def test_say_posts_channel_and_text(do_module: Any) -> None:
|
||||||
fake_client = MagicMock()
|
fake_client = MagicMock()
|
||||||
fake_client.__enter__.return_value = fake_client
|
fake_client.__enter__.return_value = fake_client
|
||||||
fake_response = MagicMock()
|
fake_response = MagicMock()
|
||||||
@@ -106,7 +107,7 @@ def test_say_posts_channel_and_text(do_module): # type: ignore[no-untyped-def]
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_dm_posts_all_fields(do_module): # type: ignore[no-untyped-def]
|
def test_dm_posts_all_fields(do_module: Any) -> None:
|
||||||
fake_client = MagicMock()
|
fake_client = MagicMock()
|
||||||
fake_client.__enter__.return_value = fake_client
|
fake_client.__enter__.return_value = fake_client
|
||||||
fake_response = MagicMock()
|
fake_response = MagicMock()
|
||||||
@@ -125,7 +126,7 @@ def test_dm_posts_all_fields(do_module): # type: ignore[no-untyped-def]
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_evidence_posts_task_id(do_module): # type: ignore[no-untyped-def]
|
def test_evidence_posts_task_id(do_module: Any) -> None:
|
||||||
fake_client = MagicMock()
|
fake_client = MagicMock()
|
||||||
fake_client.__enter__.return_value = fake_client
|
fake_client.__enter__.return_value = fake_client
|
||||||
fake_response = MagicMock()
|
fake_response = MagicMock()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ properties, factory functions, lookup helpers, and __post_init__ branches.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, cast
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -65,7 +66,8 @@ def test_agent_instance_post_init_assigns_uuid_when_falsy() -> None:
|
|||||||
# Forcing an empty UUID(int=0) is falsy → the post_init triggers re-assign.
|
# Forcing an empty UUID(int=0) is falsy → the post_init triggers re-assign.
|
||||||
inst = AgentInstance.__new__(AgentInstance)
|
inst = AgentInstance.__new__(AgentInstance)
|
||||||
# Fill required dataclass fields explicitly so __post_init__ runs cleanly.
|
# Fill required dataclass fields explicitly so __post_init__ runs cleanly.
|
||||||
inst.id = None # type: ignore[assignment]
|
cc: Any = inst
|
||||||
|
cc.id = None
|
||||||
inst.agent_id = "be-dev-1"
|
inst.agent_id = "be-dev-1"
|
||||||
inst.state = OrchestratorAgentState.OFFLINE
|
inst.state = OrchestratorAgentState.OFFLINE
|
||||||
inst.container_id = None
|
inst.container_id = None
|
||||||
@@ -185,7 +187,7 @@ def test_a2a_state_to_task_status_unknown_returns_pending() -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
fake = _FakeState()
|
fake = _FakeState()
|
||||||
assert a2a_state_to_task_status(fake) == "pending" # type: ignore[arg-type]
|
assert a2a_state_to_task_status(cast("Any", fake)) == "pending"
|
||||||
|
|
||||||
|
|
||||||
def test_a2a_state_to_task_status_known() -> None:
|
def test_a2a_state_to_task_status_known() -> None:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from roboco.config import settings
|
|||||||
from roboco.runtime.orchestrator import AgentOrchestrator
|
from roboco.runtime.orchestrator import AgentOrchestrator
|
||||||
|
|
||||||
|
|
||||||
def _orch() -> AgentOrchestrator:
|
def _orch() -> Any:
|
||||||
return AgentOrchestrator.__new__(AgentOrchestrator)
|
return AgentOrchestrator.__new__(AgentOrchestrator)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ async def test_loop_noop_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||||||
monkeypatch.setattr(settings, "ci_watch_enabled", False)
|
monkeypatch.setattr(settings, "ci_watch_enabled", False)
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
cycle = AsyncMock()
|
cycle = AsyncMock()
|
||||||
orch._run_ci_watch_cycle = cycle # type: ignore[method-assign]
|
orch._run_ci_watch_cycle = cycle
|
||||||
await orch._ci_watch_loop() # must return immediately, no infinite loop
|
await orch._ci_watch_loop() # must return immediately, no infinite loop
|
||||||
cycle.assert_not_awaited()
|
cycle.assert_not_awaited()
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ def _db_ctx(db: Any) -> Any:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_cycle_warns_and_skips_engine_when_empty() -> None:
|
async def test_cycle_warns_and_skips_engine_when_empty() -> None:
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
orch._load_ci_watch_set = AsyncMock(return_value=[]) # type: ignore[method-assign]
|
orch._load_ci_watch_set = AsyncMock(return_value=[])
|
||||||
get_eng = MagicMock()
|
get_eng = MagicMock()
|
||||||
with (
|
with (
|
||||||
patch("roboco.db.get_db_context", _db_ctx(MagicMock())),
|
patch("roboco.db.get_db_context", _db_ctx(MagicMock())),
|
||||||
@@ -69,7 +69,7 @@ async def test_cycle_warns_and_skips_engine_when_empty() -> None:
|
|||||||
async def test_cycle_runs_engine_when_watch_set_present() -> None:
|
async def test_cycle_runs_engine_when_watch_set_present() -> None:
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
watch = [MagicMock()]
|
watch = [MagicMock()]
|
||||||
orch._load_ci_watch_set = AsyncMock(return_value=watch) # type: ignore[method-assign]
|
orch._load_ci_watch_set = AsyncMock(return_value=watch)
|
||||||
db = MagicMock()
|
db = MagicMock()
|
||||||
db.commit = AsyncMock()
|
db.commit = AsyncMock()
|
||||||
engine = MagicMock()
|
engine = MagicMock()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from roboco.config import settings
|
|||||||
from roboco.runtime.orchestrator import AgentOrchestrator
|
from roboco.runtime.orchestrator import AgentOrchestrator
|
||||||
|
|
||||||
|
|
||||||
def _orch() -> AgentOrchestrator:
|
def _orch() -> Any:
|
||||||
return AgentOrchestrator.__new__(AgentOrchestrator)
|
return AgentOrchestrator.__new__(AgentOrchestrator)
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ async def test_loop_noop_when_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||||||
monkeypatch.setattr(settings, "dep_update_enabled", False)
|
monkeypatch.setattr(settings, "dep_update_enabled", False)
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
cycle = AsyncMock()
|
cycle = AsyncMock()
|
||||||
orch._run_dep_update_cycle = cycle # type: ignore[method-assign]
|
orch._run_dep_update_cycle = cycle
|
||||||
await orch._dep_update_loop()
|
await orch._dep_update_loop()
|
||||||
cycle.assert_not_awaited()
|
cycle.assert_not_awaited()
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ def _db_ctx(db: Any) -> Any:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_cycle_warns_and_skips_engine_when_empty() -> None:
|
async def test_cycle_warns_and_skips_engine_when_empty() -> None:
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
orch._load_dep_update_set = AsyncMock(return_value=[]) # type: ignore[method-assign]
|
orch._load_dep_update_set = AsyncMock(return_value=[])
|
||||||
get_eng = MagicMock()
|
get_eng = MagicMock()
|
||||||
with (
|
with (
|
||||||
patch("roboco.db.get_db_context", _db_ctx(MagicMock())),
|
patch("roboco.db.get_db_context", _db_ctx(MagicMock())),
|
||||||
@@ -69,7 +69,7 @@ async def test_cycle_warns_and_skips_engine_when_empty() -> None:
|
|||||||
async def test_cycle_runs_engine_when_eligible_present() -> None:
|
async def test_cycle_runs_engine_when_eligible_present() -> None:
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
eligible = [MagicMock()]
|
eligible = [MagicMock()]
|
||||||
orch._load_dep_update_set = AsyncMock(return_value=eligible) # type: ignore[method-assign]
|
orch._load_dep_update_set = AsyncMock(return_value=eligible)
|
||||||
db = MagicMock()
|
db = MagicMock()
|
||||||
db.commit = AsyncMock()
|
db.commit = AsyncMock()
|
||||||
engine = MagicMock()
|
engine = MagicMock()
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ or future is covered, because they all go through `spawn_agent`.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -28,7 +29,7 @@ from roboco.runtime.orchestrator import AgentOrchestrator, AgentReadinessError
|
|||||||
from roboco.seeds.initial_data import AGENT_UUIDS
|
from roboco.seeds.initial_data import AGENT_UUIDS
|
||||||
|
|
||||||
|
|
||||||
def _orch() -> AgentOrchestrator:
|
def _orch() -> Any:
|
||||||
# The human-role guard is the first statement in spawn_agent and only
|
# The human-role guard is the first statement in spawn_agent and only
|
||||||
# consults the pure `role_for_slug` + the module logger — no self state
|
# consults the pure `role_for_slug` + the module logger — no self state
|
||||||
# — so a bare (un-initialized) orchestrator is sufficient to exercise it.
|
# — so a bare (un-initialized) orchestrator is sufficient to exercise it.
|
||||||
@@ -75,7 +76,7 @@ async def test_spawn_agent_does_not_refuse_real_agent() -> None:
|
|||||||
async def _ready(_aid: str, _tid: str | None) -> str | None:
|
async def _ready(_aid: str, _tid: str | None) -> str | None:
|
||||||
return "stubbed-not-ready"
|
return "stubbed-not-ready"
|
||||||
|
|
||||||
orch._readiness_gate = _ready # type: ignore[assignment]
|
orch._readiness_gate = _ready
|
||||||
|
|
||||||
with pytest.raises(AgentReadinessError) as exc_info:
|
with pytest.raises(AgentReadinessError) as exc_info:
|
||||||
await orch.spawn_agent("be-dev-1", task_id="t-1")
|
await orch.spawn_agent("be-dev-1", task_id="t-1")
|
||||||
@@ -89,15 +90,15 @@ async def test_spawn_agent_does_not_refuse_real_agent() -> None:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def _a2a_orch(ceo_uuid: str) -> AgentOrchestrator:
|
def _a2a_orch(ceo_uuid: str) -> Any:
|
||||||
"""A bare orchestrator with the a2a-dispatch collaborators stubbed."""
|
"""A bare orchestrator with the a2a-dispatch collaborators stubbed."""
|
||||||
orch = object.__new__(AgentOrchestrator)
|
orch: Any = object.__new__(AgentOrchestrator)
|
||||||
# _dispatch_a2a_work consults: _fetch_notifications, _resolve_agent_slug,
|
# _dispatch_a2a_work consults: _fetch_notifications, _resolve_agent_slug,
|
||||||
# _is_agent_active, spawn_agent. _resolve_agent_slug is pure (module
|
# _is_agent_active, spawn_agent. _resolve_agent_slug is pure (module
|
||||||
# UUID_TO_SLUG) so it works unstubbed; stub the rest.
|
# UUID_TO_SLUG) so it works unstubbed; stub the rest.
|
||||||
orch.spawn_agent = AsyncMock() # type: ignore[method-assign]
|
orch.spawn_agent = AsyncMock()
|
||||||
orch._is_agent_active = MagicMock(return_value=False) # type: ignore[method-assign]
|
orch._is_agent_active = MagicMock(return_value=False)
|
||||||
orch._fetch_notifications = AsyncMock( # type: ignore[method-assign]
|
orch._fetch_notifications = AsyncMock(
|
||||||
return_value=[
|
return_value=[
|
||||||
{"id": "n1", "to_agents": [ceo_uuid], "body": "board handoff"},
|
{"id": "n1", "to_agents": [ceo_uuid], "body": "board handoff"},
|
||||||
]
|
]
|
||||||
@@ -119,7 +120,7 @@ async def test_dispatch_a2a_skips_ceo_target() -> None:
|
|||||||
|
|
||||||
await orch._dispatch_a2a_work(client)
|
await orch._dispatch_a2a_work(client)
|
||||||
|
|
||||||
orch.spawn_agent.assert_not_awaited() # type: ignore[attr-defined]
|
orch.spawn_agent.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -128,7 +129,7 @@ async def test_dispatch_a2a_skips_intake_and_secretary_targets() -> None:
|
|||||||
for slug in ("intake-1", "secretary-1"):
|
for slug in ("intake-1", "secretary-1"):
|
||||||
orch = _a2a_orch(AGENT_UUIDS[slug])
|
orch = _a2a_orch(AGENT_UUIDS[slug])
|
||||||
await orch._dispatch_a2a_work(MagicMock())
|
await orch._dispatch_a2a_work(MagicMock())
|
||||||
orch.spawn_agent.assert_not_awaited() # type: ignore[attr-defined]
|
orch.spawn_agent.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -141,8 +142,8 @@ async def test_dispatch_a2a_still_spawns_real_agent_target() -> None:
|
|||||||
|
|
||||||
await orch._dispatch_a2a_work(client)
|
await orch._dispatch_a2a_work(client)
|
||||||
|
|
||||||
orch.spawn_agent.assert_awaited_once() # type: ignore[attr-defined]
|
orch.spawn_agent.assert_awaited_once()
|
||||||
_args, kwargs = orch.spawn_agent.call_args # type: ignore[attr-defined]
|
_args, kwargs = orch.spawn_agent.call_args
|
||||||
assert kwargs.get("agent_id") == "be-dev-1"
|
assert kwargs.get("agent_id") == "be-dev-1"
|
||||||
|
|
||||||
|
|
||||||
@@ -152,10 +153,10 @@ async def test_dispatch_a2a_mixed_targets_skips_only_human() -> None:
|
|||||||
real agent once and never the CEO."""
|
real agent once and never the CEO."""
|
||||||
ceo_uuid = AGENT_UUIDS["ceo"]
|
ceo_uuid = AGENT_UUIDS["ceo"]
|
||||||
be_uuid = AGENT_UUIDS["be-dev-1"]
|
be_uuid = AGENT_UUIDS["be-dev-1"]
|
||||||
orch = object.__new__(AgentOrchestrator)
|
orch: Any = object.__new__(AgentOrchestrator)
|
||||||
orch.spawn_agent = AsyncMock() # type: ignore[method-assign]
|
orch.spawn_agent = AsyncMock()
|
||||||
orch._is_agent_active = MagicMock(return_value=False) # type: ignore[method-assign]
|
orch._is_agent_active = MagicMock(return_value=False)
|
||||||
orch._fetch_notifications = AsyncMock( # type: ignore[method-assign]
|
orch._fetch_notifications = AsyncMock(
|
||||||
return_value=[{"id": "n1", "to_agents": [ceo_uuid, be_uuid]}]
|
return_value=[{"id": "n1", "to_agents": [ceo_uuid, be_uuid]}]
|
||||||
)
|
)
|
||||||
client = MagicMock()
|
client = MagicMock()
|
||||||
@@ -177,11 +178,11 @@ async def test_dispatch_pm_review_skips_ceo_assignee() -> None:
|
|||||||
"""An awaiting_pm_review task assigned to the CEO must NOT respawn a CEO
|
"""An awaiting_pm_review task assigned to the CEO must NOT respawn a CEO
|
||||||
container, and must NOT abort the dispatcher's tick (which would stall
|
container, and must NOT abort the dispatcher's tick (which would stall
|
||||||
other PM-review respawns behind it). The skip leaves it for the human."""
|
other PM-review respawns behind it). The skip leaves it for the human."""
|
||||||
orch = object.__new__(AgentOrchestrator)
|
orch: Any = object.__new__(AgentOrchestrator)
|
||||||
orch.spawn_agent = AsyncMock() # type: ignore[method-assign]
|
orch.spawn_agent = AsyncMock()
|
||||||
orch._is_agent_active = MagicMock(return_value=False) # type: ignore[method-assign]
|
orch._is_agent_active = MagicMock(return_value=False)
|
||||||
orch._pm_respawn_should_gate = AsyncMock(return_value=False) # type: ignore[method-assign]
|
orch._pm_respawn_should_gate = AsyncMock(return_value=False)
|
||||||
orch._fetch_tasks = AsyncMock( # type: ignore[method-assign]
|
orch._fetch_tasks = AsyncMock(
|
||||||
return_value=[
|
return_value=[
|
||||||
{
|
{
|
||||||
"id": "t1",
|
"id": "t1",
|
||||||
|
|||||||
@@ -18,36 +18,36 @@ triggers only its own recovery helper.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import cast
|
from typing import Any, cast
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from roboco.runtime.orchestrator import AgentOrchestrator
|
from roboco.runtime.orchestrator import AgentOrchestrator
|
||||||
|
|
||||||
|
|
||||||
def _orch() -> AgentOrchestrator:
|
def _orch() -> Any:
|
||||||
with patch.object(AgentOrchestrator, "__init__", return_value=None):
|
with patch.object(AgentOrchestrator, "__init__", return_value=None):
|
||||||
return AgentOrchestrator.__new__(AgentOrchestrator)
|
return AgentOrchestrator.__new__(AgentOrchestrator)
|
||||||
|
|
||||||
|
|
||||||
def _ready_orch() -> AgentOrchestrator:
|
def _ready_orch() -> Any:
|
||||||
"""Orchestrator with every closure gate stubbed so _maybe_spawn_pm_closure
|
"""Orchestrator with every closure gate stubbed so _maybe_spawn_pm_closure
|
||||||
reaches the spawn (descendants terminal, not recently paused, not
|
reaches the spawn (descendants terminal, not recently paused, not
|
||||||
already promoted, PM idle)."""
|
already promoted, PM idle)."""
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
orch._is_recently_paused = MagicMock(return_value=False) # type: ignore[method-assign]
|
orch._is_recently_paused = MagicMock(return_value=False)
|
||||||
orch._fetch_all_descendants = AsyncMock( # type: ignore[method-assign]
|
orch._fetch_all_descendants = AsyncMock(
|
||||||
return_value=[{"id": "leaf", "status": "completed"}]
|
return_value=[{"id": "leaf", "status": "completed"}]
|
||||||
)
|
)
|
||||||
orch._all_descendants_terminal = MagicMock(return_value=True) # type: ignore[method-assign]
|
orch._all_descendants_terminal = MagicMock(return_value=True)
|
||||||
orch._already_promoted_for_closure = MagicMock(return_value=False) # type: ignore[method-assign]
|
orch._already_promoted_for_closure = MagicMock(return_value=False)
|
||||||
orch._closure_pm_for_team = MagicMock(return_value="be-pm") # type: ignore[method-assign]
|
orch._closure_pm_for_team = MagicMock(return_value="be-pm")
|
||||||
orch._is_agent_active = MagicMock(return_value=False) # type: ignore[method-assign]
|
orch._is_agent_active = MagicMock(return_value=False)
|
||||||
orch._build_pm_closure_prompt = MagicMock(return_value="PROMPT") # type: ignore[method-assign]
|
orch._build_pm_closure_prompt = MagicMock(return_value="PROMPT")
|
||||||
orch._task_git_context = MagicMock(return_value=None) # type: ignore[method-assign]
|
orch._task_git_context = MagicMock(return_value=None)
|
||||||
orch.spawn_agent = AsyncMock() # type: ignore[method-assign]
|
orch.spawn_agent = AsyncMock()
|
||||||
orch._auto_resume_paused_parent = AsyncMock() # type: ignore[method-assign]
|
orch._auto_resume_paused_parent = AsyncMock()
|
||||||
orch._auto_recover_blocked_parent = AsyncMock() # type: ignore[method-assign]
|
orch._auto_recover_blocked_parent = AsyncMock()
|
||||||
return orch
|
return orch
|
||||||
|
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ async def test_non_paused_parent_is_not_resumed() -> None:
|
|||||||
async def test_resume_skipped_when_closure_gate_blocks_spawn() -> None:
|
async def test_resume_skipped_when_closure_gate_blocks_spawn() -> None:
|
||||||
"""If descendants aren't terminal there is no spawn — and no resume."""
|
"""If descendants aren't terminal there is no spawn — and no resume."""
|
||||||
orch = _ready_orch()
|
orch = _ready_orch()
|
||||||
orch._all_descendants_terminal = MagicMock(return_value=False) # type: ignore[method-assign]
|
orch._all_descendants_terminal = MagicMock(return_value=False)
|
||||||
client = AsyncMock()
|
client = AsyncMock()
|
||||||
|
|
||||||
await orch._maybe_spawn_pm_closure(
|
await orch._maybe_spawn_pm_closure(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ reaper's live-skip and the spawn gate see the live agent immediately.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -20,7 +21,7 @@ from roboco.runtime.orchestrator import AgentOrchestrator, AgentState
|
|||||||
_EXPECTED_READOPTED = 2
|
_EXPECTED_READOPTED = 2
|
||||||
|
|
||||||
|
|
||||||
def _orch() -> AgentOrchestrator:
|
def _orch() -> Any:
|
||||||
orch = AgentOrchestrator.__new__(AgentOrchestrator) # bypass __init__
|
orch = AgentOrchestrator.__new__(AgentOrchestrator) # bypass __init__
|
||||||
orch._instances = {}
|
orch._instances = {}
|
||||||
return orch
|
return orch
|
||||||
@@ -35,7 +36,7 @@ async def test_readopts_running_containers_as_active() -> None:
|
|||||||
slug = name.removeprefix("roboco-agent-")
|
slug = name.removeprefix("roboco-agent-")
|
||||||
return (slug in running, 0)
|
return (slug in running, 0)
|
||||||
|
|
||||||
orch._inspect_container_state = AsyncMock(side_effect=inspect) # type: ignore[method-assign]
|
orch._inspect_container_state = AsyncMock(side_effect=inspect)
|
||||||
|
|
||||||
n = await orch._readopt_running_agents()
|
n = await orch._readopt_running_agents()
|
||||||
|
|
||||||
@@ -51,7 +52,7 @@ async def test_readopt_leaves_already_tracked_instance_untouched() -> None:
|
|||||||
orch = _orch()
|
orch = _orch()
|
||||||
sentinel = MagicMock()
|
sentinel = MagicMock()
|
||||||
orch._instances = {"be-dev-1": sentinel}
|
orch._instances = {"be-dev-1": sentinel}
|
||||||
orch._inspect_container_state = AsyncMock(return_value=(True, 0)) # type: ignore[method-assign]
|
orch._inspect_container_state = AsyncMock(return_value=(True, 0))
|
||||||
|
|
||||||
await orch._readopt_running_agents()
|
await orch._readopt_running_agents()
|
||||||
|
|
||||||
@@ -61,7 +62,7 @@ async def test_readopt_leaves_already_tracked_instance_untouched() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_readopt_inert_when_nothing_running() -> None:
|
async def test_readopt_inert_when_nothing_running() -> None:
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
orch._inspect_container_state = AsyncMock(return_value=(False, None)) # type: ignore[method-assign]
|
orch._inspect_container_state = AsyncMock(return_value=(False, None))
|
||||||
|
|
||||||
n = await orch._readopt_running_agents()
|
n = await orch._readopt_running_agents()
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ async def test_readopt_inert_when_nothing_running() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_readopt_swallows_probe_errors() -> None:
|
async def test_readopt_swallows_probe_errors() -> None:
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
orch._inspect_container_state = AsyncMock(side_effect=RuntimeError("no docker")) # type: ignore[method-assign]
|
orch._inspect_container_state = AsyncMock(side_effect=RuntimeError("no docker"))
|
||||||
|
|
||||||
n = await orch._readopt_running_agents()
|
n = await orch._readopt_running_agents()
|
||||||
|
|
||||||
@@ -87,8 +88,8 @@ async def test_readopt_records_container_id_so_health_check_can_see_exit() -> No
|
|||||||
# is stranded under a phantom ACTIVE instance forever. Re-adopt must capture
|
# is stranded under a phantom ACTIVE instance forever. Re-adopt must capture
|
||||||
# the real container id so the health loop can observe the later exit.
|
# the real container id so the health loop can observe the later exit.
|
||||||
orch = _orch()
|
orch = _orch()
|
||||||
orch._inspect_container_state = AsyncMock(return_value=(True, 0)) # type: ignore[method-assign]
|
orch._inspect_container_state = AsyncMock(return_value=(True, 0))
|
||||||
orch._resolve_container_id = AsyncMock(return_value="deadbeef1234") # type: ignore[method-assign]
|
orch._resolve_container_id = AsyncMock(return_value="deadbeef1234")
|
||||||
|
|
||||||
await orch._readopt_running_agents()
|
await orch._readopt_running_agents()
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ the session — while the happy path still adds + commits the savepoint.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -89,7 +89,7 @@ async def test_cache_put_tolerates_concurrent_duplicate_without_poisoning() -> N
|
|||||||
# a savepoint; _cache_put returns cleanly, the session is not poisoned, and
|
# a savepoint; _cache_put returns cleanly, the session is not poisoned, and
|
||||||
# no full rollback undoes the outer task-create transaction.
|
# no full rollback undoes the outer task-create transaction.
|
||||||
session = _FakeSession(duplicate=True)
|
session = _FakeSession(duplicate=True)
|
||||||
svc = ConventionsService(session=session) # type: ignore[arg-type]
|
svc = ConventionsService(session=cast("Any", session))
|
||||||
|
|
||||||
await svc._cache_put(uuid4(), "deadbeef", _mapping(), "ok")
|
await svc._cache_put(uuid4(), "deadbeef", _mapping(), "ok")
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ async def test_cache_put_tolerates_concurrent_duplicate_without_poisoning() -> N
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_cache_put_happy_path_adds_and_releases_savepoint() -> None:
|
async def test_cache_put_happy_path_adds_and_releases_savepoint() -> None:
|
||||||
session = _FakeSession(duplicate=False)
|
session = _FakeSession(duplicate=False)
|
||||||
svc = ConventionsService(session=session) # type: ignore[arg-type]
|
svc = ConventionsService(session=cast("Any", session))
|
||||||
|
|
||||||
await svc._cache_put(uuid4(), "deadbeef", _mapping(), "ok")
|
await svc._cache_put(uuid4(), "deadbeef", _mapping(), "ok")
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
|
|
||||||
from roboco.services.conventions import ConventionsService
|
from roboco.services.conventions import ConventionsService
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ def _git_repo(root: Path) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _svc() -> ConventionsService:
|
def _svc() -> ConventionsService:
|
||||||
return ConventionsService(session=None) # type: ignore[arg-type]
|
return ConventionsService(session=cast("Any", None))
|
||||||
|
|
||||||
|
|
||||||
def test_resolve_reads_clone_head_and_backfills(tmp_path: Path) -> None:
|
def test_resolve_reads_clone_head_and_backfills(tmp_path: Path) -> None:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import pytest
|
|||||||
from roboco.services.git import GitService
|
from roboco.services.git import GitService
|
||||||
|
|
||||||
|
|
||||||
def _git_service() -> GitService:
|
def _git_service() -> Any:
|
||||||
return GitService.__new__(GitService)
|
return GitService.__new__(GitService)
|
||||||
|
|
||||||
|
|
||||||
@@ -26,8 +26,8 @@ def _git_service() -> GitService:
|
|||||||
async def test_resolve_diff_base_uses_parent_when_pushed() -> None:
|
async def test_resolve_diff_base_uses_parent_when_pushed() -> None:
|
||||||
"""When origin/<parent> exists, use it (normal case)."""
|
"""When origin/<parent> exists, use it (normal case)."""
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._run_git = AsyncMock() # type: ignore[method-assign]
|
svc._run_git = AsyncMock()
|
||||||
svc._ref_exists = AsyncMock(return_value=True) # type: ignore[method-assign]
|
svc._ref_exists = AsyncMock(return_value=True)
|
||||||
ws = Path("/tmp/ws")
|
ws = Path("/tmp/ws")
|
||||||
|
|
||||||
base = await svc._resolve_diff_base(
|
base = await svc._resolve_diff_base(
|
||||||
@@ -42,12 +42,10 @@ async def test_resolve_diff_base_falls_back_when_parent_absent() -> None:
|
|||||||
"""When origin/<parent> does NOT exist (cell-PM branch never pushed),
|
"""When origin/<parent> does NOT exist (cell-PM branch never pushed),
|
||||||
fall back to the repo default branch via origin/HEAD."""
|
fall back to the repo default branch via origin/HEAD."""
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._run_git = AsyncMock() # type: ignore[method-assign]
|
svc._run_git = AsyncMock()
|
||||||
# parent ref absent → _ref_exists False for the parent check.
|
# parent ref absent → _ref_exists False for the parent check.
|
||||||
svc._ref_exists = AsyncMock(return_value=False) # type: ignore[method-assign]
|
svc._ref_exists = AsyncMock(return_value=False)
|
||||||
svc._default_branch_ref = AsyncMock( # type: ignore[method-assign]
|
svc._default_branch_ref = AsyncMock(return_value="origin/master")
|
||||||
return_value="origin/master"
|
|
||||||
)
|
|
||||||
ws = Path("/tmp/ws")
|
ws = Path("/tmp/ws")
|
||||||
|
|
||||||
base = await svc._resolve_diff_base(
|
base = await svc._resolve_diff_base(
|
||||||
@@ -84,7 +82,7 @@ async def test_default_branch_ref_fallback_when_no_head() -> None:
|
|||||||
# symbolic-ref fails; fetches succeed but ref never verifies.
|
# symbolic-ref fails; fetches succeed but ref never verifies.
|
||||||
return type("R", (), {"returncode": 1, "stdout": ""})()
|
return type("R", (), {"returncode": 1, "stdout": ""})()
|
||||||
|
|
||||||
svc._ref_exists = AsyncMock(return_value=False) # type: ignore[method-assign]
|
svc._ref_exists = AsyncMock(return_value=False)
|
||||||
with patch.object(svc, "_run_git", new=fake_run):
|
with patch.object(svc, "_run_git", new=fake_run):
|
||||||
ref = await svc._default_branch_ref(Path("/tmp/ws"))
|
ref = await svc._default_branch_ref(Path("/tmp/ws"))
|
||||||
assert ref == "origin/master"
|
assert ref == "origin/master"
|
||||||
@@ -107,8 +105,8 @@ _BR = "feature/backend/root1234--cellpm56--dev78901"
|
|||||||
async def test_resolve_head_ref_prefers_local_branch_in_dev_clone() -> None:
|
async def test_resolve_head_ref_prefers_local_branch_in_dev_clone() -> None:
|
||||||
"""Dev's own clone has the local branch — use it unchanged."""
|
"""Dev's own clone has the local branch — use it unchanged."""
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._run_git = AsyncMock() # type: ignore[method-assign]
|
svc._run_git = AsyncMock()
|
||||||
svc._ref_exists = AsyncMock(return_value=True) # type: ignore[method-assign]
|
svc._ref_exists = AsyncMock(return_value=True)
|
||||||
|
|
||||||
head = await svc._resolve_head_ref(Path("/tmp/ws"), _BR)
|
head = await svc._resolve_head_ref(Path("/tmp/ws"), _BR)
|
||||||
assert head == _BR
|
assert head == _BR
|
||||||
@@ -119,7 +117,7 @@ async def test_resolve_head_ref_falls_back_to_origin_in_foreign_clone() -> None:
|
|||||||
"""QA/doc/PM clone has no local branch but origin/<branch> exists
|
"""QA/doc/PM clone has no local branch but origin/<branch> exists
|
||||||
(open_pr pushed it) — diff must target origin/<branch>."""
|
(open_pr pushed it) — diff must target origin/<branch>."""
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._run_git = AsyncMock() # type: ignore[method-assign]
|
svc._run_git = AsyncMock()
|
||||||
|
|
||||||
async def ref_exists(_ws: Any, ref: str) -> bool:
|
async def ref_exists(_ws: Any, ref: str) -> bool:
|
||||||
# local branch absent; only the remote-tracking ref resolves.
|
# local branch absent; only the remote-tracking ref resolves.
|
||||||
@@ -141,7 +139,7 @@ async def test_resolve_head_ref_fetches_branch_before_resolving() -> None:
|
|||||||
calls.append(args)
|
calls.append(args)
|
||||||
return type("R", (), {"returncode": 0, "stdout": ""})()
|
return type("R", (), {"returncode": 0, "stdout": ""})()
|
||||||
|
|
||||||
svc._ref_exists = AsyncMock(return_value=True) # type: ignore[method-assign]
|
svc._ref_exists = AsyncMock(return_value=True)
|
||||||
with patch.object(svc, "_run_git", new=fake_run):
|
with patch.object(svc, "_run_git", new=fake_run):
|
||||||
await svc._resolve_head_ref(Path("/tmp/ws"), _BR)
|
await svc._resolve_head_ref(Path("/tmp/ws"), _BR)
|
||||||
assert ["fetch", "origin", _BR] in calls
|
assert ["fetch", "origin", _BR] in calls
|
||||||
@@ -153,18 +151,10 @@ async def test_diff_targets_origin_head_in_foreign_clone() -> None:
|
|||||||
base...origin/<branch>, not base...<bare-local-branch> (which is
|
base...origin/<branch>, not base...<bare-local-branch> (which is
|
||||||
unresolvable there and silently produced an empty diff)."""
|
unresolvable there and silently produced an empty diff)."""
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._workspace_for_branch = AsyncMock( # type: ignore[method-assign]
|
svc._workspace_for_branch = AsyncMock(return_value=Path("/tmp/qa-ws"))
|
||||||
return_value=Path("/tmp/qa-ws")
|
svc._resolve_diff_base = AsyncMock(return_value="origin/master")
|
||||||
)
|
svc._resolve_head_ref = AsyncMock(return_value=f"origin/{_BR}")
|
||||||
svc._resolve_diff_base = AsyncMock( # type: ignore[method-assign]
|
svc._token_for_branch = AsyncMock(return_value="tok")
|
||||||
return_value="origin/master"
|
|
||||||
)
|
|
||||||
svc._resolve_head_ref = AsyncMock( # type: ignore[method-assign]
|
|
||||||
return_value=f"origin/{_BR}"
|
|
||||||
)
|
|
||||||
svc._token_for_branch = AsyncMock( # type: ignore[method-assign]
|
|
||||||
return_value="tok"
|
|
||||||
)
|
|
||||||
captured: list[list[str]] = []
|
captured: list[list[str]] = []
|
||||||
|
|
||||||
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
|
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
|
||||||
@@ -187,18 +177,10 @@ async def test_diff_targets_origin_head_in_foreign_clone() -> None:
|
|||||||
async def test_list_changed_files_targets_origin_head_in_foreign_clone() -> None:
|
async def test_list_changed_files_targets_origin_head_in_foreign_clone() -> None:
|
||||||
"""Same fix on the files_changed path (#154 evidence)."""
|
"""Same fix on the files_changed path (#154 evidence)."""
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._workspace_for_branch = AsyncMock( # type: ignore[method-assign]
|
svc._workspace_for_branch = AsyncMock(return_value=Path("/tmp/qa-ws"))
|
||||||
return_value=Path("/tmp/qa-ws")
|
svc._resolve_diff_base = AsyncMock(return_value="origin/master")
|
||||||
)
|
svc._resolve_head_ref = AsyncMock(return_value=f"origin/{_BR}")
|
||||||
svc._resolve_diff_base = AsyncMock( # type: ignore[method-assign]
|
svc._token_for_branch = AsyncMock(return_value="tok")
|
||||||
return_value="origin/master"
|
|
||||||
)
|
|
||||||
svc._resolve_head_ref = AsyncMock( # type: ignore[method-assign]
|
|
||||||
return_value=f"origin/{_BR}"
|
|
||||||
)
|
|
||||||
svc._token_for_branch = AsyncMock( # type: ignore[method-assign]
|
|
||||||
return_value="tok"
|
|
||||||
)
|
|
||||||
captured: list[list[str]] = []
|
captured: list[list[str]] = []
|
||||||
|
|
||||||
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
|
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
|
||||||
@@ -219,18 +201,10 @@ async def test_diff_honours_explicit_base_with_resolved_head() -> None:
|
|||||||
"""The incremental dev path (base=HEAD~1) still works: explicit base
|
"""The incremental dev path (base=HEAD~1) still works: explicit base
|
||||||
is preserved, head still goes through _resolve_head_ref."""
|
is preserved, head still goes through _resolve_head_ref."""
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._workspace_for_branch = AsyncMock( # type: ignore[method-assign]
|
svc._workspace_for_branch = AsyncMock(return_value=Path("/tmp/dev-ws"))
|
||||||
return_value=Path("/tmp/dev-ws")
|
svc._resolve_diff_base = AsyncMock(return_value="SHOULD_NOT_BE_USED")
|
||||||
)
|
svc._resolve_head_ref = AsyncMock(return_value=_BR)
|
||||||
svc._resolve_diff_base = AsyncMock( # type: ignore[method-assign]
|
svc._token_for_branch = AsyncMock(return_value=None)
|
||||||
return_value="SHOULD_NOT_BE_USED"
|
|
||||||
)
|
|
||||||
svc._resolve_head_ref = AsyncMock( # type: ignore[method-assign]
|
|
||||||
return_value=_BR
|
|
||||||
)
|
|
||||||
svc._token_for_branch = AsyncMock( # type: ignore[method-assign]
|
|
||||||
return_value=None
|
|
||||||
)
|
|
||||||
captured: list[list[str]] = []
|
captured: list[list[str]] = []
|
||||||
|
|
||||||
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
|
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
|
||||||
@@ -266,10 +240,8 @@ async def test_resolve_diff_base_refetches_default_branch_with_token() -> None:
|
|||||||
return type("R", (), {"returncode": 0, "stdout": ""})()
|
return type("R", (), {"returncode": 0, "stdout": ""})()
|
||||||
|
|
||||||
# parent ref never exists → fall back to default branch.
|
# parent ref never exists → fall back to default branch.
|
||||||
svc._ref_exists = AsyncMock(return_value=False) # type: ignore[method-assign]
|
svc._ref_exists = AsyncMock(return_value=False)
|
||||||
svc._default_branch_ref = AsyncMock( # type: ignore[method-assign]
|
svc._default_branch_ref = AsyncMock(return_value="origin/master")
|
||||||
return_value="origin/master"
|
|
||||||
)
|
|
||||||
|
|
||||||
with patch.object(svc, "_run_git", new=fake_run):
|
with patch.object(svc, "_run_git", new=fake_run):
|
||||||
base = await svc._resolve_diff_base(Path("/tmp/ws"), _BR, token="tok")
|
base = await svc._resolve_diff_base(Path("/tmp/ws"), _BR, token="tok")
|
||||||
@@ -291,7 +263,7 @@ async def test_resolve_head_ref_fetch_is_authenticated() -> None:
|
|||||||
seen.append((args, kw.get("token")))
|
seen.append((args, kw.get("token")))
|
||||||
return type("R", (), {"returncode": 0, "stdout": ""})()
|
return type("R", (), {"returncode": 0, "stdout": ""})()
|
||||||
|
|
||||||
svc._ref_exists = AsyncMock(return_value=True) # type: ignore[method-assign]
|
svc._ref_exists = AsyncMock(return_value=True)
|
||||||
with patch.object(svc, "_run_git", new=fake_run):
|
with patch.object(svc, "_run_git", new=fake_run):
|
||||||
await svc._resolve_head_ref(Path("/tmp/ws"), _BR, token="tok")
|
await svc._resolve_head_ref(Path("/tmp/ws"), _BR, token="tok")
|
||||||
assert (["fetch", "origin", _BR], "tok") in seen
|
assert (["fetch", "origin", _BR], "tok") in seen
|
||||||
@@ -302,5 +274,5 @@ async def test_token_for_branch_is_best_effort_none() -> None:
|
|||||||
"""Unresolvable branch/project must yield None (degrade to unauth),
|
"""Unresolvable branch/project must yield None (degrade to unauth),
|
||||||
never raise inside the evidence-assembly path."""
|
never raise inside the evidence-assembly path."""
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._task_for_branch = AsyncMock(return_value=None) # type: ignore[method-assign]
|
svc._task_for_branch = AsyncMock(return_value=None)
|
||||||
assert await svc._token_for_branch(_BR) is None
|
assert await svc._token_for_branch(_BR) is None
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ _BASE = "feature/backend/parent12345"
|
|||||||
_HEAD = "feature/backend/abc12345"
|
_HEAD = "feature/backend/abc12345"
|
||||||
|
|
||||||
|
|
||||||
def _git_service() -> GitService:
|
def _git_service() -> Any:
|
||||||
svc = GitService.__new__(GitService)
|
svc: Any = GitService.__new__(GitService)
|
||||||
svc.log = MagicMock()
|
svc.log = MagicMock()
|
||||||
return svc
|
return svc
|
||||||
|
|
||||||
@@ -45,14 +45,14 @@ def _project() -> Any:
|
|||||||
return MagicMock(slug="roboco")
|
return MagicMock(slug="roboco")
|
||||||
|
|
||||||
|
|
||||||
async def _wire(svc: GitService, *, rev_list_stdout: str) -> AsyncMock:
|
async def _wire(svc: Any, *, rev_list_stdout: str) -> AsyncMock:
|
||||||
"""Stub the workspace/token resolution + _run_git; return the run mock."""
|
"""Stub the workspace/token resolution + _run_git; return the run mock."""
|
||||||
svc._project_for_task = AsyncMock(return_value=_project()) # type: ignore[method-assign]
|
svc._project_for_task = AsyncMock(return_value=_project())
|
||||||
svc._resolve_workspace_agent_id = MagicMock(return_value=uuid4()) # type: ignore[method-assign]
|
svc._resolve_workspace_agent_id = MagicMock(return_value=uuid4())
|
||||||
svc.get_workspace = AsyncMock(return_value=_WORKSPACE) # type: ignore[method-assign]
|
svc.get_workspace = AsyncMock(return_value=_WORKSPACE)
|
||||||
svc._get_project_token_or_raise = AsyncMock(return_value=_TOKEN) # type: ignore[method-assign]
|
svc._get_project_token_or_raise = AsyncMock(return_value=_TOKEN)
|
||||||
run = AsyncMock(side_effect=[_result(), _result(stdout=rev_list_stdout)])
|
run = AsyncMock(side_effect=[_result(), _result(stdout=rev_list_stdout)])
|
||||||
svc._run_git = run # type: ignore[method-assign]
|
svc._run_git = run
|
||||||
return run
|
return run
|
||||||
|
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ async def test_is_behind_base_requires_branch_name() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_is_behind_base_raises_when_project_missing() -> None:
|
async def test_is_behind_base_raises_when_project_missing() -> None:
|
||||||
svc = _git_service()
|
svc = _git_service()
|
||||||
svc._project_for_task = AsyncMock(return_value=None) # type: ignore[method-assign]
|
svc._project_for_task = AsyncMock(return_value=None)
|
||||||
|
|
||||||
with pytest.raises(NotFoundError):
|
with pytest.raises(NotFoundError):
|
||||||
await svc.is_behind_base(_task(), base_branch=_BASE)
|
await svc.is_behind_base(_task(), base_branch=_BASE)
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ def _link(session_id: object, relationship_type: str) -> MagicMock:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_propagate_links_every_parent_session_to_subtask() -> None:
|
async def test_propagate_links_every_parent_session_to_subtask() -> None:
|
||||||
"""Every link on the parent gets re-attached to the new subtask."""
|
"""Every link on the parent gets re-attached to the new subtask."""
|
||||||
svc = MessagingService.__new__(MessagingService)
|
svc: Any = MessagingService.__new__(MessagingService)
|
||||||
parent_session = uuid4()
|
parent_session = uuid4()
|
||||||
review_session = uuid4()
|
review_session = uuid4()
|
||||||
svc.get_sessions_for_task = AsyncMock( # type: ignore[method-assign]
|
svc.get_sessions_for_task = AsyncMock(
|
||||||
return_value=[
|
return_value=[
|
||||||
_link(parent_session, "discussion"),
|
_link(parent_session, "discussion"),
|
||||||
_link(review_session, "review"),
|
_link(review_session, "review"),
|
||||||
@@ -67,9 +67,9 @@ async def test_propagate_links_every_parent_session_to_subtask() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_propagate_no_parent_sessions_returns_empty() -> None:
|
async def test_propagate_no_parent_sessions_returns_empty() -> None:
|
||||||
"""When the parent has no session links, propagation is a no-op."""
|
"""When the parent has no session links, propagation is a no-op."""
|
||||||
svc = MessagingService.__new__(MessagingService)
|
svc: Any = MessagingService.__new__(MessagingService)
|
||||||
svc.get_sessions_for_task = AsyncMock(return_value=[]) # type: ignore[method-assign]
|
svc.get_sessions_for_task = AsyncMock(return_value=[])
|
||||||
svc.link_session_to_task = AsyncMock() # type: ignore[method-assign]
|
svc.link_session_to_task = AsyncMock()
|
||||||
|
|
||||||
out = await svc.propagate_sessions_to_subtask(uuid4(), uuid4(), uuid4())
|
out = await svc.propagate_sessions_to_subtask(uuid4(), uuid4(), uuid4())
|
||||||
assert out == []
|
assert out == []
|
||||||
@@ -80,8 +80,8 @@ async def test_propagate_no_parent_sessions_returns_empty() -> None:
|
|||||||
async def test_propagate_unknown_relationship_type_defaults_to_discussion() -> None:
|
async def test_propagate_unknown_relationship_type_defaults_to_discussion() -> None:
|
||||||
"""Garbage relationship_type on the parent link doesn't crash; it
|
"""Garbage relationship_type on the parent link doesn't crash; it
|
||||||
defaults to DISCUSSION so the subtask is still linked."""
|
defaults to DISCUSSION so the subtask is still linked."""
|
||||||
svc = MessagingService.__new__(MessagingService)
|
svc: Any = MessagingService.__new__(MessagingService)
|
||||||
svc.get_sessions_for_task = AsyncMock( # type: ignore[method-assign]
|
svc.get_sessions_for_task = AsyncMock(
|
||||||
return_value=[_link(uuid4(), "definitely-not-a-real-type")]
|
return_value=[_link(uuid4(), "definitely-not-a-real-type")]
|
||||||
)
|
)
|
||||||
calls: list[dict[str, Any]] = []
|
calls: list[dict[str, Any]] = []
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ _PCT_NONE = 0
|
|||||||
_PCT_FALLBACK = 42
|
_PCT_FALLBACK = 42
|
||||||
|
|
||||||
|
|
||||||
def _svc_with_task(task: Any) -> TaskService:
|
def _svc_with_task(task: Any) -> Any:
|
||||||
svc = TaskService.__new__(TaskService)
|
svc: Any = TaskService.__new__(TaskService)
|
||||||
svc.get = AsyncMock(return_value=task) # type: ignore[method-assign]
|
svc.get = AsyncMock(return_value=task)
|
||||||
svc.session = MagicMock()
|
svc.session = MagicMock()
|
||||||
svc.session.flush = AsyncMock()
|
svc.session.flush = AsyncMock()
|
||||||
return svc
|
return svc
|
||||||
@@ -119,6 +119,6 @@ async def test_no_checklist_falls_back_to_supplied_percentage() -> None:
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_missing_task_returns_none() -> None:
|
async def test_missing_task_returns_none() -> None:
|
||||||
svc = TaskService.__new__(TaskService)
|
svc: Any = TaskService.__new__(TaskService)
|
||||||
svc.get = AsyncMock(return_value=None) # type: ignore[method-assign]
|
svc.get = AsyncMock(return_value=None)
|
||||||
assert await svc.record_plan_progress(uuid4(), uuid4(), "x") is None
|
assert await svc.record_plan_progress(uuid4(), uuid4(), "x") is None
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from roboco.services.base import (
|
from roboco.services.base import (
|
||||||
BaseService,
|
BaseService,
|
||||||
@@ -120,7 +122,7 @@ def test_base_service_binds_session_and_logger() -> None:
|
|||||||
service_name = "x"
|
service_name = "x"
|
||||||
|
|
||||||
fake_session = object()
|
fake_session = object()
|
||||||
svc = _Svc(fake_session) # type: ignore[arg-type]
|
svc = _Svc(cast("Any", fake_session))
|
||||||
assert svc.session is fake_session
|
assert svc.session is fake_session
|
||||||
assert svc.log is not None
|
assert svc.log is not None
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ suites; here we assert the branch wiring with a mocked db context.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -52,7 +53,8 @@ async def test_create_notification_suppresses_same_purpose_duplicate() -> None:
|
|||||||
db.commit = AsyncMock()
|
db.commit = AsyncMock()
|
||||||
|
|
||||||
svc = NotificationService()
|
svc = NotificationService()
|
||||||
svc._resolve_recipients = AsyncMock(return_value=[uuid4()]) # type: ignore[method-assign]
|
cc: Any = svc
|
||||||
|
cc._resolve_recipients = AsyncMock(return_value=[uuid4()])
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
"roboco.services.notification.get_db_context",
|
"roboco.services.notification.get_db_context",
|
||||||
@@ -90,7 +92,8 @@ async def test_informational_knowledge_share_not_deduped() -> None:
|
|||||||
db.commit = AsyncMock()
|
db.commit = AsyncMock()
|
||||||
|
|
||||||
svc = NotificationService()
|
svc = NotificationService()
|
||||||
svc._resolve_recipients = AsyncMock(return_value=[uuid4()]) # type: ignore[method-assign]
|
cc: Any = svc
|
||||||
|
cc._resolve_recipients = AsyncMock(return_value=[uuid4()])
|
||||||
params = CreateNotificationParams(
|
params = CreateNotificationParams(
|
||||||
notification_type=NotificationType.KNOWLEDGE_SHARE,
|
notification_type=NotificationType.KNOWLEDGE_SHARE,
|
||||||
priority=NotificationPriority.NORMAL,
|
priority=NotificationPriority.NORMAL,
|
||||||
|
|||||||
Reference in New Issue
Block a user