fix(gateway): evidence surfaces the task's board/PM handoff journal entries

journal_highlights_for_task was a Phase-1 stub returning [], so evidence()
never surfaced the upstream Product Owner / Head of Marketing review. The
Main PM picked up a board-reviewed coordination task, saw no handoff, and
re-researched from scratch (duplicated work). Query the task's
decision/reflection/note entries across authors (slug + role), oldest
first, so the Main PM builds on the board's analysis instead of redoing it.
This commit is contained in:
Renn F
2026-06-03 08:27:10 +02:00
parent 815f3ebad3
commit a5af6159d7
2 changed files with 92 additions and 8 deletions
+49 -2
View File
@@ -42,5 +42,52 @@ class EvidenceRepo:
return [] return []
async def journal_highlights_for_task(self, task_id: UUID) -> list[dict[str, Any]]: async def journal_highlights_for_task(self, task_id: UUID) -> list[dict[str, Any]]:
del task_id """The task's upstream handoff: every author's decision / reflection /
return [] note journal entry tied to this task, oldest first.
This is what lets a downstream owner — e.g. the Main PM picking up a
board-reviewed coordination task — read the Product Owner / Head of
Marketing analysis instead of re-deriving it. Each row carries the
author (slug + role) so the reader knows whose handoff it is. Learning
and struggle entries are personal and excluded. Ownership is enforced by
the caller (``evidence`` only serves the task's assignee), so private
task-scoped entries are surfaced to the owner who needs the full handoff.
"""
from sqlalchemy import select
from roboco.db.tables import AgentTable, JournalEntryTable, JournalTable
from roboco.models.base import JournalEntryType
handoff_types = (
JournalEntryType.DECISION_LOG,
JournalEntryType.TASK_REFLECTION,
JournalEntryType.GENERAL,
)
query = (
select(
JournalEntryTable.type,
JournalEntryTable.title,
JournalEntryTable.content,
JournalEntryTable.timestamp,
AgentTable.slug,
AgentTable.role,
)
.join(JournalTable, JournalEntryTable.journal_id == JournalTable.id)
.join(AgentTable, JournalTable.agent_id == AgentTable.id)
.where(JournalEntryTable.task_id == task_id)
.where(JournalEntryTable.type.in_(handoff_types))
.order_by(JournalEntryTable.timestamp.asc())
.limit(50)
)
result = await self._db.execute(query)
return [
{
"author": row.slug,
"author_role": str(row.role),
"type": str(row.type),
"title": row.title,
"content": row.content,
"timestamp": row.timestamp.isoformat() if row.timestamp else None,
}
for row in result.all()
]
+43 -6
View File
@@ -1,13 +1,15 @@
"""roboco.services.gateway.evidence_repo coverage — Phase 1 stubs. """roboco.services.gateway.evidence_repo coverage.
EvidenceRepo currently returns empty lists for every method (Phase 1). Most methods are still Phase 1 stubs returning empty lists;
Tests confirm the contract and the constructor stores the session — when ``journal_highlights_for_task`` is wired to a real query, so it is tested
Phase 2+ wires real queries, these tests will need real-DB integration. against a mocked ``execute`` result that stands in for the DB rows.
""" """
from __future__ import annotations from __future__ import annotations
from unittest.mock import MagicMock from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4 from uuid import uuid4
import pytest import pytest
@@ -63,7 +65,42 @@ async def test_blockers_in_lane_returns_empty(repo: EvidenceRepo) -> None:
assert out == [] assert out == []
def _repo_with_rows(rows: list[object]) -> EvidenceRepo:
"""EvidenceRepo whose db.execute() yields a result with the given rows."""
db = MagicMock()
result = MagicMock()
result.all.return_value = rows
db.execute = AsyncMock(return_value=result)
return EvidenceRepo(db)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_journal_highlights_for_task_returns_empty(repo: EvidenceRepo) -> None: async def test_journal_highlights_for_task_empty_when_no_entries() -> None:
repo = _repo_with_rows([])
out = await repo.journal_highlights_for_task(uuid4()) out = await repo.journal_highlights_for_task(uuid4())
assert out == [] assert out == []
@pytest.mark.asyncio
async def test_journal_highlights_for_task_maps_rows_with_author() -> None:
ts = datetime(2026, 6, 3, 5, 30, tzinfo=UTC)
row = SimpleNamespace(
type="decision_log",
title="PO review",
content="Approve with scope amendments.",
timestamp=ts,
slug="product-owner",
role="product_owner",
)
repo = _repo_with_rows([row])
out = await repo.journal_highlights_for_task(uuid4())
assert out == [
{
"author": "product-owner",
"author_role": "product_owner",
"type": "decision_log",
"title": "PO review",
"content": "Approve with scope amendments.",
"timestamp": ts.isoformat(),
}
]