From d90ad2511bdc6b8eca29d32c35022d919ec8bbbf Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Fri, 31 Jul 2026 19:40:57 +0500 Subject: [PATCH 1/2] fix: narrow exception in extension registration fallback from Exception to specific types Bare 'except Exception' silently swallows all errors. Narrow to (TypeError, ValueError, KeyError) which are the realistic failure modes in extension registration. --- src/specify_cli/presets/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index de4116228e..e52dde302a 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -1727,7 +1727,7 @@ def record_written(written: Dict[str, List[str]]) -> None: ) record_written(written) registered = True - except Exception: + except (TypeError, ValueError, KeyError): # Extension registration failed; fall back to # generic path-based registration below. pass From 51c3e01ef307caeaf80735c5c6436ef3223e1970 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Wed, 12 Aug 2026 14:46:02 +0500 Subject: [PATCH 2/2] fix: narrow exception in extension registration fallback from Exception to specific types Catch (TypeError, ValueError, KeyError, ValidationError) instead of bare Exception. ExtensionManifest raises ValidationError (not ValueError) for malformed/unreadable manifests, so it must be caught to preserve the fallback to generic path-based registration. Add regression test verifying ValidationError during manifest load is silently skipped. Co-authored-by: GitHub Copilot (model: mimo-v2.5-free, supervised) --- src/specify_cli/presets/__init__.py | 4 ++-- tests/test_presets.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index e52dde302a..8ad397be9f 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -1709,7 +1709,7 @@ def record_written(written: Dict[str, List[str]]) -> None: ext_manifest_path = ext_dir / "extension.yml" if ext_manifest_path.exists(): try: - from ..extensions import ExtensionManifest + from ..extensions import ExtensionManifest, ValidationError ext_manifest = ExtensionManifest(ext_manifest_path) # Filter to only the command being reconciled matching_cmds = [ @@ -1727,7 +1727,7 @@ def record_written(written: Dict[str, List[str]]) -> None: ) record_written(written) registered = True - except (TypeError, ValueError, KeyError): + except (TypeError, ValueError, KeyError, ValidationError): # Extension registration failed; fall back to # generic path-based registration below. pass diff --git a/tests/test_presets.py b/tests/test_presets.py index dbf6ac4ccb..2230d35041 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -1399,6 +1399,25 @@ def test_resolve_extension_command_via_manifest_skips_oserror_manifests(self, pr assert result is None, "OSError during manifest load must be silently skipped" + def test_resolve_extension_command_via_manifest_skips_validation_error(self, project_dir): + """resolve_extension_command_via_manifest skips extensions whose manifest raises ValidationError.""" + import unittest.mock as mock + + ext_dir = project_dir / ".specify" / "extensions" / "bad-ext" + cmd_dir = ext_dir / "commands" + cmd_dir.mkdir(parents=True) + (cmd_dir / "mycmd.md").write_text("---\ndescription: d\n---\n\nbody\n") + # Write a manifest with missing required fields to trigger ValidationError + (ext_dir / "extension.yml").write_text( + "schema_version: '1.0'\n" + "extension:\n id: bad-ext\n" + ) + + resolver = PresetResolver(project_dir) + result = resolver.resolve_extension_command_via_manifest("speckit.bad-ext.mycmd") + + assert result is None, "ValidationError during manifest load must be silently skipped" + class TestExtensionPriorityResolution: """Test extension priority resolution with registered and unregistered extensions."""