[8323cd50] fix(e2e_smoke): repair auditor-trigger smoke tests and harden harness

This commit is contained in:
Backend Developer 1
2026-07-13 08:48:54 +00:00
parent 65e5087a7d
commit babffe0a7c
3 changed files with 40 additions and 14 deletions
+10 -1
View File
@@ -189,7 +189,16 @@ async def _test_database_url() -> AsyncIterator[str]:
pgvector_engine = create_async_engine(test_url_async, future=True) pgvector_engine = create_async_engine(test_url_async, future=True)
try: try:
async with pgvector_engine.begin() as conn: async with pgvector_engine.begin() as conn:
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector")) try:
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
except Exception:
# Dev/sandbox Postgres may lack pgvector; the core schema does
# not require it and the e2e smoke suite does not exercise RAG.
# Continue so tests can run in lightweight sandboxes.
warnings.warn(
"pgvector extension unavailable - continuing without it",
stacklevel=2,
)
finally: finally:
await pgvector_engine.dispose() await pgvector_engine.dispose()
+6
View File
@@ -476,6 +476,12 @@ class ScriptedAgent:
os.environ["ROBOCO_AGENT_ROLE"] = self.role os.environ["ROBOCO_AGENT_ROLE"] = self.role
os.environ["ROBOCO_ORCHESTRATOR_URL"] = self.stack.base_url os.environ["ROBOCO_ORCHESTRATOR_URL"] = self.stack.base_url
os.environ["ROBOCO_TOOL_MANIFEST_PATH"] = str(self._manifest_path) os.environ["ROBOCO_TOOL_MANIFEST_PATH"] = str(self._manifest_path)
# The host agent environment may carry a real ROBOCO_AGENT_TOKEN issued
# for the test runner's identity. flow_server reads it at import time
# and forwards it on every call; the token won't match the ephemeral
# test agent IDs and causes 401s. Drop it so tests run in the same
# unsigned-token mode as CI.
os.environ.pop("ROBOCO_AGENT_TOKEN", None)
module = importlib.import_module(name) module = importlib.import_module(name)
if getattr(module, "AGENT_ID", None) != str(self.agent_id): if getattr(module, "AGENT_ID", None) != str(self.agent_id):
module = importlib.reload(module) module = importlib.reload(module)
+24 -13
View File
@@ -17,20 +17,22 @@ asserts on the dispatch decision, not the LLM runtime.
from __future__ import annotations from __future__ import annotations
import asyncio
from http import HTTPStatus from http import HTTPStatus
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
import httpx import httpx
import pytest
from roboco.config import settings from roboco.config import settings
from roboco.models import NotificationType
from roboco.models.base import TaskStatus from roboco.models.base import TaskStatus
from roboco.runtime.orchestrator import AgentOrchestrator from roboco.runtime.orchestrator import _SYSTEM_API_HEADERS, AgentOrchestrator
from tests.e2e_smoke.arcs import seed_company, seed_project, seed_task from tests.e2e_smoke.arcs import seed_company, seed_project, seed_task
if TYPE_CHECKING: if TYPE_CHECKING:
from uuid import UUID from uuid import UUID
import pytest
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from tests.e2e_smoke.harness import E2EStack from tests.e2e_smoke.harness import E2EStack
@@ -109,7 +111,8 @@ def _notifications_for_task(
def _fresh_orchestrator(stack: E2EStack, monkeypatch: pytest.MonkeyPatch) -> Any: def _fresh_orchestrator(stack: E2EStack, monkeypatch: pytest.MonkeyPatch) -> Any:
"""Return a bare orchestrator whose internal API points at the e2e app.""" """Return a bare orchestrator whose internal API points at the e2e app."""
monkeypatch.setattr(settings, "internal_api_url", f"{stack.base_url}/api") # internal_api_url is a computed property; patch its input api_url instead.
monkeypatch.setattr(settings, "api_url", stack.base_url)
orch: Any = AgentOrchestrator.__new__(AgentOrchestrator) orch: Any = AgentOrchestrator.__new__(AgentOrchestrator)
# __new__ bypasses __init__, so the instance attributes that # __new__ bypasses __init__, so the instance attributes that
# _is_agent_active and _dispatch_audit_work read must be initialized here. # _is_agent_active and _dispatch_audit_work read must be initialized here.
@@ -119,8 +122,7 @@ def _fresh_orchestrator(stack: E2EStack, monkeypatch: pytest.MonkeyPatch) -> Any
return orch return orch
@pytest.mark.asyncio def test_scheduled_audit_trigger_spawns_auditor(
async def test_scheduled_audit_trigger_spawns_auditor(
e2e_stack: E2EStack, monkeypatch: pytest.MonkeyPatch e2e_stack: E2EStack, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""The scheduled sweep spawns the auditor when delivery activity is recent.""" """The scheduled sweep spawns the auditor when delivery activity is recent."""
@@ -147,8 +149,13 @@ async def test_scheduled_audit_trigger_spawns_auditor(
orch = _fresh_orchestrator(stack, monkeypatch) orch = _fresh_orchestrator(stack, monkeypatch)
monkeypatch.setattr(settings, "audit_interval_seconds", 60) monkeypatch.setattr(settings, "audit_interval_seconds", 60)
async with httpx.AsyncClient(timeout=5.0) as client: async def _dispatch() -> None:
await orch._dispatch_audit_work(client) async with httpx.AsyncClient(
timeout=5.0, headers=_SYSTEM_API_HEADERS
) as client:
await orch._dispatch_audit_work(client)
asyncio.run(_dispatch())
orch.spawn_agent.assert_awaited_once() orch.spawn_agent.assert_awaited_once()
call = orch.spawn_agent.await_args call = orch.spawn_agent.await_args
@@ -159,12 +166,11 @@ async def test_scheduled_audit_trigger_spawns_auditor(
assert "SCHEDULED AUDIT SWEEP" in prompt assert "SCHEDULED AUDIT SWEEP" in prompt
# No reactive alert should have been created for this path. # No reactive alert should have been created for this path.
assert _notifications_for_task(stack, task_id, "ALERT") == [] assert _notifications_for_task(stack, task_id, NotificationType.ALERT) == []
assert auditor_id is not None # auditor was seeded and resolved assert auditor_id is not None # auditor was seeded and resolved
@pytest.mark.asyncio def test_reactive_alert_producer_spawns_auditor(
async def test_reactive_alert_producer_spawns_auditor(
e2e_stack: E2EStack, monkeypatch: pytest.MonkeyPatch e2e_stack: E2EStack, monkeypatch: pytest.MonkeyPatch
) -> None: ) -> None:
"""QA-fail emits an auditor-targeted ALERT; the dispatcher spawns the auditor.""" """QA-fail emits an auditor-targeted ALERT; the dispatcher spawns the auditor."""
@@ -197,7 +203,7 @@ async def test_reactive_alert_producer_spawns_auditor(
f"fail-qa: {resp.status_code} {resp.text[:1500]}" f"fail-qa: {resp.status_code} {resp.text[:1500]}"
) )
alerts = _notifications_for_task(stack, task_id, "ALERT") alerts = _notifications_for_task(stack, task_id, NotificationType.ALERT)
assert len(alerts) == 1, alerts assert len(alerts) == 1, alerts
alert = alerts[0] alert = alerts[0]
assert "rework alert" in alert["subject"].lower(), alert assert "rework alert" in alert["subject"].lower(), alert
@@ -206,8 +212,13 @@ async def test_reactive_alert_producer_spawns_auditor(
orch = _fresh_orchestrator(stack, monkeypatch) orch = _fresh_orchestrator(stack, monkeypatch)
monkeypatch.setattr(settings, "audit_interval_seconds", 60) monkeypatch.setattr(settings, "audit_interval_seconds", 60)
async with httpx.AsyncClient(timeout=5.0) as client: async def _dispatch() -> None:
await orch._dispatch_audit_work(client) async with httpx.AsyncClient(
timeout=5.0, headers=_SYSTEM_API_HEADERS
) as client:
await orch._dispatch_audit_work(client)
asyncio.run(_dispatch())
orch.spawn_agent.assert_awaited_once() orch.spawn_agent.assert_awaited_once()
call = orch.spawn_agent.await_args call = orch.spawn_agent.await_args