diff --git a/dist/app.py b/dist/app.py index 9d378d5..088db4b 100644 --- a/dist/app.py +++ b/dist/app.py @@ -16043,18 +16043,19 @@ async def _stream(): last_seq = ev.seq yield f"data: {envelope.model_dump_json()}\n\n" # Tail: poll for new rows. Bounded by client-disconnect. - try: - while not await request.is_disconnected(): - await _asyncio.sleep(0.25) - for ev in event_log.iter_for(session_id, since=last_seq): - envelope = EventEnvelope( - seq=ev.seq, session_id=ev.session_id, - kind=ev.kind, payload=ev.payload, ts=ev.ts, - ) - last_seq = ev.seq - yield f"data: {envelope.model_dump_json()}\n\n" - except _asyncio.CancelledError: - return + # CancelledError (from task cancellation, e.g. when the + # client closes the connection) propagates naturally — no + # try/except needed; suppressing it would break asyncio's + # cancellation contract (Sonar python:S7497). + while not await request.is_disconnected(): + await _asyncio.sleep(0.25) + for ev in event_log.iter_for(session_id, since=last_seq): + envelope = EventEnvelope( + seq=ev.seq, session_id=ev.session_id, + kind=ev.kind, payload=ev.payload, ts=ev.ts, + ) + last_seq = ev.seq + yield f"data: {envelope.model_dump_json()}\n\n" return StreamingResponse(_stream(), media_type="text/event-stream") diff --git a/dist/apps/code-review.py b/dist/apps/code-review.py index 343544f..40defdf 100644 --- a/dist/apps/code-review.py +++ b/dist/apps/code-review.py @@ -16096,18 +16096,19 @@ async def _stream(): last_seq = ev.seq yield f"data: {envelope.model_dump_json()}\n\n" # Tail: poll for new rows. Bounded by client-disconnect. - try: - while not await request.is_disconnected(): - await _asyncio.sleep(0.25) - for ev in event_log.iter_for(session_id, since=last_seq): - envelope = EventEnvelope( - seq=ev.seq, session_id=ev.session_id, - kind=ev.kind, payload=ev.payload, ts=ev.ts, - ) - last_seq = ev.seq - yield f"data: {envelope.model_dump_json()}\n\n" - except _asyncio.CancelledError: - return + # CancelledError (from task cancellation, e.g. when the + # client closes the connection) propagates naturally — no + # try/except needed; suppressing it would break asyncio's + # cancellation contract (Sonar python:S7497). + while not await request.is_disconnected(): + await _asyncio.sleep(0.25) + for ev in event_log.iter_for(session_id, since=last_seq): + envelope = EventEnvelope( + seq=ev.seq, session_id=ev.session_id, + kind=ev.kind, payload=ev.payload, ts=ev.ts, + ) + last_seq = ev.seq + yield f"data: {envelope.model_dump_json()}\n\n" return StreamingResponse(_stream(), media_type="text/event-stream") diff --git a/dist/apps/incident-management.py b/dist/apps/incident-management.py index 083c713..563f9b4 100644 --- a/dist/apps/incident-management.py +++ b/dist/apps/incident-management.py @@ -16108,18 +16108,19 @@ async def _stream(): last_seq = ev.seq yield f"data: {envelope.model_dump_json()}\n\n" # Tail: poll for new rows. Bounded by client-disconnect. - try: - while not await request.is_disconnected(): - await _asyncio.sleep(0.25) - for ev in event_log.iter_for(session_id, since=last_seq): - envelope = EventEnvelope( - seq=ev.seq, session_id=ev.session_id, - kind=ev.kind, payload=ev.payload, ts=ev.ts, - ) - last_seq = ev.seq - yield f"data: {envelope.model_dump_json()}\n\n" - except _asyncio.CancelledError: - return + # CancelledError (from task cancellation, e.g. when the + # client closes the connection) propagates naturally — no + # try/except needed; suppressing it would break asyncio's + # cancellation contract (Sonar python:S7497). + while not await request.is_disconnected(): + await _asyncio.sleep(0.25) + for ev in event_log.iter_for(session_id, since=last_seq): + envelope = EventEnvelope( + seq=ev.seq, session_id=ev.session_id, + kind=ev.kind, payload=ev.payload, ts=ev.ts, + ) + last_seq = ev.seq + yield f"data: {envelope.model_dump_json()}\n\n" return StreamingResponse(_stream(), media_type="text/event-stream") diff --git a/sonar-project.properties b/sonar-project.properties index 3c31a9f..53a01b4 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -26,10 +26,23 @@ sonar.test.exclusions=tests/fixtures/** # ``skill.kind``. Same pattern as gateway above. sonar.cpd.exclusions=src/runtime/tools/gateway.py,src/runtime/agents/responsive.py -# Coverage exclusions — UI is excluded because Streamlit rendering is exercised -# manually in a browser, not by the unit-test suite. Coverage gates apply to -# the framework core (src/runtime/) only. -sonar.coverage.exclusions=src/runtime/__init__.py,examples/**/ui.py,ui/** +# Coverage exclusions — kept in sync with [tool.coverage.run].omit in +# pyproject.toml so the local CI gate and the SonarCloud gate agree on +# which files are measurement targets: +# * ``src/runtime/ui.py`` — 1573-line Streamlit shell, exercised +# manually in a browser, not by the unit-test suite. Pending a +# dedicated UI-testing milestone. +# * ``src/runtime/__main__.py`` — argparse-only CLI entry; smoke-tested +# by hand, not pytest. +# * ``src/runtime/checkpointer_postgres.py`` — prod-only Postgres +# saver; CI runs sqlite-only. +# * ``src/runtime/triggers/transports/plugin.py`` — stub transport. +# * ``examples/**`` — reference apps; their MCP servers are exercised +# via integration tests under ``tests/`` but pytest's +# ``--cov=src/runtime`` does not instrument them, so coverage data +# never reaches SonarCloud for these files. +# * ``src/runtime/__init__.py`` — re-export surface only. +sonar.coverage.exclusions=src/runtime/__init__.py,src/runtime/ui.py,src/runtime/__main__.py,src/runtime/checkpointer_postgres.py,src/runtime/triggers/transports/plugin.py,examples/**,ui/** # Suppress python:S7503 (async-without-await) for framework-driven async signatures. # LangGraph nodes and FastMCP tool handlers MUST be `async def` even when their diff --git a/src/runtime/api.py b/src/runtime/api.py index 0d3c11b..5bf8077 100644 --- a/src/runtime/api.py +++ b/src/runtime/api.py @@ -876,18 +876,19 @@ async def _stream(): last_seq = ev.seq yield f"data: {envelope.model_dump_json()}\n\n" # Tail: poll for new rows. Bounded by client-disconnect. - try: - while not await request.is_disconnected(): - await _asyncio.sleep(0.25) - for ev in event_log.iter_for(session_id, since=last_seq): - envelope = EventEnvelope( - seq=ev.seq, session_id=ev.session_id, - kind=ev.kind, payload=ev.payload, ts=ev.ts, - ) - last_seq = ev.seq - yield f"data: {envelope.model_dump_json()}\n\n" - except _asyncio.CancelledError: - return + # CancelledError (from task cancellation, e.g. when the + # client closes the connection) propagates naturally — no + # try/except needed; suppressing it would break asyncio's + # cancellation contract (Sonar python:S7497). + while not await request.is_disconnected(): + await _asyncio.sleep(0.25) + for ev in event_log.iter_for(session_id, since=last_seq): + envelope = EventEnvelope( + seq=ev.seq, session_id=ev.session_id, + kind=ev.kind, payload=ev.payload, ts=ev.ts, + ) + last_seq = ev.seq + yield f"data: {envelope.model_dump_json()}\n\n" return StreamingResponse(_stream(), media_type="text/event-stream")