merge: testing → main (reconcile 2-week divergence)

This commit is contained in:
2026-04-28 18:36:00 -04:00
parent 499836c9e4
commit 862e4dbb31
1235 changed files with 160255 additions and 7996 deletions

View File

View File

@@ -0,0 +1,126 @@
"""SSE events stream — GET /api/v1/identities/events.
Mirrors :mod:`tests.api.topology.test_events_stream` — the route is
thin glue, so we drive the generator directly to dodge the full
httpx streaming roundtrip under ASGITransport.
"""
from __future__ import annotations
import asyncio
import httpx
import pytest
from decnet.bus import app as _bus_app
from decnet.bus import topics as _topics
from decnet.bus.fake import FakeBus
from decnet.web.api import app
_V1 = "/api/v1/identities"
@pytest.fixture
def _fake_app_bus(monkeypatch):
bus = FakeBus()
async def _get() -> FakeBus:
if not bus._connected:
await bus.connect()
return bus
monkeypatch.setattr(_bus_app, "get_app_bus", _get)
from decnet.web.router.identities import api_events as _ev
monkeypatch.setattr(_ev, "get_app_bus", _get)
return bus
@pytest.mark.anyio
async def test_identity_events_unauthenticated_401():
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://test",
) as ac:
r = await ac.get(f"{_V1}/events")
assert r.status_code == 401
@pytest.mark.anyio
async def test_identity_events_emits_snapshot_and_live_event(_fake_app_bus):
"""Generator yields a snapshot frame on connect, then forwards
bus events under ``identity.>`` as named SSE events."""
from decnet.web.router.identities import api_events as _ev
class _FakeRequest:
async def is_disconnected(self) -> bool:
return False
response = await _ev.api_identities_events(
request=_FakeRequest(), # type: ignore[arg-type]
user={"role": "admin", "uuid": "00000000-0000-0000-0000-000000000000"},
)
gen = response.body_iterator
def _as_text(frame) -> str:
return frame if isinstance(frame, str) else frame.decode()
async def _publish_after_snapshot() -> None:
await asyncio.sleep(0.1)
await _fake_app_bus.publish(
_topics.identity(_topics.IDENTITY_FORMED),
{
"identity_uuid": "id-1",
"observation_uuids": ["obs-1", "obs-2"],
},
event_type=_topics.IDENTITY_FORMED,
)
await asyncio.sleep(0.05)
await _fake_app_bus.publish(
_topics.identity(_topics.IDENTITY_UNMERGED),
{"resurrected_uuid": "id-2", "former_winner_uuid": "id-1"},
event_type=_topics.IDENTITY_UNMERGED,
)
pub_task = asyncio.create_task(_publish_after_snapshot())
async def _drive() -> tuple[bool, bool, bool]:
saw_snapshot = False
saw_formed = False
saw_unmerged = False
for _ in range(8):
frame = _as_text(await gen.__anext__())
if "event: snapshot" in frame:
saw_snapshot = True
if "event: formed" in frame:
saw_formed = True
if "event: unmerged" in frame:
saw_unmerged = True
if saw_snapshot and saw_formed and saw_unmerged:
break
return saw_snapshot, saw_formed, saw_unmerged
try:
saw_snapshot, saw_formed, saw_unmerged = await asyncio.wait_for(
_drive(), timeout=5.0,
)
finally:
pub_task.cancel()
try:
await pub_task
except (asyncio.CancelledError, Exception):
pass
await gen.aclose()
assert saw_snapshot
assert saw_formed
assert saw_unmerged
def test_sse_name_maps_dotted_leaves():
"""``observation.linked`` survives the topic-to-event-name mapping
intact so the frontend can switch on the full dotted leaf."""
from decnet.web.router.identities.api_events import _sse_name_for
assert _sse_name_for("identity.formed") == "formed"
assert _sse_name_for("identity.observation.linked") == "observation.linked"
assert _sse_name_for("identity.merged") == "merged"
assert _sse_name_for("identity.unmerged") == "unmerged"
# Non-identity topics pass through unchanged.
assert _sse_name_for("system.bus.health") == "system.bus.health"