Files
roboco/tests/unit/gateway/test_choreographer_briefing.py
T

127 lines
5.6 KiB
Python
Raw Normal View History

"""``institutional_memory_status`` sentinel reaches the briefing envelope.
The ``context_briefing["institutional_memory"]`` block carries a ``status``
field distinguishing the five underlying states so an agent can tell
"searched, nothing" (below_floor / empty) from "search broke" (error) from
"subsystem off" (disabled) from "lessons injected" (ok). Additive only —
lessons is empty unless status is ``ok``.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
import pytest
from roboco.services.gateway.choreographer import Choreographer
_FLOOR = 0.6
def _choreographer(
*,
similar_memory_out: dict[str, object] | Exception,
) -> tuple[Choreographer, AsyncMock]:
repo = AsyncMock()
repo.list_unread_a2a.return_value = []
repo.list_unread_mentions.return_value = []
repo.list_pending_notifications.return_value = []
repo.task_metadata_gaps.return_value = []
repo.recent_team_activity.return_value = []
repo.blockers_in_lane.return_value = []
repo.company_goals.return_value = None
repo.journal_highlights_for_task.return_value = []
if isinstance(similar_memory_out, Exception):
repo.similar_memory = AsyncMock(side_effect=similar_memory_out)
else:
repo.similar_memory = AsyncMock(return_value=similar_memory_out)
task_svc = AsyncMock()
task_svc.agent_for.return_value = MagicMock(role="developer")
# Findings-ledger reads (ReviewFindingsRepository.list_for_task) go
# through session.execute — an unconfigured AsyncMock's awaited result
# is itself an AsyncMock, so a plain sync `.scalars()` call on it leaks
# an unawaited coroutine. Empty scalars result (no findings).
task_svc.session.execute = AsyncMock(
return_value=MagicMock(
scalars=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[])))
)
)
choreo = object.__new__(Choreographer)
choreo._deps = MagicMock(evidence_repo=repo, task=task_svc)
return choreo, repo
def _task() -> MagicMock:
return MagicMock(
title="Add retry backoff",
task_type=MagicMock(value="code"),
)
class TestInstitutionalMemoryStatus:
@pytest.mark.asyncio
async def test_disabled_when_subsystem_off(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("roboco.config.settings.org_memory_enabled", False)
choreo, _ = _choreographer(similar_memory_out={"items": [], "status": "ok"})
briefing = await choreo._briefing_for(uuid4(), uuid4(), task=_task(), full=True)
assert briefing["institutional_memory"]["status"] == "disabled"
assert briefing["institutional_memory"]["lessons"] == []
@pytest.mark.asyncio
async def test_error_when_search_raises(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("roboco.config.settings.org_memory_enabled", True)
# similar_memory itself swallows RAG errors and returns status=error;
# simulate that contract (the choreographer trusts the repo's status).
choreo, _ = _choreographer(similar_memory_out={"items": [], "status": "error"})
briefing = await choreo._briefing_for(uuid4(), uuid4(), task=_task(), full=True)
assert briefing["institutional_memory"]["status"] == "error"
assert briefing["institutional_memory"]["lessons"] == []
@pytest.mark.asyncio
async def test_empty_when_search_yields_nothing(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("roboco.config.settings.org_memory_enabled", True)
choreo, _ = _choreographer(similar_memory_out={"items": [], "status": "empty"})
briefing = await choreo._briefing_for(uuid4(), uuid4(), task=_task(), full=True)
assert briefing["institutional_memory"]["status"] == "empty"
assert briefing["institutional_memory"]["lessons"] == []
@pytest.mark.asyncio
async def test_below_floor_when_all_under_floor(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr("roboco.config.settings.org_memory_enabled", True)
choreo, _ = _choreographer(
similar_memory_out={"items": [], "status": "below_floor"}
)
briefing = await choreo._briefing_for(uuid4(), uuid4(), task=_task(), full=True)
assert briefing["institutional_memory"]["status"] == "below_floor"
assert briefing["institutional_memory"]["lessons"] == []
@pytest.mark.asyncio
async def test_ok_injects_lessons(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("roboco.config.settings.org_memory_enabled", True)
lesson = {"kind": "learning", "summary": "s", "source": "src", "score": 0.9}
choreo, _ = _choreographer(
similar_memory_out={"items": [lesson], "status": "ok"}
)
briefing = await choreo._briefing_for(uuid4(), uuid4(), task=_task(), full=True)
assert briefing["institutional_memory"]["status"] == "ok"
assert briefing["institutional_memory"]["lessons"] == [lesson]
@pytest.mark.asyncio
async def test_slim_briefing_omits_block(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Slim (non-full) briefings don't run the heavy section, so the block
is absent — preserves the existing slim/full split."""
monkeypatch.setattr("roboco.config.settings.org_memory_enabled", True)
choreo, _ = _choreographer(similar_memory_out={"items": [], "status": "ok"})
briefing = await choreo._briefing_for(uuid4(), uuid4(), task=_task())
assert "institutional_memory" not in briefing