From 6faaf1f7087cadce5c56e15c9263a467cec4d90a Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Fri, 11 Sep 2026 21:02:13 +0500 Subject: [PATCH] fix(workflows): match overlay file extensions case-insensitively `ProjectOverlaySource.collect` matched `path.suffix` verbatim: if not path.is_file() or path.suffix not in (".yml", ".yaml"): continue so a hand-placed overlay named `.YML` or `.Yaml` was skipped and never applied, with nothing reported to say the file had been ignored. Overlay files are explicitly hand-authored (docs/reference/workflows.md documents the format and tells users to write them), so the casing is the author's choice. Reproduced on main -- three overlays in one directory, differing only in extension case: files on disk: ['Mixed.Yaml', 'UPPER.YML', 'lower.yml'] COLLECTED : ['lower'] SKIPPED : ['mixed', 'upper'] Every other YAML discovery path in the package already lowercases before matching: engine.py:941, _commands.py:1325, :1894, :2128. This brings the overlay loader in line with them. Co-Authored-By: Claude Opus 5 (1M context) --- .../workflows/overlays/layer_sources.py | 7 ++- tests/workflows/test_overlay_layer_sources.py | 44 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/workflows/overlays/layer_sources.py b/src/specify_cli/workflows/overlays/layer_sources.py index a62cef9340..071b45c0eb 100644 --- a/src/specify_cli/workflows/overlays/layer_sources.py +++ b/src/specify_cli/workflows/overlays/layer_sources.py @@ -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 ``.YML`` or ``.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"]) diff --git a/tests/workflows/test_overlay_layer_sources.py b/tests/workflows/test_overlay_layer_sources.py index d852cb7622..339d1072d6 100644 --- a/tests/workflows/test_overlay_layer_sources.py +++ b/tests/workflows/test_overlay_layer_sources.py @@ -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 `.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."""