@@ -374,19 +374,21 @@ def test_windows_no_follow_open_rejects_canonical_path_mismatch(
374374 _open_regular_file_from_windows_handle (source )
375375
376376
377- def test_resolve_zip_file (tmp_path : Path ) -> None :
378- """Resolving a .zip file extracts and returns the extract dir."""
377+ @pytest .mark .parametrize ("extension" , [".zip" , ".ZIP" , ".ZiP" ])
378+ def test_resolve_zip_file (tmp_path : Path , extension : str ) -> None :
379+ """ZIP extensions are recognized regardless of case for local inputs."""
379380 import zipfile
380381
381382 (tmp_path / "SKILL.md" ).write_text ("# Skill" , encoding = "utf-8" )
382- zip_path = tmp_path / "skill.zip "
383+ zip_path = tmp_path / f "skill{ extension } "
383384 with zipfile .ZipFile (zip_path , "w" ) as zf :
384385 zf .write (tmp_path / "SKILL.md" , "SKILL.md" )
385386 handler = InputHandler ()
386387 try :
387388 resolved , source_type = handler .resolve (str (zip_path ))
388389 assert resolved .is_dir ()
389390 assert source_type == "zip"
391+ assert (resolved / "SKILL.md" ).read_text (encoding = "utf-8" ) == "# Skill"
390392 finally :
391393 handler .cleanup ()
392394
@@ -601,6 +603,45 @@ def serve(request: httpx.Request) -> httpx.Response:
601603 handler .cleanup ()
602604
603605
606+ @pytest .mark .parametrize ("budgeted" , [False , True ], ids = ["direct" , "workflow-budget" ])
607+ @pytest .mark .parametrize ("extension" , [".zip" , ".ZIP" , ".ZiP" ])
608+ def test_file_url_download_extracts_zip_regardless_of_extension_case (
609+ monkeypatch : pytest .MonkeyPatch , extension : str , budgeted : bool
610+ ) -> None :
611+ """ZIP filenames trigger extraction even without a ZIP content type."""
612+ import io
613+ import zipfile
614+
615+ skill = b"---\n name: demo\n description: demo\n ---\n # Demo\n "
616+ archive = io .BytesIO ()
617+ with zipfile .ZipFile (archive , "w" ) as zf :
618+ zf .writestr ("SKILL.md" , skill )
619+ url = f"https://raw.githubusercontent.com/org/repo/main/skill{ extension } "
620+ requested : list [str ] = []
621+
622+ def serve (request : httpx .Request ) -> httpx .Response :
623+ requested .append (str (request .url ))
624+ return httpx .Response (
625+ 200 , content = archive .getvalue (), headers = {"content-type" : "application/octet-stream" }
626+ )
627+
628+ real_client = httpx .Client
629+ monkeypatch .setattr (
630+ "skillspector.input_handler.httpx.Client" ,
631+ lambda * args , ** kwargs : real_client (* args , transport = httpx .MockTransport (serve ), ** kwargs ),
632+ )
633+ monkeypatch .setattr ("skillspector.input_handler._is_private_ip" , lambda _host : False )
634+ handler = InputHandler (transitive_budget = WorkflowResourceBudget () if budgeted else None )
635+ try :
636+ resolved , source_type = handler .resolve (url )
637+
638+ assert source_type == "url"
639+ assert requested == [url ]
640+ assert (resolved / "SKILL.md" ).read_bytes () == skill
641+ finally :
642+ handler .cleanup ()
643+
644+
604645def test_validate_url_host_scp_extracts_github () -> None :
605646 """_validate_url_host extracts 'github.com' from an scp-style URL."""
606647 with patch ("skillspector.input_handler._is_private_ip" , return_value = False ):
0 commit comments