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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions