@@ -202,3 +202,115 @@ def test_missing_oracle_env_falls_back_gracefully(self, caplog):
202202 assert "chorus setup --oracle enformer" in msgs , (
203203 "log must quote the exact command the user needs to run"
204204 )
205+
206+
207+ # ---------------------------------------------------------------------------
208+ # v10 additions
209+ # ---------------------------------------------------------------------------
210+
211+ class TestTFHubCorruptCacheRecovery :
212+ """If TensorFlow Hub's on-disk cache has a partial/corrupt download
213+ (missing ``saved_model.pb``), ``_load_enformer_with_tfhub_recovery``
214+ must clear the bad directory and retry ``hub.load`` once."""
215+
216+ def test_corrupt_cache_is_cleared_and_retry_succeeds (self , tmp_path ):
217+ from chorus .oracles .enformer import _load_enformer_with_tfhub_recovery
218+
219+ bad_dir = tmp_path / "tfhub_modules" / "corrupt"
220+ bad_dir .mkdir (parents = True )
221+ (bad_dir / "variables" ).mkdir () # partial — missing saved_model.pb
222+
223+ calls = []
224+ class FakeHub :
225+ def load (self , weights ):
226+ calls .append (weights )
227+ if len (calls ) == 1 :
228+ raise ValueError (
229+ f"Trying to load a model of incompatible/unknown type. "
230+ f"'{ bad_dir } ' contains neither 'saved_model.pb' "
231+ f"nor 'saved_model.pbtxt'."
232+ )
233+ return {"loaded" : True }
234+
235+ result = _load_enformer_with_tfhub_recovery (FakeHub (), "https://tfhub.dev/enformer" )
236+ assert result == {"loaded" : True }
237+ assert len (calls ) == 2 , "should retry exactly once"
238+ assert not bad_dir .exists (), "corrupt cache dir must be removed before retry"
239+
240+ def test_unrelated_errors_propagate_unchanged (self ):
241+ from chorus .oracles .enformer import _load_enformer_with_tfhub_recovery
242+ class FakeHub :
243+ def load (self , weights ):
244+ raise RuntimeError ("network unreachable" )
245+ with pytest .raises (RuntimeError , match = "network unreachable" ):
246+ _load_enformer_with_tfhub_recovery (FakeHub (), "https://tfhub.dev/enformer" )
247+
248+
249+ class TestIGVFallbackViaHuggingFace :
250+ """When stdlib urllib fails (SSL MITM), ``_ensure_igv_local`` must
251+ try the HuggingFace mirror as a second fallback before giving up."""
252+
253+ def test_hf_fallback_when_cdn_fails (self , tmp_path , monkeypatch ):
254+ from chorus .analysis import _igv_report
255+
256+ monkeypatch .setattr (_igv_report , "_IGV_LOCAL" , tmp_path / "igv.min.js" )
257+
258+ # CDN path raises SSL error (stdlib urllib on MITM'd network)
259+ def fake_download_with_resume (url , dest , ** kw ):
260+ import ssl
261+ raise ssl .SSLError ("CERTIFICATE_VERIFY_FAILED" )
262+ monkeypatch .setattr (
263+ "chorus.utils.http.download_with_resume" , fake_download_with_resume
264+ )
265+
266+ # HF path succeeds — writes to the local_dir param
267+ hf_calls = []
268+ def fake_hf_hub_download (repo_id , filename , repo_type , local_dir , ** kw ):
269+ hf_calls .append ((repo_id , filename , repo_type , local_dir ))
270+ target = Path (local_dir ) / "igv.min.js"
271+ target .parent .mkdir (parents = True , exist_ok = True )
272+ target .write_text ("// fake igv.min.js payload " * 50 )
273+ return str (target )
274+
275+ import huggingface_hub as _hfh
276+ monkeypatch .setattr (_hfh , "hf_hub_download" , fake_hf_hub_download )
277+
278+ result = _igv_report ._ensure_igv_local ()
279+ assert result is not None
280+ assert result == tmp_path / "igv.min.js"
281+ assert result .exists ()
282+ assert len (hf_calls ) == 1
283+ assert hf_calls [0 ][0 ] == "lucapinello/chorus-backgrounds"
284+ assert hf_calls [0 ][1 ] == "igv.min.js"
285+
286+ def test_returns_none_when_both_fail (self , tmp_path , monkeypatch ):
287+ from chorus .analysis import _igv_report
288+
289+ monkeypatch .setattr (_igv_report , "_IGV_LOCAL" , tmp_path / "igv.min.js" )
290+ monkeypatch .setattr (
291+ "chorus.utils.http.download_with_resume" ,
292+ lambda url , dest , ** kw : (_ for _ in ()).throw (RuntimeError ("cdn fail" )),
293+ )
294+ import huggingface_hub as _hfh
295+ monkeypatch .setattr (
296+ _hfh , "hf_hub_download" ,
297+ lambda * a , ** kw : (_ for _ in ()).throw (FileNotFoundError ("hf fail" )),
298+ )
299+
300+ assert _igv_report ._ensure_igv_local () is None
301+
302+
303+ class TestChorusImportPatchesPath :
304+ """Importing chorus must prepend the Python env's bin/ to PATH so
305+ coolbox subprocess calls find bgzip/tabix when nbconvert is
306+ launched outside ``mamba activate``."""
307+
308+ def test_env_bin_on_path_after_import (self ):
309+ import os
310+ import sys
311+ import chorus # noqa: F401
312+ env_bin = os .path .dirname (sys .executable )
313+ assert env_bin in os .environ ["PATH" ].split (os .pathsep ), (
314+ f"{ env_bin } must be on PATH after importing chorus so coolbox "
315+ f"can find bgzip/tabix"
316+ )
0 commit comments