3333
3434# ===== Fixtures =====
3535
36+
3637@pytest .fixture
3738def temp_dir ():
3839 """Create a temporary directory for tests."""
@@ -87,8 +88,9 @@ def extension_dir(temp_dir, valid_manifest_data):
8788
8889 # Write manifest
8990 import yaml
91+
9092 manifest_path = ext_dir / "extension.yml"
91- with open (manifest_path , 'w' ) as f :
93+ with open (manifest_path , "w" ) as f :
9294 yaml .dump (valid_manifest_data , f )
9395
9496 # Create commands directory
@@ -124,6 +126,7 @@ def project_dir(temp_dir):
124126
125127# ===== ExtensionManifest Tests =====
126128
129+
127130class TestExtensionManifest :
128131 """Test ExtensionManifest validation and parsing."""
129132
@@ -144,7 +147,7 @@ def test_missing_required_field(self, temp_dir):
144147 import yaml
145148
146149 manifest_path = temp_dir / "extension.yml"
147- with open (manifest_path , 'w' ) as f :
150+ with open (manifest_path , "w" ) as f :
148151 yaml .dump ({"schema_version" : "1.0" }, f ) # Missing 'extension'
149152
150153 with pytest .raises (ValidationError , match = "Missing required field" ):
@@ -157,7 +160,7 @@ def test_invalid_extension_id(self, temp_dir, valid_manifest_data):
157160 valid_manifest_data ["extension" ]["id" ] = "Invalid_ID" # Uppercase not allowed
158161
159162 manifest_path = temp_dir / "extension.yml"
160- with open (manifest_path , 'w' ) as f :
163+ with open (manifest_path , "w" ) as f :
161164 yaml .dump (valid_manifest_data , f )
162165
163166 with pytest .raises (ValidationError , match = "Invalid extension ID" ):
@@ -170,7 +173,7 @@ def test_invalid_version(self, temp_dir, valid_manifest_data):
170173 valid_manifest_data ["extension" ]["version" ] = "invalid"
171174
172175 manifest_path = temp_dir / "extension.yml"
173- with open (manifest_path , 'w' ) as f :
176+ with open (manifest_path , "w" ) as f :
174177 yaml .dump (valid_manifest_data , f )
175178
176179 with pytest .raises (ValidationError , match = "Invalid version" ):
@@ -183,7 +186,7 @@ def test_invalid_command_name(self, temp_dir, valid_manifest_data):
183186 valid_manifest_data ["provides" ]["commands" ][0 ]["name" ] = "invalid-name"
184187
185188 manifest_path = temp_dir / "extension.yml"
186- with open (manifest_path , 'w' ) as f :
189+ with open (manifest_path , "w" ) as f :
187190 yaml .dump (valid_manifest_data , f )
188191
189192 with pytest .raises (ValidationError , match = "Invalid command name" ):
@@ -196,7 +199,7 @@ def test_no_commands(self, temp_dir, valid_manifest_data):
196199 valid_manifest_data ["provides" ]["commands" ] = []
197200
198201 manifest_path = temp_dir / "extension.yml"
199- with open (manifest_path , 'w' ) as f :
202+ with open (manifest_path , "w" ) as f :
200203 yaml .dump (valid_manifest_data , f )
201204
202205 with pytest .raises (ValidationError , match = "must provide at least one command" ):
@@ -214,6 +217,7 @@ def test_manifest_hash(self, extension_dir):
214217
215218# ===== ExtensionRegistry Tests =====
216219
220+
217221class TestExtensionRegistry :
218222 """Test ExtensionRegistry operations."""
219223
@@ -281,6 +285,7 @@ def test_registry_persistence(self, temp_dir):
281285
282286# ===== ExtensionManager Tests =====
283287
288+
284289class TestExtensionManager :
285290 """Test ExtensionManager installation and removal."""
286291
@@ -309,7 +314,7 @@ def test_install_from_directory(self, extension_dir, project_dir):
309314 manifest = manager .install_from_directory (
310315 extension_dir ,
311316 "0.1.0" ,
312- register_commands = False # Skip command registration for now
317+ register_commands = False , # Skip command registration for now
313318 )
314319
315320 assert manifest .id == "test-ext"
@@ -330,7 +335,9 @@ def test_install_duplicate(self, extension_dir, project_dir):
330335
331336 # Try to install again
332337 with pytest .raises (ExtensionError , match = "already installed" ):
333- manager .install_from_directory (extension_dir , "0.1.0" , register_commands = False )
338+ manager .install_from_directory (
339+ extension_dir , "0.1.0" , register_commands = False
340+ )
334341
335342 def test_remove_extension (self , extension_dir , project_dir ):
336343 """Test removing an installed extension."""
@@ -399,14 +406,15 @@ def test_config_backup_on_remove(self, extension_dir, project_dir):
399406
400407# ===== CommandRegistrar Tests =====
401408
409+
402410class TestCommandRegistrar :
403411 """Test CommandRegistrar command registration."""
404412
405413 def test_kiro_cli_agent_config_present (self ):
406- """Kiro CLI should be mapped to .kiro/prompts and legacy q removed ."""
414+ """Kiro CLI should be mapped to .kiro/prompts and q should be present ."""
407415 assert "kiro-cli" in CommandRegistrar .AGENT_CONFIGS
408416 assert CommandRegistrar .AGENT_CONFIGS ["kiro-cli" ]["dir" ] == ".kiro/prompts"
409- assert "q" not in CommandRegistrar .AGENT_CONFIGS
417+ assert "q" in CommandRegistrar .AGENT_CONFIGS
410418
411419 def test_parse_frontmatter_valid (self ):
412420 """Test parsing valid YAML frontmatter."""
@@ -440,10 +448,7 @@ def test_parse_frontmatter_no_frontmatter(self):
440448
441449 def test_render_frontmatter (self ):
442450 """Test rendering frontmatter to YAML."""
443- frontmatter = {
444- "description" : "Test command" ,
445- "tools" : ["tool1" , "tool2" ]
446- }
451+ frontmatter = {"description" : "Test command" , "tools" : ["tool1" , "tool2" ]}
447452
448453 registrar = CommandRegistrar ()
449454 output = registrar .render_frontmatter (frontmatter )
@@ -463,9 +468,7 @@ def test_register_commands_for_claude(self, extension_dir, project_dir):
463468
464469 registrar = CommandRegistrar ()
465470 registered = registrar .register_commands_for_claude (
466- manifest ,
467- extension_dir ,
468- project_dir
471+ manifest , extension_dir , project_dir
469472 )
470473
471474 assert len (registered ) == 1
@@ -510,18 +513,22 @@ def test_command_with_aliases(self, project_dir, temp_dir):
510513 },
511514 }
512515
513- with open (ext_dir / "extension.yml" , 'w' ) as f :
516+ with open (ext_dir / "extension.yml" , "w" ) as f :
514517 yaml .dump (manifest_data , f )
515518
516519 (ext_dir / "commands" ).mkdir ()
517- (ext_dir / "commands" / "cmd.md" ).write_text ("---\n description: Test\n ---\n \n Test" )
520+ (ext_dir / "commands" / "cmd.md" ).write_text (
521+ "---\n description: Test\n ---\n \n Test"
522+ )
518523
519524 claude_dir = project_dir / ".claude" / "commands"
520525 claude_dir .mkdir (parents = True )
521526
522527 manifest = ExtensionManifest (ext_dir / "extension.yml" )
523528 registrar = CommandRegistrar ()
524- registered = registrar .register_commands_for_claude (manifest , ext_dir , project_dir )
529+ registered = registrar .register_commands_for_claude (
530+ manifest , ext_dir , project_dir
531+ )
525532
526533 assert len (registered ) == 2
527534 assert "speckit.alias.cmd" in registered
@@ -570,7 +577,9 @@ def test_copilot_companion_prompt_created(self, extension_dir, project_dir):
570577 )
571578
572579 # Verify companion .prompt.md file exists
573- prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
580+ prompt_file = (
581+ project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
582+ )
574583 assert prompt_file .exists ()
575584
576585 # Verify content has correct agent frontmatter
@@ -647,6 +656,7 @@ def test_non_copilot_agent_no_companion_file(self, extension_dir, project_dir):
647656
648657# ===== Utility Function Tests =====
649658
659+
650660class TestVersionSatisfies :
651661 """Test version_satisfies utility function."""
652662
@@ -675,6 +685,7 @@ def test_version_satisfies_invalid(self):
675685
676686# ===== Integration Tests =====
677687
688+
678689class TestIntegration :
679690 """Integration tests for complete workflows."""
680691
@@ -686,11 +697,7 @@ def test_full_install_and_remove_workflow(self, extension_dir, project_dir):
686697 manager = ExtensionManager (project_dir )
687698
688699 # Install
689- manager .install_from_directory (
690- extension_dir ,
691- "0.1.0" ,
692- register_commands = True
693- )
700+ manager .install_from_directory (extension_dir , "0.1.0" , register_commands = True )
694701
695702 # Verify installation
696703 assert manager .registry .is_installed ("test-ext" )
@@ -707,8 +714,7 @@ def test_full_install_and_remove_workflow(self, extension_dir, project_dir):
707714 registered_commands = metadata ["registered_commands" ]
708715 # Check that the command is registered for at least one agent
709716 assert any (
710- "speckit.test.hello" in cmds
711- for cmds in registered_commands .values ()
717+ "speckit.test.hello" in cmds for cmds in registered_commands .values ()
712718 )
713719
714720 # Remove
@@ -734,7 +740,9 @@ def test_copilot_cleanup_removes_prompt_files(self, extension_dir, project_dir):
734740
735741 # Verify files exist before cleanup
736742 agent_file = agents_dir / "speckit.test.hello.agent.md"
737- prompt_file = project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
743+ prompt_file = (
744+ project_dir / ".github" / "prompts" / "speckit.test.hello.prompt.md"
745+ )
738746 assert agent_file .exists ()
739747 assert prompt_file .exists ()
740748
@@ -773,17 +781,23 @@ def test_multiple_extensions(self, temp_dir, project_dir):
773781 },
774782 }
775783
776- with open (ext_dir / "extension.yml" , 'w' ) as f :
784+ with open (ext_dir / "extension.yml" , "w" ) as f :
777785 yaml .dump (manifest_data , f )
778786
779787 (ext_dir / "commands" ).mkdir ()
780- (ext_dir / "commands" / "cmd.md" ).write_text ("---\n description: Test\n ---\n Test" )
788+ (ext_dir / "commands" / "cmd.md" ).write_text (
789+ "---\n description: Test\n ---\n Test"
790+ )
781791
782792 manager = ExtensionManager (project_dir )
783793
784794 # Install both
785- manager .install_from_directory (temp_dir / "ext1" , "0.1.0" , register_commands = False )
786- manager .install_from_directory (temp_dir / "ext2" , "0.1.0" , register_commands = False )
795+ manager .install_from_directory (
796+ temp_dir / "ext1" , "0.1.0" , register_commands = False
797+ )
798+ manager .install_from_directory (
799+ temp_dir / "ext2" , "0.1.0" , register_commands = False
800+ )
787801
788802 # Verify both installed
789803 installed = manager .list_installed ()
@@ -1235,6 +1249,7 @@ def test_clear_cache(self, temp_dir):
12351249
12361250# ===== CatalogEntry Tests =====
12371251
1252+
12381253class TestCatalogEntry :
12391254 """Test CatalogEntry dataclass."""
12401255
@@ -1254,6 +1269,7 @@ def test_catalog_entry_creation(self):
12541269
12551270# ===== Catalog Stack Tests =====
12561271
1272+
12571273class TestCatalogStack :
12581274 """Test multi-catalog stack support."""
12591275
@@ -1421,7 +1437,9 @@ def test_load_catalog_config_missing_file(self, temp_dir):
14211437 project_dir = self ._make_project (temp_dir )
14221438 catalog = ExtensionCatalog (project_dir )
14231439
1424- result = catalog ._load_catalog_config (project_dir / ".specify" / "nonexistent.yml" )
1440+ result = catalog ._load_catalog_config (
1441+ project_dir / ".specify" / "nonexistent.yml"
1442+ )
14251443 assert result is None
14261444
14271445 def test_load_catalog_config_localhost_allowed (self , temp_dir ):
@@ -1487,13 +1505,20 @@ def test_merge_conflict_higher_priority_wins(self, temp_dir):
14871505 catalog .cache_dir .mkdir (parents = True , exist_ok = True )
14881506 catalog .cache_file .write_text (json .dumps (primary_data ))
14891507 catalog .cache_metadata_file .write_text (
1490- json .dumps ({"cached_at" : datetime .now (timezone .utc ).isoformat (), "catalog_url" : "http://test.com" })
1508+ json .dumps (
1509+ {
1510+ "cached_at" : datetime .now (timezone .utc ).isoformat (),
1511+ "catalog_url" : "http://test.com" ,
1512+ }
1513+ )
14911514 )
14921515
14931516 # Write secondary cache (URL-hash-based) with jira v1.0.0 (should lose)
14941517 import hashlib
14951518
1496- url_hash = hashlib .sha256 (ExtensionCatalog .COMMUNITY_CATALOG_URL .encode ()).hexdigest ()[:16 ]
1519+ url_hash = hashlib .sha256 (
1520+ ExtensionCatalog .COMMUNITY_CATALOG_URL .encode ()
1521+ ).hexdigest ()[:16 ]
14971522 secondary_cache = catalog .cache_dir / f"catalog-{ url_hash } .json"
14981523 secondary_meta = catalog .cache_dir / f"catalog-{ url_hash } -metadata.json"
14991524 secondary_data = {
@@ -1515,7 +1540,12 @@ def test_merge_conflict_higher_priority_wins(self, temp_dir):
15151540 }
15161541 secondary_cache .write_text (json .dumps (secondary_data ))
15171542 secondary_meta .write_text (
1518- json .dumps ({"cached_at" : datetime .now (timezone .utc ).isoformat (), "catalog_url" : ExtensionCatalog .COMMUNITY_CATALOG_URL })
1543+ json .dumps (
1544+ {
1545+ "cached_at" : datetime .now (timezone .utc ).isoformat (),
1546+ "catalog_url" : ExtensionCatalog .COMMUNITY_CATALOG_URL ,
1547+ }
1548+ )
15191549 )
15201550
15211551 results = catalog .search ()
0 commit comments