Files
roboco/tests/unit/gateway/test_briefing_scope.py
T
ba7135ba50 feat(gateway): carry intake technical depth down the chain + widen review coherence scope (#491)
Two structural issues flagged by the CEO:

1. Task technical-depth dilution — intake's rich analysis (file:line
   targets, code examples, rationale) was getting lost as it traveled
   umbrella -> root-subtask -> cell -> dev. The detail IS preserved in
   Task.description; the dilution was in delegation (PMs re-authoring)
   and the intake prompt not demanding depth.

   Fixes:
   - evidence_repo: ancestor_context_for_task walks the parent chain
     (cycle-guarded, depth-capped 16, desc-clipped 1500) and surfaces it
     as parent_context in the evidence payload, so a leaf dev finally
     sees the upstream intake analysis instead of a bare title.
   - evidence_builder: Task.description now rides in the payload;
     EvidencePayload gains description + parent_context (omit-when-empty
     so no null noise).
   - orchestrator: _description_body (capped 4000) injects the
     description into the dev spawn prompt + SessionStart briefing.
   - role prompts (main_pm/cell_pm/developer/prompter): teach pass-the-
     torch, don't-dim-it; prompter now demands file:line/code-examples
     in the_work/notes (reconciled with the no-code-level-ACs-on-roots
     rule). main_pm's brief-not-a-spec scoped: not-a-spec applies to the
     solution only, facts forward verbatim.

2. PR-review/QA scope too narrow — they only checked the AC checklist,
   not whether the change is coherent with project structure/intent.

   Fixes:
   - qa.md + pr_reviewer.md: Coherence & intent rule (intent via
     description+parent_context, coherence with project patterns,
     standards). Criterion-less major findings allowed for intent drift
     (Finding.criterion is optional).
   - parent_context + description wired into the gate/QA/inbound-PR
     evidence builders (fail-open, logged).

Skipped per YAGNI: a technical_spec JSONB column (detail is already in
description) and a criterion_kind enum (criterion is already optional).

All gates green: ruff, mypy (1152), pytest (12883 passed, 94.82% cov),
xenon, vulture, bandit, pip-audit, deptry, alembic, import-linter,
foundation-check.

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-13 08:06:57 +02:00

161 lines
6.4 KiB
Python

"""Claim-scoped context briefing — `_briefing_for(full=...)`.
Only context-acquisition verbs (give_me_work / claim / plan / resume / triage)
carry the heavy, verb-invariant sections (company_goals, recent_team_activity,
blockers_in_my_lane, task_handoff, institutional_memory). Every other verb gets
the slim signals-only briefing, and the heavy repo queries are not even issued.
"""
from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
import pytest
from roboco.services.gateway.choreographer import Choreographer
from roboco.services.gateway.evidence_builder import (
EVIDENCE_DIFF_CAP_CHARS,
build_evidence_for_task,
truncate_diff,
)
_PR_NUMBER = 8
def _choreographer_with_repo() -> tuple[Choreographer, AsyncMock]:
repo = AsyncMock()
repo.list_unread_a2a.return_value = [{"conversation_id": "c1", "unread": 2}]
repo.list_unread_mentions.return_value = []
repo.list_pending_notifications.return_value = [{"notification_id": "n1"}]
repo.task_metadata_gaps.return_value = []
repo.recent_team_activity.return_value = [{"task_id": "t1", "status": "pending"}]
repo.blockers_in_lane.return_value = [{"task_id": "b1"}]
repo.company_goals.return_value = {"north_star": "win"}
repo.journal_highlights_for_task.return_value = []
choreo = object.__new__(Choreographer)
choreo._deps = MagicMock(evidence_repo=repo)
return choreo, repo
class TestBriefingScope:
@pytest.mark.asyncio
async def test_slim_default_carries_signals_only(self) -> None:
choreo, repo = _choreographer_with_repo()
briefing = await choreo._briefing_for(uuid4(), None)
assert briefing["unread_a2a"] == [{"conversation_id": "c1", "unread": 2}]
assert briefing["pending_notifications"] == [{"notification_id": "n1"}]
for heavy in (
"company_goals",
"recent_team_activity",
"blockers_in_my_lane",
"task_handoff",
"institutional_memory",
):
assert heavy not in briefing
# The heavy queries are not even issued on the slim path.
repo.recent_team_activity.assert_not_awaited()
repo.blockers_in_lane.assert_not_awaited()
repo.company_goals.assert_not_awaited()
repo.journal_highlights_for_task.assert_not_awaited()
@pytest.mark.asyncio
async def test_full_carries_heavy_sections(self) -> None:
choreo, repo = _choreographer_with_repo()
briefing = await choreo._briefing_for(uuid4(), None, full=True)
assert briefing["company_goals"] == {"north_star": "win"}
assert briefing["recent_team_activity"] == [
{"task_id": "t1", "status": "pending"}
]
assert briefing["blockers_in_my_lane"] == [{"task_id": "b1"}]
repo.company_goals.assert_awaited_once()
@pytest.mark.asyncio
async def test_full_with_task_builds_handoff(self) -> None:
choreo, repo = _choreographer_with_repo()
task_id = uuid4()
task = MagicMock(
pr_number=_PR_NUMBER,
pr_url="https://github.com/x/y/pull/8",
branch_name="feature/backend/abc",
commits=[{"sha": "abc123", "message": "feat: x"}],
quick_context=None,
)
briefing = await choreo._briefing_for(uuid4(), task_id, task=task, full=True)
assert briefing["task_handoff"]["pr_number"] == _PR_NUMBER
repo.journal_highlights_for_task.assert_awaited_once_with(
task_id, include_ancestors=True
)
@pytest.mark.asyncio
async def test_slim_with_task_omits_handoff(self) -> None:
choreo, repo = _choreographer_with_repo()
task = MagicMock(pr_number=8, commits=[{"sha": "abc123"}])
briefing = await choreo._briefing_for(uuid4(), uuid4(), task=task)
assert "task_handoff" not in briefing
repo.journal_highlights_for_task.assert_not_awaited()
@pytest.mark.asyncio
async def test_include_company_goals_fetches_only_that_heavy_field(self) -> None:
"""The board_triage idle-branch opt-in: company_goals reaches the
briefing without pulling in the rest of ``full``'s heavy sections."""
choreo, repo = _choreographer_with_repo()
briefing = await choreo._briefing_for(uuid4(), None, include_company_goals=True)
assert briefing["company_goals"] == {"north_star": "win"}
repo.company_goals.assert_awaited_once()
for heavy in (
"recent_team_activity",
"blockers_in_my_lane",
"task_handoff",
"institutional_memory",
):
assert heavy not in briefing
# No heavy queries beyond the one company_goals lookup.
repo.recent_team_activity.assert_not_awaited()
repo.blockers_in_lane.assert_not_awaited()
repo.journal_highlights_for_task.assert_not_awaited()
@pytest.mark.asyncio
async def test_full_true_ignores_include_company_goals_no_double_fetch(
self,
) -> None:
"""``full=True`` already resolves company_goals via the heavy-sections
batch; passing include_company_goals=True too must not issue a second
query."""
choreo, repo = _choreographer_with_repo()
briefing = await choreo._briefing_for(
uuid4(), None, full=True, include_company_goals=True
)
assert briefing["company_goals"] == {"north_star": "win"}
repo.company_goals.assert_awaited_once()
class TestPayloadCaps:
def test_truncate_diff_caps_and_annotates(self) -> None:
big = "x" * (EVIDENCE_DIFF_CAP_CHARS + 5_000)
capped = truncate_diff(big)
assert capped is not None
assert len(capped) < len(big)
assert capped.startswith("x" * 100) # head preserved
assert "diff truncated" in capped
def test_truncate_diff_passes_small_and_none(self) -> None:
assert truncate_diff("small diff") == "small diff"
assert truncate_diff(None) is None
def test_build_evidence_caps_the_diff(self) -> None:
task = MagicMock(
pr_number=None,
pr_url=None,
commits=[],
dev_notes=None,
acceptance_criteria_status=[],
)
ev = build_evidence_for_task(
task,
journal_highlights=[],
files_changed=[],
pr_diff_summary="y" * (EVIDENCE_DIFF_CAP_CHARS * 2),
)
assert ev.pr_diff_summary is not None
assert "diff truncated" in ev.pr_diff_summary