Spaces:
Running
Running
SilverElixir commited on
Commit ·
e2c6512
1
Parent(s): cd11fc0
Exit nonzero when the HTTP server dies first
Browse files- bot.py +9 -0
- tests/test_bot_admin.py +49 -0
bot.py
CHANGED
|
@@ -1559,6 +1559,15 @@ async def main() -> None:
|
|
| 1559 |
with contextlib.suppress(Exception, asyncio.CancelledError):
|
| 1560 |
await serve_task
|
| 1561 |
raise SystemExit(1) from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1562 |
finally:
|
| 1563 |
startup_task.cancel()
|
| 1564 |
flush_task.cancel()
|
|
|
|
| 1559 |
with contextlib.suppress(Exception, asyncio.CancelledError):
|
| 1560 |
await serve_task
|
| 1561 |
raise SystemExit(1) from exc
|
| 1562 |
+
# Сервер упал первым (порт занят, падение uvicorn): без этой ветки
|
| 1563 |
+
# исключение терялось и процесс тихо выходил с кодом 0.
|
| 1564 |
+
if serve_task in done and not serve_task.cancelled():
|
| 1565 |
+
exc = serve_task.exception()
|
| 1566 |
+
if exc is not None:
|
| 1567 |
+
log.error("[serve] HTTP server failed, stopping: %s", exc)
|
| 1568 |
+
raise SystemExit(1) from exc
|
| 1569 |
+
log.error("[serve] HTTP server exited unexpectedly, stopping")
|
| 1570 |
+
raise SystemExit(1)
|
| 1571 |
finally:
|
| 1572 |
startup_task.cancel()
|
| 1573 |
flush_task.cancel()
|
tests/test_bot_admin.py
CHANGED
|
@@ -537,6 +537,55 @@ def test_webhook_setup_succeeds_after_retries(monkeypatch):
|
|
| 537 |
assert sleeps == [1.5, 5.0, 10.0, 3600]
|
| 538 |
|
| 539 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
def test_probe_url_returns_ok_shape_and_redacts_secret_on_error():
|
| 541 |
# Общий зонд /diag и /selftest: успех — статус, неуспех — текст без секрета.
|
| 542 |
class _FakeResp:
|
|
|
|
| 537 |
assert sleeps == [1.5, 5.0, 10.0, 3600]
|
| 538 |
|
| 539 |
|
| 540 |
+
def test_main_exits_nonzero_when_server_dies_first(monkeypatch, caplog):
|
| 541 |
+
# Падение uvicorn первым (порт занят) раньше терялось: процесс тихо выходил
|
| 542 |
+
# с кодом 0. Теперь исключение сервера логируется и даёт SystemExit(1).
|
| 543 |
+
from types import SimpleNamespace
|
| 544 |
+
from unittest.mock import AsyncMock
|
| 545 |
+
import lumen_chat_state
|
| 546 |
+
monkeypatch.setattr(bot, "_require_bot_token", lambda: "123:abc")
|
| 547 |
+
monkeypatch.setattr(bot, "Bot", lambda *a, **k: SimpleNamespace())
|
| 548 |
+
monkeypatch.setattr(bot, "IPv4AiohttpSession", lambda *a, **k: SimpleNamespace())
|
| 549 |
+
monkeypatch.setattr(bot.genai, "Client", lambda *a, **k: SimpleNamespace())
|
| 550 |
+
|
| 551 |
+
async def _never():
|
| 552 |
+
await asyncio.sleep(3600)
|
| 553 |
+
|
| 554 |
+
monkeypatch.setattr(bot, "_webhook_startup", _never)
|
| 555 |
+
monkeypatch.setattr(bot, "_flush_dirty_state", _never)
|
| 556 |
+
monkeypatch.setattr(bot, "_drain_inflight_tasks", AsyncMock())
|
| 557 |
+
monkeypatch.setattr(bot, "_close_sessions", AsyncMock())
|
| 558 |
+
monkeypatch.setattr(bot.uvicorn, "Config", lambda *a, **k: SimpleNamespace())
|
| 559 |
+
|
| 560 |
+
class _DeadServer:
|
| 561 |
+
should_exit = False
|
| 562 |
+
|
| 563 |
+
async def serve(self):
|
| 564 |
+
raise RuntimeError("port busy")
|
| 565 |
+
|
| 566 |
+
monkeypatch.setattr(bot.uvicorn, "Server", lambda cfg: _DeadServer())
|
| 567 |
+
orig_bot, orig_client = bot.bot, bot.client
|
| 568 |
+
orig_dirty, orig_deletions = set(bot._dirty_chat_ids), set(bot._pending_chat_deletions)
|
| 569 |
+
orig_index_dirty, orig_quota_dirty = lumen_chat_state._index_dirty, lumen_chat_state._quota_dirty
|
| 570 |
+
bot._dirty_chat_ids.clear()
|
| 571 |
+
bot._pending_chat_deletions.clear()
|
| 572 |
+
lumen_chat_state._index_dirty = False
|
| 573 |
+
lumen_chat_state._quota_dirty = False
|
| 574 |
+
try:
|
| 575 |
+
with caplog.at_level(logging.ERROR, logger="bot"), pytest.raises(SystemExit) as exc_info:
|
| 576 |
+
asyncio.run(bot.main())
|
| 577 |
+
assert exc_info.value.code == 1
|
| 578 |
+
assert "[serve] HTTP server failed" in caplog.text
|
| 579 |
+
finally:
|
| 580 |
+
bot.bot, bot.client = orig_bot, orig_client
|
| 581 |
+
bot._dirty_chat_ids.clear()
|
| 582 |
+
bot._dirty_chat_ids.update(orig_dirty)
|
| 583 |
+
bot._pending_chat_deletions.clear()
|
| 584 |
+
bot._pending_chat_deletions.update(orig_deletions)
|
| 585 |
+
lumen_chat_state._index_dirty = orig_index_dirty
|
| 586 |
+
lumen_chat_state._quota_dirty = orig_quota_dirty
|
| 587 |
+
|
| 588 |
+
|
| 589 |
def test_probe_url_returns_ok_shape_and_redacts_secret_on_error():
|
| 590 |
# Общий зонд /diag и /selftest: успех — статус, неуспех — текст без секрета.
|
| 591 |
class _FakeResp:
|