fix(gateway): A1 review-fixes — re-entry ordering, gate unit-coverage, approach check

Three fixes from the code-quality review of a1009c0:

1. Critical: _pm_sub_tasks_gate ran before _handle_pm_reentry, breaking
   idempotent re-entry for PMs whose containers crashed mid-run. Moved
   the gate to after the re-entry short-circuit so initial-claim is the
   only path that hits the gate.

2. Critical: gate had no direct unit test (the HTTP-layer test mocked
   the choreographer). Added tests/unit/gateway/test_i_will_plan_sub_tasks_gate.py
   with six tests: empty sub_tasks → incomplete_input, missing rich_plan
   → incomplete_input, filled sub_tasks → gate passes, developer with
   empty sub_tasks → gate passes (devs don't decompose), sub_tasks filled
   but approach empty → incomplete_input, in_progress re-entry short-
   circuits before gate even with no sub_tasks.

3. Important: approach was only enforced at the HTTP Pydantic boundary.
   Direct service-layer callers (MCP, test fixtures, orchestrator-
   internal) could persist a plan with no approach. Gate now also checks
   approach >= 20 chars (_PM_APPROACH_MIN_LEN constant) and includes it
   in the rejection's missing list when absent.

Plus: stale docstring on i_will_plan corrected; _handle_pm_reentry
docstring rewritten to lead with the domain reason (re-entry contracts)
not the PLR0911 linter justification; unused pytest import removed from
test_i_will_plan_rich_required.py; pre-existing PLR2004/PLC0415 issues
in that file fixed.
This commit is contained in:
Renn F
2026-05-12 02:45:48 +02:00
parent a1009c05e8
commit cfb7424c80
3 changed files with 459 additions and 54 deletions
@@ -9,16 +9,17 @@ from __future__ import annotations
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from roboco.api.deps import get_choreographer
from roboco.api.routes.v2.flow_main_pm import router
from roboco.api.schemas.v2.flow import IWillPlanRequest
_AGENT_ID = "00000000-0000-0000-0004-000000000001"
_HEADERS = {"X-Agent-ID": _AGENT_ID, "X-Agent-Role": "main_pm"}
_HTTP_UNPROCESSABLE = 422
_HTTP_OK = 200
_MIN_APPROACH_LEN = 20
def _make_envelope(error: str, missing: list[str] | None = None) -> MagicMock:
@@ -52,7 +53,7 @@ def test_i_will_plan_rejects_missing_approach() -> None:
# no approach, no sub_tasks
},
)
assert resp.status_code == 422, resp.text
assert resp.status_code == _HTTP_UNPROCESSABLE, resp.text
detail = resp.json()
assert any(
"approach" in str(err.get("loc", []))
@@ -87,7 +88,7 @@ def test_i_will_plan_rejects_empty_subtasks_for_pm() -> None:
# The gateway-side gate returns an envelope with error='incomplete_input'
# and missing=['sub_tasks']. HTTP status is 200 (envelope carries error).
body = resp.json()
if resp.status_code == 200:
if resp.status_code == _HTTP_OK:
assert body.get("error") == "incomplete_input", body
assert "sub_tasks" in (body.get("missing") or []), body
else:
@@ -98,8 +99,6 @@ def test_i_will_plan_rejects_empty_subtasks_for_pm() -> None:
def test_i_will_plan_schema_accepts_rich_plan() -> None:
"""Pydantic schema accepts a fully-formed request with rich plan fields."""
from roboco.api.schemas.v2.flow import IWillPlanRequest
req = IWillPlanRequest(
task_id=uuid4(),
plan="Route to backend",
@@ -113,5 +112,5 @@ def test_i_will_plan_schema_accepts_rich_plan() -> None:
risks=[],
open_questions=[],
)
assert len(req.approach) >= 20
assert len(req.approach) >= _MIN_APPROACH_LEN
assert len(req.sub_tasks) == 1