- ~60 new/extended test files across api, core, plugins, services, schemas: routes, superset clients, task_manager, lineage, git, translate, dashboard-testing, load-testing, migration, llm_analysis, scheduler, ssl - .coveragerc: enable branch coverage; exclude src/__tests__ (test files) and src/scripts (CLI/ops tools) from the denominator - bug fixes found while testing: * settings: PUT /settings/reports registered under duplicated prefix * schemas/lineage: FleetReportDTO missing run_status (route always 500) * dashboard_testing/baseline_inheritance: visual entry read wrong field * superset_client/_databases: logger extra name shadowed LogRecord attr * routes/datasets: _yaml_string_paths recursion without yield from * translate/sql_generator: restore explicit-type timestamp contract * baseline_catalog: remove unreachable dashboard_id fallback - conftest fixes: pytest_plugins to rootdir conftest (pytest 9), test filename collision, TMPDIR-safe integration fixtures
407 lines
20 KiB
Python
407 lines
20 KiB
Python
# #region Test.AppModule.LogsEndpoint [C:3] [TYPE Module] [SEMANTICS test,app,logs,recent,websocket]
|
|
# @BRIEF Tests for app.py — get_recent_app_logs REST endpoint and app_logs_websocket live log tail.
|
|
# @RELATION BINDS_TO -> [App.AppModule]
|
|
# @TEST_EDGE: level_filter -> lines below min level are dropped
|
|
# @TEST_EDGE: task_id_filter -> lines without matching task id are dropped
|
|
# @TEST_EDGE: after_seq -> get_since snapshot used instead of recent
|
|
# @TEST_EDGE: limit_clamp -> out-of-range limits clamped to [1, 3000]
|
|
# @TEST_EDGE: ws_auth_missing_token -> 4001 close
|
|
# @TEST_EDGE: ws_permission_denied -> 4003 close
|
|
# @TEST_EDGE: ws_poll_failure -> stream logs then re-raises
|
|
# @TEST_INVARIANT: ws_auth_gate -> VERIFIED_BY: test_app_logs_ws_auth_rejected, test_app_logs_ws_permission_rejected
|
|
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
import sys
|
|
from types import SimpleNamespace
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from starlette.websockets import WebSocketDisconnect
|
|
|
|
_DEFAULT_TS = datetime(2025, 1, 15, 10, 0, 0, tzinfo=timezone.utc)
|
|
|
|
|
|
# #region _make_log_line [C:1] [TYPE Function]
|
|
def _make_log_line(seq, level, raw, timestamp=_DEFAULT_TS, logger_name="cot"):
|
|
return SimpleNamespace(
|
|
seq=seq,
|
|
level=level,
|
|
raw=raw,
|
|
timestamp=timestamp,
|
|
logger_name=logger_name,
|
|
)
|
|
# #endregion _make_log_line
|
|
|
|
|
|
# #region _make_handler_mock [C:1] [TYPE Function]
|
|
def _make_handler_mock(lines, seq=3, capacity=3000):
|
|
handler = MagicMock()
|
|
handler.get_recent_logs.return_value = lines
|
|
handler.current_seq.return_value = seq
|
|
handler.get_since.return_value = (lines, seq)
|
|
handler.capacity = capacity
|
|
return handler
|
|
# #endregion _make_handler_mock
|
|
|
|
|
|
class TestGetRecentAppLogs:
|
|
"""get_recent_app_logs — REST snapshot of the app/cot log ring buffer."""
|
|
|
|
# #region Test.AppModule.TestRecentLogsNoFilters [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_no_filters(self):
|
|
from src.app import get_recent_app_logs
|
|
l1 = _make_log_line(1, "INFO", "one")
|
|
l2 = _make_log_line(2, "WARNING", "two")
|
|
handler = _make_handler_mock([l1, l2], seq=2)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(_user=None)
|
|
assert result["current_seq"] == 2
|
|
assert result["capacity"] == 3000
|
|
assert [i["seq"] for i in result["items"]] == [1, 2]
|
|
assert result["items"][0]["raw"] == "one"
|
|
assert result["items"][0]["logger"] == "cot"
|
|
assert result["items"][0]["timestamp"] == "2025-01-15T10:00:00+00:00"
|
|
# #endregion Test.AppModule.TestRecentLogsNoFilters
|
|
|
|
# #region Test.AppModule.TestRecentLogsLevelFilter [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_level_filter(self):
|
|
from src.app import get_recent_app_logs
|
|
lines = [
|
|
_make_log_line(1, "INFO", "a"),
|
|
_make_log_line(2, "ERROR", "b"),
|
|
_make_log_line(3, "WARNING", "c"),
|
|
]
|
|
handler = _make_handler_mock(lines, seq=3)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(level="ERROR", _user=None)
|
|
assert [i["level"] for i in result["items"]] == ["ERROR"]
|
|
# #endregion Test.AppModule.TestRecentLogsLevelFilter
|
|
|
|
# #region Test.AppModule.TestRecentLogsLevelCaseInsensitive [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_level_case_insensitive(self):
|
|
from src.app import get_recent_app_logs
|
|
lines = [
|
|
_make_log_line(1, "INFO", "a"),
|
|
_make_log_line(2, "WARNING", "b"),
|
|
_make_log_line(3, "ERROR", "c"),
|
|
]
|
|
handler = _make_handler_mock(lines, seq=3)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(level="warning", _user=None)
|
|
assert [i["level"] for i in result["items"]] == ["WARNING", "ERROR"]
|
|
# #endregion Test.AppModule.TestRecentLogsLevelCaseInsensitive
|
|
|
|
# #region Test.AppModule.TestRecentLogsUnknownLevel [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_unknown_level(self):
|
|
from src.app import get_recent_app_logs
|
|
lines = [_make_log_line(1, "INFO", "a"), _make_log_line(2, "ERROR", "b")]
|
|
handler = _make_handler_mock(lines, seq=2)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(level="verbose", _user=None)
|
|
# unknown requested level -> min_level 0 -> everything passes
|
|
assert len(result["items"]) == 2
|
|
# #endregion Test.AppModule.TestRecentLogsUnknownLevel
|
|
|
|
# #region Test.AppModule.TestRecentLogsTaskIdFilter [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_task_id_filter(self):
|
|
from src.app import get_recent_app_logs
|
|
lines = [
|
|
_make_log_line(1, "INFO", '{"task_id": "t1"}'),
|
|
_make_log_line(2, "INFO", '{"task_id": "t2"}'),
|
|
]
|
|
handler = _make_handler_mock(lines, seq=2)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(task_id="t1", _user=None)
|
|
assert [i["seq"] for i in result["items"]] == [1]
|
|
# #endregion Test.AppModule.TestRecentLogsTaskIdFilter
|
|
|
|
# #region Test.AppModule.TestRecentLogsMultipleTaskIds [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_multiple_task_ids(self):
|
|
from src.app import get_recent_app_logs
|
|
lines = [
|
|
_make_log_line(1, "INFO", '{"task_id": "t1"}'),
|
|
_make_log_line(2, "INFO", '{"task_id": "t2"}'),
|
|
_make_log_line(3, "INFO", '{"task_id": "t3"}'),
|
|
]
|
|
handler = _make_handler_mock(lines, seq=3)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(task_id="t1, t2", _user=None)
|
|
assert [i["seq"] for i in result["items"]] == [1, 2]
|
|
# #endregion Test.AppModule.TestRecentLogsMultipleTaskIds
|
|
|
|
# #region Test.AppModule.TestRecentLogsEmptyTaskIdNoFilter [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_empty_task_id_no_filter(self):
|
|
from src.app import get_recent_app_logs
|
|
lines = [_make_log_line(1, "INFO", "a"), _make_log_line(2, "INFO", "b")]
|
|
handler = _make_handler_mock(lines, seq=2)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(task_id=" , ", _user=None)
|
|
# comma-only task_id -> no task ids -> no filtering
|
|
assert len(result["items"]) == 2
|
|
# #endregion Test.AppModule.TestRecentLogsEmptyTaskIdNoFilter
|
|
|
|
# #region Test.AppModule.TestRecentLogsAfterSeq [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_after_seq_uses_get_since(self):
|
|
from src.app import get_recent_app_logs
|
|
newer = [_make_log_line(9, "INFO", "nine")]
|
|
handler = _make_handler_mock([], seq=9)
|
|
handler.get_since.return_value = (newer, 9)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(after_seq=5, _user=None)
|
|
handler.get_since.assert_called_once_with(5, limit=500)
|
|
handler.get_recent_logs.assert_not_called()
|
|
assert result["current_seq"] == 9
|
|
assert result["items"][0]["seq"] == 9
|
|
# #endregion Test.AppModule.TestRecentLogsAfterSeq
|
|
|
|
# #region Test.AppModule.TestRecentLogsLimitClampedHigh [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_limit_clamped_high(self):
|
|
from src.app import get_recent_app_logs
|
|
handler = _make_handler_mock([], seq=0)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
await get_recent_app_logs(limit=5000, _user=None)
|
|
handler.get_recent_logs.assert_called_once_with(limit=3000)
|
|
# #endregion Test.AppModule.TestRecentLogsLimitClampedHigh
|
|
|
|
# #region Test.AppModule.TestRecentLogsLimitClampedLow [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_limit_clamped_low(self):
|
|
from src.app import get_recent_app_logs
|
|
handler = _make_handler_mock([], seq=0)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
await get_recent_app_logs(limit=-5, _user=None)
|
|
handler.get_recent_logs.assert_called_once_with(limit=1)
|
|
# #endregion Test.AppModule.TestRecentLogsLimitClampedLow
|
|
|
|
# #region Test.AppModule.TestRecentLogsLimitZeroMeansDefault [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_limit_zero_means_default(self):
|
|
from src.app import get_recent_app_logs
|
|
handler = _make_handler_mock([], seq=0)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
await get_recent_app_logs(limit=0, _user=None)
|
|
# falsy limit falls back to 500 before clamping
|
|
handler.get_recent_logs.assert_called_once_with(limit=500)
|
|
# #endregion Test.AppModule.TestRecentLogsLimitZeroMeansDefault
|
|
|
|
# #region Test.AppModule.TestRecentLogsTimestampAndRawNone [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_timestamp_and_raw_none(self):
|
|
from src.app import get_recent_app_logs
|
|
line = _make_log_line(1, "INFO", None, timestamp=None)
|
|
handler = _make_handler_mock([line], seq=1)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(_user=None)
|
|
assert result["items"][0]["timestamp"] is None
|
|
assert result["items"][0]["raw"] == ""
|
|
# #endregion Test.AppModule.TestRecentLogsTimestampAndRawNone
|
|
|
|
# #region Test.AppModule.TestRecentLogsUnknownLineLevel [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_unknown_line_level(self):
|
|
from src.app import get_recent_app_logs
|
|
line = _make_log_line(1, "TRACE", "x")
|
|
handler = _make_handler_mock([line], seq=1)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(level="ERROR", _user=None)
|
|
# TRACE maps to level 0 < min_level 3 -> dropped
|
|
assert result["items"] == []
|
|
# #endregion Test.AppModule.TestRecentLogsUnknownLineLevel
|
|
|
|
# #region Test.AppModule.TestRecentLogsEmpty [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_recent_logs_empty(self):
|
|
from src.app import get_recent_app_logs
|
|
handler = _make_handler_mock([], seq=0)
|
|
with patch("src.core.logger.get_app_log_handler", return_value=handler):
|
|
result = await get_recent_app_logs(_user=None)
|
|
assert result["items"] == []
|
|
assert result["current_seq"] == 0
|
|
# #endregion Test.AppModule.TestRecentLogsEmpty
|
|
|
|
|
|
class TestAppLogsWebSocket:
|
|
"""app_logs_websocket — live tail of the process-wide log ring buffer."""
|
|
|
|
# #region Test.AppModule.TestAppLogsWsAuthRejected [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_app_logs_ws_auth_rejected(self):
|
|
from src.app import app_logs_websocket
|
|
ws = MagicMock(); ws.query_params = {}
|
|
ws.accept = AsyncMock(); ws.close = AsyncMock()
|
|
with patch("src.app._authenticate_websocket", return_value=False):
|
|
await app_logs_websocket(ws)
|
|
ws.accept.assert_called_once()
|
|
ws.close.assert_called_once_with(code=4001, reason="Authentication required")
|
|
# #endregion Test.AppModule.TestAppLogsWsAuthRejected
|
|
|
|
# #region Test.AppModule.TestAppLogsWsPermissionRejected [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_app_logs_ws_permission_rejected(self):
|
|
from src.app import app_logs_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid"}
|
|
ws.accept = AsyncMock(); ws.close = AsyncMock()
|
|
with (
|
|
patch("src.app._authenticate_websocket", return_value=True),
|
|
patch("src.app._authorize_websocket", return_value=False),
|
|
):
|
|
await app_logs_websocket(ws)
|
|
ws.accept.assert_called_once()
|
|
ws.close.assert_called_once_with(code=4003, reason="Insufficient permissions")
|
|
# #endregion Test.AppModule.TestAppLogsWsPermissionRejected
|
|
|
|
# #region Test.AppModule.TestAppLogsWsStreamsRecentThenPolls [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_app_logs_ws_streams_recent_then_polls(self):
|
|
from src.app import app_logs_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid"}
|
|
ws.accept = AsyncMock()
|
|
l1 = _make_log_line(1, "INFO", "one")
|
|
l2 = _make_log_line(2, "INFO", "two")
|
|
l3 = _make_log_line(3, "WARNING", "three", timestamp=None)
|
|
handler = _make_handler_mock([l1, l2], seq=2)
|
|
handler.get_since.return_value = ([l3], 3)
|
|
# snapshot: 2 sends; poll iteration 1: 1 send; poll iteration 2: disconnect
|
|
ws.send_json = AsyncMock(side_effect=[None, None, None, WebSocketDisconnect()])
|
|
with (
|
|
patch("src.app._authenticate_websocket", return_value=True),
|
|
patch("src.app._authorize_websocket", return_value=True),
|
|
patch("src.core.logger.get_app_log_handler", return_value=handler),
|
|
patch("asyncio.sleep", new=AsyncMock()),
|
|
):
|
|
await app_logs_websocket(ws)
|
|
handler.get_since.assert_any_call(2, limit=500)
|
|
sent = [c[0][0] for c in ws.send_json.call_args_list if isinstance(c[0][0], dict)]
|
|
assert [s["seq"] for s in sent] == [1, 2, 3, 3]
|
|
assert sent[2]["timestamp"] is None
|
|
assert sent[2]["logger"] == "cot"
|
|
# #endregion Test.AppModule.TestAppLogsWsStreamsRecentThenPolls
|
|
|
|
# #region Test.AppModule.TestAppLogsWsLevelFilter [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_app_logs_ws_level_filter(self):
|
|
from src.app import app_logs_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid"}
|
|
ws.accept = AsyncMock()
|
|
dbg = _make_log_line(1, "DEBUG", "dbg")
|
|
inf = _make_log_line(2, "INFO", "inf")
|
|
err = _make_log_line(3, "ERROR", "err")
|
|
handler = _make_handler_mock([dbg, inf, err], seq=3)
|
|
handler.get_since.return_value = ([err], 3)
|
|
ws.send_json = AsyncMock(side_effect=[None, WebSocketDisconnect()])
|
|
with (
|
|
patch("src.app._authenticate_websocket", return_value=True),
|
|
patch("src.app._authorize_websocket", return_value=True),
|
|
patch("src.core.logger.get_app_log_handler", return_value=handler),
|
|
patch("asyncio.sleep", new=AsyncMock()),
|
|
):
|
|
await app_logs_websocket(ws, level="ERROR")
|
|
sent = [c[0][0] for c in ws.send_json.call_args_list if isinstance(c[0][0], dict)]
|
|
assert sent, "expected at least one forwarded line"
|
|
assert all(s["level"] == "ERROR" for s in sent)
|
|
# #endregion Test.AppModule.TestAppLogsWsLevelFilter
|
|
|
|
# #region Test.AppModule.TestAppLogsWsTaskIdFilter [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_app_logs_ws_task_id_filter(self):
|
|
from src.app import app_logs_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid"}
|
|
ws.accept = AsyncMock()
|
|
match = _make_log_line(1, "INFO", '{"task_id": "t1"}')
|
|
other = _make_log_line(2, "INFO", '{"task_id": "t2"}')
|
|
handler = _make_handler_mock([match, other], seq=2)
|
|
handler.get_since.return_value = ([match], 2)
|
|
ws.send_json = AsyncMock(side_effect=[None, WebSocketDisconnect()])
|
|
with (
|
|
patch("src.app._authenticate_websocket", return_value=True),
|
|
patch("src.app._authorize_websocket", return_value=True),
|
|
patch("src.core.logger.get_app_log_handler", return_value=handler),
|
|
patch("asyncio.sleep", new=AsyncMock()),
|
|
):
|
|
await app_logs_websocket(ws, task_id="t1")
|
|
sent = [c[0][0] for c in ws.send_json.call_args_list if isinstance(c[0][0], dict)]
|
|
assert sent, "expected at least one forwarded line"
|
|
assert all('"task_id": "t1"' in s["raw"] for s in sent)
|
|
# #endregion Test.AppModule.TestAppLogsWsTaskIdFilter
|
|
|
|
# #region Test.AppModule.TestAppLogsWsNoTaskIdNoFilter [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_app_logs_ws_no_task_id_no_filter(self):
|
|
from src.app import app_logs_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid"}
|
|
ws.accept = AsyncMock()
|
|
a = _make_log_line(1, "INFO", "aaa")
|
|
b = _make_log_line(2, "INFO", "bbb")
|
|
handler = _make_handler_mock([a, b], seq=2)
|
|
handler.get_since.return_value = ([b], 2)
|
|
ws.send_json = AsyncMock(side_effect=[None, None, WebSocketDisconnect()])
|
|
with (
|
|
patch("src.app._authenticate_websocket", return_value=True),
|
|
patch("src.app._authorize_websocket", return_value=True),
|
|
patch("src.core.logger.get_app_log_handler", return_value=handler),
|
|
patch("asyncio.sleep", new=AsyncMock()),
|
|
):
|
|
await app_logs_websocket(ws, task_id=" , ")
|
|
sent = [c[0][0] for c in ws.send_json.call_args_list if isinstance(c[0][0], dict)]
|
|
assert len(sent) == 3
|
|
# #endregion Test.AppModule.TestAppLogsWsNoTaskIdNoFilter
|
|
|
|
# #region Test.AppModule.TestAppLogsWsPollFiltersNonMatching [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_app_logs_ws_poll_filters_non_matching(self):
|
|
"""A non-matching line arriving during polling is dropped (1205->1204)."""
|
|
from src.app import app_logs_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid"}
|
|
ws.accept = AsyncMock()
|
|
dbg = _make_log_line(1, "DEBUG", "dbg")
|
|
inf = _make_log_line(2, "INFO", "inf")
|
|
err = _make_log_line(3, "ERROR", "err")
|
|
handler = _make_handler_mock([dbg, inf, err], seq=3)
|
|
handler.get_since.side_effect = [([dbg], 3), ([err], 3)]
|
|
ws.send_json = AsyncMock(side_effect=[None, WebSocketDisconnect()])
|
|
with (
|
|
patch("src.app._authenticate_websocket", return_value=True),
|
|
patch("src.app._authorize_websocket", return_value=True),
|
|
patch("src.core.logger.get_app_log_handler", return_value=handler),
|
|
patch("asyncio.sleep", new=AsyncMock()),
|
|
):
|
|
await app_logs_websocket(ws, level="ERROR")
|
|
# snapshot forwards only ERROR; poll iteration 1 drops DEBUG; iteration 2 forwards ERROR
|
|
sent = [c[0][0] for c in ws.send_json.call_args_list if isinstance(c[0][0], dict)]
|
|
assert sent, "expected at least one forwarded line"
|
|
assert all(s["level"] == "ERROR" for s in sent)
|
|
# #endregion Test.AppModule.TestAppLogsWsPollFiltersNonMatching
|
|
|
|
# #region Test.AppModule.TestAppLogsWsPollFailureReraises [C:2] [TYPE Function]
|
|
@pytest.mark.asyncio
|
|
async def test_app_logs_ws_poll_failure_reraises(self):
|
|
from src.app import app_logs_websocket
|
|
ws = MagicMock(); ws.query_params = {"token": "valid"}
|
|
ws.accept = AsyncMock(); ws.send_json = AsyncMock()
|
|
handler = _make_handler_mock([], seq=0)
|
|
handler.get_since.side_effect = RuntimeError("buffer gone")
|
|
with (
|
|
patch("src.app._authenticate_websocket", return_value=True),
|
|
patch("src.app._authorize_websocket", return_value=True),
|
|
patch("src.core.logger.get_app_log_handler", return_value=handler),
|
|
patch("asyncio.sleep", new=AsyncMock()),
|
|
patch("src.app.logger.explore") as mexplore,
|
|
):
|
|
with pytest.raises(RuntimeError, match="buffer gone"):
|
|
await app_logs_websocket(ws)
|
|
assert any("App log stream failed" in str(c) for c in mexplore.call_args_list)
|
|
# #endregion Test.AppModule.TestAppLogsWsPollFailureReraises
|
|
# #endregion Test.AppModule.LogsEndpoint
|