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
8 changes: 8 additions & 0 deletions src/specify_cli/workflows/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ def __init__(self, data: dict[str, Any], source_path: Path | None = None) -> Non
self.source_path = source_path

workflow = data.get("workflow", {})
# A present-but-non-mapping ``workflow:`` block (bare ``workflow:`` ->
# None, or ``workflow: <str/list>``) would crash the following
# ``workflow.get(...)`` calls with AttributeError before validate can
# report the malformed shape. Normalize the local to {} (self.data
# keeps the raw value for validate_workflow), mirroring the
# default_options guard below.
if not isinstance(workflow, dict):
workflow = {}
self.id: str = workflow.get("id", "")
self.name: str = workflow.get("name", "")
self.version: str = workflow.get("version", "0.0.0")
Expand Down
24 changes: 24 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -3612,6 +3612,30 @@ def test_from_string(self, sample_workflow_yaml):
assert definition.id == "test-workflow"
assert len(definition.inputs) == 2

@pytest.mark.parametrize(
"block", ["workflow:\nsteps: []\n", "workflow: hi\nsteps: []\n", "workflow: [a]\nsteps: []\n"]
)
def test_non_mapping_workflow_block_parses_then_validates(self, block):
# A present-but-non-mapping `workflow:` block must not crash construction
# with AttributeError; it should parse (empty header) and let
# validate_workflow report the missing id/name, mirroring how the other
# raw fields are validated later.
from specify_cli.workflows.engine import WorkflowDefinition, validate_workflow

definition = WorkflowDefinition.from_string(block) # must not raise
assert definition.id == ""
errors = validate_workflow(definition)
assert any("workflow.id" in e for e in errors)
# The RAW malformed value is preserved on .data (the guard only
# normalizes the local var, not self.data). Assert it was NOT replaced
# with {} by comparing against the original parse and confirming it is
# still a non-mapping.
import yaml

raw_workflow = yaml.safe_load(block).get("workflow")
assert definition.data["workflow"] == raw_workflow
assert not isinstance(definition.data["workflow"], dict)

def test_from_string_invalid(self):
from specify_cli.workflows.engine import WorkflowDefinition

Expand Down