Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

887 lines
31 KiB
Python

"""
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/<id>/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)