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
840 lines
31 KiB
Python
840 lines
31 KiB
Python
"""
|
|
High-value edge case tests for benchmarks/optimization/optuna_optimizer.py
|
|
|
|
Tests cover:
|
|
- Edge cases in parameter optimization (_objective with different param types)
|
|
- Trial generation and convergence scenarios
|
|
- Constraint violation / error handling paths (all try/except blocks)
|
|
- Boundary conditions in speed score calculation
|
|
- Invalid input handling (zero weights, empty param spaces)
|
|
- State management edge cases (_optimization_callback, trials_history)
|
|
- Visualization error paths and corner cases
|
|
- _save_results with missing/partial study data
|
|
- Convenience function argument forwarding
|
|
"""
|
|
|
|
import numpy as np
|
|
import pytest
|
|
from unittest.mock import Mock, patch
|
|
|
|
|
|
MODULE = "local_deep_research.benchmarks.optimization.optuna_optimizer"
|
|
|
|
|
|
def _make_optimizer(**kwargs):
|
|
"""Helper to create an OptunaOptimizer with mocked evaluator."""
|
|
from local_deep_research.benchmarks.optimization.optuna_optimizer import (
|
|
OptunaOptimizer,
|
|
)
|
|
|
|
defaults = {"base_query": "test query"}
|
|
defaults.update(kwargs)
|
|
return OptunaOptimizer(**defaults)
|
|
|
|
|
|
class TestObjectiveParameterTypes:
|
|
"""Tests for _objective handling of different parameter types (int, float, categorical)."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_objective_handles_float_param_type(self, mock_evaluator):
|
|
"""Test that _objective correctly suggests float parameters."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
|
|
mock_trial = Mock()
|
|
mock_trial.number = 0
|
|
mock_trial.suggest_float.return_value = 0.5
|
|
mock_trial.set_user_attr = Mock()
|
|
|
|
param_space = {
|
|
"learning_rate": {
|
|
"type": "float",
|
|
"low": 0.01,
|
|
"high": 1.0,
|
|
"step": None,
|
|
"log": True,
|
|
}
|
|
}
|
|
|
|
with patch.object(optimizer, "_run_experiment") as mock_run:
|
|
mock_run.return_value = {"score": 0.6}
|
|
score = optimizer._objective(mock_trial, param_space=param_space)
|
|
|
|
mock_trial.suggest_float.assert_called_once_with(
|
|
"learning_rate", 0.01, 1.0, step=None, log=True
|
|
)
|
|
assert score == 0.6
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_objective_handles_unknown_param_type_silently(
|
|
self, mock_evaluator
|
|
):
|
|
"""Test that _objective skips params with unrecognized type strings."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
|
|
mock_trial = Mock()
|
|
mock_trial.number = 0
|
|
mock_trial.set_user_attr = Mock()
|
|
|
|
# A param type that doesn't match any branch
|
|
param_space = {
|
|
"unknown_param": {
|
|
"type": "boolean",
|
|
"default": True,
|
|
}
|
|
}
|
|
|
|
with patch.object(optimizer, "_run_experiment") as mock_run:
|
|
mock_run.return_value = {"score": 0.5}
|
|
score = optimizer._objective(mock_trial, param_space=param_space)
|
|
|
|
# The unknown param type should be silently skipped
|
|
assert score == 0.5
|
|
mock_trial.suggest_int.assert_not_called()
|
|
mock_trial.suggest_float.assert_not_called()
|
|
mock_trial.suggest_categorical.assert_not_called()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_objective_with_empty_param_space(self, mock_evaluator):
|
|
"""Test _objective with an empty parameter space dict."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
|
|
mock_trial = Mock()
|
|
mock_trial.number = 0
|
|
mock_trial.set_user_attr = Mock()
|
|
|
|
with patch.object(optimizer, "_run_experiment") as mock_run:
|
|
mock_run.return_value = {"score": 0.3}
|
|
score = optimizer._objective(mock_trial, param_space={})
|
|
|
|
assert score == 0.3
|
|
# _run_experiment should be called with empty params dict
|
|
mock_run.assert_called_once_with({})
|
|
|
|
|
|
class TestObjectiveErrorAndCallback:
|
|
"""Tests for _objective error handling and progress callback invocation."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_objective_error_triggers_progress_callback_with_error_status(
|
|
self, mock_evaluator
|
|
):
|
|
"""Test that when _run_experiment raises, the progress callback receives error status."""
|
|
mock_evaluator.return_value = Mock()
|
|
callback = Mock()
|
|
optimizer = _make_optimizer(progress_callback=callback)
|
|
|
|
mock_trial = Mock()
|
|
mock_trial.number = 5
|
|
mock_trial.suggest_int.return_value = 2
|
|
mock_trial.suggest_categorical.return_value = "iterdrag"
|
|
mock_trial.set_user_attr = Mock()
|
|
|
|
with patch.object(optimizer, "_run_experiment") as mock_run:
|
|
mock_run.side_effect = RuntimeError("GPU OOM")
|
|
param_space = optimizer._get_default_param_space()
|
|
score = optimizer._objective(mock_trial, param_space=param_space)
|
|
|
|
assert score == float("-inf")
|
|
# Check that the error callback was invoked
|
|
error_calls = [
|
|
c
|
|
for c in callback.call_args_list
|
|
if c[0][2].get("status") == "error"
|
|
]
|
|
assert len(error_calls) == 1
|
|
assert "GPU OOM" in error_calls[0][0][2]["error"]
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_objective_success_appends_to_trials_history(self, mock_evaluator):
|
|
"""Test that a successful trial is appended to trials_history with correct fields."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
|
|
mock_trial = Mock()
|
|
mock_trial.number = 7
|
|
mock_trial.suggest_int.return_value = 3
|
|
mock_trial.suggest_categorical.return_value = "standard"
|
|
mock_trial.set_user_attr = Mock()
|
|
|
|
with patch.object(optimizer, "_run_experiment") as mock_run:
|
|
mock_run.return_value = {"score": 0.92, "quality_score": 0.95}
|
|
param_space = optimizer._get_default_param_space()
|
|
optimizer._objective(mock_trial, param_space=param_space)
|
|
|
|
assert len(optimizer.trials_history) == 1
|
|
entry = optimizer.trials_history[0]
|
|
assert entry["trial_number"] == 7
|
|
assert entry["score"] == 0.92
|
|
assert "params" in entry
|
|
assert "duration" in entry
|
|
assert "timestamp" in entry
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_objective_error_does_not_append_to_trials_history(
|
|
self, mock_evaluator
|
|
):
|
|
"""Test that a failed trial is NOT appended to trials_history."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
|
|
mock_trial = Mock()
|
|
mock_trial.number = 0
|
|
mock_trial.suggest_int.return_value = 1
|
|
mock_trial.suggest_categorical.return_value = "rapid"
|
|
mock_trial.set_user_attr = Mock()
|
|
|
|
with patch.object(optimizer, "_run_experiment") as mock_run:
|
|
mock_run.side_effect = ValueError("bad config")
|
|
param_space = optimizer._get_default_param_space()
|
|
optimizer._objective(mock_trial, param_space=param_space)
|
|
|
|
assert len(optimizer.trials_history) == 0
|
|
|
|
|
|
class TestRunExperimentEdgeCases:
|
|
"""Tests for _run_experiment edge cases and error paths."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.SpeedProfiler")
|
|
def test_run_experiment_error_returns_failure_dict(
|
|
self, mock_profiler_cls, mock_evaluator
|
|
):
|
|
"""Test that _run_experiment catches exceptions and returns a failure dict."""
|
|
mock_eval_instance = Mock()
|
|
mock_eval_instance.evaluate.side_effect = ConnectionError(
|
|
"network down"
|
|
)
|
|
mock_evaluator.return_value = mock_eval_instance
|
|
|
|
mock_profiler = Mock()
|
|
mock_profiler_cls.return_value = mock_profiler
|
|
|
|
optimizer = _make_optimizer()
|
|
result = optimizer._run_experiment({"iterations": 1})
|
|
|
|
assert result["success"] is False
|
|
assert result["score"] == 0.0
|
|
assert "network down" in result["error"]
|
|
# Profiler stop should still be called on error
|
|
mock_profiler.stop.assert_called()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.SpeedProfiler")
|
|
def test_speed_score_boundary_very_fast(
|
|
self, mock_profiler_cls, mock_evaluator
|
|
):
|
|
"""Test speed_score is clamped to 1.0 for very fast durations (<60s)."""
|
|
mock_eval_instance = Mock()
|
|
mock_eval_instance.evaluate.return_value = {
|
|
"quality_score": 0.9,
|
|
"benchmark_results": {},
|
|
}
|
|
mock_evaluator.return_value = mock_eval_instance
|
|
|
|
mock_profiler = Mock()
|
|
mock_profiler.get_summary.return_value = {"total_duration": 10.0}
|
|
mock_profiler_cls.return_value = mock_profiler
|
|
|
|
optimizer = _make_optimizer(
|
|
metric_weights={"quality": 0.5, "speed": 0.5}
|
|
)
|
|
result = optimizer._run_experiment({"iterations": 1})
|
|
|
|
# speed_score = max(0, min(1, 1 - (10-60)/180)) = min(1, 1 + 50/180) = 1.0
|
|
assert result["speed_score"] == 1.0
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.SpeedProfiler")
|
|
def test_speed_score_boundary_very_slow(
|
|
self, mock_profiler_cls, mock_evaluator
|
|
):
|
|
"""Test speed_score is clamped to 0.0 for very slow durations (>240s)."""
|
|
mock_eval_instance = Mock()
|
|
mock_eval_instance.evaluate.return_value = {
|
|
"quality_score": 0.5,
|
|
"benchmark_results": {},
|
|
}
|
|
mock_evaluator.return_value = mock_eval_instance
|
|
|
|
mock_profiler = Mock()
|
|
mock_profiler.get_summary.return_value = {"total_duration": 500.0}
|
|
mock_profiler_cls.return_value = mock_profiler
|
|
|
|
optimizer = _make_optimizer()
|
|
result = optimizer._run_experiment({"iterations": 5})
|
|
|
|
# speed_score = max(0, min(1, 1 - (500-60)/180)) = max(0, 1 - 2.44) = 0.0
|
|
assert result["speed_score"] == 0.0
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.SpeedProfiler")
|
|
def test_run_experiment_default_params_when_missing(
|
|
self, mock_profiler_cls, mock_evaluator
|
|
):
|
|
"""Test _run_experiment uses defaults when params dict is sparse."""
|
|
mock_eval_instance = Mock()
|
|
mock_eval_instance.evaluate.return_value = {
|
|
"quality_score": 0.7,
|
|
"benchmark_results": {},
|
|
}
|
|
mock_evaluator.return_value = mock_eval_instance
|
|
|
|
mock_profiler = Mock()
|
|
mock_profiler.get_summary.return_value = {"total_duration": 100.0}
|
|
mock_profiler_cls.return_value = mock_profiler
|
|
|
|
optimizer = _make_optimizer()
|
|
result = optimizer._run_experiment({}) # empty params
|
|
|
|
assert result["success"] is True
|
|
# Should have used defaults and not crashed
|
|
assert "score" in result
|
|
|
|
|
|
class TestSpeedScoreCalculation:
|
|
"""Tests for the speed score formula boundary conditions."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.SpeedProfiler")
|
|
def test_speed_score_at_exact_60_seconds(
|
|
self, mock_profiler_cls, mock_evaluator
|
|
):
|
|
"""Test speed_score at exactly 60 seconds (should be 1.0)."""
|
|
mock_eval_instance = Mock()
|
|
mock_eval_instance.evaluate.return_value = {
|
|
"quality_score": 0.5,
|
|
"benchmark_results": {},
|
|
}
|
|
mock_evaluator.return_value = mock_eval_instance
|
|
|
|
mock_profiler = Mock()
|
|
mock_profiler.get_summary.return_value = {"total_duration": 60.0}
|
|
mock_profiler_cls.return_value = mock_profiler
|
|
|
|
optimizer = _make_optimizer()
|
|
result = optimizer._run_experiment({"iterations": 1})
|
|
|
|
# speed_score = max(0, min(1, 1 - (60-60)/180)) = 1.0
|
|
assert result["speed_score"] == pytest.approx(1.0)
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.SpeedProfiler")
|
|
def test_speed_score_at_exact_240_seconds(
|
|
self, mock_profiler_cls, mock_evaluator
|
|
):
|
|
"""Test speed_score at exactly 240 seconds (should be 0.0)."""
|
|
mock_eval_instance = Mock()
|
|
mock_eval_instance.evaluate.return_value = {
|
|
"quality_score": 0.5,
|
|
"benchmark_results": {},
|
|
}
|
|
mock_evaluator.return_value = mock_eval_instance
|
|
|
|
mock_profiler = Mock()
|
|
mock_profiler.get_summary.return_value = {"total_duration": 240.0}
|
|
mock_profiler_cls.return_value = mock_profiler
|
|
|
|
optimizer = _make_optimizer()
|
|
result = optimizer._run_experiment({"iterations": 1})
|
|
|
|
# speed_score = max(0, min(1, 1 - (240-60)/180)) = max(0, 0) = 0.0
|
|
assert result["speed_score"] == pytest.approx(0.0)
|
|
|
|
|
|
class TestWeightNormalizationEdgeCases:
|
|
"""Tests for metric weight normalization edge cases."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_zero_total_weight_preserved(self, mock_evaluator):
|
|
"""Test that zero total weight does not cause division by zero."""
|
|
mock_evaluator.return_value = Mock()
|
|
# All weights are zero - normalization guard: total_weight > 0 is False
|
|
optimizer = _make_optimizer(metric_weights={"quality": 0, "speed": 0})
|
|
|
|
# Weights should remain as-is (all zeros) since total is 0
|
|
assert optimizer.metric_weights["quality"] == 0
|
|
assert optimizer.metric_weights["speed"] == 0
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_single_metric_weight_normalizes_to_one(self, mock_evaluator):
|
|
"""Test that a single metric weight normalizes to 1.0."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer(metric_weights={"quality": 5.0})
|
|
|
|
assert optimizer.metric_weights["quality"] == pytest.approx(1.0)
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_many_metrics_normalize_correctly(self, mock_evaluator):
|
|
"""Test normalization with many metrics."""
|
|
mock_evaluator.return_value = Mock()
|
|
weights = {"quality": 3.0, "speed": 2.0, "resource": 1.0, "cost": 4.0}
|
|
optimizer = _make_optimizer(metric_weights=weights)
|
|
|
|
total = sum(optimizer.metric_weights.values())
|
|
assert total == pytest.approx(1.0)
|
|
assert optimizer.metric_weights["quality"] == pytest.approx(0.3)
|
|
assert optimizer.metric_weights["cost"] == pytest.approx(0.4)
|
|
|
|
|
|
class TestOptimizationCallbackEdgeCases:
|
|
"""Tests for _optimization_callback periodic save behavior."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_callback_saves_at_trial_10(self, mock_evaluator):
|
|
"""Test that _optimization_callback triggers save at trial 10."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
|
|
mock_study = Mock()
|
|
mock_trial = Mock()
|
|
mock_trial.number = 10
|
|
|
|
with (
|
|
patch.object(optimizer, "_save_results") as mock_save,
|
|
patch.object(optimizer, "_create_quick_visualizations") as mock_viz,
|
|
):
|
|
optimizer._optimization_callback(mock_study, mock_trial)
|
|
|
|
mock_save.assert_called_once()
|
|
mock_viz.assert_called_once()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_callback_does_not_save_at_trial_0(self, mock_evaluator):
|
|
"""Test that _optimization_callback does NOT trigger save at trial 0."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
|
|
mock_study = Mock()
|
|
mock_trial = Mock()
|
|
mock_trial.number = 0
|
|
|
|
with (
|
|
patch.object(optimizer, "_save_results") as mock_save,
|
|
patch.object(optimizer, "_create_quick_visualizations") as mock_viz,
|
|
):
|
|
optimizer._optimization_callback(mock_study, mock_trial)
|
|
|
|
mock_save.assert_not_called()
|
|
mock_viz.assert_not_called()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_callback_does_not_save_at_non_multiple_of_10(self, mock_evaluator):
|
|
"""Test that _optimization_callback does NOT save at trial 7."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
|
|
mock_study = Mock()
|
|
mock_trial = Mock()
|
|
mock_trial.number = 7
|
|
|
|
with patch.object(optimizer, "_save_results") as mock_save:
|
|
optimizer._optimization_callback(mock_study, mock_trial)
|
|
|
|
mock_save.assert_not_called()
|
|
|
|
|
|
class TestSaveResultsEdgeCases:
|
|
"""Tests for _save_results with edge case data."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.joblib")
|
|
@patch(
|
|
"local_deep_research.security.file_write_verifier.write_json_verified"
|
|
)
|
|
def test_save_results_with_numpy_values_in_nested_dicts(
|
|
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
|
):
|
|
"""Test _save_results converts numpy types in nested trial dicts."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
|
|
|
mock_study = Mock()
|
|
mock_study.best_params = {"iterations": 2}
|
|
mock_study.best_value = np.float64(0.85)
|
|
mock_study.best_trial = Mock()
|
|
mock_study.best_trial.user_attrs = {}
|
|
mock_study.trials = [Mock()]
|
|
optimizer.study = mock_study
|
|
optimizer.best_params = {"iterations": 2}
|
|
|
|
# Include numpy values in trials_history
|
|
optimizer.trials_history = [
|
|
{
|
|
"trial_number": 0,
|
|
"params": {"iterations": np.int64(2)},
|
|
"score": np.float64(0.85),
|
|
"result": {"quality_score": np.float32(0.9)},
|
|
}
|
|
]
|
|
|
|
optimizer._save_results()
|
|
|
|
# Verify write_json_verified was called (history + best params)
|
|
assert mock_write_json.call_count == 2
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.joblib")
|
|
@patch(
|
|
"local_deep_research.security.file_write_verifier.write_json_verified"
|
|
)
|
|
def test_save_results_with_no_study_skips_best_params(
|
|
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
|
):
|
|
"""Test _save_results when study is None skips best_params and study save."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
|
optimizer.study = None
|
|
optimizer.best_params = None
|
|
optimizer.trials_history = []
|
|
|
|
optimizer._save_results()
|
|
|
|
# Only history file should be written, not best_params
|
|
assert mock_write_json.call_count == 1
|
|
mock_joblib.dump.assert_not_called()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.joblib")
|
|
@patch(
|
|
"local_deep_research.security.file_write_verifier.write_json_verified"
|
|
)
|
|
def test_save_results_study_without_best_params(
|
|
self, mock_write_json, mock_joblib, mock_evaluator, tmp_path
|
|
):
|
|
"""Test _save_results when study exists but best_params is empty/falsy."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
|
|
|
mock_study = Mock()
|
|
mock_study.best_params = {} # empty dict is falsy
|
|
mock_study.best_value = 0.0
|
|
mock_study.trials = []
|
|
optimizer.study = mock_study
|
|
optimizer.best_params = {}
|
|
optimizer.trials_history = []
|
|
|
|
optimizer._save_results()
|
|
|
|
# history is written + study.pkl is saved, but best_params JSON is skipped
|
|
# because self.study.best_params is {} which is falsy
|
|
assert mock_write_json.call_count == 1
|
|
mock_joblib.dump.assert_called_once()
|
|
|
|
|
|
class TestVisualizationEdgeCases:
|
|
"""Tests for visualization methods handling edge cases."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.PLOTTING_AVAILABLE", False)
|
|
def test_create_visualizations_returns_early_without_matplotlib(
|
|
self, mock_evaluator
|
|
):
|
|
"""Test _create_visualizations returns early when matplotlib is unavailable."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer()
|
|
optimizer.study = Mock()
|
|
optimizer.study.trials = [Mock(), Mock()]
|
|
|
|
# Should not raise and should return early
|
|
optimizer._create_visualizations()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
|
def test_create_visualizations_returns_early_with_less_than_2_trials(
|
|
self, mock_evaluator, tmp_path
|
|
):
|
|
"""Test _create_visualizations returns early with fewer than 2 trials."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
|
optimizer.study = Mock()
|
|
optimizer.study.trials = [Mock()] # Only 1 trial
|
|
optimizer.trials_history = []
|
|
|
|
# Should not raise - returns early due to insufficient trials
|
|
with patch.object(
|
|
optimizer, "_create_optuna_visualizations"
|
|
) as mock_optuna_viz:
|
|
optimizer._create_visualizations()
|
|
mock_optuna_viz.assert_not_called()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_create_custom_visualizations_returns_early_with_no_history(
|
|
self, mock_evaluator, tmp_path
|
|
):
|
|
"""Test _create_custom_visualizations returns early with empty trials_history."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
|
optimizer.trials_history = []
|
|
|
|
# Should not raise or try to create any plots
|
|
with patch(f"{MODULE}.plt") as mock_plt:
|
|
optimizer._create_custom_visualizations(str(tmp_path))
|
|
mock_plt.figure.assert_not_called()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
def test_quality_vs_speed_plot_no_successful_trials(
|
|
self, mock_evaluator, tmp_path
|
|
):
|
|
"""Test _create_quality_vs_speed_plot with only failed trials."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
|
optimizer.trials_history = [
|
|
{"result": {"success": False}, "params": {}, "score": 0.0},
|
|
]
|
|
|
|
with patch(f"{MODULE}.plt") as mock_plt:
|
|
optimizer._create_quality_vs_speed_plot(str(tmp_path), "20260304")
|
|
# Should return early because no successful trials
|
|
mock_plt.figure.assert_not_called()
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.PLOTTING_AVAILABLE", True)
|
|
def test_quick_visualizations_handles_plot_error(
|
|
self, mock_evaluator, tmp_path
|
|
):
|
|
"""Test _create_quick_visualizations gracefully handles plot errors."""
|
|
mock_evaluator.return_value = Mock()
|
|
optimizer = _make_optimizer(output_dir=str(tmp_path))
|
|
optimizer.study = Mock()
|
|
optimizer.study.trials = [Mock(), Mock()]
|
|
|
|
with patch(f"{MODULE}.plot_optimization_history") as mock_plot:
|
|
mock_plot.side_effect = RuntimeError("plot failed")
|
|
# Should not raise
|
|
optimizer._create_quick_visualizations()
|
|
|
|
|
|
class TestOptimizeMethodEdgeCases:
|
|
"""Tests for optimize() method edge cases."""
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.optuna")
|
|
def test_optimize_calls_progress_callback_on_start(
|
|
self, mock_optuna, mock_evaluator
|
|
):
|
|
"""Test optimize() calls progress_callback with 'starting' status."""
|
|
mock_evaluator.return_value = Mock()
|
|
callback = Mock()
|
|
|
|
mock_study = Mock()
|
|
mock_study.best_params = {"iterations": 1}
|
|
mock_study.best_value = 0.5
|
|
mock_study.best_trial = Mock()
|
|
mock_study.best_trial.user_attrs = {}
|
|
mock_study.trials = []
|
|
mock_optuna.create_study.return_value = mock_study
|
|
|
|
optimizer = _make_optimizer(n_trials=1, progress_callback=callback)
|
|
|
|
with (
|
|
patch.object(optimizer, "_save_results"),
|
|
patch.object(optimizer, "_create_visualizations"),
|
|
):
|
|
optimizer.optimize()
|
|
|
|
# First callback call should have status "starting"
|
|
first_call = callback.call_args_list[0]
|
|
assert first_call[0][0] == 0 # trial_num = 0
|
|
assert first_call[0][2]["status"] == "starting"
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.optuna")
|
|
def test_optimize_calls_progress_callback_on_completion(
|
|
self, mock_optuna, mock_evaluator
|
|
):
|
|
"""Test optimize() calls progress_callback with 'completed' status at the end."""
|
|
mock_evaluator.return_value = Mock()
|
|
callback = Mock()
|
|
|
|
mock_study = Mock()
|
|
mock_study.best_params = {"iterations": 2}
|
|
mock_study.best_value = 0.9
|
|
mock_study.best_trial = Mock()
|
|
mock_study.best_trial.user_attrs = {}
|
|
mock_study.trials = [Mock()]
|
|
mock_optuna.create_study.return_value = mock_study
|
|
|
|
optimizer = _make_optimizer(n_trials=1, progress_callback=callback)
|
|
|
|
with (
|
|
patch.object(optimizer, "_save_results"),
|
|
patch.object(optimizer, "_create_visualizations"),
|
|
):
|
|
optimizer.optimize()
|
|
|
|
# Last callback call should have status "completed"
|
|
last_call = callback.call_args_list[-1]
|
|
assert last_call[0][2]["status"] == "completed"
|
|
assert last_call[0][2]["best_value"] == 0.9
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.optuna")
|
|
def test_optimize_handles_keyboard_interrupt(
|
|
self, mock_optuna, mock_evaluator
|
|
):
|
|
"""Test optimize() handles KeyboardInterrupt, saves results, and returns best."""
|
|
mock_evaluator.return_value = Mock()
|
|
|
|
mock_study = Mock()
|
|
mock_study.best_params = {"iterations": 1}
|
|
mock_study.best_value = 0.3
|
|
mock_study.best_trial = Mock()
|
|
mock_study.best_trial.user_attrs = {}
|
|
mock_study.trials = [Mock()]
|
|
mock_study.optimize.side_effect = KeyboardInterrupt()
|
|
mock_optuna.create_study.return_value = mock_study
|
|
|
|
optimizer = _make_optimizer(n_trials=10)
|
|
|
|
with (
|
|
patch.object(optimizer, "_save_results") as mock_save,
|
|
patch.object(optimizer, "_create_visualizations") as mock_viz,
|
|
):
|
|
best_params, best_value = optimizer.optimize()
|
|
|
|
# Should still save and visualize
|
|
mock_save.assert_called_once()
|
|
mock_viz.assert_called_once()
|
|
assert best_params == {"iterations": 1}
|
|
assert best_value == 0.3
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.optuna")
|
|
def test_optimize_keyboard_interrupt_with_callback(
|
|
self, mock_optuna, mock_evaluator
|
|
):
|
|
"""Test optimize() invokes callback with 'interrupted' on KeyboardInterrupt."""
|
|
mock_evaluator.return_value = Mock()
|
|
callback = Mock()
|
|
|
|
mock_study = Mock()
|
|
mock_study.best_params = {"iterations": 1}
|
|
mock_study.best_value = 0.2
|
|
mock_study.best_trial = Mock()
|
|
mock_study.best_trial.user_attrs = {}
|
|
mock_study.trials = [Mock(), Mock()]
|
|
mock_study.optimize.side_effect = KeyboardInterrupt()
|
|
mock_optuna.create_study.return_value = mock_study
|
|
|
|
optimizer = _make_optimizer(n_trials=10, progress_callback=callback)
|
|
|
|
with (
|
|
patch.object(optimizer, "_save_results"),
|
|
patch.object(optimizer, "_create_visualizations"),
|
|
):
|
|
optimizer.optimize()
|
|
|
|
# Find the interrupted callback
|
|
interrupted_calls = [
|
|
c
|
|
for c in callback.call_args_list
|
|
if c[0][2].get("status") == "interrupted"
|
|
]
|
|
assert len(interrupted_calls) == 1
|
|
assert interrupted_calls[0][0][2]["trials_completed"] == 2
|
|
|
|
@patch(f"{MODULE}.CompositeBenchmarkEvaluator")
|
|
@patch(f"{MODULE}.optuna")
|
|
def test_optimize_uses_custom_param_space(
|
|
self, mock_optuna, mock_evaluator
|
|
):
|
|
"""Test optimize() passes custom param_space through to _objective."""
|
|
mock_evaluator.return_value = Mock()
|
|
|
|
mock_study = Mock()
|
|
mock_study.best_params = {"custom_param": 5}
|
|
mock_study.best_value = 0.7
|
|
mock_study.best_trial = Mock()
|
|
mock_study.best_trial.user_attrs = {}
|
|
mock_study.trials = []
|
|
mock_optuna.create_study.return_value = mock_study
|
|
|
|
optimizer = _make_optimizer(n_trials=1)
|
|
|
|
custom_space = {
|
|
"custom_param": {"type": "int", "low": 1, "high": 10, "step": 1}
|
|
}
|
|
|
|
with (
|
|
patch.object(optimizer, "_save_results"),
|
|
patch.object(optimizer, "_create_visualizations"),
|
|
):
|
|
best_params, best_value = optimizer.optimize(
|
|
param_space=custom_space
|
|
)
|
|
|
|
# Verify study.optimize was called (the partial wrapping the custom space)
|
|
mock_study.optimize.assert_called_once()
|
|
assert best_params == {"custom_param": 5}
|
|
|
|
|
|
class TestConvenienceFunctionForwarding:
|
|
"""Tests for convenience functions forwarding arguments correctly."""
|
|
|
|
@patch(f"{MODULE}.OptunaOptimizer")
|
|
def test_optimize_parameters_forwards_all_kwargs(self, mock_cls):
|
|
"""Test optimize_parameters forwards all keyword arguments."""
|
|
from local_deep_research.benchmarks.optimization.optuna_optimizer import (
|
|
optimize_parameters,
|
|
)
|
|
|
|
mock_instance = Mock()
|
|
mock_instance.optimize.return_value = ({"a": 1}, 0.5)
|
|
mock_cls.return_value = mock_instance
|
|
|
|
callback = Mock()
|
|
optimize_parameters(
|
|
query="my query",
|
|
output_dir="/tmp/out",
|
|
model_name="gpt-4",
|
|
provider="openai",
|
|
search_tool="google",
|
|
temperature=0.3,
|
|
n_trials=15,
|
|
timeout=600,
|
|
n_jobs=2,
|
|
study_name="my_study",
|
|
optimization_metrics=["quality"],
|
|
metric_weights={"quality": 1.0},
|
|
progress_callback=callback,
|
|
benchmark_weights={"simpleqa": 0.5, "browsecomp": 0.5},
|
|
)
|
|
|
|
call_kwargs = mock_cls.call_args[1]
|
|
assert call_kwargs["base_query"] == "my query"
|
|
assert call_kwargs["output_dir"] == "/tmp/out"
|
|
assert call_kwargs["model_name"] == "gpt-4"
|
|
assert call_kwargs["provider"] == "openai"
|
|
assert call_kwargs["temperature"] == 0.3
|
|
assert call_kwargs["n_trials"] == 15
|
|
assert call_kwargs["timeout"] == 600
|
|
assert call_kwargs["n_jobs"] == 2
|
|
assert call_kwargs["study_name"] == "my_study"
|
|
assert call_kwargs["progress_callback"] is callback
|
|
assert call_kwargs["benchmark_weights"] == {
|
|
"simpleqa": 0.5,
|
|
"browsecomp": 0.5,
|
|
}
|
|
|
|
@patch(f"{MODULE}.OptunaOptimizer")
|
|
def test_optimize_for_speed_passes_reduced_param_space(self, mock_cls):
|
|
"""Test optimize_for_speed provides a reduced param space (max iterations=3)."""
|
|
from local_deep_research.benchmarks.optimization.optuna_optimizer import (
|
|
optimize_for_speed,
|
|
)
|
|
|
|
mock_instance = Mock()
|
|
mock_instance.optimize.return_value = ({}, 0.0)
|
|
mock_cls.return_value = mock_instance
|
|
|
|
optimize_for_speed(query="test", n_trials=5)
|
|
|
|
# Check the param_space passed to optimize()
|
|
optimize_call = mock_instance.optimize.call_args
|
|
param_space = optimize_call[1].get("param_space") or optimize_call[0][0]
|
|
assert param_space["iterations"]["high"] == 3
|
|
assert "focused-iteration" in param_space["search_strategy"]["choices"]
|