from pathlib import Path import pytest import yaml from src.config_loader import load_config from src.exceptions import ConfigurationError DEFAULT = Path(__file__).parents[1] / "config" / "default.yaml" def write_yaml(tmp_path: Path, values: dict[str, object]) -> Path: path = tmp_path / "experiment.yaml" path.write_text(yaml.safe_dump(values), encoding="utf-8") return path def test_recursive_merge_and_experiment_precedence(tmp_path: Path) -> None: path = write_yaml(tmp_path, {"detector": {"confidence_threshold": 0.8}, "experiment": {"location": "lab"}}) config = load_config(DEFAULT, path) assert config.detector.confidence_threshold == 0.8 assert config.detector.iou_threshold == 0.5 assert config.experiment.location == "lab" assert config.experiment.experiment_id == "phase1_default" def test_cli_override_is_last(tmp_path: Path) -> None: path = write_yaml(tmp_path, {"detector": {"confidence_threshold": 0.6}}) config = load_config(DEFAULT, path, {"detector": {"confidence_threshold": 0.7}}) assert config.detector.confidence_threshold == 0.7 @pytest.mark.parametrize("override", [ {"detector": {"confidence_threshold": 1.1}}, {"evaluation_region": {"x1": -0.1}}, {"evaluation_region": {"x1": 0.9, "x2": 0.8}}, {"input": {"type": "video", "video_path": None}}, {"tracker": {"match_thresh": 1.1}}, {"crossing": {"minimum_track_points": 1}}, {"performance": {"skip_frames": 1}}, {"face": {"process_every_n_frames": 0}}, {"face": {"roi_height_ratio": 1.1}}, {"face": {"min_detection_confidence": -0.1}}, {"head_pose": {"ema_alpha": 0.0}}, {"head_pose": {"max_abs_angle": 0}}, {"face_logging": {"flush_interval_frames": 0}}, {"turn_detection": {"signage_yaw_direction": "sideways"}}, {"turn_detection": {"baseline_strategy": "unknown"}}, {"turn_detection": {"baseline_min_samples": 0}}, {"turn_detection": {"subtle_threshold_deg": 25, "weak_threshold_deg": 20}}, {"turn_detection": {"min_pose_estimation_rate_for_evaluation": 1.1}}, {"turn_logging": {"flush_interval_frames": 0}}, {"audio": {"backend": "unknown"}}, {"audio": {"volume": 1.1}}, {"audio": {"overlap_policy": "mix"}}, {"voice_prompt": {"mode": "random"}}, {"voice_prompt": {"trigger_strategy": "unknown"}}, {"voice_prompt": {"response_window_sec": 0}}, {"voice_prompt": {"fixed_position_axis": "z"}}, {"voice_logging": {"flush_interval_frames": 0}}, {"person_state": {"min_track_duration_sec": -1}}, {"person_state_logging": {"flush_interval_frames": 0}}, {"unified_logging": {"flush_interval_frames": 0}}, {"analysis": {"alpha": 1.0}}, {"analysis": {"response_rate_test": "bad"}}, {"gui": {"preview_max_fps": 0}}, {"gui": {"enabled": "yes"}}, {"pilot_diagnostics": {"min_valid_voice_analysis_rate": 1.1}}, {"pilot_diagnostics": {"max_condition_valid_count_ratio": 0.5}}, ]) def test_invalid_config(tmp_path: Path, override: dict[str, object]) -> None: with pytest.raises(ConfigurationError): load_config(DEFAULT, write_yaml(tmp_path, override)) def test_skip_frames_allowed_when_tracker_disabled(tmp_path: Path) -> None: path = write_yaml(tmp_path, {"tracker": {"enabled": False}, "performance": {"skip_frames": 2}}) assert load_config(DEFAULT, path).performance.skip_frames == 2