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
7 changes: 6 additions & 1 deletion src/specify_cli/workflows/overlays/layer_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,12 @@ def collect(self, workflow_id: str, *, include_disabled: bool = False) -> list[L
workflow_overlay_dir, [f"Cannot enumerate overlays: {exc}"]
) from exc
for path in entries:
if not path.is_file() or path.suffix not in (".yml", ".yaml"):
# Match the extension case-insensitively. A hand-placed overlay
# named ``<id>.YML`` or ``<id>.Yaml`` was skipped here and never
# applied, with nothing reported to say the file had been ignored.
# Every other YAML discovery path in the package already lowercases
# before matching (engine.py:941, _commands.py:1325/1894/2128).
if not path.is_file() or path.suffix.lower() not in (".yml", ".yaml"):
continue
if path.is_symlink():
raise OverlayLoadError(path, ["Symlinked overlay files are not allowed"])
Expand Down
44 changes: 44 additions & 0 deletions tests/workflows/test_overlay_layer_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,50 @@ def test_empty_document_still_reports_missing_fields(
)


class TestProjectOverlaySourceExtensionMatching:
"""Overlay file extensions are matched case-insensitively."""

@pytest.mark.parametrize(
"filename", ["upper.YML", "mixed.Yaml", "shouty.YAML", "title.Yml"]
)
def test_uppercase_extension_is_collected(
self, project_dir: Path, filename: str
) -> None:
"""A hand-placed `<id>.YML` must not be silently ignored.

`collect` matched `path.suffix` verbatim against `(".yml", ".yaml")`,
so an overlay whose extension differed only in case was skipped with
nothing reported — the author's overlay simply never applied. Every
other YAML discovery path in the package lowercases before matching.
"""
ov_dir = project_dir / ".specify" / "workflows" / "overlays" / "wf"
ov_dir.mkdir(parents=True, exist_ok=True)
(ov_dir / filename).write_text(
yaml.safe_dump(
{
"id": "lint",
"extends": "wf",
"priority": 10,
"edits": [{"remove": "a"}],
}
),
encoding="utf-8",
)

layers = ProjectOverlaySource(project_dir).collect("wf")

assert [layer.content.id for layer in layers] == ["lint"]

def test_non_yaml_extensions_are_still_skipped(self, project_dir: Path) -> None:
"""Broadening case must not broaden which extensions are accepted."""
ov_dir = project_dir / ".specify" / "workflows" / "overlays" / "wf"
ov_dir.mkdir(parents=True, exist_ok=True)
for name in ("notes.txt", "backup.yml.bak", "README.md", "data.json"):
(ov_dir / name).write_text("id: lint\n", encoding="utf-8")

assert ProjectOverlaySource(project_dir).collect("wf") == []


class TestProjectOverlaySourceFileReadErrors:
"""File-read errors must be wrapped in OverlayLoadError, not leaked as raw tracebacks."""

Expand Down