diff --git a/src/specify_cli/workflows/_commands.py b/src/specify_cli/workflows/_commands.py index 3ab027cead..be8b731d51 100644 --- a/src/specify_cli/workflows/_commands.py +++ b/src/specify_cli/workflows/_commands.py @@ -1555,7 +1555,7 @@ def _validate_and_install_local( # precedence over --from so a URL that would be ignored is never fetched. if dev: dev_path = Path(source).expanduser() - if dev_path.is_file() and dev_path.suffix in (".yml", ".yaml"): + if dev_path.is_file() and dev_path.suffix.lower() in (".yml", ".yaml"): _validate_and_install_local(dev_path, str(dev_path)) return if dev_path.is_dir(): @@ -1714,7 +1714,7 @@ def _validate_and_install_local( # Try as a local file/directory source_path = Path(source) if source_path.exists(): - if source_path.is_file() and source_path.suffix in (".yml", ".yaml"): + if source_path.is_file() and source_path.suffix.lower() in (".yml", ".yaml"): _validate_and_install_local(source_path, str(source_path)) return elif source_path.is_dir(): diff --git a/tests/test_workflows.py b/tests/test_workflows.py index cd1b235411..d16a57b7b9 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -7499,6 +7499,63 @@ def rename_boom(src, dst): assert "[stage]permissiondenied" in output_compact assert "[reg]diskfull" in output_compact + +class TestWorkflowAddCaseInsensitiveSuffix: + """`workflow add` must detect a local YAML file case-insensitively, matching + `workflow run` (_commands.py:workflow_run) and the engine loader + (engine.py:WorkflowEngine.load_workflow), which both use `.suffix.lower()`. + Without it, `workflow run Sample.YAML` works but `workflow add Sample.YAML` + fails — an add/run inconsistency for an uppercase extension.""" + + def test_plain_path_accepts_uppercase_extension(self, temp_dir, monkeypatch, sample_workflow_yaml): + from typer.testing import CliRunner + from specify_cli import app + + (temp_dir / ".specify" / "workflows").mkdir(parents=True) + src = temp_dir / "Sample.YAML" + src.write_text(sample_workflow_yaml, encoding="utf-8") + + monkeypatch.chdir(temp_dir) + result = CliRunner().invoke(app, ["workflow", "add", str(src)]) + + # Before the fix: `.suffix in (...)` is case-sensitive, so ".YAML" is not + # recognized as a local file; the path falls through to catalog lookup + # and fails. After the fix it installs like the lowercase happy path. + assert result.exit_code == 0, result.output + assert "installed" in result.output + + def test_dev_path_accepts_uppercase_extension(self, temp_dir, monkeypatch, sample_workflow_yaml): + from typer.testing import CliRunner + from specify_cli import app + + (temp_dir / ".specify" / "workflows").mkdir(parents=True) + src = temp_dir / "Sample.YAML" + src.write_text(sample_workflow_yaml, encoding="utf-8") + + monkeypatch.chdir(temp_dir) + result = CliRunner().invoke(app, ["workflow", "add", "--dev", str(src)]) + + # Before the fix the --dev branch rejects ".YAML" with + # "--dev source must be a workflow YAML file ...". + assert result.exit_code == 0, result.output + assert "installed" in result.output + + def test_lowercase_extension_still_installs(self, temp_dir, monkeypatch, sample_workflow_yaml): + """Happy path (lowercase .yml) is unchanged by the case-normalization.""" + from typer.testing import CliRunner + from specify_cli import app + + (temp_dir / ".specify" / "workflows").mkdir(parents=True) + src = temp_dir / "sample.yml" + src.write_text(sample_workflow_yaml, encoding="utf-8") + + monkeypatch.chdir(temp_dir) + result = CliRunner().invoke(app, ["workflow", "add", str(src)]) + + assert result.exit_code == 0, result.output + assert "installed" in result.output + + class TestWorkflowAddSymlinkGuard: def test_add_malformed_ipv6_url_exits_cleanly(self, temp_dir, monkeypatch): """A malformed IPv6 URL must produce a clean error, not a ValueError traceback."""