Summary
The command step accepts an options: field that is expected to be a mapping of step-level option overrides. If the workflow author sets options: to a non-mapping (e.g. a list, a string, or null-with-a-truthy-branch-taken), execution crashes with a cryptic TypeError or ValueError from dict.update, and validate() does not catch it.
Sibling report to #3492 (same file, related pattern). Also fits the recent fix(workflows) cleanup thread — #3327, #3270, #3448.
Reproduction
Against main (0.12.13.dev0):
from specify_cli.workflows.steps.command import CommandStep
from specify_cli.workflows.base import StepContext
step = CommandStep()
ctx = StepContext(project_root="/tmp")
# options: [1, 2]
step.execute({"id": "t", "command": "/x", "options": [1, 2]}, ctx)
# -> TypeError: cannot convert dictionary update sequence element #0 to a sequence
# options: "foo"
step.execute({"id": "t", "command": "/x", "options": "foo"}, ctx)
# -> ValueError: dictionary update sequence element #0 has length 1; 2 is required
Offending code in src/specify_cli/workflows/steps/command/__init__.py:50-54:
options = dict(context.default_options)
step_options = config.get("options", {})
if step_options: # truthy for non-empty lists / strings
options.update(step_options) # crashes when step_options is not a mapping
The same pattern applies to input (see #3492) — same file, same class of gap.
Natural YAML that triggers this:
- id: draft
type: command
command: /speckit.plan
options:
- approve
- reject # authored options as a list; step-level overrides expect a mapping
Suggested fix
Mirror the treatment already given to shell / while-loop / do-while validators:
- In
validate(): reject non-mapping options (allow absence) with a clear message pointing at the field.
- In
execute(): guard step_options with isinstance(step_options, dict) before the .update() so a caller that bypasses WorkflowEngine.validate() still fails cleanly rather than raising an internal dict.update traceback.
Happy to open a PR — I have a fix + tests ready.
Disclosure: this issue and the accompanying PR were prepared with Claude (Anthropic) assistance. The reproducer was executed against a fresh clone of main and the fix is covered by new unit tests in tests/test_workflows.py.
Summary
The
commandstep accepts anoptions:field that is expected to be a mapping of step-level option overrides. If the workflow author setsoptions:to a non-mapping (e.g. a list, a string, ornull-with-a-truthy-branch-taken), execution crashes with a crypticTypeErrororValueErrorfromdict.update, andvalidate()does not catch it.Sibling report to #3492 (same file, related pattern). Also fits the recent
fix(workflows)cleanup thread — #3327, #3270, #3448.Reproduction
Against
main(0.12.13.dev0):Offending code in
src/specify_cli/workflows/steps/command/__init__.py:50-54:The same pattern applies to
input(see #3492) — same file, same class of gap.Natural YAML that triggers this:
Suggested fix
Mirror the treatment already given to shell / while-loop / do-while validators:
validate(): reject non-mappingoptions(allow absence) with a clear message pointing at the field.execute(): guardstep_optionswithisinstance(step_options, dict)before the.update()so a caller that bypassesWorkflowEngine.validate()still fails cleanly rather than raising an internaldict.updatetraceback.Happy to open a PR — I have a fix + tests ready.
Disclosure: this issue and the accompanying PR were prepared with Claude (Anthropic) assistance. The reproducer was executed against a fresh clone of
mainand the fix is covered by new unit tests intests/test_workflows.py.