From 4817f6261ea99547e48574164ac0d58d41f77ed6 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Sat, 15 Aug 2026 18:19:24 +0500 Subject: [PATCH] fix(bundler): pass explicit option values when delegating to workflow_add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `specify bundle install` can never install a workflow component. It fails 100% of the time with a nonsensical error about `--dev`. The bundler delegates to the Typer command callables in-process: lambda: workflow_add(component.id) `workflow_add` declares two `typer.Option` parameters. Called from Python rather than through Typer, those keep their `OptionInfo` sentinels as the value — and the sentinel is truthy and is not None: dev default -> OptionInfo truthy=True from_url default -> OptionInfo is None=False So `if dev:` takes the local-path branch for every catalog install: Error: --dev source must be a workflow YAML file, supported archive, or directory containing workflow.yml: code-review BundlerError: Failed to install workflow 'code-review'. `workflow_add` is the only one of the four delegated commands that declares options; workflow_remove / workflow_step_add / workflow_step_remove take a bare `typer.Argument` and are safe as written. Co-Authored-By: Claude Opus 5 (1M context) --- .../bundler/services/primitives.py | 11 +++++- tests/unit/test_bundler_primitives.py | 38 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/bundler/services/primitives.py b/src/specify_cli/bundler/services/primitives.py index 31b1126a34..d4a0a6f8ce 100644 --- a/src/specify_cli/bundler/services/primitives.py +++ b/src/specify_cli/bundler/services/primitives.py @@ -337,7 +337,16 @@ def install(self, component: ComponentRef) -> None: with _chdir(self._root): _delegate_command( "install", f"workflow '{component.id}'", - lambda: workflow_add(component.id), + # Pass the options explicitly. Called in-process rather than + # through Typer, an omitted ``typer.Option`` parameter keeps its + # ``OptionInfo`` sentinel as the value -- which is TRUTHY and is + # not ``None`` -- so ``workflow_add``'s ``if dev:`` took the + # local-path branch for every catalog install and failed with + # "--dev source must be a workflow YAML file ...". It is the only + # one of the four delegated commands that declares options; + # workflow_remove / workflow_step_add / workflow_step_remove take + # a bare ``typer.Argument`` and are safe as written. + lambda: workflow_add(component.id, dev=False, from_url=None), ) def refresh(self, component: ComponentRef) -> None: diff --git a/tests/unit/test_bundler_primitives.py b/tests/unit/test_bundler_primitives.py index dc39106b50..8be15f39d1 100644 --- a/tests/unit/test_bundler_primitives.py +++ b/tests/unit/test_bundler_primitives.py @@ -78,7 +78,9 @@ def test_offline_workflow_allows_bundled(tmp_path: Path, monkeypatch): assets, "_locate_bundled_workflow", lambda wid: tmp_path / "wf" ) calls: list[str] = [] - monkeypatch.setattr(specify_cli, "workflow_add", lambda wid: calls.append(wid)) + monkeypatch.setattr( + specify_cli, "workflow_add", lambda wid, **kwargs: calls.append(wid) + ) manager = primitive_manager("workflows", tmp_path, allow_network=False) manager.install(_component("workflows", "bundled-wf")) @@ -86,6 +88,40 @@ def test_offline_workflow_allows_bundled(tmp_path: Path, monkeypatch): assert calls == ["bundled-wf"] +def test_workflow_install_passes_explicit_typer_options(tmp_path: Path, monkeypatch): + """The bundler calls ``workflow_add`` in-process, so it must pass the + ``typer.Option`` values explicitly. + + Outside Typer, an omitted option parameter keeps its ``OptionInfo`` + sentinel as the value. That sentinel is truthy and is not ``None``, so + ``workflow_add``'s ``if dev:`` took the local-path branch for *every* + catalog install and failed with "--dev source must be a workflow YAML + file, supported archive, or directory containing workflow.yml". + """ + import specify_cli + import specify_cli._assets as assets + + monkeypatch.setattr( + assets, "_locate_bundled_workflow", lambda wid: tmp_path / "wf" + ) + seen: list[dict] = [] + + def _capture(wid, *args, **kwargs): + seen.append({"id": wid, "args": args, "kwargs": kwargs}) + + monkeypatch.setattr(specify_cli, "workflow_add", _capture) + + manager = primitive_manager("workflows", tmp_path, allow_network=False) + manager.install(_component("workflows", "bundled-wf")) + + assert len(seen) == 1, seen + call = seen[0] + assert call["id"] == "bundled-wf" + # Both options must arrive as real values, never as Typer sentinels. + assert call["kwargs"].get("dev") is False, call["kwargs"] + assert call["kwargs"].get("from_url") is None, call["kwargs"] + + def test_assert_pinned_version_matches_passes(): from specify_cli.bundler.services.primitives import _assert_pinned_version