Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/specify_cli/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,15 @@ def _resolve_event_command_argv(
"""
from .integrations.base import IntegrationBase

content = template_path.read_text(encoding="utf-8")
try:
content = template_path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
# An unreadable or undecodable template cannot declare a runnable
# script. Degrade to "no argv" like every other failure in this
# resolver (missing frontmatter, malformed YAML, absent scripts)
# instead of leaking a raw traceback through
# resolve_and_run_event_command.
return None
m = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not m:
return None
Expand Down
47 changes: 47 additions & 0 deletions tests/integrations/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,53 @@ def test_unparseable_script_command_returns_none(self, tmp_path):
)

argv = _resolve_event_command_argv(cmd_dir / "boot.md", tmp_path, None)

assert argv is None

def test_unreadable_template_returns_none(self, tmp_path):
"""A command template that cannot be read must resolve to no argv.

Every other failure inside ``_resolve_event_command_argv`` — missing
frontmatter, malformed YAML, absent scripts — degrades to ``None`` so
the dispatcher treats the command as declaring no runnable script.
The initial ``read_text`` was the one step outside that boundary: a
non-UTF-8 template raised a raw ``UnicodeDecodeError`` through
``resolve_and_run_event_command`` and out of ``specify event run``.
"""
from specify_cli.events import _resolve_event_command_argv

cmd_dir = tmp_path / ".specify" / "templates" / "commands"
cmd_dir.mkdir(parents=True)
(cmd_dir / "boot.md").write_bytes(
b"---\ndescription: \"B\xff\xfeoot\"\n---\nBody\n"
)

argv = _resolve_event_command_argv(cmd_dir / "boot.md", tmp_path, None)
assert argv is None

def test_permission_denied_template_returns_none(self, tmp_path, monkeypatch):
"""The same boundary must cover ``OSError`` (e.g. permission denied).

Mocked rather than chmod-based so the case also holds under
privileged CI where permission bits are not enforced.
"""
from specify_cli.events import _resolve_event_command_argv

cmd_dir = tmp_path / ".specify" / "templates" / "commands"
cmd_dir.mkdir(parents=True)
template = cmd_dir / "boot.md"
template.write_text("---\ndescription: Boot\n---\nBody\n")

original_read_text = Path.read_text

def failing_read_text(self_path, *args, **kwargs):
if self_path == template:
raise PermissionError(13, "Permission denied")
return original_read_text(self_path, *args, **kwargs)

monkeypatch.setattr(Path, "read_text", failing_read_text)

argv = _resolve_event_command_argv(template, tmp_path, None)
assert argv is None

def test_ps_variant_prefixed_with_powershell_launcher(self, tmp_path):
Expand Down