rewrite phase 1
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import asyncio
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
import agentci.app as app_module
|
||||
|
||||
|
||||
class BlockingWorker:
|
||||
def __init__(self, events: list[str]) -> None:
|
||||
self.events = events
|
||||
self.started = asyncio.Event()
|
||||
|
||||
async def run(self, stop: asyncio.Event) -> None:
|
||||
self.events.append("worker-started")
|
||||
self.started.set()
|
||||
try:
|
||||
await stop.wait()
|
||||
except asyncio.CancelledError:
|
||||
self.events.append(f"worker-cancelled:{stop.is_set()}")
|
||||
raise
|
||||
|
||||
|
||||
class FailingWorker:
|
||||
def __init__(self) -> None:
|
||||
self.started = asyncio.Event()
|
||||
|
||||
async def run(self, _stop: asyncio.Event) -> None:
|
||||
self.started.set()
|
||||
raise RuntimeError("worker failed")
|
||||
|
||||
|
||||
class FakeRuntime:
|
||||
def __init__(self, worker: object, events: list[str]) -> None:
|
||||
self.worker = worker
|
||||
self.events = events
|
||||
self.closed = False
|
||||
|
||||
async def close(self) -> None:
|
||||
self.events.append("runtime-closed")
|
||||
self.closed = True
|
||||
|
||||
|
||||
async def test_lifespan_starts_worker_cancels_it_and_closes_runtime(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
events: list[str] = []
|
||||
worker = BlockingWorker(events)
|
||||
runtime = FakeRuntime(worker, events)
|
||||
selected_settings = SimpleNamespace(name="selected")
|
||||
built_with: list[object] = []
|
||||
configured: list[bool] = []
|
||||
|
||||
async def build(settings: object) -> FakeRuntime:
|
||||
built_with.append(settings)
|
||||
return runtime
|
||||
|
||||
monkeypatch.setattr(app_module, "build_runtime", build)
|
||||
monkeypatch.setattr(app_module, "configure_logging", lambda: configured.append(True))
|
||||
application = app_module.create_app(selected_settings) # type: ignore[arg-type]
|
||||
|
||||
async with application.router.lifespan_context(application):
|
||||
await worker.started.wait()
|
||||
assert application.state.runtime is runtime
|
||||
assert not runtime.closed
|
||||
|
||||
assert built_with == [selected_settings]
|
||||
assert configured == [True]
|
||||
assert events == ["worker-started", "worker-cancelled:True", "runtime-closed"]
|
||||
|
||||
|
||||
async def test_lifespan_closes_runtime_when_worker_task_fails(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
events: list[str] = []
|
||||
worker = FailingWorker()
|
||||
runtime = FakeRuntime(worker, events)
|
||||
|
||||
async def build(_settings: object) -> FakeRuntime:
|
||||
return runtime
|
||||
|
||||
monkeypatch.setattr(app_module, "build_runtime", build)
|
||||
monkeypatch.setattr(app_module, "configure_logging", lambda: None)
|
||||
application = app_module.create_app(SimpleNamespace()) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(RuntimeError, match="worker failed"):
|
||||
async with application.router.lifespan_context(application):
|
||||
await worker.started.wait()
|
||||
|
||||
assert runtime.closed
|
||||
assert events == ["runtime-closed"]
|
||||
|
||||
|
||||
async def test_lifespan_propagates_runtime_startup_failure_without_starting_worker(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
configured: list[bool] = []
|
||||
|
||||
async def fail_build(_settings: object) -> None:
|
||||
raise RuntimeError("database unavailable")
|
||||
|
||||
monkeypatch.setattr(app_module, "build_runtime", fail_build)
|
||||
monkeypatch.setattr(app_module, "configure_logging", lambda: configured.append(True))
|
||||
application = app_module.create_app(SimpleNamespace()) # type: ignore[arg-type]
|
||||
|
||||
with pytest.raises(RuntimeError, match="database unavailable"):
|
||||
async with application.router.lifespan_context(application):
|
||||
pytest.fail("startup failure must prevent serving requests")
|
||||
|
||||
assert configured == [True]
|
||||
assert not hasattr(application.state, "runtime")
|
||||
|
||||
|
||||
async def test_global_exception_handler_returns_safe_json() -> None:
|
||||
application = app_module.create_app(SimpleNamespace()) # type: ignore[arg-type]
|
||||
|
||||
@application.get("/explode")
|
||||
async def explode() -> None:
|
||||
raise RuntimeError("sensitive provider detail")
|
||||
|
||||
async with AsyncClient(
|
||||
transport=ASGITransport(app=application, raise_app_exceptions=False),
|
||||
base_url="http://test",
|
||||
) as client:
|
||||
response = await client.get("/explode")
|
||||
|
||||
assert response.status_code == 500
|
||||
assert response.json() == {"detail": "Internal server error. See service logs for diagnostics."}
|
||||
Reference in New Issue
Block a user