@@ -318,6 +318,81 @@ def fetch(source, _refresh):
318318 assert catalog .get_pack_info ("sample" , "1.0.0" ) is None
319319
320320
321+ @pytest .mark .parametrize (
322+ "bad_payload, error" ,
323+ [
324+ (_duplicate_release_json , "duplicate JSON key" ),
325+ (lambda : b'{"schema_version": "1.0", "presets": []}' , "Invalid preset catalog format" ),
326+ (lambda : b'{"schema_version":' , "invalid JSON" ),
327+ ],
328+ )
329+ def test_invalid_discovery_catalog_cannot_delegate_install (
330+ project_dir , bad_payload , error
331+ ):
332+ high_url = "https://example.com/discovery.json"
333+ low_url = "https://example.com/trusted.json"
334+ sources = [
335+ PresetCatalogEntry (high_url , "discovery" , 1 , False ),
336+ PresetCatalogEntry (low_url , "trusted" , 2 , True ),
337+ ]
338+ old_bytes = _archive ()
339+ lower = json .dumps ({
340+ "schema_version" : "1.0" ,
341+ "presets" : {"sample" : _entry (old_bytes )},
342+ }).encode ()
343+ opened : list [str ] = []
344+
345+ def open_url (_self , url , ** _kwargs ):
346+ opened .append (url )
347+ data = {
348+ high_url : bad_payload (),
349+ low_url : lower ,
350+ OLD_URL : old_bytes ,
351+ }
352+ return _response (data [url ], url )
353+
354+ with (
355+ patch .object (PresetCatalog , "get_active_catalogs" , return_value = sources ),
356+ patch .object (PresetCatalog , "_open_url" , open_url ),
357+ patch .object (Path , "cwd" , return_value = project_dir ),
358+ patch ("specify_cli.get_speckit_version" , return_value = "1.0.0" ),
359+ ):
360+ with pytest .raises (PresetError , match = error ):
361+ PresetCatalog (project_dir ).get_pack_info ("sample" , "1.0.0" )
362+ result = CliRunner ().invoke (
363+ app , ["preset" , "add" , "sample" , "--version" , "1.0.0" ]
364+ )
365+ info = CliRunner ().invoke (app , ["preset" , "info" , "sample" ])
366+ search = CliRunner ().invoke (app , ["preset" , "search" , "sample" ])
367+ assert result .exit_code == 1 , result .output
368+ assert error in result .output
369+ assert info .exit_code == 1 and error in info .output
370+ assert search .exit_code == 1 and error in search .output
371+ assert OLD_URL not in opened
372+ assert PresetManager (project_dir ).get_pack ("sample" ) is None
373+
374+
375+ def test_unreachable_high_priority_catalog_still_uses_lower_source (project_dir ):
376+ catalog = PresetCatalog (project_dir )
377+ sources = [
378+ PresetCatalogEntry ("https://example.com/unavailable.json" , "high" , 1 , False ),
379+ PresetCatalogEntry ("https://example.com/trusted.json" , "low" , 2 , True ),
380+ ]
381+
382+ def fetch (source , _refresh ):
383+ if source .name == "high" :
384+ raise PresetError ("Failed to fetch preset catalog: offline" )
385+ return {"presets" : {"sample" : _entry ()}}
386+
387+ with (
388+ patch .object (catalog , "get_active_catalogs" , return_value = sources ),
389+ patch .object (catalog , "_fetch_single_catalog" , side_effect = fetch ),
390+ ):
391+ selected = catalog .get_pack_info ("sample" , "1.0.0" )
392+ assert selected ["_catalog_name" ] == "low"
393+ assert selected ["_install_allowed" ] is True
394+
395+
321396def test_discovery_only_winner_does_not_delegate_exact_release (project_dir ):
322397 catalog = PresetCatalog (project_dir )
323398 sources = [
0 commit comments