@@ -408,6 +408,20 @@ def test_tar_extension_archive_rejects_special_members(self, tmp_path):
408408 with pytest .raises (ValidationError , match = "Unsupported TAR member type" ):
409409 _install_extension_archive (object (), archive_path , "0.0.0" )
410410
411+ def test_extension_archive_error_message_lists_plain_tar (self , tmp_path ):
412+ """Unsupported extension archive message should include plain .tar."""
413+ from specify_cli import _install_extension_archive
414+ from specify_cli .extensions import ValidationError
415+
416+ archive_path = tmp_path / "not-an-archive.txt"
417+ archive_path .write_text ("not an archive" , encoding = "utf-8" )
418+
419+ with pytest .raises (
420+ ValidationError ,
421+ match = r"ZIP, \.tar, \.tar\.gz, or \.tgz" ,
422+ ):
423+ _install_extension_archive (object (), archive_path , "0.0.0" )
424+
411425 def test_extension_url_downloads_in_bounded_chunks (self , tmp_path , monkeypatch ):
412426 """URL extension downloads stream to disk instead of reading all bytes."""
413427 import urllib .request
@@ -464,6 +478,60 @@ def fake_install(manager, archive_path, speckit_version, priority=10):
464478 specify_cli .DOWNLOAD_CHUNK_BYTES ,
465479 ]
466480
481+ def test_extension_add_from_url_uses_shared_bounded_download_helper (self , tmp_path , monkeypatch ):
482+ """extension add --from should reuse the bounded URL download helper."""
483+ from types import SimpleNamespace
484+ from typer .testing import CliRunner
485+ import specify_cli
486+ from specify_cli import app
487+ from specify_cli .extensions import ExtensionManager
488+
489+ project = tmp_path / "url-extension-add"
490+ project .mkdir ()
491+ (project / ".specify" ).mkdir ()
492+
493+ captured = {}
494+
495+ def fake_download_and_install (manager , project_path , source_url , speckit_version , priority = 10 ):
496+ captured ["manager" ] = manager
497+ captured ["project_path" ] = project_path
498+ captured ["source_url" ] = source_url
499+ captured ["speckit_version" ] = speckit_version
500+ captured ["priority" ] = priority
501+ return SimpleNamespace (
502+ id = "url-ext" ,
503+ name = "URL Extension" ,
504+ version = "1.0.0" ,
505+ description = "Downloaded from URL" ,
506+ warnings = [],
507+ commands = [],
508+ )
509+
510+ monkeypatch .setattr (
511+ specify_cli ,
512+ "_download_and_install_extension_url" ,
513+ fake_download_and_install ,
514+ )
515+
516+ runner = CliRunner ()
517+ old_cwd = os .getcwd ()
518+ try :
519+ os .chdir (project )
520+ result = runner .invoke (
521+ app ,
522+ ["extension" , "add" , "url-ext" , "--from" , "https://example.com/url-ext.zip" ],
523+ catch_exceptions = False ,
524+ )
525+ finally :
526+ os .chdir (old_cwd )
527+
528+ assert result .exit_code == 0 , result .output
529+ assert isinstance (captured ["manager" ], ExtensionManager )
530+ assert captured ["project_path" ] == project
531+ assert captured ["source_url" ] == "https://example.com/url-ext.zip"
532+ assert captured ["speckit_version" ] == specify_cli .get_speckit_version ()
533+ assert captured ["priority" ] == 10
534+
467535
468536class TestForceExistingDirectory :
469537 """Tests for --force merging into an existing named directory."""
0 commit comments