mirror of
https://github.com/zvx-echo6/central.git
synced 2026-08-26 09:21:36 +00:00
gui: fix consumers_info coroutine usage + list-returning test mock + None-guard counts
- routes.py: change `async for ci in js.consumers_info(stream_name)` to `for ci in await js.consumers_info(stream_name)` — nats-py 2.14.0 consumers_info() is a plain coroutine returning list[ConsumerInfo], not an async iterable; the old form threw TypeError silently (swallowed by except), causing every stream to show "unavailable" and zero consumers. - test_consumers.py: replace async-generator mock with AsyncMock returning a list, matching the real API; also fix inline consumers_info_raising in the error test (remove dead yield); add explicit regression guard asserting consumer names appear in the template context. - consumers_list.html: guard num_pending/num_ack_pending/num_redelivered/ num_waiting with `… if … is not none else '—'` to prevent "None" in cells. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
84de144b30
commit
40ea23e904
3 changed files with 18 additions and 15 deletions
|
|
@ -2217,7 +2217,7 @@ async def consumers_list(request: Request) -> HTMLResponse:
|
|||
|
||||
if js is not None:
|
||||
try:
|
||||
async for ci in js.consumers_info(stream_name):
|
||||
for ci in await js.consumers_info(stream_name):
|
||||
consumers.append({
|
||||
"name": ci.name,
|
||||
"num_pending": ci.num_pending,
|
||||
|
|
|
|||
|
|
@ -32,10 +32,10 @@ has no active subscriber — it is safe to delete if it is not a central-owned c
|
|||
{% for c in stream.consumers %}
|
||||
<tr>
|
||||
<td style="padding: 0.25rem 0.5rem;">{{ c.name }}</td>
|
||||
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_pending }}</td>
|
||||
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_ack_pending }}</td>
|
||||
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_redelivered }}</td>
|
||||
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_waiting }}</td>
|
||||
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_pending if c.num_pending is not none else '—' }}</td>
|
||||
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_ack_pending if c.num_ack_pending is not none else '—' }}</td>
|
||||
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_redelivered if c.num_redelivered is not none else '—' }}</td>
|
||||
<td style="text-align: right; padding: 0.25rem 0.5rem;">{{ c.num_waiting if c.num_waiting is not none else '—' }}</td>
|
||||
<td style="padding: 0.25rem 0.5rem;">{{ c.created.isoformat() if c.created else '—' }}</td>
|
||||
<td style="text-align: center; padding: 0.25rem 0.5rem;">
|
||||
{% if c.protected %}
|
||||
|
|
|
|||
|
|
@ -25,14 +25,11 @@ def _make_consumer_info(name: str, num_pending: int = 0, num_ack_pending: int =
|
|||
|
||||
|
||||
def _make_js_with_consumers(consumers_by_stream: dict):
|
||||
"""Build a mock JetStreamContext whose consumers_info is an async generator."""
|
||||
"""Build a mock JetStreamContext whose consumers_info is a coroutine returning a list."""
|
||||
mock_js = MagicMock()
|
||||
|
||||
async def consumers_info(stream_name):
|
||||
for ci in consumers_by_stream.get(stream_name, []):
|
||||
yield ci
|
||||
|
||||
mock_js.consumers_info = consumers_info
|
||||
mock_js.consumers_info = AsyncMock(
|
||||
side_effect=lambda stream, **kw: consumers_by_stream.get(stream, [])
|
||||
)
|
||||
mock_js.consumer_info = AsyncMock()
|
||||
mock_js.delete_consumer = AsyncMock()
|
||||
return mock_js
|
||||
|
|
@ -108,6 +105,13 @@ class TestConsumersListWithConsumers:
|
|||
assert meshai_c["num_pending"] == 1000
|
||||
assert meshai_c["num_waiting"] == 0
|
||||
|
||||
# Regression guard: consumer names must appear in the template context so
|
||||
# they are rendered into the HTML body (guards against the coroutine/iterator
|
||||
# bug where consumers_info was consumed as an async-iterable instead of awaited).
|
||||
consumer_names_in_context = {c["name"] for c in wx["consumers"]}
|
||||
assert "archive-CENTRAL_WX" in consumer_names_in_context
|
||||
assert "meshai-wx" in consumer_names_in_context
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_with_no_consumers_shows_empty(self):
|
||||
from central.gui.routes import consumers_list
|
||||
|
|
@ -147,9 +151,8 @@ class TestConsumersListWithConsumers:
|
|||
async def consumers_info_raising(stream_name):
|
||||
if stream_name == "CENTRAL_FIRE":
|
||||
raise RuntimeError("stream not found")
|
||||
# other streams: empty
|
||||
return
|
||||
yield # make it an async generator
|
||||
# other streams: empty list (coroutine returning a list, not an async generator)
|
||||
return []
|
||||
|
||||
mock_js.consumers_info = consumers_info_raising
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue