Skip to content

fix(workflows): validate command step input/options are mappings#3262

Open
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/command-step-validate-non-mapping
Open

fix(workflows): validate command step input/options are mappings#3262
jawwad-ali wants to merge 2 commits into
github:mainfrom
jawwad-ali:fix/command-step-validate-non-mapping

Conversation

@jawwad-ali

Copy link
Copy Markdown
Contributor

Description

CommandStep.validate() only checks that command is present. But execute() then does:

for key, value in input_data.items(): ...        # input_data = config.get("input", {})
options.update(step_options)                       # step_options = config.get("options", {})

So a non-mapping input: or options: (e.g. a YAML list or scalar) raises AttributeError at run time, bypassing the per-step FAILED/continue_on_error contract. Every sibling step validates the shape of its structured fields (switch 'cases', fan-out 'step') — CommandStep was the lone outlier.

Fix

  • validate(): report 'input' must be a mapping / 'options' must be a mapping (mirroring the sibling steps).
  • execute(): defense-in-depth — coerce a non-mapping input/skip non-mapping options rather than crash, since the engine does not auto-validate before running a step.

Testing

  • uvx ruff check clean; TestCommandStep passes (11 tests).
  • New test_validate_rejects_non_mapping_input_and_options: input ∈ {None, str, list, int} and options: 42 now return the mapping error (fail-before/pass-after); a valid mapping config still returns []; execute() with input: None returns a result instead of raising.

AI Disclosure

  • I did use AI assistance (describe below)

Found and fixed with Claude Code (Claude Opus 4.8) under my direction. AI identified the validate()-contract gap versus the sibling steps; I reproduced the run-time AttributeError, confirmed the validate + execute-guard fix, and reviewed the diff before submitting.

CommandStep.validate() only checked for 'command'; execute() then does input.items() and options.update(step_options). A non-mapping input:/options: (e.g. a YAML list or scalar) raised AttributeError at run time, bypassing the per-step FAILED/continue-on-error contract -- unlike the sibling steps (switch 'cases', fan-out 'step') which type-check their config fields in validate(). Add the same checks, plus a defense-in-depth coercion in execute() since the engine does not auto-validate before running a step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR closes a validation gap in the workflow command step so malformed input: / options: values (non-mappings) are caught during validation (and handled defensively at runtime), avoiding AttributeError crashes that bypass the step failure/continue_on_error contract.

Changes:

  • Extend CommandStep.validate() to reject non-mapping input and options with clear validation errors.
  • Harden CommandStep.execute() to coerce/ignore non-mapping input/options defensively since the engine may execute unvalidated workflows.
  • Add regression tests covering invalid shapes and the execute-time defense-in-depth behavior.
Show a summary per file
File Description
tests/test_workflows.py Adds a regression test ensuring non-mapping input/options are rejected by validate() and don’t crash execute().
src/specify_cli/workflows/steps/command/init.py Adds mapping-shape validation and defensive handling in execute() for input/options.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Low
errors.append(
f"Command step {config.get('id', '?')!r} is missing 'command' field."
)
# execute() iterates input.items() and options.update(options); a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 77598a5. The comment now reads options.update(step_options) to match the code. Comment-only change, no behavior change. (AI-assisted: Claude Code under my review.)

The explanatory comment said options.update(options) but execute() does
options.update(step_options). Comment-only change; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

3 participants