From d37848569624452ea202d6b2aab8e115bd476da0 Mon Sep 17 00:00:00 2001 From: Ali jawwad <33836051+jawwad-ali@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:05:51 +0500 Subject: [PATCH] fix(workflows): reject infinite number-input default instead of raising OverflowError (#3199) WorkflowEngine._coerce_input normalizes a whole-valued number to int via int(value). For an infinite float (e.g. a 'type: number' input with YAML 'default: .inf') int(inf) raises OverflowError, which is not in the except (ValueError, TypeError) tuple. validate_workflow eager-coerces declared defaults and is documented to RETURN a list of errors, but it only catches ValueError -- so the OverflowError escaped and validate_workflow raised instead of reporting, breaking its contract. (NaN already surfaced cleanly because int(nan) raises ValueError.) Add OverflowError to the except tuple so an infinite default surfaces as the same clean 'expected a number' ValueError as NaN, consistent with the function's existing fail-fast-on-authoring-mistakes design. Finite values (5.0 -> 5, 3.5 -> 3.5) are unaffected. Co-authored-by: Claude Opus 4.8 (1M context) --- src/specify_cli/workflows/engine.py | 7 ++++- tests/test_workflows.py | 41 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/workflows/engine.py b/src/specify_cli/workflows/engine.py index aff5e92e2..0e11a6b7d 100644 --- a/src/specify_cli/workflows/engine.py +++ b/src/specify_cli/workflows/engine.py @@ -1010,7 +1010,12 @@ class WorkflowEngine: value = float(value) if value == int(value): value = int(value) - except (ValueError, TypeError): + except (ValueError, TypeError, OverflowError): + # OverflowError: `int(value)` raises it for an infinite float + # (e.g. a `default: .inf` authoring mistake), which would + # otherwise escape validate_workflow's `except ValueError` and + # break its "return errors, never raise" contract. Surface it as + # the same clean "expected a number" error as NaN does. msg = f"Input {name!r} expected a number, got {value!r}." raise ValueError(msg) from None elif input_type == "boolean": diff --git a/tests/test_workflows.py b/tests/test_workflows.py index b239cb9a4..cee02c46b 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -2846,6 +2846,47 @@ steps: errors = validate_workflow(definition) assert any("invalid default" in e for e in errors), errors + def test_coerce_number_input_rejects_infinity_cleanly(self): + """An infinite float must surface as a clean ValueError (like NaN), not + let ``int(inf)``'s OverflowError escape: ``int()`` of an infinity raises + OverflowError, which is not ValueError/TypeError. + """ + from specify_cli.workflows.engine import WorkflowEngine + + for value in (float("inf"), float("-inf"), "inf", "Infinity", "-inf"): + with pytest.raises(ValueError, match="expected a number"): + WorkflowEngine._coerce_input("count", value, {"type": "number"}) + # Finite values still coerce (whole floats normalize to int). + assert WorkflowEngine._coerce_input("count", 5.0, {"type": "number"}) == 5 + assert WorkflowEngine._coerce_input("count", 3.5, {"type": "number"}) == 3.5 + + def test_validate_workflow_rejects_infinite_default_for_number_type(self): + """``type: number`` with an infinite default (YAML ``.inf``) must be + reported as an error, not raise. ``int(inf)`` raises OverflowError during + coercion, which previously escaped validate_workflow's ValueError handler + and broke its "return a list of errors" contract. + """ + from specify_cli.workflows.engine import WorkflowDefinition, validate_workflow + + definition = WorkflowDefinition.from_string(""" +schema_version: "1.0" +workflow: + id: "inf-as-number" + name: "Inf As Number" + version: "1.0.0" +inputs: + count: + type: number + default: .inf +steps: + - id: noop + type: gate + message: "noop" + options: [approve] +""") + errors = validate_workflow(definition) + assert any("invalid default" in e for e in errors), errors + def test_validate_workflow_rejects_non_string_default_for_string_type(self): """``type: string`` must require an actual string — a numeric YAML default like ``5`` would otherwise slip through unvalidated.