@@ -562,6 +562,86 @@ def test_resolve_auto_routes_to_source(
562562 assert version == Version ("2.0" )
563563
564564
565+ def test_cache_resolution_stores_immutable_tuple (tmp_context : WorkContext ) -> None :
566+ """cache_resolution() stores an immutable tuple, not the original list."""
567+ resolver = BootstrapRequirementResolver (tmp_context )
568+ req = Requirement ("mypkg>=1.0" )
569+ original = [("https://example.com/mypkg-1.0.tar.gz" , Version ("1.0" ))]
570+
571+ resolver .cache_resolution (req , pre_built = False , result = original )
572+ cached = resolver .get_cached_resolution (req , pre_built = False )
573+
574+ # Cached value should be a tuple
575+ assert isinstance (cached , tuple )
576+
577+ # Mutating the original list must not affect the cache
578+ original .append (("https://example.com/mypkg-2.0.tar.gz" , Version ("2.0" )))
579+ cached_after = resolver .get_cached_resolution (req , pre_built = False )
580+ assert cached_after is not None
581+ assert len (cached_after ) == 1
582+
583+
584+ def test_get_cached_resolution_returns_immutable (tmp_context : WorkContext ) -> None :
585+ """get_cached_resolution() returns a tuple that cannot be mutated."""
586+ resolver = BootstrapRequirementResolver (tmp_context )
587+ req = Requirement ("mypkg>=1.0" )
588+
589+ resolver .cache_resolution (
590+ req ,
591+ pre_built = False ,
592+ result = [("https://example.com/mypkg-1.0.tar.gz" , Version ("1.0" ))],
593+ )
594+ cached = resolver .get_cached_resolution (req , pre_built = False )
595+ assert cached is not None
596+
597+ with pytest .raises (AttributeError ):
598+ cached .append (("https://example.com/bad.tar.gz" , Version ("2.0" ))) # type: ignore[attr-defined, union-attr]
599+
600+ with pytest .raises (TypeError ):
601+ cached [0 ] = ("https://example.com/bad.tar.gz" , Version ("2.0" )) # type: ignore[index]
602+
603+
604+ @patch ("fromager.resolver.find_all_matching_from_provider" )
605+ def test_resolve_cache_returns_independent_lists (
606+ mock_resolve : MagicMock ,
607+ tmp_context : WorkContext ,
608+ ) -> None :
609+ """resolve() returns independent list copies from the cache, not shared references."""
610+ req = Requirement ("mypkg>=1.0" )
611+ mock_resolve .return_value = [
612+ ("https://example.com/mypkg-2.0.tar.gz" , Version ("2.0" )),
613+ ("https://example.com/mypkg-1.5.tar.gz" , Version ("1.5" )),
614+ ]
615+
616+ resolver = BootstrapRequirementResolver (tmp_context )
617+
618+ # First call populates cache
619+ results1 = resolver .resolve (
620+ req = req ,
621+ req_type = RequirementType .INSTALL ,
622+ parent_req = None ,
623+ pre_built = False ,
624+ return_all_versions = True ,
625+ )
626+
627+ # Mutate the returned list
628+ results1 .append (("https://example.com/injected.tar.gz" , Version ("9.9" )))
629+
630+ # Second call should return clean cached data, unaffected by mutation
631+ results2 = resolver .resolve (
632+ req = req ,
633+ req_type = RequirementType .INSTALL ,
634+ parent_req = None ,
635+ pre_built = False ,
636+ return_all_versions = True ,
637+ )
638+
639+ assert len (results2 ) == 2
640+ assert results1 is not results2
641+ # Only called once — second call used cache
642+ mock_resolve .assert_called_once ()
643+
644+
565645@patch ("fromager.resolver.find_all_matching_from_provider" )
566646def test_resolve_prebuilt_after_source_uses_separate_cache (
567647 mock_resolve : MagicMock ,
0 commit comments