fix(notifications): one duplicate-tolerant role lookup for every singleton-role site

The duplicate-CEO-row fix landed on one call site but three identical
bare 'select(...).where(role == ...)' + scalar_one_or_none() lookups
remained in the orchestrator (auditor + two CEO), each still raising
MultipleResultsFound under the same condition. All five sites now route
through a shared get_agent_by_role helper (earliest-created row wins).
The five event-bus notification handlers also thread task_title so a
revived producer renders titles instead of #id8 (protocol signatures
updated to match the service), the pull_request CI trigger mirrors the
push trigger's paths so a fork PR touching only those paths still runs
CI, and a stale a2a comment about auditor/pr_reviewer lacking read_a2a
is corrected.
This commit is contained in:
Renn F
2026-07-22 03:30:50 +02:00
parent 161b36b563
commit 5143aab698
10 changed files with 196 additions and 43 deletions
@@ -240,3 +240,49 @@ async def test_get_ceo_agent_tolerates_duplicate_ceo_rows(env: dict) -> None:
# Does not raise, and pins to the earliest-created (never the later row).
assert resolved is not None
assert resolved.id != later_ceo.id
@pytest.mark.asyncio
async def test_get_auditor_agent_tolerates_duplicate_auditor_rows(env: dict) -> None:
"""Mirrors `test_get_ceo_agent_tolerates_duplicate_ceo_rows`: a second
role=AUDITOR row must not make `_get_auditor_agent()` raise
MultipleResultsFound — both now delegate to the shared
`get_agent_by_role` helper."""
db = env["db"]
first_auditor = AgentTable(
id=uuid4(),
name="Auditor 1",
slug=f"auditor-{uuid4().hex[:6]}",
role=AgentRole.AUDITOR,
team=None,
status=AgentStatus.ACTIVE,
model_config={},
system_prompt="x",
capabilities=[],
permissions={},
metrics={},
)
db.add(first_auditor)
await db.flush()
later_auditor = AgentTable(
id=uuid4(),
name="Auditor 2",
slug=f"auditor-{uuid4().hex[:6]}",
role=AgentRole.AUDITOR,
team=None,
status=AgentStatus.ACTIVE,
model_config={},
system_prompt="x",
capabilities=[],
permissions={},
metrics={},
created_at=datetime.now(UTC) + timedelta(hours=1),
)
db.add(later_auditor)
await db.flush()
delivery = get_notification_delivery_service(db)
resolved = await delivery._get_auditor_agent()
assert resolved is not None
assert resolved.id != later_auditor.id
+46
View File
@@ -13,6 +13,7 @@ from roboco.models import AgentRole, AgentStatus, Team
from roboco.services.repositories.query_helpers import (
agent_id_filter,
days_ago,
get_agent_by_role,
get_agent_by_slug,
get_agent_slug,
pagination,
@@ -256,3 +257,48 @@ async def test_get_agent_by_slug(db_session: AsyncSession) -> None:
@pytest.mark.asyncio
async def test_get_agent_by_slug_missing(db_session: AsyncSession) -> None:
assert await get_agent_by_slug(db_session, "ghost-slug") is None
@pytest.mark.asyncio
async def test_get_agent_by_role_tolerates_duplicate_rows(
db_session: AsyncSession,
) -> None:
"""A second row of the same singleton role (a real hazard for CEO/AUDITOR
in prod, and for the shared test DB here) must not raise
MultipleResultsFound — the later-created duplicate is never selected."""
earlier = AgentTable(
id=uuid4(),
name="Auditor Earlier",
slug=f"qh-role-a-{uuid4().hex[:8]}",
role=AgentRole.AUDITOR,
team=None,
status=AgentStatus.ACTIVE,
model_config={},
system_prompt="x",
capabilities=[],
permissions={},
metrics={},
)
db_session.add(earlier)
await db_session.flush()
later = AgentTable(
id=uuid4(),
name="Auditor Later",
slug=f"qh-role-b-{uuid4().hex[:8]}",
role=AgentRole.AUDITOR,
team=None,
status=AgentStatus.ACTIVE,
model_config={},
system_prompt="x",
capabilities=[],
permissions={},
metrics={},
created_at=datetime.now(UTC) + timedelta(hours=1),
)
db_session.add(later)
await db_session.flush()
resolved = await get_agent_by_role(db_session, AgentRole.AUDITOR)
assert resolved is not None
assert resolved.id != later.id