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

581 lines
19 KiB
Python

"""Coverage tests for token_counter.py targeting specific logic paths."""
import json
import threading
from unittest.mock import MagicMock, Mock, patch
from local_deep_research.metrics.token_counter import (
TokenCounter,
TokenCountingCallback,
)
def _make_callback(**overrides):
"""Create a TokenCountingCallback with sensible defaults."""
ctx = overrides.pop(
"research_context",
{"research_query": "test query", "research_mode": "quick"},
)
cb = TokenCountingCallback(
research_id=overrides.pop("research_id", "r-100"),
research_context=ctx,
)
cb.current_model = overrides.pop("current_model", "gpt-4")
cb.current_provider = overrides.pop("current_provider", "openai")
cb.response_time_ms = overrides.pop("response_time_ms", 200)
cb.success_status = overrides.pop("success_status", "success")
cb.error_type = overrides.pop("error_type", None)
cb.calling_file = overrides.pop("calling_file", "runner.py")
cb.calling_function = overrides.pop("calling_function", "execute")
cb.call_stack = overrides.pop("call_stack", None)
for k, v in overrides.items():
setattr(cb, k, v)
return cb
def _patch_main_thread():
t = MagicMock()
t.name = "MainThread"
return patch.object(threading, "current_thread", return_value=t)
def _patch_worker_thread():
t = MagicMock()
t.name = "WorkerThread-42"
return patch.object(threading, "current_thread", return_value=t)
def _mock_db_session():
s = MagicMock()
s.query.return_value.filter_by.return_value.first.return_value = None
return s
def _patch_get_user_db_session(mock_session):
ctx = MagicMock()
ctx.__enter__ = Mock(return_value=mock_session)
ctx.__exit__ = Mock(return_value=False)
return patch(
"local_deep_research.database.session_context.get_user_db_session",
return_value=ctx,
)
class TestSaveToDbMainThreadCoverage:
def test_saves_token_usage_and_new_model_usage(self):
cb = _make_callback()
mock_session = _mock_db_session()
added = []
mock_session.add.side_effect = lambda obj: added.append(obj)
with _patch_main_thread():
with patch("flask.session", {"username": "alice"}, create=True):
with _patch_get_user_db_session(mock_session):
cb._save_to_db(80, 40)
assert len(added) == 2
assert added[0].prompt_tokens == 80
assert added[0].total_tokens == 120
assert added[1].model_name == "gpt-4"
assert added[1].total_calls == 1
mock_session.commit.assert_called_once()
def test_updates_existing_model_usage(self):
cb = _make_callback()
existing = MagicMock()
existing.total_tokens = 500
existing.total_calls = 3
mock_session = _mock_db_session()
mock_session.query.return_value.filter_by.return_value.first.return_value = existing
with _patch_main_thread():
with patch("flask.session", {"username": "alice"}, create=True):
with _patch_get_user_db_session(mock_session):
cb._save_to_db(100, 50)
assert existing.total_tokens == 650
assert existing.total_calls == 4
assert mock_session.add.call_count == 1
def test_skips_without_username(self):
cb = _make_callback()
with _patch_main_thread():
with patch("flask.session", {}, create=True):
with patch(
"local_deep_research.database.session_context.get_user_db_session"
) as mock_gs:
cb._save_to_db(10, 5)
mock_gs.assert_not_called()
def test_handles_db_exception(self):
cb = _make_callback()
with _patch_main_thread():
with patch("flask.session", {"username": "alice"}, create=True):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=RuntimeError("connection lost"),
):
cb._save_to_db(10, 5)
def test_list_search_engines_planned_converted_to_json(self):
cb = _make_callback(
research_context={
"research_query": "q",
"search_engines_planned": ["google", "brave"],
}
)
mock_session = _mock_db_session()
added = []
mock_session.add.side_effect = lambda obj: added.append(obj)
with _patch_main_thread():
with patch("flask.session", {"username": "alice"}, create=True):
with _patch_get_user_db_session(mock_session):
cb._save_to_db(50, 25)
assert isinstance(added[0].search_engines_planned, str)
assert json.loads(added[0].search_engines_planned) == [
"google",
"brave",
]
def test_string_search_engines_planned_unchanged(self):
cb = _make_callback(
research_context={
"research_query": "q",
"search_engines_planned": '["already_json"]',
}
)
mock_session = _mock_db_session()
added = []
mock_session.add.side_effect = lambda obj: added.append(obj)
with _patch_main_thread():
with patch("flask.session", {"username": "alice"}, create=True):
with _patch_get_user_db_session(mock_session):
cb._save_to_db(10, 5)
assert added[0].search_engines_planned == '["already_json"]'
class TestSaveToDbBackgroundThreadCoverage:
def test_skips_without_username_in_context(self):
cb = _make_callback(research_context={})
with _patch_worker_thread():
cb._save_to_db(100, 50)
def test_skips_without_password(self):
cb = _make_callback(research_context={"username": "bob"})
mock_writer = MagicMock()
with _patch_worker_thread():
with patch(
"local_deep_research.database.thread_metrics.metrics_writer",
mock_writer,
):
cb._save_to_db(100, 50)
mock_writer.write_token_metrics.assert_not_called()
def test_handles_metrics_writer_exception(self):
cb = _make_callback(
research_context={"username": "bob", "user_password": "secret"}
)
mock_writer = MagicMock()
mock_writer.write_token_metrics.side_effect = RuntimeError("fail")
with _patch_worker_thread():
with patch(
"local_deep_research.database.thread_metrics.metrics_writer",
mock_writer,
):
cb._save_to_db(100, 50)
def test_writes_token_data_with_context_overflow_fields(self):
cb = _make_callback(
research_context={
"username": "bob",
"user_password": "secret",
"research_query": "AI trends",
"search_engines_planned": ["google"],
}
)
cb.context_limit = 4096
cb.context_truncated = True
cb.tokens_truncated = 200
mock_writer = MagicMock()
with _patch_worker_thread():
with patch(
"local_deep_research.database.thread_metrics.metrics_writer",
mock_writer,
):
cb._save_to_db(100, 50)
token_data = mock_writer.write_token_metrics.call_args[0][2]
assert token_data["context_limit"] == 4096
assert token_data["context_truncated"] is True
def test_list_search_engines_planned_json_in_thread(self):
cb = _make_callback(
research_context={
"username": "bob",
"user_password": "secret",
"search_engines_planned": ["bing", "duckduckgo"],
}
)
mock_writer = MagicMock()
with _patch_worker_thread():
with patch(
"local_deep_research.database.thread_metrics.metrics_writer",
mock_writer,
):
cb._save_to_db(50, 25)
token_data = mock_writer.write_token_metrics.call_args[0][2]
assert isinstance(token_data["search_engines_planned"], str)
assert json.loads(token_data["search_engines_planned"]) == [
"bing",
"duckduckgo",
]
class TestGetResearchMetricsCoverage:
def test_empty_without_username(self):
counter = TokenCounter()
with patch("flask.session", {}):
result = counter.get_research_metrics("res-1")
assert result["total_tokens"] == 0
assert result["model_usage"] == []
def test_queries_and_formats_results(self):
counter = TokenCounter()
row1 = MagicMock(
model_name="gpt-4",
model_provider="openai",
prompt_tokens=200,
completion_tokens=100,
total_tokens=300,
calls=2,
)
row2 = MagicMock(
model_name="claude-3",
model_provider="anthropic",
prompt_tokens=150,
completion_tokens=50,
total_tokens=200,
calls=1,
)
mock_session = MagicMock()
chain = mock_session.query.return_value
chain.filter_by.return_value = chain
chain.group_by.return_value = chain
chain.order_by.return_value = chain
chain.all.return_value = [row1, row2]
with patch("flask.session", {"username": "alice"}):
with _patch_get_user_db_session(mock_session):
result = counter.get_research_metrics("res-42")
assert result["total_tokens"] == 500
assert result["total_calls"] == 3
assert len(result["model_usage"]) == 2
class TestGetMetricsFromEncryptedDbCoverage:
def test_empty_without_username(self):
counter = TokenCounter()
with patch("flask.session", {}):
result = counter._get_metrics_from_encrypted_db("30d", "all")
assert result["total_tokens"] == 0
def test_exception_returns_empty_metrics(self):
counter = TokenCounter()
with patch("flask.session", {"username": "alice"}):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=Exception("DB down"),
):
result = counter._get_metrics_from_encrypted_db("30d", "all")
assert result["total_tokens"] == 0
assert result["by_model"] == []
def test_complete_metrics_build_with_30d_period(self):
counter = TokenCounter()
mock_session = MagicMock()
mock_query = MagicMock()
mock_session.query.return_value = mock_query
for attr in (
"filter",
"filter_by",
"with_entities",
"group_by",
"order_by",
"limit",
"distinct",
):
getattr(mock_query, attr).return_value = mock_query
mock_query.scalar.return_value = 0
mock_query.all.return_value = []
breakdown = MagicMock(
total_input_tokens=3000,
total_output_tokens=2000,
avg_input_tokens=150,
avg_output_tokens=100,
avg_total_tokens=250,
)
mock_query.first.return_value = breakdown
mock_query.count.return_value = 0
with patch("flask.session", {"username": "alice"}):
with _patch_get_user_db_session(mock_session):
result = counter._get_metrics_from_encrypted_db("30d", "all")
assert "rate_limiting" in result
assert result["rate_limiting"]["total_attempts"] == 0
class TestGetEnhancedMetricsCoverage:
def test_empty_without_username(self):
counter = TokenCounter()
with patch("flask.session", {}):
result = counter.get_enhanced_metrics()
assert result["performance_stats"]["total_enhanced_calls"] == 0
assert result["time_series_data"] == []
def test_exception_returns_empty_structure(self):
counter = TokenCounter()
with patch("flask.session", {"username": "alice"}):
with patch(
"local_deep_research.database.session_context.get_user_db_session",
side_effect=Exception("refused"),
):
result = counter.get_enhanced_metrics()
assert result["mode_breakdown"] == []
def test_time_series_and_performance_stats(self):
counter = TokenCounter()
mock_session = MagicMock()
mock_query = MagicMock()
mock_session.query.return_value = mock_query
for attr in (
"filter",
"with_entities",
"group_by",
"order_by",
"limit",
):
getattr(mock_query, attr).return_value = mock_query
mock_query.all.return_value = []
mock_query.count.return_value = 5
mock_query.scalar.return_value = 350
with patch("flask.session", {"username": "alice"}):
with _patch_get_user_db_session(mock_session):
result = counter.get_enhanced_metrics("30d", "all")
assert "performance_stats" in result
assert "time_series_data" in result
class TestGetResearchTimelineMetricsCoverage:
def test_empty_without_username(self):
counter = TokenCounter()
with patch("flask.session", {}):
result = counter.get_research_timeline_metrics("res-1")
assert result["timeline"] == []
assert result["summary"]["total_calls"] == 0
def test_sql_timeline_build(self):
counter = TokenCounter()
mock_session = MagicMock()
row1 = (
"2026-01-01 10:00:00",
100,
60,
40,
150,
"success",
None,
"search",
1,
"google",
"gpt-4",
"runner.py",
"execute",
None,
)
row2 = (
"2026-01-01 10:01:00",
200,
120,
80,
250,
"success",
None,
"analysis",
2,
"brave",
"gpt-4",
"analyzer.py",
"analyze",
"stack",
)
research_info = (
"AI trends",
"quick",
"completed",
"2026-01-01",
"2026-01-01",
)
mock_session.execute = Mock(
side_effect=[
MagicMock(fetchall=Mock(return_value=[row1, row2])),
MagicMock(fetchone=Mock(return_value=research_info)),
]
)
with patch("flask.session", {"username": "alice"}):
with _patch_get_user_db_session(mock_session):
result = counter.get_research_timeline_metrics("res-5")
assert len(result["timeline"]) == 2
assert result["timeline"][1]["cumulative_tokens"] == 300
assert result["research_details"]["query"] == "AI trends"
assert result["summary"]["total_tokens"] == 300
def test_no_research_info(self):
counter = TokenCounter()
mock_session = MagicMock()
row1 = (
"2026-01-01",
50,
30,
20,
100,
"success",
None,
"search",
1,
None,
"gpt-4",
None,
None,
None,
)
mock_session.execute = Mock(
side_effect=[
MagicMock(fetchall=Mock(return_value=[row1])),
MagicMock(fetchone=Mock(return_value=None)),
]
)
with patch("flask.session", {"username": "alice"}):
with _patch_get_user_db_session(mock_session):
result = counter.get_research_timeline_metrics("res-missing")
assert result["research_details"] == {}
def test_phase_stats_aggregation(self):
counter = TokenCounter()
mock_session = MagicMock()
row1 = (
"2026-01-01",
100,
60,
40,
200,
"success",
None,
"search",
1,
"google",
"gpt-4",
None,
None,
None,
)
row2 = (
"2026-01-01",
150,
90,
60,
400,
"success",
None,
"search",
2,
"google",
"gpt-4",
None,
None,
None,
)
row3 = (
"2026-01-01",
200,
120,
80,
300,
"error",
"TimeoutError",
"analysis",
1,
None,
"gpt-4",
None,
None,
None,
)
mock_session.execute = Mock(
side_effect=[
MagicMock(fetchall=Mock(return_value=[row1, row2, row3])),
MagicMock(fetchone=Mock(return_value=None)),
]
)
with patch("flask.session", {"username": "alice"}):
with _patch_get_user_db_session(mock_session):
result = counter.get_research_timeline_metrics("res-ps")
assert result["phase_stats"]["search"]["count"] == 2
assert result["phase_stats"]["search"]["avg_response_time"] == 300
assert result["phase_stats"]["analysis"]["tokens"] == 200
def test_success_rate_in_summary(self):
counter = TokenCounter()
mock_session = MagicMock()
row_ok = (
"2026-01-01",
100,
60,
40,
100,
"success",
None,
"search",
1,
None,
"gpt-4",
None,
None,
None,
)
row_err = (
"2026-01-01",
50,
30,
20,
200,
"error",
"ValueError",
"search",
2,
None,
"gpt-4",
None,
None,
None,
)
mock_session.execute = Mock(
side_effect=[
MagicMock(fetchall=Mock(return_value=[row_ok, row_err])),
MagicMock(fetchone=Mock(return_value=None)),
]
)
with patch("flask.session", {"username": "alice"}):
with _patch_get_user_db_session(mock_session):
result = counter.get_research_timeline_metrics("res-sr")
assert result["summary"]["success_rate"] == 50.0
def test_empty_timeline_data(self):
counter = TokenCounter()
mock_session = MagicMock()
mock_session.execute = Mock(
side_effect=[
MagicMock(fetchall=Mock(return_value=[])),
MagicMock(fetchone=Mock(return_value=None)),
]
)
with patch("flask.session", {"username": "alice"}):
with _patch_get_user_db_session(mock_session):
result = counter.get_research_timeline_metrics("res-empty")
assert result["timeline"] == []
assert result["summary"]["total_calls"] == 0
assert result["phase_stats"] == {}