diff --git a/src/specify_cli/presets/__init__.py b/src/specify_cli/presets/__init__.py index 6a359f5b29..c2fa6ec0df 100644 --- a/src/specify_cli/presets/__init__.py +++ b/src/specify_cli/presets/__init__.py @@ -107,7 +107,7 @@ def _constitution_provenance_matches_preset( return False try: metadata = json.loads(provenance.read_text(encoding="utf-8")) - except (json.JSONDecodeError, UnicodeDecodeError): + except (OSError, json.JSONDecodeError, UnicodeDecodeError): return False return ( isinstance(metadata, dict) diff --git a/tests/test_presets.py b/tests/test_presets.py index 317a1b437c..35f91aee2a 100644 --- a/tests/test_presets.py +++ b/tests/test_presets.py @@ -784,6 +784,30 @@ def test_list_by_priority_includes_disabled_when_requested(self, temp_dir): # ===== PresetManager Tests ===== +def test_unreadable_constitution_provenance_fails_closed( + project_dir, monkeypatch +): + from specify_cli.presets import _constitution_provenance_matches_preset + + memory = project_dir / ".specify" / "memory" / "constitution.md" + memory.parent.mkdir(parents=True, exist_ok=True) + memory.write_text("# Constitution\n", encoding="utf-8") + provenance = memory.parent / ".constitution-template.json" + provenance.write_text("{}", encoding="utf-8") + real_read_text = Path.read_text + + def unreadable(path, *args, **kwargs): + if path == provenance: + raise OSError("simulated read failure") + return real_read_text(path, *args, **kwargs) + + monkeypatch.setattr(Path, "read_text", unreadable) + + assert not _constitution_provenance_matches_preset( + project_dir, memory, "example", "1.0.0" + ) + + class TestPresetManager: """Test PresetManager installation and removal."""