""" Branch-coverage tests for context_overflow_api.py Flask blueprint. Covers: - get_context_overflow_metrics: - period "30d" (default), "7d", "3m", "1y", "all" - invalid period defaults to "30d" - per_page clamping (>500, <1) - page clamping (<1) - with truncated recent_requests data (truncation_rate > 0 branch) - with time series data including truncated entry (ollama_used branch) - with model_stats/context_limits populated - all_requests pagination (total_count > 0 branch) - DB exception returns 500 - get_research_context_overflow: - empty token_usage returns early success - token_usage with truncated and non-truncated entries - research_phase=None → "unknown" - no context_limit on any entry - DB exception returns 500 """ from contextlib import contextmanager from datetime import datetime, timezone from unittest.mock import MagicMock, patch import pytest from flask import Flask from local_deep_research.web.auth.routes import auth_bp from local_deep_research.web.routes.context_overflow_api import ( context_overflow_bp, ) # --------------------------------------------------------------------------- # Constants # --------------------------------------------------------------------------- MODULE = "local_deep_research.web.routes.context_overflow_api" AUTH_DB_MANAGER = "local_deep_research.web.auth.decorators.db_manager" # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _create_test_app(): app = Flask(__name__) app.config["SECRET_KEY"] = "test-secret" app.config["TESTING"] = True app.config["WTF_CSRF_ENABLED"] = False app.register_blueprint(auth_bp) app.register_blueprint(context_overflow_bp) return app def _make_chainable_query(): """Create a mock query that supports SQLAlchemy method chaining.""" query = MagicMock() for method in [ "filter", "filter_by", "order_by", "limit", "offset", "group_by", "with_entities", "having", ]: getattr(query, method).return_value = query return query def _make_overview_row(total=0, with_ctx=0, truncated=0): row = MagicMock() row.total_requests = total row.requests_with_context = with_ctx row.truncated_requests = truncated return row def _make_token_summary_row( total_requests=0, total_tokens=0, total_prompt=0, total_completion=0, avg_prompt=0, avg_completion=0, max_prompt=0, ): row = MagicMock() row.total_requests = total_requests row.total_tokens = total_tokens row.total_prompt_tokens = total_prompt row.total_completion_tokens = total_completion row.avg_prompt_tokens = avg_prompt row.avg_completion_tokens = avg_completion row.max_prompt_tokens = max_prompt return row def _make_token_usage(**kwargs): tu = MagicMock() tu.id = kwargs.get("id", 1) tu.timestamp = kwargs.get( "timestamp", datetime(2026, 1, 1, tzinfo=timezone.utc) ) tu.research_id = kwargs.get("research_id", "r1") tu.model_name = kwargs.get("model_name", "gpt-4") tu.model_provider = kwargs.get("model_provider", "openai") tu.prompt_tokens = kwargs.get("prompt_tokens", 100) tu.completion_tokens = kwargs.get("completion_tokens", 50) tu.total_tokens = kwargs.get("total_tokens", 150) tu.context_limit = kwargs.get("context_limit", 8192) tu.context_truncated = kwargs.get("context_truncated", False) tu.tokens_truncated = kwargs.get("tokens_truncated", 0) tu.truncation_ratio = kwargs.get("truncation_ratio", 0.0) tu.ollama_prompt_eval_count = kwargs.get("ollama_prompt_eval_count", None) tu.research_query = kwargs.get("research_query", "test query") tu.research_phase = kwargs.get("research_phase", "search") tu.calling_function = kwargs.get("calling_function", "test_fn") tu.response_time_ms = kwargs.get("response_time_ms", 500) tu.content_preview = kwargs.get("content_preview", None) return tu @contextmanager def _mock_db_session(mock_session): yield mock_session # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture() def app(): """Minimal Flask app with context_overflow blueprint and mocked auth.""" with patch(AUTH_DB_MANAGER) as mock_dbm: mock_dbm.is_user_connected.return_value = True application = _create_test_app() yield application @pytest.fixture() def client(app): return app.test_client() @pytest.fixture() def authed_client(client): with client.session_transaction() as sess: sess["username"] = "testuser" with patch( f"{MODULE}.SettingsManager", return_value=MagicMock(get_setting=MagicMock(return_value=8192)), ): yield client # --------------------------------------------------------------------------- # Shared mock DB setup for the main endpoint # --------------------------------------------------------------------------- def _build_main_endpoint_session( overview_row=None, token_summary_row=None, model_token_rows=None, phase_rows=None, context_limit_rows=None, recent_truncated_rows=None, time_series_rows=None, model_stat_rows=None, all_requests_rows=None, all_requests_count=0, avg_tokens_scalar=0, ): """ Build a mock session whose query chain supports the main endpoint's multiple with_entities().first(), .all(), .scalar(), and .count() calls. All calls to session.query() return the same single chainable mock because mock_session.query.return_value is set to one query object. The terminal methods (.all, .first, .scalar, .count) use side_effect lists to return the right value on each successive call. Exact call order in the source (get_context_overflow_metrics): .first() calls: 1. overview_row — merged scalar aggregates: counts + AVG(tokens_truncated) + token-summary sums/avgs (single query). .all() calls: 1. model_token_query (query.with_entities(...).group_by(...).all()) 2. phase_query (query.with_entities(...).group_by(...).all()) 3. context_limits (session.query(...).filter(...).group_by(...).all()) 4. recent_truncated (query.filter(...).order_by(...).limit(20).all()) 5. time_series_data (query.order_by(...).limit(...).all()) 6. model_stats (session.query(...).filter(...).group_by(...).all()) 7. all_requests_data (query.order_by(...).offset(...).limit(...).all()) .count() calls: 1. all_requests_total (query.order_by(...).count()) """ if overview_row is None: overview_row = _make_overview_row() if token_summary_row is None: token_summary_row = _make_token_summary_row() if model_token_rows is None: model_token_rows = [] if phase_rows is None: phase_rows = [] if context_limit_rows is None: context_limit_rows = [] if recent_truncated_rows is None: recent_truncated_rows = [] if time_series_rows is None: time_series_rows = [] if model_stat_rows is None: model_stat_rows = [] if all_requests_rows is None: all_requests_rows = [] query = _make_chainable_query() # The route now issues a single .first() call returning a row with all # overview + token-summary + avg_tokens_truncated fields combined. overview_row.total_tokens = token_summary_row.total_tokens overview_row.total_prompt_tokens = token_summary_row.total_prompt_tokens overview_row.total_completion_tokens = ( token_summary_row.total_completion_tokens ) overview_row.avg_prompt_tokens = token_summary_row.avg_prompt_tokens overview_row.avg_completion_tokens = token_summary_row.avg_completion_tokens overview_row.max_prompt_tokens = token_summary_row.max_prompt_tokens overview_row.avg_tokens_truncated = avg_tokens_scalar query.first.return_value = overview_row # .count() — call 1: all_requests_total query.count.side_effect = [all_requests_count] # .all() — 7 calls in source order (see docstring above) query.all.side_effect = [ model_token_rows, # 1. model_token_query phase_rows, # 2. phase_query context_limit_rows, # 3. context_limits recent_truncated_rows, # 4. recent_truncated time_series_rows, # 5. time_series_data model_stat_rows, # 6. model_stats all_requests_rows, # 7. all_requests_data ] mock_session = MagicMock() mock_session.query.return_value = query return mock_session # --------------------------------------------------------------------------- # Tests: get_context_overflow_metrics # --------------------------------------------------------------------------- class TestGetContextOverflowMetrics: """Branch coverage for GET /api/context-overflow.""" def test_default_period_30d_empty_data(self, authed_client): """Default period (30d) with empty DB returns success.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "success" assert data["overview"]["total_requests"] == 0 assert data["overview"]["truncation_rate"] == 0 assert data["pagination"]["per_page"] == 50 assert data["pagination"]["page"] == 1 def test_period_all_no_date_filter(self, authed_client): """period=all skips start_date calculation branch.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?period=all") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "success" def test_period_7d(self, authed_client): """period=7d takes the 7d timedelta branch.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?period=7d") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "success" def test_period_3m(self, authed_client): """period=3m takes the 90-day timedelta branch.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?period=3m") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "success" def test_period_1y(self, authed_client): """period=1y takes the 365-day timedelta branch.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?period=1y") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "success" def test_invalid_period_defaults_to_30d(self, authed_client): """An unrecognised period string is replaced with '30d'.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?period=bogus") assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "success" # The response builds normally (no crash), confirming the fallback ran assert "pagination" in data def test_per_page_clamped_to_max_500(self, authed_client): """per_page > 500 is clamped to 500.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?per_page=9999") assert resp.status_code == 200 assert resp.get_json()["pagination"]["per_page"] == 500 def test_per_page_clamped_to_min_1(self, authed_client): """per_page < 1 is clamped to 1.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?per_page=0") assert resp.status_code == 200 assert resp.get_json()["pagination"]["per_page"] == 1 def test_page_clamped_to_min_1(self, authed_client): """page < 1 is clamped to 1.""" mock_session = _build_main_endpoint_session() with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?page=-3") assert resp.status_code == 200 assert resp.get_json()["pagination"]["page"] == 1 def test_truncation_rate_calculated_when_context_data_present( self, authed_client ): """When requests_with_context > 0, truncation_rate branch executes.""" overview_row = _make_overview_row(total=10, with_ctx=8, truncated=4) mock_session = _build_main_endpoint_session( overview_row=overview_row, token_summary_row=_make_token_summary_row(total_requests=10), avg_tokens_scalar=200, ) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow") assert resp.status_code == 200 data = resp.get_json() assert data["overview"]["truncation_rate"] == 50.0 assert data["overview"]["avg_tokens_truncated"] == 200 def test_with_recent_truncated_rows_in_response(self, authed_client): """recent_truncated rows are formatted correctly in the response.""" trunc_req = _make_token_usage( context_truncated=True, tokens_truncated=512, truncation_ratio=0.0625, ollama_prompt_eval_count=None, ) mock_session = _build_main_endpoint_session( overview_row=_make_overview_row(total=1, with_ctx=1, truncated=1), token_summary_row=_make_token_summary_row(total_requests=1), recent_truncated_rows=[trunc_req], ) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow") assert resp.status_code == 200 data = resp.get_json() assert len(data["recent_truncated"]) == 1 entry = data["recent_truncated"][0] assert entry["tokens_truncated"] == 512 assert entry["truncation_ratio"] == pytest.approx(0.0625) def test_chart_data_with_truncated_entry_uses_ollama_tokens( self, authed_client ): """ When context_truncated=True and ollama_prompt_eval_count is set, original_tokens = ollama_used + tokens_truncated (not prompt_tokens). """ ts_entry = _make_token_usage( context_truncated=True, tokens_truncated=100, ollama_prompt_eval_count=900, prompt_tokens=800, ) mock_session = _build_main_endpoint_session( overview_row=_make_overview_row(total=1), token_summary_row=_make_token_summary_row(total_requests=1), time_series_rows=[ts_entry], ) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?period=7d") assert resp.status_code == 200 data = resp.get_json() assert len(data["chart_data"]) == 1 point = data["chart_data"][0] # original = ollama_used(900) + tokens_truncated(100) assert point["original_prompt_tokens"] == 1000 assert point["ollama_prompt_tokens"] == 900 assert point["truncated"] is True def test_chart_data_non_truncated_entry_uses_actual_prompt( self, authed_client ): """ When context_truncated=False, original_tokens == actual_prompt. With no ollama count, actual_prompt = prompt_tokens. """ ts_entry = _make_token_usage( context_truncated=False, tokens_truncated=0, ollama_prompt_eval_count=None, prompt_tokens=500, ) mock_session = _build_main_endpoint_session( overview_row=_make_overview_row(total=1), token_summary_row=_make_token_summary_row(total_requests=1), time_series_rows=[ts_entry], ) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?period=30d") assert resp.status_code == 200 data = resp.get_json() assert len(data["chart_data"]) == 1 point = data["chart_data"][0] assert point["original_prompt_tokens"] == 500 assert point["truncated"] is False def test_longer_period_uses_500_limit_for_time_series(self, authed_client): """ For period in {"3m", "1y"} the time-series query uses .limit(500), while "7d" / "30d" uses .limit(1000). Both paths complete without error. """ mock_session = _build_main_endpoint_session( overview_row=_make_overview_row(), token_summary_row=_make_token_summary_row(), ) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?period=3m") assert resp.status_code == 200 def test_all_requests_pagination_total_pages_calculated( self, authed_client ): """When all_requests_total > 0, total_pages is derived from count.""" req = _make_token_usage() mock_session = _build_main_endpoint_session( overview_row=_make_overview_row(total=1), token_summary_row=_make_token_summary_row(total_requests=1), all_requests_rows=[req], all_requests_count=1, ) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow?per_page=50&page=1") assert resp.status_code == 200 data = resp.get_json() pagination = data["pagination"] assert pagination["total_count"] == 1 assert pagination["total_pages"] == 1 assert len(data["all_requests"]) == 1 entry = data["all_requests"][0] assert entry["model"] == "gpt-4" assert entry["provider"] == "openai" def test_model_token_stats_and_phase_breakdown_populated( self, authed_client ): """model_token_stats and phase_breakdown rows are serialised.""" model_row = MagicMock() model_row.model_name = "claude-3" model_row.model_provider = "anthropic" model_row.total_requests = 5 model_row.total_tokens = 2000 model_row.avg_prompt = 300.0 model_row.max_prompt = 600 model_row.avg_response_time_ms = 250.0 phase_row = MagicMock() phase_row.research_phase = "synthesis" phase_row.count = 3 phase_row.total_tokens = 900 phase_row.avg_tokens = 300.0 mock_session = _build_main_endpoint_session( model_token_rows=[model_row], phase_rows=[phase_row], ) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow") assert resp.status_code == 200 data = resp.get_json() assert len(data["model_token_stats"]) == 1 assert data["model_token_stats"][0]["model"] == "claude-3" assert len(data["phase_breakdown"]) == 1 assert data["phase_breakdown"][0]["phase"] == "synthesis" def test_model_stats_with_none_avg_context_limit(self, authed_client): """model_stats entry with avg_context_limit=None produces None in output.""" stat = MagicMock() stat.model_name = "llama" stat.model_provider = "ollama" stat.total_requests = 2 stat.truncated_count = 0 stat.avg_context_limit = None mock_session = _build_main_endpoint_session( model_stat_rows=[stat], ) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow") assert resp.status_code == 200 data = resp.get_json() assert len(data["model_stats"]) == 1 assert data["model_stats"][0]["avg_context_limit"] is None def test_model_stats_truncation_rate_when_total_requests_zero( self, authed_client ): """model_stats entry with total_requests=0 yields truncation_rate=0.""" stat = MagicMock() stat.model_name = "mystery" stat.model_provider = "unknown" stat.total_requests = 0 stat.truncated_count = 0 stat.avg_context_limit = 4096 mock_session = _build_main_endpoint_session(model_stat_rows=[stat]) with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/context-overflow") assert resp.status_code == 200 data = resp.get_json() assert data["model_stats"][0]["truncation_rate"] == 0 def test_db_exception_returns_500(self, authed_client): """When get_user_db_session raises, endpoint returns 500.""" @contextmanager def _exploding(*args, **kwargs): raise RuntimeError("simulated DB failure") yield # pragma: no cover with patch(f"{MODULE}.get_user_db_session", side_effect=_exploding): resp = authed_client.get("/api/context-overflow") assert resp.status_code == 500 data = resp.get_json() assert data["status"] == "error" assert "context overflow metrics" in data["message"] def test_unauthenticated_returns_401(self, client): """Without a session the endpoint rejects the request.""" resp = client.get("/api/context-overflow") assert resp.status_code in (401, 302) # --------------------------------------------------------------------------- # Tests: get_research_context_overflow # --------------------------------------------------------------------------- class TestGetResearchContextOverflow: """Branch coverage for GET /api/research//context-overflow.""" def test_empty_token_usage_returns_early_empty_response( self, authed_client ): """No rows for research_id → early return with zero overview.""" mock_session = MagicMock() query = _make_chainable_query() query.all.return_value = [] mock_session.query.return_value = query with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get( "/api/research/nonexistent/context-overflow" ) assert resp.status_code == 200 data = resp.get_json() assert data["status"] == "success" assert data["data"]["overview"]["total_requests"] == 0 assert data["data"]["overview"]["total_tokens"] == 0 assert data["data"]["overview"]["truncation_occurred"] is False assert data["data"]["requests"] == [] def test_with_mixed_truncated_and_normal_usage(self, authed_client): """Token usage with both truncated and non-truncated records.""" normal = _make_token_usage( id=1, context_truncated=False, tokens_truncated=0, research_phase="search", total_tokens=200, prompt_tokens=150, completion_tokens=50, context_limit=8192, ) truncated = _make_token_usage( id=2, context_truncated=True, tokens_truncated=300, research_phase="search", total_tokens=500, prompt_tokens=450, completion_tokens=50, context_limit=8192, ) mock_session = MagicMock() query = _make_chainable_query() query.all.return_value = [normal, truncated] mock_session.query.return_value = query with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/research/r1/context-overflow") assert resp.status_code == 200 data = resp.get_json() overview = data["data"]["overview"] assert overview["total_requests"] == 2 assert overview["total_tokens"] == 700 assert overview["truncation_occurred"] is True assert overview["truncated_count"] == 1 assert overview["tokens_lost"] == 300 assert overview["context_limit"] == 8192 assert overview["max_tokens_used"] == 450 assert data["data"]["model"] == "gpt-4" assert data["data"]["provider"] == "openai" def test_phase_stats_accumulation(self, authed_client): """Phase stats correctly accumulate counts and token sums per phase.""" entry_a = _make_token_usage( research_phase="search", total_tokens=100, prompt_tokens=80, completion_tokens=20, context_truncated=False, tokens_truncated=0, ) entry_b = _make_token_usage( research_phase="search", total_tokens=120, prompt_tokens=90, completion_tokens=30, context_truncated=True, tokens_truncated=50, ) entry_c = _make_token_usage( research_phase="synthesis", total_tokens=200, prompt_tokens=160, completion_tokens=40, context_truncated=False, tokens_truncated=0, ) mock_session = MagicMock() query = _make_chainable_query() query.all.return_value = [entry_a, entry_b, entry_c] mock_session.query.return_value = query with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/research/r1/context-overflow") assert resp.status_code == 200 phase_stats = resp.get_json()["data"]["phase_stats"] assert "search" in phase_stats assert "synthesis" in phase_stats assert phase_stats["search"]["count"] == 2 assert phase_stats["search"]["truncated_count"] == 1 assert phase_stats["search"]["total_tokens"] == 220 assert phase_stats["synthesis"]["count"] == 1 assert phase_stats["synthesis"]["truncated_count"] == 0 def test_none_research_phase_maps_to_unknown(self, authed_client): """research_phase=None is bucketed under 'unknown'.""" entry = _make_token_usage(research_phase=None) mock_session = MagicMock() query = _make_chainable_query() query.all.return_value = [entry] mock_session.query.return_value = query with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/research/r1/context-overflow") assert resp.status_code == 200 phase_stats = resp.get_json()["data"]["phase_stats"] assert "unknown" in phase_stats def test_no_context_limit_on_any_entry(self, authed_client): """When no entry has a context_limit, overview.context_limit is None.""" entry = _make_token_usage(context_limit=None) mock_session = MagicMock() query = _make_chainable_query() query.all.return_value = [entry] mock_session.query.return_value = query with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/research/r1/context-overflow") assert resp.status_code == 200 data = resp.get_json() assert data["data"]["overview"]["context_limit"] is None def test_requests_list_contains_correct_fields(self, authed_client): """Each entry in requests[] includes all expected keys.""" entry = _make_token_usage( ollama_prompt_eval_count=42, calling_function="my_fn", response_time_ms=300, tokens_truncated=0, context_truncated=False, ) mock_session = MagicMock() query = _make_chainable_query() query.all.return_value = [entry] mock_session.query.return_value = query with patch( f"{MODULE}.get_user_db_session", return_value=_mock_db_session(mock_session), ): resp = authed_client.get("/api/research/r1/context-overflow") assert resp.status_code == 200 req_entry = resp.get_json()["data"]["requests"][0] for key in [ "timestamp", "phase", "prompt_tokens", "completion_tokens", "total_tokens", "context_limit", "context_truncated", "tokens_truncated", "ollama_prompt_eval_count", "calling_function", "response_time_ms", ]: assert key in req_entry, f"missing key: {key}" assert req_entry["ollama_prompt_eval_count"] == 42 assert req_entry["calling_function"] == "my_fn" assert req_entry["context_truncated"] is False def test_db_exception_returns_500(self, authed_client): """When get_user_db_session raises, endpoint returns 500.""" @contextmanager def _exploding(*args, **kwargs): raise RuntimeError("simulated DB failure") yield # pragma: no cover with patch(f"{MODULE}.get_user_db_session", side_effect=_exploding): resp = authed_client.get("/api/research/r1/context-overflow") assert resp.status_code == 500 data = resp.get_json() assert data["status"] == "error" assert "context overflow data" in data["message"] def test_unauthenticated_returns_401(self, client): """Without a session the research endpoint rejects the request.""" resp = client.get("/api/research/r1/context-overflow") assert resp.status_code in (401, 302)