mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
[8323cd50] fix(e2e_smoke): repair auditor-trigger smoke tests and harden harness
This commit is contained in:
@@ -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:
|
||||||
|
try:
|
||||||
await conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
|
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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,9 +149,14 @@ 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:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=5.0, headers=_SYSTEM_API_HEADERS
|
||||||
|
) as client:
|
||||||
await orch._dispatch_audit_work(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
|
||||||
assert call is not None
|
assert call is not None
|
||||||
@@ -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,9 +212,14 @@ 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:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=5.0, headers=_SYSTEM_API_HEADERS
|
||||||
|
) as client:
|
||||||
await orch._dispatch_audit_work(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
|
||||||
assert call is not None
|
assert call is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user