Skip to content

fix(workflows): reject non-list input 'enum' instead of crashing#3552

Open
Noor-ul-ain001 wants to merge 2 commits into
github:mainfrom
Noor-ul-ain001:fix/input-enum-noniterable-crash
Open

fix(workflows): reject non-list input 'enum' instead of crashing#3552
Noor-ul-ain001 wants to merge 2 commits into
github:mainfrom
Noor-ul-ain001:fix/input-enum-noniterable-crash

Conversation

@Noor-ul-ain001

Copy link
Copy Markdown
Contributor

Problem

A workflow input enum is never type-checked. A scalar value crashes the validator and the runtime input resolver with a raw TypeError instead of producing a clean error:

inputs:
  scope:
    type: string
    enum: 5        # or `true`, `3.14`, ...
TypeError: argument of type 'int' is not a container or iterable

The crash comes from the value not in enum_values membership test in WorkflowEngine._coerce_input. Two paths are affected:

  1. Authoring timevalidate_workflow() documents a "return errors, never raise" contract, but the TypeError escapes its except ValueError and propagates.
  2. Run timeexecute() does not auto-validate the definition (see load_workflow's docstring), so _resolve_inputs hits the same crash when a provided value is coerced.

A bare-string enum (enum: "abc") is silently worse: value in "abc" becomes a substring/character test rather than enum membership, so it passes validation while doing the wrong thing at runtime.

Fix

Require enum to be a list in both places:

  • _coerce_input (the single runtime choke point) raises a clean ValueError on a non-list enum.
  • The authoring-time inputs loop in validate_workflow reports it as a validation error — needed independently because _coerce_input is only reached from validation when a default is present, and the integration: auto case strips enum before coercing.

The redundant "invalid default" report is suppressed when enum is already flagged, so the operator sees one clear error rather than the same problem re-framed twice.

This is the same unvalidated-config crash class as the existing switch / fan-in / fan-out non-list guards.

Tests

Added to tests/test_workflows.py:

  • scalar enum (no default) → clean validation error, no crash
  • string enum → rejected + reported exactly once (no duplicate)
  • scalar enum at run time via _resolve_inputs → clean ValueError, not TypeError
  • valid list enum → still validates and still checks the default against membership

Full test_workflows.py passes except the pre-existing Windows-without-elevation symlink-guard tests (unrelated to this change).

🤖 Generated with Claude Code

A workflow input `enum` was never type-checked, so a scalar value
(`enum: 5`, `enum: true`) made the `value not in enum_values`
membership test raise a raw `TypeError` ("argument of type 'int' is not
... iterable"). This escaped `validate_workflow`'s `except ValueError`,
breaking its documented "return errors, never raise" contract, and
crashed `_resolve_inputs` outright at run time (`execute` does not
auto-validate the definition first). A bare-string `enum` was silently
worse: `value in "abc"` is a substring/character test, not enum
membership.

Require `enum` to be a list in both `_coerce_input` (the single runtime
choke point) and the authoring-time inputs loop (so a bad `enum` on an
input with no `default`, or the `integration: auto` case that strips
`enum` before coercion, is still caught). Suppress the redundant
"invalid default" report when `enum` is already flagged, so the operator
sees one clear error.

Same unvalidated-config crash class as the switch/fan-in/fan-out
non-list guards.

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

Adds workflow enum type validation to prevent raw runtime errors.

Changes:

  • Rejects non-list enums during validation and coercion.
  • Adds regression tests for scalar, string, runtime, and valid-list enums.
Show a summary per file
File Description
src/specify_cli/workflows/engine.py Adds enum shape validation and error handling.
tests/test_workflows.py Covers malformed and valid enums.

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: 2
  • Review effort level: Medium

Comment on lines +1450 to +1455
if enum_values is not None and not isinstance(enum_values, list):
msg = (
f"Input {name!r} has invalid 'enum': must be a list, got "
f"{type(enum_values).__name__}."
)
raise ValueError(msg)
Comment thread src/specify_cli/workflows/engine.py Outdated
Comment on lines +223 to +224
enum_is_valid = enum_values is None or isinstance(enum_values, list)
if "default" in input_def and enum_is_valid:

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Address Copilot feedback

Copilot review on github#3552 found two holes in the non-list `enum` guard:

1. Runtime `integration: auto` path bypassed the guard. `_resolve_inputs`
   stripped `enum` before calling `_coerce_input` whenever `"enum" in
   input_def`, so a malformed `enum: 5` on the auto-integration input was
   removed and never reached the shape check — it resolved successfully
   instead of raising the promised clean `ValueError`. Now only a *list*
   `enum` is stripped; a scalar/string `enum` stays in so `_coerce_input`
   rejects it.

2. Validation hid independent default-type errors. When `enum` was
   malformed, `validate_workflow` skipped the default coercion entirely,
   so `type: string` + `default: 5` + `enum: 5` reported only the enum
   error and silently accepted the wrong-typed default. Now the enum is
   stripped from the coercion input (suppressing the duplicate membership
   error) but coercion still runs, so the default's type is validated and
   both errors surface.

Adds regression tests for both cases; both fail on the pre-fix code
(test-the-test verified).

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

Copy link
Copy Markdown
Contributor Author

@
Addressed both Copilot comments in a6ed066.

1. integration: auto runtime path bypassed the guard (comment on engine.py:1455)

_resolve_inputs stripped enum before coercion whenever "enum" in input_def, so a malformed enum: 5 on the auto-integration input was removed and never reached the shape check — it resolved successfully instead of raising the clean ValueError. Now only a list enum is stripped (isinstance(input_def.get("enum"), list)); a scalar/string enum stays in so _coerce_input rejects it.

2. Skipping default coercion hid independent default errors (comment on engine.py:224)

When enum was malformed, validation skipped the default check entirely, so type: string + default: 5 + enum: 5 reported only the enum error and silently accepted the wrong-typed default. Now the malformed enum is removed only from validation_input_def (suppressing the duplicate membership error) while _coerce_input still runs, so the defaults type is validated and both errors surface.

Tests

  • test_validate_workflow_scalar_enum_still_reports_bad_default_type — asserts the enum error appears exactly once and the wrong-typed default is still reported.
  • test_resolve_inputs_auto_integration_rejects_scalar_enum — asserts the auto sentinel with enum: 5 raises a clean ValueError, not a silent pass.

Both fail on the pre-fix code (test-the-test verified). Full test_workflows.py passes except the pre-existing Windows symlink-guard and cross-project-registry tests, which fail on the clean tree too and are unrelated to this change.
@

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants