Files
wehub-resource-sync 7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

325 lines
11 KiB
Python

"""Tests for database/thread_metrics.py."""
import pytest
import threading
from unittest.mock import Mock, patch
class TestThreadSafeMetricsWriter:
"""Tests for ThreadSafeMetricsWriter class."""
def test_init_creates_thread_local_storage(self):
"""Test that initialization creates thread-local storage."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
assert hasattr(writer, "_thread_local")
assert isinstance(writer._thread_local, threading.local)
def test_set_user_password_stores_in_thread_local(self):
"""Test that set_user_password stores password in thread-local storage."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("testuser", "testpass")
assert hasattr(writer._thread_local, "passwords")
assert writer._thread_local.passwords["testuser"] == "testpass"
def test_set_user_password_creates_dict_if_missing(self):
"""Test that set_user_password creates passwords dict if it doesn't exist."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
# Initially no passwords dict
assert not hasattr(writer._thread_local, "passwords")
writer.set_user_password("user1", "pass1")
assert hasattr(writer._thread_local, "passwords")
assert isinstance(writer._thread_local.passwords, dict)
def test_set_user_password_supports_multiple_users(self):
"""Test that multiple users can have passwords stored."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("user1", "pass1")
writer.set_user_password("user2", "pass2")
assert writer._thread_local.passwords["user1"] == "pass1"
assert writer._thread_local.passwords["user2"] == "pass2"
def test_get_session_raises_when_no_password_set(self):
"""Test that get_session raises error when no password is set."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
with pytest.raises(ValueError, match="No password set"):
with writer.get_session("testuser"):
pass
def test_get_session_raises_when_user_password_missing(self):
"""Test that get_session raises error when user's password is missing."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("otheruser", "otherpass")
with pytest.raises(ValueError, match="No password available"):
with writer.get_session("testuser"):
pass
def test_get_session_creates_session_with_password(self):
"""Test that get_session creates session with stored password."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("testuser", "testpass")
with patch(
"local_deep_research.database.thread_metrics.db_manager"
) as mock_db:
mock_session = Mock()
mock_db.create_thread_safe_session_for_metrics.return_value = (
mock_session
)
with writer.get_session("testuser") as session:
assert session is mock_session
mock_db.create_thread_safe_session_for_metrics.assert_called_once_with(
"testuser", "testpass"
)
def test_get_session_commits_on_success(self):
"""Test that get_session commits the session on successful exit."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("testuser", "testpass")
with patch(
"local_deep_research.database.thread_metrics.db_manager"
) as mock_db:
mock_session = Mock()
mock_db.create_thread_safe_session_for_metrics.return_value = (
mock_session
)
with writer.get_session("testuser"):
pass
mock_session.commit.assert_called_once()
mock_session.close.assert_called_once()
def test_get_session_rollbacks_on_error(self):
"""Test that get_session rolls back on error."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("testuser", "testpass")
with patch(
"local_deep_research.database.thread_metrics.db_manager"
) as mock_db:
mock_session = Mock()
mock_db.create_thread_safe_session_for_metrics.return_value = (
mock_session
)
with pytest.raises(RuntimeError):
with writer.get_session("testuser"):
raise RuntimeError("Test error")
mock_session.rollback.assert_called_once()
mock_session.close.assert_called_once()
def test_get_session_raises_when_session_creation_fails(self):
"""Test that get_session raises error when session creation fails."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("testuser", "testpass")
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.get_session("testuser"):
pass
class TestWriteTokenMetrics:
"""Tests for write_token_metrics method."""
def test_write_token_metrics_creates_token_usage_record(self):
"""Test that write_token_metrics creates a TokenUsage record."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("testuser", "testpass")
mock_session = Mock()
mock_token_usage_class = Mock()
with patch(
"local_deep_research.database.thread_metrics.db_manager"
) as mock_db:
mock_db.create_thread_safe_session_for_metrics.return_value = (
mock_session
)
with patch(
"local_deep_research.database.models.TokenUsage",
mock_token_usage_class,
):
with patch("local_deep_research.database.models.ModelUsage"):
mock_session.query.return_value.filter_by.return_value.first.return_value = None
token_data = {
"model_name": "gpt-4",
"provider": "openai",
"prompt_tokens": 100,
"completion_tokens": 50,
}
writer.write_token_metrics("testuser", 123, token_data)
# Verify TokenUsage was created and added to session
mock_token_usage_class.assert_called_once()
assert mock_session.add.call_count == 2
class TestGlobalInstance:
"""Tests for the global metrics_writer instance."""
def test_global_instance_is_correct_type(self):
"""Test that global instance is ThreadSafeMetricsWriter."""
from local_deep_research.database.thread_metrics import (
metrics_writer,
ThreadSafeMetricsWriter,
)
assert isinstance(metrics_writer, ThreadSafeMetricsWriter)
class TestClearPasswords:
"""Tests for clear_passwords() and its integration with cleanup_current_thread()."""
def test_clear_passwords_empties_dict(self):
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
writer.set_user_password("userA", "passA")
writer.set_user_password("userB", "passB")
assert writer._thread_local.passwords == {
"userA": "passA",
"userB": "passB",
}
writer.clear_passwords()
assert writer._thread_local.passwords == {}
def test_clear_passwords_is_noop_when_no_passwords_set(self):
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
# No passwords dict exists; clear_passwords should not raise.
writer.clear_passwords()
assert not hasattr(writer._thread_local, "passwords")
def test_cleanup_current_thread_clears_passwords(self):
"""Regression test — cleanup_current_thread() must clear the passwords dict
on the module-level metrics_writer so pooled workers don't retain creds."""
from local_deep_research.database.thread_metrics import metrics_writer
from local_deep_research.database.thread_local_session import (
cleanup_current_thread,
)
metrics_writer.set_user_password("leaked_user", "leaked_pass")
assert (
metrics_writer._thread_local.passwords.get("leaked_user")
== "leaked_pass"
)
cleanup_current_thread()
assert metrics_writer._thread_local.passwords == {}
class TestThreadIsolation:
"""Tests for thread isolation of password storage."""
def test_passwords_are_thread_isolated(self):
"""Test that passwords stored in one thread are not visible in another."""
from local_deep_research.database.thread_metrics import (
ThreadSafeMetricsWriter,
)
writer = ThreadSafeMetricsWriter()
results = {}
def thread1_work():
writer.set_user_password("user1", "pass1")
results["thread1_has_password"] = hasattr(
writer._thread_local, "passwords"
)
results["thread1_user1"] = writer._thread_local.passwords.get(
"user1"
)
def thread2_work():
# Small delay to ensure thread1 runs first
import time
time.sleep(0.01)
results["thread2_has_password"] = hasattr(
writer._thread_local, "passwords"
)
t1 = threading.Thread(target=thread1_work)
t2 = threading.Thread(target=thread2_work)
t1.start()
t2.start()
t1.join()
t2.join()
# Thread 1 should have its password
assert results["thread1_has_password"] is True
assert results["thread1_user1"] == "pass1"
# Thread 2 should NOT have access to thread 1's passwords
assert results["thread2_has_password"] is False