From 3326575d64d0c7daacf400749b4742ba61c7c32d Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Wed, 12 Aug 2026 23:53:45 +0200 Subject: [PATCH] fix(presets): fail closed on unreadable provenance Assisted-by: GitHub Copilot (model: gpt-5.6-sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/specify_cli/presets/__init__.py | 2 +- tests/test_presets.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) 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."""