Skip to content

fix(workflows): command step crashes with TypeError on non-mapping options #3493

Description

@thejesh23

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:

  1. In validate(): reject non-mapping options (allow absence) with a clear message pointing at the field.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions