From f680db34c673fa4afb28ba0a2c3f447fac9df443 Mon Sep 17 00:00:00 2001 From: Renn F Date: Tue, 12 May 2026 04:05:44 +0200 Subject: [PATCH] =?UTF-8?q?fix(gateway):=20B3=20canonical=20say()=20return?= =?UTF-8?q?=20status=20=E2=80=94=20always=20'posted'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke run 3 showed inconsistent return strings — main-pm got status='sent', be-pm got status='posted' for the same verb. Confirmed say() already returns 'posted' at its sole success exit. Added test_say_status.py to pin the canonical past-tense pattern (note->'noted', say->'posted', notify_ack->'acked') and prevent regression. dm() and notify() retain 'sent' — different verbs, different semantics. Spec ref: docs/superpowers/specs/2026-05-12-post-smoke-3-fixes-design.md section B3. --- tests/unit/gateway/test_say_status.py | 94 +++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/unit/gateway/test_say_status.py diff --git a/tests/unit/gateway/test_say_status.py b/tests/unit/gateway/test_say_status.py new file mode 100644 index 00000000..7e5cbc2f --- /dev/null +++ b/tests/unit/gateway/test_say_status.py @@ -0,0 +1,94 @@ +"""Wave B3 (2026-05-12): say() returns canonical 'posted' status. + +Smoke run 3 showed main-pm got status='sent' while be-pm got status='posted' +for the same verb. This test pins the canonical past-tense pattern: + note() -> 'noted' + say() -> 'posted' (this file) + notify_ack() -> 'acked' + +dm() and notify() return 'sent' — those are different verbs with their own +semantics and are intentionally not touched here. +""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock +from uuid import uuid4 + +import pytest +from roboco.services.gateway.content_actions import ContentActions, ContentActionsDeps + + +def _make_deps(**overrides: AsyncMock) -> ContentActionsDeps: + if "task" in overrides: + task = overrides["task"] + else: + task = AsyncMock() + task.agent_for.return_value = MagicMock(role="developer", slug="be-dev-1") + task.get_active_task_for_agent.return_value = None + + git = overrides.get("git", AsyncMock()) + messaging = overrides.get("messaging", AsyncMock()) + a2a = overrides.get("a2a", AsyncMock()) + journal = overrides.get("journal", AsyncMock()) + workspace = overrides.get("workspace", AsyncMock()) + notifications = overrides.get("notifications", AsyncMock()) + notification_delivery = overrides.get("notification_delivery", AsyncMock()) + return ContentActionsDeps( + task=task, + git=git, + messaging=messaging, + a2a=a2a, + journal=journal, + workspace=workspace, + notifications=notifications, + notification_delivery=notification_delivery, + ) + + +@pytest.mark.asyncio +async def test_say_returns_posted_status() -> None: + """say() returns status='posted' (past-tense, aligned with 'noted'/'acked').""" + ca = ContentActions(_make_deps()) + env = await ca.say( + agent_id=uuid4(), + channel="backend-cell", + text="hello team", + ) + body = env.as_dict() + assert body["error"] is None, body + assert body["status"] == "posted", body + + +@pytest.mark.asyncio +async def test_say_posted_status_with_active_task() -> None: + """say() returns 'posted' even when an active task is auto-injected.""" + task_id = uuid4() + task_obj = MagicMock(id=task_id, status="in_progress") + task_svc = AsyncMock() + task_svc.agent_for.return_value = MagicMock(role="developer", slug="be-dev-1") + task_svc.get_active_task_for_agent.return_value = task_obj + + ca = ContentActions(_make_deps(task=task_svc)) + env = await ca.say( + agent_id=uuid4(), + channel="backend-cell", + text="progress update", + ) + body = env.as_dict() + assert body["error"] is None, body + assert body["status"] == "posted", body + assert body["task_id"] == str(task_id), body + + +@pytest.mark.asyncio +async def test_say_next_is_continue() -> None: + """say() sets next='continue' so agents know to keep working.""" + ca = ContentActions(_make_deps()) + env = await ca.say( + agent_id=uuid4(), + channel="dev-all", + text="syncing up", + ) + body = env.as_dict() + assert body["next"] == "continue", body