From 2f39bf48831073fb2a53d7931ff5c2bf9aeade55 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 08:09:26 +0000 Subject: [PATCH 1/2] fix(api)+chore(sonar): re-raise CancelledError + sync coverage exclusions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes that together flip the SonarCloud gate from ERROR to OK: 1. src/runtime/api.py:889 — re-raise asyncio.CancelledError after cleanup in the SSE _stream() generator instead of returning silently. Suppressing CancelledError breaks asyncio's cancellation propagation contract; Sonar python:S7497 caught it. The single MAJOR bug was driving new_reliability_rating from A to C. 2. sonar-project.properties — sync sonar.coverage.exclusions with pyproject.toml's [tool.coverage.run].omit list (ui.py, __main__.py, checkpointer_postgres.py, triggers/transports/plugin.py) and add examples/** since pytest's --cov=src/runtime never instruments the reference apps. Without this, Sonar's new-code coverage was 71.7% (gate threshold 80%) while local CI saw 87.21% on the same suite — pure config drift between two gates that should agree. Verification: ruff check src/ tests/ — passed pyright src/runtime — 0 errors / 0 warnings pytest -x — 1265 passed / 8 skipped pytest --cov=src/runtime --cov-fail-under=85 — 87.21% build_single_file.py — dist/* regenerated Projected SonarCloud gate after merge: new_reliability_rating 3 (C) -> 1 (A) ✓ new_security_hotspots_reviewed 0% -> 100% ✓ (already done via API) new_coverage 71.7% -> ~87.2% ✓ --- dist/app.py | 2 +- dist/apps/code-review.py | 2 +- dist/apps/incident-management.py | 2 +- sonar-project.properties | 21 +++++++++++++++++---- src/runtime/api.py | 2 +- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/dist/app.py b/dist/app.py index 9d378d5..8a74010 100644 --- a/dist/app.py +++ b/dist/app.py @@ -16054,7 +16054,7 @@ async def _stream(): last_seq = ev.seq yield f"data: {envelope.model_dump_json()}\n\n" except _asyncio.CancelledError: - return + raise return StreamingResponse(_stream(), media_type="text/event-stream") diff --git a/dist/apps/code-review.py b/dist/apps/code-review.py index 343544f..b4fef8b 100644 --- a/dist/apps/code-review.py +++ b/dist/apps/code-review.py @@ -16107,7 +16107,7 @@ async def _stream(): last_seq = ev.seq yield f"data: {envelope.model_dump_json()}\n\n" except _asyncio.CancelledError: - return + raise return StreamingResponse(_stream(), media_type="text/event-stream") diff --git a/dist/apps/incident-management.py b/dist/apps/incident-management.py index 083c713..4ef4b10 100644 --- a/dist/apps/incident-management.py +++ b/dist/apps/incident-management.py @@ -16119,7 +16119,7 @@ async def _stream(): last_seq = ev.seq yield f"data: {envelope.model_dump_json()}\n\n" except _asyncio.CancelledError: - return + raise 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..4b88b76 100644 --- a/src/runtime/api.py +++ b/src/runtime/api.py @@ -887,7 +887,7 @@ async def _stream(): last_seq = ev.seq yield f"data: {envelope.model_dump_json()}\n\n" except _asyncio.CancelledError: - return + raise return StreamingResponse(_stream(), media_type="text/event-stream") From 7594171543881a68b66048724efa172a08e08b05 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Fri, 15 May 2026 08:50:24 +0000 Subject: [PATCH 2/2] fix(api): drop redundant try/except CancelledError wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sonar python:S2737 caught the ``except CancelledError: raise`` from the previous commit as a code smell — the catch-and-rethrow does nothing observable; Python propagates CancelledError automatically without any try/except in the SSE _stream() generator. Removing the wrapper achieves the same correct cancellation semantics S7497 was asking for, without adding the dead handler S2737 then flagged. Comment retains the rationale so a future reader doesn't re-add a suppressing handler. Verification: ruff check src/ — passed pyright src/runtime — 0 errors / 0 warnings pytest -x — 1265 passed / 8 skipped build_single_file.py — dist/* regenerated Projected SonarCloud after this commit (PR #13): new_maintainability_rating 3 (C) -> 1 (A) --- dist/app.py | 25 +++++++++++++------------ dist/apps/code-review.py | 25 +++++++++++++------------ dist/apps/incident-management.py | 25 +++++++++++++------------ src/runtime/api.py | 25 +++++++++++++------------ 4 files changed, 52 insertions(+), 48 deletions(-) diff --git a/dist/app.py b/dist/app.py index 8a74010..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: - raise + # 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 b4fef8b..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: - raise + # 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 4ef4b10..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: - raise + # 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/src/runtime/api.py b/src/runtime/api.py index 4b88b76..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: - raise + # 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")