"""Tests for src/models/train_final.py's `_load_validation_metrics`, using a synthetic tmp_path file rather than the real data/models/logo_cv_metrics.json so tests don't depend on notebooks/03 having been re-run recently.""" from __future__ import annotations import json import pytest from src.models import train_final from src.models.train_final import _load_validation_metrics class TestLoadValidationMetrics: def test_returns_none_when_file_absent(self, tmp_path): missing = tmp_path / "logo_cv_metrics.json" assert _load_validation_metrics(missing) is None def test_returns_parsed_dict_when_present(self, tmp_path): path = tmp_path / "logo_cv_metrics.json" payload = {"primary": {"mape": 0.234, "smape": 0.212, "n_folds": 5}} path.write_text(json.dumps(payload)) assert _load_validation_metrics(path) == payload def test_default_path_does_not_raise(self): # Whether or not the real file exists on this machine, calling with no argument must never raise — it's a best-effort informational read, not a hard requirement of train_and_save(). result = _load_validation_metrics() assert result is None or isinstance(result, dict) def test_refuses_symlinked_file(self, tmp_path): """Same guard predictor.py applies to feature_metadata.json/prophet_v1.json — a symlinked metrics file could point at attacker-controlled content committed into validation_metrics with no warning.""" real = tmp_path / "real.json" real.write_text(json.dumps({"primary": {"mape": 0.0}})) link = tmp_path / "link.json" link.symlink_to(real) with pytest.raises(ValueError, match="symlink"): _load_validation_metrics(link) def test_refuses_oversized_file(self, tmp_path, monkeypatch): monkeypatch.setattr(train_final, "_MAX_VALIDATION_METRICS_BYTES", 10) p = tmp_path / "logo_cv_metrics.json" p.write_text(json.dumps({"primary": {"mape": 0.234}})) with pytest.raises(ValueError, match="too large"): _load_validation_metrics(p)