learningcircuit--local-deep-research
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
1801 行
61 KiB
Python
1801 行
61 KiB
Python
"""Comprehensive coverage tests for benchmarks/web_api/benchmark_service.py.
|
|
|
|
Focuses on uncovered code paths: complex branching in _run_benchmark_thread,
|
|
_process_benchmark_task, _sync_results_to_database, get_benchmark_status,
|
|
sync_pending_results, cancel_benchmark, update_benchmark_status, and more.
|
|
"""
|
|
|
|
import json
|
|
from datetime import UTC, datetime, timedelta
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers to import from the module under test
|
|
# ---------------------------------------------------------------------------
|
|
MODULE = "local_deep_research.benchmarks.web_api.benchmark_service"
|
|
SETTINGS_CTX_MODULE = "local_deep_research.config.thread_settings"
|
|
|
|
|
|
def _import_service():
|
|
"""Import BenchmarkService with SocketIOService mocked."""
|
|
with patch(f"{MODULE}.SocketIOService"):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkService,
|
|
)
|
|
|
|
return BenchmarkService
|
|
|
|
|
|
def _make_service(socket=None):
|
|
"""Create a BenchmarkService with a mock socket service."""
|
|
cls = _import_service()
|
|
svc = cls(socket_service=socket or MagicMock())
|
|
return svc
|
|
|
|
|
|
# ============================================================
|
|
# BenchmarkQueueTracker – deeper coverage
|
|
# ============================================================
|
|
|
|
|
|
class TestQueueTrackerCleanup:
|
|
"""Cover cleanup_completed_tasks edge cases."""
|
|
|
|
def test_cleanup_removes_old_completed(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
BenchmarkTaskStatus,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
tracker.add_task("old1", "user")
|
|
tracker.update_task_status("old1", BenchmarkTaskStatus.COMPLETED)
|
|
# Manually set updated_at to the past
|
|
tracker.tasks["old1"]["updated_at"] = datetime.now(UTC) - timedelta(
|
|
seconds=7200
|
|
)
|
|
|
|
tracker.cleanup_completed_tasks(max_age_seconds=3600)
|
|
assert "old1" not in tracker.tasks
|
|
|
|
def test_cleanup_keeps_recent_completed(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
BenchmarkTaskStatus,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
tracker.add_task("new1", "user")
|
|
tracker.update_task_status("new1", BenchmarkTaskStatus.COMPLETED)
|
|
|
|
tracker.cleanup_completed_tasks(max_age_seconds=3600)
|
|
assert "new1" in tracker.tasks
|
|
|
|
def test_cleanup_removes_old_failed(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
BenchmarkTaskStatus,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
tracker.add_task("fail1", "user")
|
|
tracker.update_task_status("fail1", BenchmarkTaskStatus.FAILED)
|
|
tracker.tasks["fail1"]["updated_at"] = datetime.now(UTC) - timedelta(
|
|
seconds=7200
|
|
)
|
|
|
|
tracker.cleanup_completed_tasks(max_age_seconds=3600)
|
|
assert "fail1" not in tracker.tasks
|
|
|
|
def test_cleanup_removes_old_cancelled(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
BenchmarkTaskStatus,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
tracker.add_task("canc1", "user")
|
|
tracker.update_task_status("canc1", BenchmarkTaskStatus.CANCELLED)
|
|
tracker.tasks["canc1"]["updated_at"] = datetime.now(UTC) - timedelta(
|
|
seconds=7200
|
|
)
|
|
|
|
tracker.cleanup_completed_tasks(max_age_seconds=3600)
|
|
assert "canc1" not in tracker.tasks
|
|
|
|
def test_cleanup_keeps_processing_tasks(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
BenchmarkTaskStatus,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
tracker.add_task("proc1", "user")
|
|
tracker.update_task_status("proc1", BenchmarkTaskStatus.PROCESSING)
|
|
tracker.tasks["proc1"]["updated_at"] = datetime.now(UTC) - timedelta(
|
|
seconds=7200
|
|
)
|
|
|
|
tracker.cleanup_completed_tasks(max_age_seconds=3600)
|
|
assert "proc1" in tracker.tasks
|
|
|
|
def test_cleanup_uses_created_at_when_no_updated_at(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
BenchmarkTaskStatus,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
tracker.add_task("t1", "user")
|
|
# Set status directly without going through update (no updated_at)
|
|
tracker.tasks["t1"]["status"] = BenchmarkTaskStatus.COMPLETED.value
|
|
tracker.tasks["t1"]["created_at"] = datetime.now(UTC) - timedelta(
|
|
seconds=7200
|
|
)
|
|
if "updated_at" in tracker.tasks["t1"]:
|
|
del tracker.tasks["t1"]["updated_at"]
|
|
|
|
tracker.cleanup_completed_tasks(max_age_seconds=3600)
|
|
assert "t1" not in tracker.tasks
|
|
|
|
def test_update_nonexistent_task_logs_warning(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
BenchmarkTaskStatus,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
# Should not raise; logs a warning
|
|
tracker.update_task_status("nonexistent", BenchmarkTaskStatus.COMPLETED)
|
|
|
|
def test_remove_task(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
tracker.add_task("rm1", "user")
|
|
tracker.remove_task("rm1")
|
|
assert tracker.get_task_status("rm1") is None
|
|
|
|
def test_remove_nonexistent_task(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkQueueTracker,
|
|
)
|
|
|
|
tracker = BenchmarkQueueTracker()
|
|
# Should not raise
|
|
tracker.remove_task("doesnotexist")
|
|
|
|
|
|
# ============================================================
|
|
# BenchmarkService – initialization
|
|
# ============================================================
|
|
|
|
|
|
class TestBenchmarkServiceInit:
|
|
def test_init_with_provided_socket(self):
|
|
mock_socket = MagicMock()
|
|
svc = _make_service(socket=mock_socket)
|
|
assert svc.socket_service is mock_socket
|
|
|
|
def test_init_fallback_socket(self):
|
|
"""When SocketIOService raises, a MockSocketService is created."""
|
|
with patch(
|
|
f"{MODULE}.SocketIOService", side_effect=RuntimeError("no app")
|
|
):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkService,
|
|
)
|
|
|
|
svc = BenchmarkService()
|
|
# The mock socket should have emit_to_room method
|
|
svc.socket_service.emit_to_room("room", "event", {})
|
|
|
|
|
|
# ============================================================
|
|
# generate_config_hash / generate_query_hash
|
|
# ============================================================
|
|
|
|
|
|
class TestHashGeneration:
|
|
def test_config_hash_deterministic(self):
|
|
svc = _make_service()
|
|
cfg = {"iterations": 5, "search_tool": "searxng", "model_name": "gpt-4"}
|
|
h1 = svc.generate_config_hash(cfg)
|
|
h2 = svc.generate_config_hash(cfg)
|
|
assert h1 == h2
|
|
assert len(h1) == 8
|
|
|
|
def test_config_hash_ignores_none_values(self):
|
|
svc = _make_service()
|
|
cfg1 = {"iterations": 5, "search_tool": None}
|
|
cfg2 = {"iterations": 5}
|
|
assert svc.generate_config_hash(cfg1) == svc.generate_config_hash(cfg2)
|
|
|
|
def test_config_hash_differs_for_different_configs(self):
|
|
svc = _make_service()
|
|
h1 = svc.generate_config_hash({"iterations": 5})
|
|
h2 = svc.generate_config_hash({"iterations": 10})
|
|
assert h1 != h2
|
|
|
|
def test_query_hash_deterministic(self):
|
|
svc = _make_service()
|
|
h1 = svc.generate_query_hash("What is AI?", "simpleqa")
|
|
h2 = svc.generate_query_hash("What is AI?", "simpleqa")
|
|
assert h1 == h2
|
|
|
|
def test_query_hash_strips_whitespace(self):
|
|
svc = _make_service()
|
|
h1 = svc.generate_query_hash(" What is AI? ", "simpleqa")
|
|
h2 = svc.generate_query_hash("What is AI?", "simpleqa")
|
|
assert h1 == h2
|
|
|
|
def test_query_hash_case_insensitive_dataset(self):
|
|
svc = _make_service()
|
|
h1 = svc.generate_query_hash("What is AI?", "SimpleQA")
|
|
h2 = svc.generate_query_hash("What is AI?", "simpleqa")
|
|
assert h1 == h2
|
|
|
|
|
|
# ============================================================
|
|
# create_benchmark_run
|
|
# ============================================================
|
|
|
|
|
|
class TestCreateBenchmarkRun:
|
|
@patch(f"{MODULE}.BenchmarkRun")
|
|
def test_create_benchmark_run_success(self, mock_run_cls):
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_run_instance = MagicMock()
|
|
mock_run_instance.id = 42
|
|
mock_run_cls.return_value = mock_run_instance
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
result = svc.create_benchmark_run(
|
|
run_name="test",
|
|
search_config={"iterations": 5},
|
|
evaluation_config={},
|
|
datasets_config={"simpleqa": {"count": 10}},
|
|
username="user1",
|
|
)
|
|
|
|
assert result == 42
|
|
mock_session.add.assert_called_once_with(mock_run_instance)
|
|
mock_session.commit.assert_called_once()
|
|
|
|
@patch(f"{MODULE}.BenchmarkRun")
|
|
def test_create_benchmark_run_db_error(self, mock_run_cls):
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_session.commit.side_effect = RuntimeError("db error")
|
|
mock_run_cls.return_value = MagicMock()
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
with pytest.raises(RuntimeError, match="db error"):
|
|
svc.create_benchmark_run(
|
|
run_name="test",
|
|
search_config={},
|
|
evaluation_config={},
|
|
datasets_config={},
|
|
)
|
|
|
|
mock_session.rollback.assert_called_once()
|
|
|
|
|
|
# ============================================================
|
|
# _create_task_queue
|
|
# ============================================================
|
|
|
|
|
|
class TestCreateTaskQueue:
|
|
@patch(f"{MODULE}.load_dataset")
|
|
def test_creates_tasks_from_dataset(self, mock_load):
|
|
svc = _make_service()
|
|
mock_load.return_value = [
|
|
{"id": "ex1", "problem": "Q1?", "answer": "A1"},
|
|
{"id": "ex2", "problem": "Q2?", "answer": "A2"},
|
|
]
|
|
|
|
tasks = svc._create_task_queue(
|
|
{"simpleqa": {"count": 2}},
|
|
benchmark_run_id=1,
|
|
)
|
|
|
|
assert len(tasks) == 2
|
|
assert tasks[0]["question"] == "Q1?"
|
|
assert tasks[0]["dataset_type"] == "simpleqa"
|
|
|
|
@patch(f"{MODULE}.load_dataset")
|
|
def test_passes_seed_from_dataset_config(self, mock_load):
|
|
"""A "seed" in the dataset config drives reproducible sampling."""
|
|
svc = _make_service()
|
|
mock_load.return_value = [
|
|
{"id": "ex1", "problem": "Q1?", "answer": "A1"},
|
|
]
|
|
|
|
svc._create_task_queue(
|
|
{"simpleqa": {"count": 1, "seed": 7}},
|
|
benchmark_run_id=1,
|
|
)
|
|
|
|
mock_load.assert_called_once_with(
|
|
dataset_type="simpleqa", num_examples=1, seed=7
|
|
)
|
|
|
|
@patch(f"{MODULE}.load_dataset")
|
|
def test_no_seed_means_random_sampling(self, mock_load):
|
|
"""Without a configured seed the sample stays random (seed=None)."""
|
|
svc = _make_service()
|
|
mock_load.return_value = [
|
|
{"id": "ex1", "problem": "Q1?", "answer": "A1"},
|
|
]
|
|
|
|
svc._create_task_queue(
|
|
{"simpleqa": {"count": 1}},
|
|
benchmark_run_id=1,
|
|
)
|
|
|
|
mock_load.assert_called_once_with(
|
|
dataset_type="simpleqa", num_examples=1, seed=None
|
|
)
|
|
|
|
@patch(f"{MODULE}.load_dataset")
|
|
def test_queues_every_sampled_question(self, mock_load):
|
|
"""Regression test for #4498: every sampled question becomes a task.
|
|
|
|
The removed cross-run reuse feature silently skipped questions that
|
|
had results in previous compatible runs, which broke the
|
|
completed/total accounting (#4451). A run must always process its
|
|
full sample, even if identical questions were answered before."""
|
|
svc = _make_service()
|
|
examples = [
|
|
{"id": f"ex{i}", "problem": f"Q{i}?", "answer": f"A{i}"}
|
|
for i in range(5)
|
|
]
|
|
mock_load.return_value = examples
|
|
|
|
tasks = svc._create_task_queue(
|
|
{"simpleqa": {"count": 5}},
|
|
benchmark_run_id=1,
|
|
)
|
|
|
|
assert len(tasks) == len(examples)
|
|
assert [t["question"] for t in tasks] == [
|
|
e["problem"] for e in examples
|
|
]
|
|
assert [t["task_index"] for t in tasks] == list(range(5))
|
|
|
|
@patch(f"{MODULE}.load_dataset")
|
|
def test_skips_zero_count_datasets(self, mock_load):
|
|
svc = _make_service()
|
|
tasks = svc._create_task_queue(
|
|
{"simpleqa": {"count": 0}},
|
|
benchmark_run_id=1,
|
|
)
|
|
assert len(tasks) == 0
|
|
mock_load.assert_not_called()
|
|
|
|
@patch(f"{MODULE}.load_dataset")
|
|
def test_browsecomp_dataset_type(self, mock_load):
|
|
svc = _make_service()
|
|
mock_load.return_value = [
|
|
{"id": "b1", "problem": "Browse Q?", "answer": "Browse A"},
|
|
]
|
|
|
|
tasks = svc._create_task_queue(
|
|
{"browsecomp": {"count": 1}},
|
|
benchmark_run_id=1,
|
|
)
|
|
|
|
assert len(tasks) == 1
|
|
assert tasks[0]["dataset_type"] == "browsecomp"
|
|
|
|
@patch(f"{MODULE}.load_dataset")
|
|
def test_example_without_id_gets_default(self, mock_load):
|
|
svc = _make_service()
|
|
mock_load.return_value = [
|
|
{"problem": "Q?", "answer": "A"}, # no id
|
|
]
|
|
|
|
tasks = svc._create_task_queue(
|
|
{"simpleqa": {"count": 1}},
|
|
benchmark_run_id=1,
|
|
)
|
|
|
|
assert tasks[0]["example_id"] == "example_0"
|
|
|
|
|
|
# ============================================================
|
|
# _process_benchmark_task
|
|
# ============================================================
|
|
|
|
|
|
class TestProcessBenchmarkTask:
|
|
def _make_task(self, **overrides):
|
|
task = {
|
|
"benchmark_run_id": 1,
|
|
"example_id": "ex1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "What is 2+2?",
|
|
"correct_answer": "4",
|
|
"query_hash": "abc123",
|
|
"task_index": 0,
|
|
"username": "user1",
|
|
"user_password": None,
|
|
}
|
|
task.update(overrides)
|
|
return task
|
|
|
|
@patch(f"{MODULE}.grade_single_result")
|
|
@patch(f"{MODULE}.extract_answer_from_response")
|
|
@patch(f"{MODULE}.quick_summary")
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_successful_processing_with_grading(
|
|
self, mock_format, mock_summary, mock_extract, mock_grade
|
|
):
|
|
svc = _make_service()
|
|
mock_format.return_value = "formatted query"
|
|
mock_summary.return_value = {
|
|
"summary": "The answer is 4",
|
|
"sources": [{"url": "http://example.com"}],
|
|
}
|
|
mock_extract.return_value = {
|
|
"extracted_answer": "4",
|
|
"confidence": "95",
|
|
}
|
|
mock_grade.return_value = {
|
|
"is_correct": True,
|
|
"graded_confidence": "98",
|
|
"grader_response": "Correct answer",
|
|
}
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
result = svc._process_benchmark_task(
|
|
self._make_task(),
|
|
{"iterations": 5},
|
|
{},
|
|
)
|
|
|
|
assert result["is_correct"] is True
|
|
assert result["extracted_answer"] == "4"
|
|
assert result["response"] == "The answer is 4"
|
|
|
|
@patch(f"{MODULE}.grade_single_result")
|
|
@patch(f"{MODULE}.extract_answer_from_response")
|
|
@patch(f"{MODULE}.quick_summary")
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_grading_error_in_result(
|
|
self, mock_format, mock_summary, mock_extract, mock_grade
|
|
):
|
|
svc = _make_service()
|
|
mock_format.return_value = "q"
|
|
mock_summary.return_value = {"summary": "resp", "sources": []}
|
|
mock_extract.return_value = {
|
|
"extracted_answer": "ans",
|
|
"confidence": "50",
|
|
}
|
|
mock_grade.return_value = {"grading_error": "model unavailable"}
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
result = svc._process_benchmark_task(self._make_task(), {}, {})
|
|
|
|
assert result["is_correct"] is None
|
|
assert "model unavailable" in result["grader_response"]
|
|
|
|
@patch(f"{MODULE}.grade_single_result")
|
|
@patch(f"{MODULE}.extract_answer_from_response")
|
|
@patch(f"{MODULE}.quick_summary")
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_grading_returns_none(
|
|
self, mock_format, mock_summary, mock_extract, mock_grade
|
|
):
|
|
svc = _make_service()
|
|
mock_format.return_value = "q"
|
|
mock_summary.return_value = {"summary": "resp", "sources": []}
|
|
mock_extract.return_value = {"extracted_answer": "ans"}
|
|
mock_grade.return_value = None
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
result = svc._process_benchmark_task(self._make_task(), {}, {})
|
|
|
|
assert result["is_correct"] is None
|
|
assert "No evaluation results returned" in result["grader_response"]
|
|
|
|
@patch(f"{MODULE}.grade_single_result")
|
|
@patch(f"{MODULE}.extract_answer_from_response")
|
|
@patch(f"{MODULE}.quick_summary")
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_grading_exception(
|
|
self, mock_format, mock_summary, mock_extract, mock_grade
|
|
):
|
|
svc = _make_service()
|
|
mock_format.return_value = "q"
|
|
mock_summary.return_value = {"summary": "resp", "sources": []}
|
|
mock_extract.return_value = {"extracted_answer": "ans"}
|
|
mock_grade.side_effect = ValueError("grade fail")
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
result = svc._process_benchmark_task(self._make_task(), {}, {})
|
|
|
|
assert result["is_correct"] is None
|
|
assert "grade fail" in result["grader_response"]
|
|
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_research_error(self, mock_format):
|
|
svc = _make_service()
|
|
mock_format.side_effect = RuntimeError("research crashed")
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
result = svc._process_benchmark_task(self._make_task(), {}, {})
|
|
|
|
assert "research_error" in result
|
|
assert "research crashed" in result["research_error"]
|
|
|
|
@patch(f"{MODULE}.grade_single_result")
|
|
@patch(f"{MODULE}.extract_answer_from_response")
|
|
@patch(f"{MODULE}.quick_summary")
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_extract_returns_string(
|
|
self, mock_format, mock_summary, mock_extract, mock_grade
|
|
):
|
|
"""When extract_answer_from_response returns a string instead of dict."""
|
|
svc = _make_service()
|
|
mock_format.return_value = "q"
|
|
mock_summary.return_value = {"summary": "resp", "sources": []}
|
|
mock_extract.return_value = "plain string answer"
|
|
mock_grade.return_value = {
|
|
"is_correct": False,
|
|
"graded_confidence": "10",
|
|
"grader_response": "Wrong",
|
|
}
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
result = svc._process_benchmark_task(self._make_task(), {}, {})
|
|
|
|
assert result["extracted_answer"] == "plain string answer"
|
|
assert result["confidence"] == "100"
|
|
|
|
@patch(f"{MODULE}.grade_single_result")
|
|
@patch(f"{MODULE}.extract_answer_from_response")
|
|
@patch(f"{MODULE}.quick_summary")
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_sources_from_all_links(
|
|
self, mock_format, mock_summary, mock_extract, mock_grade
|
|
):
|
|
"""When sources is empty but all_links_of_system is present."""
|
|
svc = _make_service()
|
|
mock_format.return_value = "q"
|
|
mock_summary.return_value = {
|
|
"summary": "resp",
|
|
"sources": [],
|
|
"all_links_of_system": ["http://link1.com"],
|
|
}
|
|
mock_extract.return_value = {"extracted_answer": "ans"}
|
|
mock_grade.return_value = {
|
|
"is_correct": True,
|
|
"graded_confidence": "90",
|
|
"grader_response": "ok",
|
|
}
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
result = svc._process_benchmark_task(self._make_task(), {}, {})
|
|
|
|
sources = json.loads(result["sources"])
|
|
assert "http://link1.com" in sources
|
|
|
|
|
|
# ============================================================
|
|
# _send_progress_update
|
|
# ============================================================
|
|
|
|
|
|
class TestSendProgressUpdate:
|
|
def test_sends_progress_via_socket(self):
|
|
mock_socket = MagicMock()
|
|
svc = _make_service(socket=mock_socket)
|
|
|
|
svc._send_progress_update(1, 5, 10)
|
|
|
|
mock_socket.emit_to_subscribers.assert_called_once()
|
|
call_args = mock_socket.emit_to_subscribers.call_args
|
|
assert call_args[0][0] == "research_progress"
|
|
assert call_args[0][2]["progress"] == 50.0
|
|
|
|
def test_sends_zero_progress_when_total_zero(self):
|
|
mock_socket = MagicMock()
|
|
svc = _make_service(socket=mock_socket)
|
|
|
|
svc._send_progress_update(1, 0, 0)
|
|
|
|
call_args = mock_socket.emit_to_subscribers.call_args
|
|
assert call_args[0][2]["progress"] == 0
|
|
|
|
def test_exception_in_send_is_caught(self):
|
|
mock_socket = MagicMock()
|
|
mock_socket.emit_to_subscribers.side_effect = RuntimeError(
|
|
"socket error"
|
|
)
|
|
svc = _make_service(socket=mock_socket)
|
|
|
|
# Should not raise
|
|
svc._send_progress_update(1, 5, 10)
|
|
|
|
|
|
# ============================================================
|
|
# sync_pending_results
|
|
# ============================================================
|
|
|
|
|
|
class TestSyncPendingResults:
|
|
def test_returns_zero_when_no_active_run(self):
|
|
svc = _make_service()
|
|
assert svc.sync_pending_results(999) == 0
|
|
|
|
def test_saves_new_results(self):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {"username": "user1", "user_password": None},
|
|
"results": [
|
|
{
|
|
"example_id": "ex1",
|
|
"query_hash": "h1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"task_index": 0,
|
|
},
|
|
],
|
|
}
|
|
|
|
mock_session = MagicMock()
|
|
# No rows persisted for this run yet
|
|
mock_session.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
count = svc.sync_pending_results(1, "user1")
|
|
|
|
assert count == 1
|
|
mock_session.add.assert_called_once()
|
|
mock_session.commit.assert_called_once()
|
|
|
|
def test_skips_already_saved_indices(self):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {"username": "user1", "user_password": None},
|
|
"results": [
|
|
{
|
|
"example_id": "ex1",
|
|
"query_hash": "h1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"task_index": 0,
|
|
},
|
|
],
|
|
"saved_indices": {0},
|
|
}
|
|
|
|
mock_session = MagicMock()
|
|
# No rows persisted for this run yet; idx 0 is skipped via saved_indices.
|
|
mock_session.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
count = svc.sync_pending_results(1)
|
|
|
|
assert count == 0
|
|
|
|
def test_skips_existing_db_result(self):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {"username": "user1", "user_password": None},
|
|
"results": [
|
|
{
|
|
"example_id": "ex1",
|
|
"query_hash": "h1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"task_index": 0,
|
|
},
|
|
],
|
|
}
|
|
|
|
mock_session = MagicMock()
|
|
# query_hash "h1" already persisted for this run
|
|
mock_session.query.return_value.filter.return_value.all.return_value = [
|
|
("h1",)
|
|
]
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
count = svc.sync_pending_results(1)
|
|
|
|
assert count == 0
|
|
mock_session.add.assert_not_called()
|
|
|
|
def test_handles_db_error(self):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {"username": "user1", "user_password": None},
|
|
"results": [
|
|
{
|
|
"example_id": "ex1",
|
|
"query_hash": "h1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"task_index": 0,
|
|
},
|
|
],
|
|
}
|
|
|
|
mock_session = MagicMock()
|
|
mock_session.query.side_effect = RuntimeError("db error")
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
count = svc.sync_pending_results(1)
|
|
|
|
assert count == 0
|
|
|
|
def test_uses_username_from_run_data(self):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {"username": "fromdata", "user_password": "pw"},
|
|
"results": [],
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.all.return_value = []
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc.sync_pending_results(1)
|
|
|
|
mock_get_session.assert_called_once_with("fromdata", "pw")
|
|
|
|
|
|
# ============================================================
|
|
# _sync_results_to_database
|
|
# ============================================================
|
|
|
|
|
|
class TestSyncResultsToDatabase:
|
|
def test_returns_early_if_no_active_run(self):
|
|
svc = _make_service()
|
|
# Should not raise
|
|
svc._sync_results_to_database(999)
|
|
|
|
def test_returns_early_if_thread_not_complete(self):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {"thread_complete": False}
|
|
svc._sync_results_to_database(1)
|
|
# No DB calls expected
|
|
|
|
def test_syncs_completed_run(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"thread_complete": True,
|
|
"data": {"username": "user1", "user_password": None},
|
|
"completion_info": {
|
|
"status": BenchmarkStatus.COMPLETED,
|
|
"end_time": datetime.now(UTC),
|
|
"completed_examples": 2,
|
|
"failed_examples": 0,
|
|
},
|
|
"results": [
|
|
{
|
|
"example_id": "ex1",
|
|
"query_hash": "h1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"is_correct": True,
|
|
"processing_time": 10.0,
|
|
"task_index": 0,
|
|
},
|
|
{
|
|
"example_id": "ex2",
|
|
"query_hash": "h2",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q2?",
|
|
"correct_answer": "A2",
|
|
"is_correct": False,
|
|
"processing_time": 20.0,
|
|
"task_index": 1,
|
|
},
|
|
],
|
|
}
|
|
|
|
mock_session = MagicMock()
|
|
mock_benchmark_run = MagicMock()
|
|
mock_benchmark_run.status = BenchmarkStatus.COMPLETED
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_benchmark_run
|
|
# _persist_unsaved_results reads existing query_hashes for the run.
|
|
mock_session.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc._sync_results_to_database(1)
|
|
|
|
mock_session.commit.assert_called_once()
|
|
# Active run should be cleaned up
|
|
assert 1 not in svc.active_runs
|
|
|
|
def test_calculates_accuracy_correctly(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"thread_complete": True,
|
|
"data": {"username": "user1", "user_password": None},
|
|
"completion_info": {
|
|
"status": BenchmarkStatus.COMPLETED,
|
|
"completed_examples": 2,
|
|
"failed_examples": 0,
|
|
},
|
|
"results": [
|
|
{
|
|
"example_id": "ex1",
|
|
"query_hash": "h1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"is_correct": True,
|
|
"processing_time": 10.0,
|
|
"task_index": 0,
|
|
},
|
|
{
|
|
"example_id": "ex2",
|
|
"query_hash": "h2",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q2?",
|
|
"correct_answer": "A2",
|
|
"is_correct": True,
|
|
"processing_time": 20.0,
|
|
"task_index": 1,
|
|
},
|
|
],
|
|
}
|
|
|
|
mock_session = MagicMock()
|
|
mock_benchmark_run = MagicMock()
|
|
mock_benchmark_run.status = BenchmarkStatus.COMPLETED
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_benchmark_run
|
|
# _persist_unsaved_results reads existing query_hashes for the run.
|
|
mock_session.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc._sync_results_to_database(1)
|
|
|
|
# 2 correct out of 2 = 100%
|
|
assert mock_benchmark_run.overall_accuracy == 100.0
|
|
|
|
def test_skips_already_saved_results(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"thread_complete": True,
|
|
"data": {"username": "user1", "user_password": None},
|
|
"completion_info": {
|
|
"status": BenchmarkStatus.FAILED,
|
|
"error_message": "something failed",
|
|
},
|
|
"results": [
|
|
{
|
|
"example_id": "ex1",
|
|
"query_hash": "h1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"task_index": 0,
|
|
},
|
|
],
|
|
"saved_indices": {0},
|
|
}
|
|
|
|
mock_session = MagicMock()
|
|
mock_benchmark_run = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_benchmark_run
|
|
# _persist_unsaved_results reads existing query_hashes for the run.
|
|
mock_session.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc._sync_results_to_database(1)
|
|
|
|
# Only the run status update, no result adds
|
|
assert mock_session.add.call_count == 0
|
|
|
|
def test_handles_db_exception(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"thread_complete": True,
|
|
"data": {"username": "user1", "user_password": None},
|
|
"completion_info": {
|
|
"status": BenchmarkStatus.COMPLETED,
|
|
},
|
|
"results": [],
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
side_effect=RuntimeError("db down")
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
# Should not raise
|
|
svc._sync_results_to_database(1)
|
|
|
|
def test_no_accuracy_for_non_completed_status(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"thread_complete": True,
|
|
"data": {"username": "user1", "user_password": None},
|
|
"completion_info": {
|
|
"status": BenchmarkStatus.FAILED,
|
|
"error_message": "crashed",
|
|
},
|
|
"results": [
|
|
{
|
|
"example_id": "ex1",
|
|
"query_hash": "h1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"is_correct": True,
|
|
"processing_time": 10.0,
|
|
"task_index": 0,
|
|
},
|
|
],
|
|
}
|
|
|
|
mock_session = MagicMock()
|
|
mock_benchmark_run = MagicMock()
|
|
mock_benchmark_run.status = BenchmarkStatus.FAILED
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_benchmark_run
|
|
# _persist_unsaved_results reads existing query_hashes for the run.
|
|
mock_session.query.return_value.filter.return_value.all.return_value = []
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc._sync_results_to_database(1)
|
|
|
|
# Should NOT set overall_accuracy for failed runs
|
|
# (the code only calculates accuracy when status == COMPLETED)
|
|
# The mock starts with whatever default it has; we just verify
|
|
# the condition was checked by checking the status was set to FAILED
|
|
assert mock_benchmark_run.status == BenchmarkStatus.FAILED
|
|
|
|
|
|
# ============================================================
|
|
# _calculate_final_accuracy
|
|
# ============================================================
|
|
|
|
|
|
class TestCalculateFinalAccuracy:
|
|
def test_calculates_accuracy(self):
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
|
|
mock_r1 = MagicMock()
|
|
mock_r1.is_correct = True
|
|
mock_r1.processing_time = 30.0
|
|
|
|
mock_r2 = MagicMock()
|
|
mock_r2.is_correct = False
|
|
mock_r2.processing_time = 60.0
|
|
|
|
mock_session.query.return_value.filter.return_value.filter.return_value.all.return_value = [
|
|
mock_r1,
|
|
mock_r2,
|
|
]
|
|
|
|
mock_run = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_run
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc._calculate_final_accuracy(1, "user1")
|
|
|
|
assert mock_run.overall_accuracy == 50.0
|
|
|
|
def test_no_results(self):
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.filter.return_value.all.return_value = []
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
# Should not raise
|
|
svc._calculate_final_accuracy(1)
|
|
|
|
def test_handles_exception(self):
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_session.query.side_effect = RuntimeError("db error")
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
# Should not raise
|
|
svc._calculate_final_accuracy(1)
|
|
|
|
|
|
# ============================================================
|
|
# update_benchmark_status
|
|
# ============================================================
|
|
|
|
|
|
class TestUpdateBenchmarkStatus:
|
|
def test_update_status_in_progress(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_run = MagicMock()
|
|
mock_run.start_time = None
|
|
mock_run.end_time = None
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_run
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc.update_benchmark_status(1, BenchmarkStatus.IN_PROGRESS)
|
|
|
|
assert mock_run.status == BenchmarkStatus.IN_PROGRESS
|
|
assert mock_run.start_time is not None
|
|
|
|
def test_update_status_completed(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_run = MagicMock()
|
|
mock_run.start_time = datetime.now(UTC)
|
|
mock_run.end_time = None
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_run
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc.update_benchmark_status(1, BenchmarkStatus.COMPLETED)
|
|
|
|
assert mock_run.end_time is not None
|
|
|
|
def test_update_status_failed_sets_end_time(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_run = MagicMock()
|
|
mock_run.start_time = datetime.now(UTC)
|
|
mock_run.end_time = None
|
|
mock_session.query.return_value.filter.return_value.first.return_value = mock_run
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
svc.update_benchmark_status(
|
|
1, BenchmarkStatus.FAILED, error_message="oops"
|
|
)
|
|
|
|
assert mock_run.end_time is not None
|
|
assert mock_run.error_message == "oops"
|
|
|
|
def test_update_status_not_found(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
# Should not raise
|
|
svc.update_benchmark_status(1, BenchmarkStatus.COMPLETED)
|
|
|
|
def test_update_status_db_error(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_session.query.side_effect = RuntimeError("db error")
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
# Should not raise
|
|
svc.update_benchmark_status(1, BenchmarkStatus.COMPLETED)
|
|
|
|
mock_session.rollback.assert_called_once()
|
|
|
|
|
|
# ============================================================
|
|
# cancel_benchmark
|
|
# ============================================================
|
|
|
|
|
|
class TestCancelBenchmark:
|
|
def test_cancel_active_run(self):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {"status": "running"}
|
|
|
|
with patch.object(svc, "update_benchmark_status"):
|
|
result = svc.cancel_benchmark(1, "user1")
|
|
|
|
assert result is True
|
|
assert svc.active_runs[1]["status"] == "cancelled"
|
|
|
|
def test_cancel_nonexistent_run(self):
|
|
svc = _make_service()
|
|
|
|
with patch.object(svc, "update_benchmark_status"):
|
|
result = svc.cancel_benchmark(999, "user1")
|
|
|
|
assert result is True
|
|
|
|
def test_cancel_exception(self):
|
|
svc = _make_service()
|
|
|
|
with patch.object(
|
|
svc, "update_benchmark_status", side_effect=RuntimeError("err")
|
|
):
|
|
result = svc.cancel_benchmark(1)
|
|
|
|
assert result is False
|
|
|
|
|
|
# ============================================================
|
|
# get_benchmark_status
|
|
# ============================================================
|
|
|
|
|
|
class TestGetBenchmarkStatus:
|
|
def test_returns_none_when_not_found(self):
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
result = svc.get_benchmark_status(999)
|
|
|
|
assert result is None
|
|
|
|
def test_returns_status_with_running_accuracy(self):
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
|
|
mock_run = MagicMock()
|
|
mock_run.id = 1
|
|
mock_run.run_name = "test run"
|
|
mock_run.status = BenchmarkStatus.IN_PROGRESS
|
|
mock_run.completed_examples = 2
|
|
mock_run.total_examples = 10
|
|
mock_run.failed_examples = 0
|
|
mock_run.overall_accuracy = None
|
|
mock_run.processing_rate = None
|
|
mock_run.config_hash = "abc123"
|
|
mock_run.created_at = datetime.now(UTC)
|
|
mock_run.start_time = datetime.now(UTC) - timedelta(seconds=60)
|
|
mock_run.end_time = None
|
|
mock_run.error_message = None
|
|
|
|
mock_r1 = MagicMock()
|
|
mock_r1.is_correct = True
|
|
mock_r1.dataset_type = MagicMock()
|
|
mock_r1.dataset_type.value = "simpleqa"
|
|
|
|
mock_r2 = MagicMock()
|
|
mock_r2.is_correct = False
|
|
mock_r2.dataset_type = MagicMock()
|
|
mock_r2.dataset_type.value = "simpleqa"
|
|
|
|
# Mock the chain of queries
|
|
# We need to handle multiple query() calls differently
|
|
call_count = {"n": 0}
|
|
|
|
def side_effect_query(*args):
|
|
call_count["n"] += 1
|
|
m = MagicMock()
|
|
if call_count["n"] == 1:
|
|
# benchmark_run query
|
|
m.filter.return_value.first.return_value = mock_run
|
|
elif call_count["n"] == 2:
|
|
# current_results query
|
|
m.filter.return_value.filter.return_value.all.return_value = [
|
|
mock_r1,
|
|
mock_r2,
|
|
]
|
|
elif call_count["n"] == 3:
|
|
# all_results_for_timing query
|
|
m.filter.return_value.all.return_value = [mock_r1, mock_r2]
|
|
return m
|
|
|
|
mock_session.query.side_effect = side_effect_query
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
result = svc.get_benchmark_status(1, "user1")
|
|
|
|
assert result is not None
|
|
assert result["running_accuracy"] == 50.0
|
|
assert "simpleqa_accuracy" in result
|
|
|
|
def test_handles_exception(self):
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_session.query.side_effect = RuntimeError("db error")
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
result = svc.get_benchmark_status(1)
|
|
|
|
assert result is None
|
|
|
|
|
|
# ============================================================
|
|
# _run_benchmark_thread
|
|
# ============================================================
|
|
|
|
|
|
class TestRunBenchmarkThread:
|
|
@patch(f"{MODULE}._global_research_semaphore")
|
|
@patch(f"{SETTINGS_CTX_MODULE}.set_settings_context")
|
|
def test_thread_runs_tasks(self, mock_set_ctx, mock_semaphore):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {
|
|
"username": "user1",
|
|
"user_password": None,
|
|
"config_hash": "abc",
|
|
"datasets_config": {"simpleqa": {"count": 1}},
|
|
"search_config": {"iterations": 1},
|
|
"evaluation_config": {},
|
|
"settings_snapshot": {},
|
|
},
|
|
"results": [],
|
|
}
|
|
|
|
mock_task_result = {"example_id": "ex1", "is_correct": True}
|
|
|
|
with (
|
|
patch.object(
|
|
svc,
|
|
"_create_task_queue",
|
|
return_value=[
|
|
{
|
|
"benchmark_run_id": 1,
|
|
"example_id": "ex1",
|
|
"task_index": 0,
|
|
}
|
|
],
|
|
),
|
|
patch.object(
|
|
svc, "_process_benchmark_task", return_value=mock_task_result
|
|
),
|
|
patch.object(svc, "_send_progress_update"),
|
|
patch.object(svc, "_sync_results_to_database"),
|
|
):
|
|
svc._run_benchmark_thread(1)
|
|
|
|
assert len(svc.active_runs[1]["results"]) == 1
|
|
assert svc.active_runs[1]["thread_complete"] is True
|
|
|
|
@patch(f"{MODULE}._global_research_semaphore")
|
|
@patch(f"{SETTINGS_CTX_MODULE}.set_settings_context")
|
|
def test_thread_handles_cancelled_run(self, mock_set_ctx, mock_semaphore):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {
|
|
"username": "user1",
|
|
"user_password": None,
|
|
"config_hash": "abc",
|
|
"datasets_config": {},
|
|
"search_config": {},
|
|
"evaluation_config": {},
|
|
"settings_snapshot": {},
|
|
},
|
|
"status": "cancelled",
|
|
"results": [],
|
|
}
|
|
|
|
with (
|
|
patch.object(
|
|
svc,
|
|
"_create_task_queue",
|
|
return_value=[
|
|
{
|
|
"benchmark_run_id": 1,
|
|
"example_id": "ex1",
|
|
"task_index": 0,
|
|
}
|
|
],
|
|
),
|
|
patch.object(svc, "_sync_results_to_database"),
|
|
):
|
|
svc._run_benchmark_thread(1)
|
|
|
|
info = svc.active_runs[1]["completion_info"]
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
assert info["status"] == BenchmarkStatus.CANCELLED
|
|
|
|
@patch(f"{MODULE}._global_research_semaphore")
|
|
@patch(f"{SETTINGS_CTX_MODULE}.set_settings_context")
|
|
def test_thread_handles_task_error_rate_limit(
|
|
self, mock_set_ctx, mock_semaphore
|
|
):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {
|
|
"username": "user1",
|
|
"user_password": None,
|
|
"config_hash": "abc",
|
|
"datasets_config": {},
|
|
"search_config": {},
|
|
"evaluation_config": {},
|
|
"settings_snapshot": {},
|
|
},
|
|
"results": [],
|
|
}
|
|
|
|
with (
|
|
patch.object(
|
|
svc,
|
|
"_create_task_queue",
|
|
return_value=[
|
|
{
|
|
"benchmark_run_id": 1,
|
|
"example_id": "ex1",
|
|
"task_index": 0,
|
|
}
|
|
],
|
|
),
|
|
patch.object(
|
|
svc,
|
|
"_process_benchmark_task",
|
|
side_effect=RuntimeError("403 Forbidden rate limit"),
|
|
),
|
|
patch.object(svc, "_sync_results_to_database"),
|
|
):
|
|
svc._run_benchmark_thread(1)
|
|
|
|
assert svc.rate_limit_detected.get(1) is True
|
|
|
|
@patch(f"{MODULE}._global_research_semaphore")
|
|
@patch(f"{SETTINGS_CTX_MODULE}.set_settings_context")
|
|
def test_thread_handles_missing_data(self, mock_set_ctx, mock_semaphore):
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {} # No "data" key
|
|
|
|
with patch.object(svc, "_sync_results_to_database"):
|
|
svc._run_benchmark_thread(1)
|
|
|
|
info = svc.active_runs[1]["completion_info"]
|
|
from local_deep_research.benchmarks.web_api.benchmark_service import (
|
|
BenchmarkStatus,
|
|
)
|
|
|
|
assert info["status"] == BenchmarkStatus.FAILED
|
|
|
|
@patch(f"{MODULE}._global_research_semaphore")
|
|
@patch(f"{SETTINGS_CTX_MODULE}.set_settings_context")
|
|
def test_thread_settings_context_values(self, mock_set_ctx, mock_semaphore):
|
|
"""Test that SettingsContext correctly extracts values from setting objects."""
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {
|
|
"username": "user1",
|
|
"user_password": None,
|
|
"config_hash": "abc",
|
|
"datasets_config": {},
|
|
"search_config": {},
|
|
"evaluation_config": {},
|
|
"settings_snapshot": {
|
|
"key1": {"value": "val1"},
|
|
"key2": "direct_val",
|
|
},
|
|
},
|
|
"results": [],
|
|
}
|
|
|
|
captured_ctx = {}
|
|
|
|
def capture_ctx(ctx):
|
|
captured_ctx["ctx"] = ctx
|
|
|
|
mock_set_ctx.side_effect = capture_ctx
|
|
|
|
with (
|
|
patch.object(svc, "_create_task_queue", return_value=[]),
|
|
patch.object(svc, "_sync_results_to_database"),
|
|
):
|
|
svc._run_benchmark_thread(1)
|
|
|
|
ctx = captured_ctx["ctx"]
|
|
assert ctx.get_setting("key1") == "val1"
|
|
assert ctx.get_setting("key2") == "direct_val"
|
|
assert ctx.get_setting("nonexistent", "default") == "default"
|
|
|
|
@patch(f"{MODULE}._global_research_semaphore")
|
|
@patch(f"{SETTINGS_CTX_MODULE}.set_settings_context")
|
|
def test_thread_total_zero_examples_progress(
|
|
self, mock_set_ctx, mock_semaphore
|
|
):
|
|
"""Cover branch where total_examples is 0 for progress calculation."""
|
|
svc = _make_service()
|
|
svc.active_runs[1] = {
|
|
"data": {
|
|
"username": "user1",
|
|
"user_password": None,
|
|
"config_hash": "abc",
|
|
"datasets_config": {},
|
|
"search_config": {},
|
|
"evaluation_config": {},
|
|
"settings_snapshot": {},
|
|
},
|
|
"results": [],
|
|
}
|
|
|
|
with (
|
|
patch.object(svc, "_create_task_queue", return_value=[]),
|
|
patch.object(svc, "_sync_results_to_database"),
|
|
):
|
|
svc._run_benchmark_thread(1)
|
|
|
|
# Completion should still happen (progress = 0 since total = 0)
|
|
assert svc.active_runs[1]["thread_complete"] is True
|
|
|
|
|
|
# ============================================================
|
|
# start_benchmark
|
|
# ============================================================
|
|
|
|
|
|
class TestStartBenchmark:
|
|
def test_start_benchmark_run_not_found(self):
|
|
svc = _make_service()
|
|
mock_session = MagicMock()
|
|
mock_session.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
with patch(
|
|
"local_deep_research.database.session_context.get_user_db_session"
|
|
) as mock_get_session:
|
|
mock_get_session.return_value.__enter__ = Mock(
|
|
return_value=mock_session
|
|
)
|
|
mock_get_session.return_value.__exit__ = Mock(return_value=False)
|
|
|
|
result = svc.start_benchmark(999, "user1")
|
|
|
|
assert result is False
|
|
|
|
|
|
# ============================================================
|
|
# Progress callback in _process_benchmark_task
|
|
# ============================================================
|
|
|
|
|
|
class TestProgressCallback:
|
|
@patch(f"{MODULE}.grade_single_result")
|
|
@patch(f"{MODULE}.extract_answer_from_response")
|
|
@patch(f"{MODULE}.quick_summary")
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_callback_milestone_types(
|
|
self, mock_format, mock_summary, mock_extract, mock_grade
|
|
):
|
|
"""Test that the progress callback categorizes log types correctly."""
|
|
mock_socket = MagicMock()
|
|
svc = _make_service(socket=mock_socket)
|
|
|
|
def capture_quick_summary(**kwargs):
|
|
cb = kwargs.get("progress_callback")
|
|
if cb:
|
|
# Test various status strings
|
|
cb("Starting phase", 10, {"phase": "init"})
|
|
cb("Completed search", 50, {})
|
|
cb("Error occurred", 80, {})
|
|
cb("Processing data", 60, {})
|
|
return {"summary": "result", "sources": []}
|
|
|
|
mock_format.return_value = "q"
|
|
mock_summary.side_effect = capture_quick_summary
|
|
mock_extract.return_value = {"extracted_answer": "ans"}
|
|
mock_grade.return_value = {
|
|
"is_correct": True,
|
|
"graded_confidence": "90",
|
|
"grader_response": "ok",
|
|
}
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
task = {
|
|
"benchmark_run_id": 1,
|
|
"example_id": "ex1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"query_hash": "h1",
|
|
"task_index": 0,
|
|
"username": "user1",
|
|
"user_password": None,
|
|
}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
svc._process_benchmark_task(task, {}, {})
|
|
|
|
# Verify socket was called multiple times (from callback + regular calls)
|
|
assert mock_socket.emit_to_subscribers.call_count >= 4
|
|
|
|
@patch(f"{MODULE}.grade_single_result")
|
|
@patch(f"{MODULE}.extract_answer_from_response")
|
|
@patch(f"{MODULE}.quick_summary")
|
|
@patch(f"{MODULE}.format_query")
|
|
def test_callback_handles_exception(
|
|
self, mock_format, mock_summary, mock_extract, mock_grade
|
|
):
|
|
"""Test that exceptions in the progress callback are caught."""
|
|
mock_socket = MagicMock()
|
|
mock_socket.emit_to_subscribers.side_effect = RuntimeError(
|
|
"socket dead"
|
|
)
|
|
svc = _make_service(socket=mock_socket)
|
|
|
|
def trigger_callback(**kwargs):
|
|
cb = kwargs.get("progress_callback")
|
|
if cb:
|
|
cb("test status", 50, {}) # Should not raise
|
|
return {"summary": "result", "sources": []}
|
|
|
|
mock_format.return_value = "q"
|
|
mock_summary.side_effect = trigger_callback
|
|
mock_extract.return_value = {"extracted_answer": "ans"}
|
|
mock_grade.return_value = {
|
|
"is_correct": True,
|
|
"graded_confidence": "90",
|
|
"grader_response": "ok",
|
|
}
|
|
|
|
mock_settings = MagicMock()
|
|
mock_settings.snapshot = {}
|
|
|
|
task = {
|
|
"benchmark_run_id": 1,
|
|
"example_id": "ex1",
|
|
"dataset_type": "simpleqa",
|
|
"question": "Q?",
|
|
"correct_answer": "A",
|
|
"query_hash": "h1",
|
|
"task_index": 0,
|
|
"username": "user1",
|
|
"user_password": None,
|
|
}
|
|
|
|
with patch(
|
|
f"{SETTINGS_CTX_MODULE}.get_settings_context",
|
|
return_value=mock_settings,
|
|
):
|
|
# Should not raise despite socket error in callback
|
|
result = svc._process_benchmark_task(task, {}, {})
|
|
|
|
assert result is not None
|