@@ -551,11 +551,99 @@ def test_strict_mode(self):
551551 Skill .from_content (content , strict = True )
552552
553553
554+ class TestSkillFromUrl :
555+ """Tests for Skill.from_url."""
556+
557+ _SKILL_MODULE = "strands.vended_plugins.skills.skill"
558+ _SAMPLE_CONTENT = "---\n name: my-skill\n description: A remote skill\n ---\n Remote instructions.\n "
559+
560+ def _mock_urlopen (self , content ):
561+ """Create a mock urlopen context manager returning the given content."""
562+ from unittest .mock import MagicMock
563+
564+ mock_response = MagicMock ()
565+ mock_response .read .return_value = content .encode ("utf-8" )
566+ mock_response .__enter__ = MagicMock (return_value = mock_response )
567+ mock_response .__exit__ = MagicMock (return_value = False )
568+ return mock_response
569+
570+ def test_from_url_returns_skill (self ):
571+ """Test loading a skill from a URL returns a single Skill."""
572+ from unittest .mock import patch
573+
574+ mock_response = self ._mock_urlopen (self ._SAMPLE_CONTENT )
575+ with patch (f"{ self ._SKILL_MODULE } .urllib.request.urlopen" , return_value = mock_response ):
576+ skill = Skill .from_url ("https://raw.githubusercontent.com/org/repo/main/SKILL.md" )
577+
578+ assert isinstance (skill , Skill )
579+ assert skill .name == "my-skill"
580+ assert skill .description == "A remote skill"
581+ assert "Remote instructions." in skill .instructions
582+ assert skill .path is None
583+
584+ def test_from_url_invalid_url_raises (self ):
585+ """Test that a non-HTTPS URL raises ValueError."""
586+ with pytest .raises (ValueError , match = "not a valid HTTPS URL" ):
587+ Skill .from_url ("./local-path" )
588+
589+ def test_from_url_http_rejected (self ):
590+ """Test that http:// URLs are rejected."""
591+ with pytest .raises (ValueError , match = "not a valid HTTPS URL" ):
592+ Skill .from_url ("http://example.com/SKILL.md" )
593+
594+ def test_from_url_http_error_raises (self ):
595+ """Test that HTTP errors propagate as RuntimeError."""
596+ import urllib .error
597+ from unittest .mock import patch
598+
599+ with patch (
600+ f"{ self ._SKILL_MODULE } .urllib.request.urlopen" ,
601+ side_effect = urllib .error .HTTPError (
602+ url = "https://example.com" , code = 404 , msg = "Not Found" , hdrs = None , fp = None
603+ ),
604+ ):
605+ with pytest .raises (RuntimeError , match = "HTTP 404" ):
606+ Skill .from_url ("https://example.com/SKILL.md" )
607+
608+ def test_from_url_network_error_raises (self ):
609+ """Test that network errors propagate as RuntimeError."""
610+ import urllib .error
611+ from unittest .mock import patch
612+
613+ with patch (
614+ f"{ self ._SKILL_MODULE } .urllib.request.urlopen" ,
615+ side_effect = urllib .error .URLError ("Connection refused" ),
616+ ):
617+ with pytest .raises (RuntimeError , match = "failed to fetch" ):
618+ Skill .from_url ("https://example.com/SKILL.md" )
619+
620+ def test_from_url_strict_mode (self ):
621+ """Test that strict mode is forwarded to from_content."""
622+ from unittest .mock import patch
623+
624+ bad_content = "---\n name: BAD_NAME\n description: Bad\n ---\n Body."
625+
626+ with patch (f"{ self ._SKILL_MODULE } .urllib.request.urlopen" , return_value = self ._mock_urlopen (bad_content )):
627+ with pytest .raises (ValueError ):
628+ Skill .from_url ("https://example.com/SKILL.md" , strict = True )
629+
630+ def test_from_url_invalid_content_raises (self ):
631+ """Test that non-SKILL.md content (e.g. HTML page) raises ValueError."""
632+ from unittest .mock import patch
633+
634+ html_content = "<html><body>Not a SKILL.md</body></html>"
635+
636+ with patch (f"{ self ._SKILL_MODULE } .urllib.request.urlopen" , return_value = self ._mock_urlopen (html_content )):
637+ with pytest .raises (ValueError , match = "frontmatter" ):
638+ Skill .from_url ("https://example.com/SKILL.md" )
639+
640+
554641class TestSkillClassmethods :
555642 """Tests for Skill classmethod existence."""
556643
557644 def test_skill_classmethods_exist (self ):
558- """Test that Skill has from_file, from_content, and from_directory classmethods."""
645+ """Test that Skill has from_file, from_content, from_directory, and from_url classmethods."""
559646 assert callable (getattr (Skill , "from_file" , None ))
560647 assert callable (getattr (Skill , "from_content" , None ))
561648 assert callable (getattr (Skill , "from_directory" , None ))
649+ assert callable (getattr (Skill , "from_url" , None ))
0 commit comments