mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
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:
@@ -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()
|
||||||
|
]
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user