mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
Fix: CEO reject conduit and main pm routing (#91)
* fix(tasks): deliver CEO change-requests to the reworker and route integration rejects to the Main PM ceo_reject previously appended the CEO's required-changes only to quick_context (never surfaced to an agent) and always reassigned to the original developer, so the feedback never reached whoever reworked the task and a coordination/integration root went to a developer instead of the Main PM. Now ceo_reject records the reason as a DECISION_LOG handoff journal entry — the channel evidence/journal_highlights already serves to the task's assignee — and routes a coordination task (no project, has product) to team=main_pm + the Main PM so it can delegate the rework. Leaf dev tasks still return to the original developer. The journal write is best-effort (author-exists guard) and never blocks a reject. Adds tests for the routing and the handoff-journal write. * feat(gateway): wire the agent context_briefing receive-path EvidenceRepo's agent-scoped methods were stubbed to return empty lists, so agents never received notifications, A2A DMs, @mentions, recent team activity, or in-lane blockers through their briefing — the system was effectively send-only. This implements all five as single capped queries over the live tables (plus a light task-metadata-gap check), mirroring the existing journal_highlights query. Each runs on the per-verb briefing path, so each stays a single LIMIT-10 indexed lookup. Unit tests cover the row mapping + empty paths; an integration suite exercises the real SQL (array contains, the a2a slug filter, team+status) against Postgres. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""roboco.services.gateway.evidence_repo coverage.
|
||||
"""roboco.services.gateway.evidence_repo coverage (mapping + empty paths).
|
||||
|
||||
Most methods are still Phase 1 stubs returning empty lists;
|
||||
``journal_highlights_for_task`` is wired to a real query, so it is tested
|
||||
against a mocked ``execute`` result that stands in for the DB rows.
|
||||
These are mocked unit tests for the row-mapping and empty/not-found paths. The
|
||||
actual SQL (WHERE clauses, array operators) is exercised against a real DB in
|
||||
tests/integration/test_evidence_repo_queries.py.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -16,69 +16,118 @@ import pytest
|
||||
from roboco.services.gateway.evidence_repo import EvidenceRepo
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repo() -> EvidenceRepo:
|
||||
"""Build an EvidenceRepo with a stub session — no DB access in Phase 1."""
|
||||
return EvidenceRepo(MagicMock())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor_stores_db_session() -> None:
|
||||
fake_db = MagicMock()
|
||||
repo = EvidenceRepo(fake_db)
|
||||
assert repo._db is fake_db
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_unread_a2a_returns_empty(repo: EvidenceRepo) -> None:
|
||||
out = await repo.list_unread_a2a(uuid4())
|
||||
assert out == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_unread_mentions_returns_empty(repo: EvidenceRepo) -> None:
|
||||
out = await repo.list_unread_mentions(uuid4())
|
||||
assert out == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_pending_notifications_returns_empty(repo: EvidenceRepo) -> None:
|
||||
out = await repo.list_pending_notifications(uuid4())
|
||||
assert out == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_metadata_gaps_returns_empty(repo: EvidenceRepo) -> None:
|
||||
out = await repo.task_metadata_gaps(uuid4())
|
||||
assert out == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recent_team_activity_returns_empty(repo: EvidenceRepo) -> None:
|
||||
out = await repo.recent_team_activity(uuid4())
|
||||
assert out == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_blockers_in_lane_returns_empty(repo: EvidenceRepo) -> None:
|
||||
out = await repo.blockers_in_lane(uuid4())
|
||||
assert out == []
|
||||
|
||||
|
||||
def _repo_with_rows(rows: list[object]) -> EvidenceRepo:
|
||||
"""EvidenceRepo whose db.execute() yields a result with the given rows."""
|
||||
def _empty_repo() -> EvidenceRepo:
|
||||
"""Repo whose scalar() and execute() both yield nothing."""
|
||||
db = MagicMock()
|
||||
db.scalar = AsyncMock(return_value=None)
|
||||
result = MagicMock()
|
||||
result.all.return_value = []
|
||||
result.scalars.return_value.all.return_value = []
|
||||
db.execute = AsyncMock(return_value=result)
|
||||
return EvidenceRepo(db)
|
||||
|
||||
|
||||
def _repo_with_rows(rows: list[object], *, scalar: object = None) -> EvidenceRepo:
|
||||
"""Repo whose execute().all()/scalars().all() yields rows; scalar() → scalar."""
|
||||
db = MagicMock()
|
||||
db.scalar = AsyncMock(return_value=scalar)
|
||||
result = MagicMock()
|
||||
result.all.return_value = rows
|
||||
result.scalars.return_value.all.return_value = rows
|
||||
db.execute = AsyncMock(return_value=result)
|
||||
return EvidenceRepo(db)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_constructor_stores_db_session() -> None:
|
||||
fake_db = MagicMock()
|
||||
assert EvidenceRepo(fake_db)._db is fake_db
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"method",
|
||||
[
|
||||
"list_unread_a2a",
|
||||
"list_unread_mentions",
|
||||
"list_pending_notifications",
|
||||
"task_metadata_gaps",
|
||||
"recent_team_activity",
|
||||
"blockers_in_lane",
|
||||
],
|
||||
)
|
||||
async def test_methods_return_empty_when_no_data(method: str) -> None:
|
||||
repo = _empty_repo()
|
||||
assert await getattr(repo, method)(uuid4()) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pending_notifications_maps_rows() -> None:
|
||||
ts = datetime(2026, 6, 9, 19, 27, tzinfo=UTC)
|
||||
nid, frm, tid = uuid4(), uuid4(), uuid4()
|
||||
row = SimpleNamespace(
|
||||
id=nid,
|
||||
type="alert",
|
||||
priority="high",
|
||||
subject="CEO change request",
|
||||
body="redo the contract",
|
||||
from_agent=frm,
|
||||
related_task_id=tid,
|
||||
timestamp=ts,
|
||||
)
|
||||
out = await _repo_with_rows([row]).list_pending_notifications(uuid4())
|
||||
assert out == [
|
||||
{
|
||||
"notification_id": str(nid),
|
||||
"type": "alert",
|
||||
"priority": "high",
|
||||
"subject": "CEO change request",
|
||||
"body": "redo the contract",
|
||||
"from_agent": str(frm),
|
||||
"task_id": str(tid),
|
||||
"timestamp": ts.isoformat(),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unread_a2a_maps_other_agent_and_unread_count() -> None:
|
||||
cid = uuid4()
|
||||
conv = SimpleNamespace(
|
||||
id=cid,
|
||||
agent_a="be-pm",
|
||||
agent_b="main-pm",
|
||||
unread_by_a=3,
|
||||
unread_by_b=0,
|
||||
topic="rework",
|
||||
task_id=None,
|
||||
)
|
||||
out = await _repo_with_rows([conv], scalar="be-pm").list_unread_a2a(uuid4())
|
||||
assert out == [
|
||||
{
|
||||
"conversation_id": str(cid),
|
||||
"from_agent": "main-pm",
|
||||
"unread": 3,
|
||||
"topic": "rework",
|
||||
"task_id": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_metadata_gaps_flags_missing_fields() -> None:
|
||||
repo = _repo_with_rows(
|
||||
[], scalar=SimpleNamespace(acceptance_criteria=[], description="")
|
||||
)
|
||||
assert await repo.task_metadata_gaps(uuid4()) == [
|
||||
"no acceptance criteria",
|
||||
"no description",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_journal_highlights_for_task_empty_when_no_entries() -> None:
|
||||
repo = _repo_with_rows([])
|
||||
out = await repo.journal_highlights_for_task(uuid4())
|
||||
assert out == []
|
||||
assert await _repo_with_rows([]).journal_highlights_for_task(uuid4()) == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -92,8 +141,7 @@ async def test_journal_highlights_for_task_maps_rows_with_author() -> None:
|
||||
slug="product-owner",
|
||||
role="product_owner",
|
||||
)
|
||||
repo = _repo_with_rows([row])
|
||||
out = await repo.journal_highlights_for_task(uuid4())
|
||||
out = await _repo_with_rows([row]).journal_highlights_for_task(uuid4())
|
||||
assert out == [
|
||||
{
|
||||
"author": "product-owner",
|
||||
|
||||
Reference in New Issue
Block a user