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

641 lines
23 KiB
Python

"""Comprehensive tests for ThreadSafeMetricsWriter in database/thread_metrics.py."""
import threading
from unittest.mock import MagicMock, patch
import pytest
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
metrics_writer,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def writer():
"""Return a fresh ThreadSafeMetricsWriter instance."""
return ThreadSafeMetricsWriter()
@pytest.fixture
def writer_with_password(writer):
"""Return a writer that already has a password stored for 'testuser'."""
writer.set_user_password("testuser", "testpass")
return writer
@pytest.fixture
def mock_db_manager():
"""Patch db_manager and yield (mock_db_manager, mock_session)."""
with patch(
"local_deep_research.database.thread_metrics.db_manager"
) as mock_db:
mock_session = MagicMock()
mock_db.create_thread_safe_session_for_metrics.return_value = (
mock_session
)
yield mock_db, mock_session
# ===========================================================================
# set_user_password
# ===========================================================================
class TestSetUserPassword:
"""Tests for ThreadSafeMetricsWriter.set_user_password."""
def test_stores_password_in_thread_local(self, writer):
"""set_user_password stores the password under _thread_local.passwords."""
writer.set_user_password("alice", "secret123")
assert writer._thread_local.passwords["alice"] == "secret123"
def test_creates_passwords_dict_if_not_exists(self, writer):
"""Passwords dict is lazily created on first call."""
assert not hasattr(writer._thread_local, "passwords")
writer.set_user_password("alice", "secret")
assert hasattr(writer._thread_local, "passwords")
assert isinstance(writer._thread_local.passwords, dict)
def test_can_store_multiple_users(self, writer):
"""Multiple users can be stored simultaneously."""
writer.set_user_password("alice", "pass_a")
writer.set_user_password("bob", "pass_b")
writer.set_user_password("charlie", "pass_c")
assert writer._thread_local.passwords == {
"alice": "pass_a",
"bob": "pass_b",
"charlie": "pass_c",
}
def test_overwrites_existing_password(self, writer):
"""Setting a password for the same user replaces the old value."""
writer.set_user_password("alice", "old_password")
writer.set_user_password("alice", "new_password")
assert writer._thread_local.passwords["alice"] == "new_password"
assert len(writer._thread_local.passwords) == 1
# ===========================================================================
# get_session
# ===========================================================================
class TestGetSession:
"""Tests for ThreadSafeMetricsWriter.get_session."""
# --- error paths -------------------------------------------------------
def test_raises_when_no_passwords_dict(self, writer):
"""Raises ValueError when passwords dict has never been created."""
with pytest.raises(ValueError, match="No password set"):
with writer.get_session("testuser"):
pass
def test_raises_when_password_not_found_for_user(self, writer):
"""Raises ValueError when the requested user has no stored password."""
writer.set_user_password("other_user", "other_pass")
with pytest.raises(ValueError, match="No password available"):
with writer.get_session("missing_user"):
pass
def test_raises_when_username_none_and_no_flask_context(self, writer):
"""Raises ValueError when username=None and Flask context is absent."""
writer.set_user_password("alice", "pass")
# The real code does `from flask import session as flask_session`
# inside the function body. A RuntimeError simulates no app context.
with pytest.raises((ValueError, RuntimeError)):
with writer.get_session(username=None):
pass
def test_raises_when_db_manager_returns_none_session(
self, writer_with_password
):
"""Raises ValueError when db_manager returns None for the session."""
with patch(
"local_deep_research.database.thread_metrics.db_manager"
) as mock_db:
mock_db.create_thread_safe_session_for_metrics.return_value = None
with pytest.raises(ValueError, match="Failed to create session"):
with writer_with_password.get_session("testuser"):
pass
# --- happy path --------------------------------------------------------
def test_yields_session_on_success(
self, writer_with_password, mock_db_manager
):
"""Successfully yields a session object when password is available."""
_mock_db, mock_session = mock_db_manager
with writer_with_password.get_session("testuser") as session:
assert session is mock_session
def test_creates_session_with_correct_args(
self, writer_with_password, mock_db_manager
):
"""Passes correct username and password to db_manager."""
mock_db, _mock_session = mock_db_manager
with writer_with_password.get_session("testuser"):
pass
mock_db.create_thread_safe_session_for_metrics.assert_called_once_with(
"testuser", "testpass"
)
def test_commits_session_on_success(
self, writer_with_password, mock_db_manager
):
"""Session is committed when context manager exits normally."""
_mock_db, mock_session = mock_db_manager
with writer_with_password.get_session("testuser"):
pass
mock_session.commit.assert_called_once()
mock_session.rollback.assert_not_called()
def test_rollback_on_exception(self, writer_with_password, mock_db_manager):
"""Session is rolled back when an exception occurs inside the block."""
_mock_db, mock_session = mock_db_manager
with pytest.raises(RuntimeError, match="boom"):
with writer_with_password.get_session("testuser"):
raise RuntimeError("boom")
mock_session.rollback.assert_called_once()
mock_session.commit.assert_not_called()
def test_closes_session_on_success(
self, writer_with_password, mock_db_manager
):
"""Session is closed in the finally block after normal exit."""
_mock_db, mock_session = mock_db_manager
with writer_with_password.get_session("testuser"):
pass
mock_session.close.assert_called_once()
def test_closes_session_on_exception(
self, writer_with_password, mock_db_manager
):
"""Session is closed in the finally block even after an exception."""
_mock_db, mock_session = mock_db_manager
with pytest.raises(RuntimeError):
with writer_with_password.get_session("testuser"):
raise RuntimeError("failure")
mock_session.close.assert_called_once()
def test_closes_session_when_db_manager_returns_none(
self, writer_with_password
):
"""When db_manager returns None, session is not closed (it is None)."""
with patch(
"local_deep_research.database.thread_metrics.db_manager"
) as mock_db:
mock_db.create_thread_safe_session_for_metrics.return_value = None
with pytest.raises(ValueError, match="Failed to create session"):
with writer_with_password.get_session("testuser"):
pass
# session was None, so close should never be called on it
# ===========================================================================
# write_token_metrics
# ===========================================================================
class TestWriteTokenMetrics:
"""Tests for ThreadSafeMetricsWriter.write_token_metrics."""
def _run_write(self, writer, mock_db_manager, token_data, research_id=42):
"""Helper: call write_token_metrics with mocked session and TokenUsage."""
mock_db, mock_session = mock_db_manager
mock_token_cls = MagicMock()
with patch(
"local_deep_research.database.models.TokenUsage",
mock_token_cls,
):
writer.write_token_metrics("testuser", research_id, token_data)
return mock_token_cls, mock_session
# --- field mapping -----------------------------------------------------
def test_creates_token_usage_with_correct_fields(
self, writer_with_password, mock_db_manager
):
"""TokenUsage is constructed with all expected fields from token_data."""
token_data = {
"model_name": "gpt-4",
"provider": "openai",
"prompt_tokens": 100,
"completion_tokens": 50,
"research_query": "test query",
"research_mode": "deep",
"research_phase": "search",
"search_iteration": 2,
"response_time_ms": 1234,
"success_status": "success",
"error_type": None,
"search_engines_planned": ["google", "bing"],
"search_engine_selected": "google",
"calling_file": "main.py",
"calling_function": "run_search",
"call_stack": ["a", "b"],
"context_limit": 4096,
"context_truncated": True,
"tokens_truncated": 500,
"truncation_ratio": 0.12,
"ollama_prompt_eval_count": 90,
"ollama_eval_count": 45,
"ollama_total_duration": 999999,
"ollama_load_duration": 111111,
"ollama_prompt_eval_duration": 222222,
"ollama_eval_duration": 333333,
}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data, research_id=7
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["research_id"] == 7
assert call_kwargs["model_name"] == "gpt-4"
assert call_kwargs["model_provider"] == "openai"
assert call_kwargs["prompt_tokens"] == 100
assert call_kwargs["completion_tokens"] == 50
assert call_kwargs["total_tokens"] == 150
assert call_kwargs["research_query"] == "test query"
assert call_kwargs["research_mode"] == "deep"
assert call_kwargs["research_phase"] == "search"
assert call_kwargs["search_iteration"] == 2
assert call_kwargs["response_time_ms"] == 1234
assert call_kwargs["success_status"] == "success"
assert call_kwargs["error_type"] is None
assert call_kwargs["search_engines_planned"] == ["google", "bing"]
assert call_kwargs["search_engine_selected"] == "google"
assert call_kwargs["calling_file"] == "main.py"
assert call_kwargs["calling_function"] == "run_search"
assert call_kwargs["call_stack"] == ["a", "b"]
assert call_kwargs["context_limit"] == 4096
assert call_kwargs["context_truncated"] is True
assert call_kwargs["tokens_truncated"] == 500
assert call_kwargs["truncation_ratio"] == 0.12
assert call_kwargs["ollama_prompt_eval_count"] == 90
assert call_kwargs["ollama_eval_count"] == 45
assert call_kwargs["ollama_total_duration"] == 999999
assert call_kwargs["ollama_load_duration"] == 111111
assert call_kwargs["ollama_prompt_eval_duration"] == 222222
assert call_kwargs["ollama_eval_duration"] == 333333
def test_adds_record_to_session(
self, writer_with_password, mock_db_manager
):
"""The TokenUsage record is added to the session via session.add()."""
token_data = {
"model_name": "gpt-4",
"provider": "openai",
"prompt_tokens": 10,
"completion_tokens": 5,
}
mock_token_cls, mock_session = self._run_write(
writer_with_password, mock_db_manager, token_data
)
mock_session.add.assert_called_once_with(mock_token_cls.return_value)
def test_total_tokens_calculated_as_sum(
self, writer_with_password, mock_db_manager
):
"""total_tokens equals prompt_tokens + completion_tokens."""
token_data = {
"prompt_tokens": 200,
"completion_tokens": 75,
}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["total_tokens"] == 275
assert (
call_kwargs["total_tokens"]
== call_kwargs["prompt_tokens"] + call_kwargs["completion_tokens"]
)
# --- defaults ----------------------------------------------------------
def test_default_prompt_tokens_zero(
self, writer_with_password, mock_db_manager
):
"""prompt_tokens defaults to 0 when not provided."""
token_data = {"completion_tokens": 30}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["prompt_tokens"] == 0
def test_default_completion_tokens_zero(
self, writer_with_password, mock_db_manager
):
"""completion_tokens defaults to 0 when not provided."""
token_data = {"prompt_tokens": 50}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["completion_tokens"] == 0
def test_default_success_status(
self, writer_with_password, mock_db_manager
):
"""success_status defaults to 'success' when not provided."""
token_data = {}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["success_status"] == "success"
def test_default_context_truncated_false(
self, writer_with_password, mock_db_manager
):
"""context_truncated defaults to False when not provided."""
token_data = {}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["context_truncated"] is False
def test_default_total_tokens_zero_when_both_missing(
self, writer_with_password, mock_db_manager
):
"""total_tokens is 0 when both prompt_tokens and completion_tokens are absent."""
token_data = {}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["total_tokens"] == 0
# --- missing optional fields -------------------------------------------
def test_missing_optional_fields_are_none(
self, writer_with_password, mock_db_manager
):
"""Optional fields not present in token_data resolve to None via .get()."""
token_data = {}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["model_name"] is None
assert call_kwargs["model_provider"] is None
assert call_kwargs["research_query"] is None
assert call_kwargs["research_mode"] is None
assert call_kwargs["research_phase"] is None
assert call_kwargs["search_iteration"] is None
assert call_kwargs["response_time_ms"] is None
assert call_kwargs["error_type"] is None
assert call_kwargs["search_engines_planned"] is None
assert call_kwargs["search_engine_selected"] is None
assert call_kwargs["calling_file"] is None
assert call_kwargs["calling_function"] is None
assert call_kwargs["call_stack"] is None
assert call_kwargs["context_limit"] is None
assert call_kwargs["tokens_truncated"] is None
assert call_kwargs["truncation_ratio"] is None
assert call_kwargs["ollama_prompt_eval_count"] is None
assert call_kwargs["ollama_eval_count"] is None
assert call_kwargs["ollama_total_duration"] is None
assert call_kwargs["ollama_load_duration"] is None
assert call_kwargs["ollama_prompt_eval_duration"] is None
assert call_kwargs["ollama_eval_duration"] is None
def test_research_id_passed_directly(
self, writer_with_password, mock_db_manager
):
"""research_id comes from the function argument, not from token_data."""
token_data = {"model_name": "test-model"}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data, research_id=999
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["research_id"] == 999
def test_research_id_none(self, writer_with_password, mock_db_manager):
"""research_id can be None."""
token_data = {"model_name": "test-model"}
mock_token_cls, _ = self._run_write(
writer_with_password, mock_db_manager, token_data, research_id=None
)
call_kwargs = mock_token_cls.call_args[1]
assert call_kwargs["research_id"] is None
# --- ModelUsage upsert -------------------------------------------------
def test_creates_model_usage_when_none_exists(
self, writer_with_password, mock_db_manager
):
"""New ModelUsage record is created when none exists."""
token_data = {
"model_name": "gpt-4",
"provider": "openai",
"prompt_tokens": 100,
"completion_tokens": 50,
}
mock_db, mock_session = mock_db_manager
mock_session.query.return_value.filter_by.return_value.first.return_value = None
with patch(
"local_deep_research.database.models.TokenUsage", MagicMock()
):
with patch(
"local_deep_research.database.models.ModelUsage"
) as mock_model_cls:
writer_with_password.write_token_metrics(
"testuser", 1, token_data
)
mock_model_cls.assert_called_once_with(
model_name="gpt-4",
model_provider="openai",
total_tokens=150,
total_calls=1,
)
assert mock_session.add.call_count == 2
def test_updates_existing_model_usage(
self, writer_with_password, mock_db_manager
):
"""Existing ModelUsage record is incremented."""
token_data = {
"model_name": "gpt-4",
"provider": "openai",
"prompt_tokens": 100,
"completion_tokens": 50,
}
mock_db, mock_session = mock_db_manager
existing = MagicMock()
existing.total_tokens = 1000
existing.total_calls = 5
mock_session.query.return_value.filter_by.return_value.first.return_value = existing
with patch(
"local_deep_research.database.models.TokenUsage", MagicMock()
):
writer_with_password.write_token_metrics("testuser", 1, token_data)
assert existing.total_tokens == 1150
assert existing.total_calls == 6
# ===========================================================================
# Global instance
# ===========================================================================
class TestGlobalInstance:
"""Tests for the module-level metrics_writer singleton."""
def test_metrics_writer_exists(self):
"""A global metrics_writer instance is importable."""
assert metrics_writer is not None
def test_metrics_writer_is_correct_type(self):
"""The global instance is a ThreadSafeMetricsWriter."""
assert isinstance(metrics_writer, ThreadSafeMetricsWriter)
def test_metrics_writer_has_thread_local(self):
"""The global instance has _thread_local attribute."""
assert hasattr(metrics_writer, "_thread_local")
assert isinstance(metrics_writer._thread_local, threading.local)
# ===========================================================================
# Thread isolation
# ===========================================================================
class TestThreadIsolation:
"""Verify that thread-local storage is genuinely isolated between threads."""
def test_passwords_not_shared_across_threads(self, writer):
"""Password set in one thread is invisible in another thread."""
results = {}
def thread_a():
writer.set_user_password("alice", "alice_pass")
results["a_passwords"] = dict(writer._thread_local.passwords)
def thread_b():
# Wait for thread_a to finish writing
import time
time.sleep(0.05)
results["b_has_passwords"] = hasattr(
writer._thread_local, "passwords"
)
t_a = threading.Thread(target=thread_a)
t_b = threading.Thread(target=thread_b)
t_a.start()
t_b.start()
t_a.join()
t_b.join()
assert results["a_passwords"] == {"alice": "alice_pass"}
assert results["b_has_passwords"] is False
def test_each_thread_has_own_password_store(self, writer):
"""Two threads can independently store passwords without interference."""
results = {}
barrier = threading.Barrier(2)
def thread_one():
writer.set_user_password("user_one", "pass_one")
barrier.wait()
results["one"] = dict(writer._thread_local.passwords)
def thread_two():
writer.set_user_password("user_two", "pass_two")
barrier.wait()
results["two"] = dict(writer._thread_local.passwords)
t1 = threading.Thread(target=thread_one)
t2 = threading.Thread(target=thread_two)
t1.start()
t2.start()
t1.join()
t2.join()
assert results["one"] == {"user_one": "pass_one"}
assert results["two"] == {"user_two": "pass_two"}
# Neither thread sees the other's password
assert "user_two" not in results["one"]
assert "user_one" not in results["two"]
def test_get_session_fails_in_different_thread(self, writer):
"""get_session in a thread that hasn't called set_user_password raises."""
writer.set_user_password("alice", "pass")
errors = []
def other_thread():
try:
with writer.get_session("alice"):
pass
except ValueError as e:
errors.append(str(e))
t = threading.Thread(target=other_thread)
t.start()
t.join()
assert len(errors) == 1
assert "No password set" in errors[0]