Compare commits
2 Commits
feature/is
...
feature/is
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aceba86186 | ||
|
|
77577f3f4d |
12
src/main.py
12
src/main.py
@@ -1300,18 +1300,10 @@ def _start_dashboard_server(settings: Settings) -> threading.Thread | None:
|
||||
if not settings.DASHBOARD_ENABLED:
|
||||
return None
|
||||
|
||||
# Validate dependencies before spawning the thread so startup failures are
|
||||
# reported synchronously (avoids the misleading "started" → "failed" log pair).
|
||||
try:
|
||||
import uvicorn # noqa: F401
|
||||
from src.dashboard import create_dashboard_app # noqa: F401
|
||||
except ImportError as exc:
|
||||
logger.warning("Dashboard server unavailable (missing dependency): %s", exc)
|
||||
return None
|
||||
|
||||
def _serve() -> None:
|
||||
try:
|
||||
import uvicorn
|
||||
|
||||
from src.dashboard import create_dashboard_app
|
||||
|
||||
app = create_dashboard_app(settings.DB_PATH)
|
||||
@@ -1322,7 +1314,7 @@ def _start_dashboard_server(settings: Settings) -> threading.Thread | None:
|
||||
log_level="info",
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.warning("Dashboard server stopped unexpectedly: %s", exc)
|
||||
logger.warning("Dashboard server failed to start: %s", exc)
|
||||
|
||||
thread = threading.Thread(
|
||||
target=_serve,
|
||||
|
||||
@@ -604,9 +604,19 @@ class TelegramCommandHandler:
|
||||
async with session.post(url, json=payload) as resp:
|
||||
if resp.status != 200:
|
||||
error_text = await resp.text()
|
||||
logger.error(
|
||||
"getUpdates API error (status=%d): %s", resp.status, error_text
|
||||
)
|
||||
if resp.status == 409:
|
||||
# Another bot instance is already polling — stop this poller entirely.
|
||||
# Retrying would keep conflicting with the other instance.
|
||||
self._running = False
|
||||
logger.warning(
|
||||
"Telegram conflict (409): another instance is already polling. "
|
||||
"Disabling Telegram commands for this process. "
|
||||
"Ensure only one instance of The Ouroboros is running at a time.",
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"getUpdates API error (status=%d): %s", resp.status, error_text
|
||||
)
|
||||
return []
|
||||
|
||||
data = await resp.json()
|
||||
|
||||
@@ -2194,29 +2194,6 @@ def test_start_dashboard_server_enabled_starts_thread() -> None:
|
||||
mock_thread.start.assert_called_once()
|
||||
|
||||
|
||||
def test_start_dashboard_server_returns_none_when_uvicorn_missing() -> None:
|
||||
"""Returns None (no thread) and logs a warning when uvicorn is not installed."""
|
||||
settings = Settings(
|
||||
KIS_APP_KEY="test_key",
|
||||
KIS_APP_SECRET="test_secret",
|
||||
KIS_ACCOUNT_NO="12345678-01",
|
||||
GEMINI_API_KEY="test_gemini_key",
|
||||
DASHBOARD_ENABLED=True,
|
||||
)
|
||||
import builtins
|
||||
real_import = builtins.__import__
|
||||
|
||||
def mock_import(name: str, *args: object, **kwargs: object) -> object:
|
||||
if name == "uvicorn":
|
||||
raise ImportError("No module named 'uvicorn'")
|
||||
return real_import(name, *args, **kwargs)
|
||||
|
||||
with patch("builtins.__import__", side_effect=mock_import):
|
||||
thread = _start_dashboard_server(settings)
|
||||
|
||||
assert thread is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# market_outlook BUY confidence threshold tests (#173)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -876,6 +876,54 @@ class TestGetUpdates:
|
||||
|
||||
assert updates == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_updates_409_stops_polling(self) -> None:
|
||||
"""409 Conflict response stops the poller (_running = False) and returns empty list."""
|
||||
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||
handler = TelegramCommandHandler(client)
|
||||
handler._running = True # simulate active poller
|
||||
|
||||
mock_resp = AsyncMock()
|
||||
mock_resp.status = 409
|
||||
mock_resp.text = AsyncMock(
|
||||
return_value='{"ok":false,"error_code":409,"description":"Conflict"}'
|
||||
)
|
||||
mock_resp.__aenter__ = AsyncMock(return_value=mock_resp)
|
||||
mock_resp.__aexit__ = AsyncMock(return_value=False)
|
||||
|
||||
with patch("aiohttp.ClientSession.post", return_value=mock_resp):
|
||||
updates = await handler._get_updates()
|
||||
|
||||
assert updates == []
|
||||
assert handler._running is False # poller stopped
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_poll_loop_exits_after_409(self) -> None:
|
||||
"""_poll_loop exits naturally after _running is set to False by a 409 response."""
|
||||
import asyncio as _asyncio
|
||||
|
||||
client = TelegramClient(bot_token="123:abc", chat_id="456", enabled=True)
|
||||
handler = TelegramCommandHandler(client)
|
||||
|
||||
call_count = 0
|
||||
|
||||
async def mock_get_updates_409() -> list[dict]:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
# Simulate 409 stopping the poller
|
||||
handler._running = False
|
||||
return []
|
||||
|
||||
handler._get_updates = mock_get_updates_409 # type: ignore[method-assign]
|
||||
|
||||
handler._running = True
|
||||
task = _asyncio.create_task(handler._poll_loop())
|
||||
await _asyncio.wait_for(task, timeout=2.0)
|
||||
|
||||
# _get_updates called exactly once, then loop exited
|
||||
assert call_count == 1
|
||||
assert handler._running is False
|
||||
|
||||
|
||||
class TestCommandWithArgs:
|
||||
"""Test register_command_with_args and argument dispatch."""
|
||||
|
||||
Reference in New Issue
Block a user