SilverElixir commited on
Commit
e2c6512
·
1 Parent(s): cd11fc0

Exit nonzero when the HTTP server dies first

Browse files
Files changed (2) hide show
  1. bot.py +9 -0
  2. 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: