From 1c7a50abcb60333bbaa414a102f3a6008a066891 Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 22 Jul 2026 10:10:59 +0500 Subject: [PATCH] fix(workflows): gate prompt uses isdecimal() so a superscript digit doesn't crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interactive gate prompt guarded numeric choices with raw.isdigit(), but str.isdigit() returns True for characters int() rejects — superscripts/subscripts like '²'. So typing '²' passed the guard and int('²') raised an uncaught ValueError, crashing the prompt loop. Use raw.isdecimal(), which is exactly the decimal-digit set int() accepts (Numeric_Type=Decimal), so such input is treated as an invalid choice and re-prompted. No behavior change for valid input. Test: input '²' then '1' returns the first option (fails before: ValueError). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/specify_cli/workflows/steps/gate/__init__.py | 6 +++++- tests/test_workflows.py | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/workflows/steps/gate/__init__.py b/src/specify_cli/workflows/steps/gate/__init__.py index 8882af48a4..e686a6a247 100644 --- a/src/specify_cli/workflows/steps/gate/__init__.py +++ b/src/specify_cli/workflows/steps/gate/__init__.py @@ -168,7 +168,11 @@ def _prompt(message: str, options: list[str]) -> str: except (EOFError, KeyboardInterrupt): print() return options[-1] # default to last (usually reject) - if raw.isdigit() and 1 <= int(raw) <= len(options): + # isdecimal() (not isdigit()): int() accepts exactly the decimal-digit + # set, whereas isdigit() also returns True for superscripts/subscripts + # (e.g. "²") that int() then rejects with ValueError — crashing + # this interactive loop. + if raw.isdecimal() and 1 <= int(raw) <= len(options): return options[int(raw) - 1] # Also accept the option name directly if raw.lower() in [o.lower() for o in options]: diff --git a/tests/test_workflows.py b/tests/test_workflows.py index cd1b235411..20aee888ff 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -1985,6 +1985,19 @@ def test_interactive_prompt_renders_show_file(self, tmp_path, monkeypatch, capsy assert result.status == StepStatus.COMPLETED assert result.output["choice"] == "approve" + def test_interactive_prompt_rejects_non_decimal_digit(self, monkeypatch, capsys): + """A Unicode digit int() can't parse — e.g. the superscript '²', which + str.isdigit() accepts but int() rejects — must be treated as an invalid + choice, not crash the prompt loop with an uncaught ValueError.""" + from specify_cli.workflows.steps.gate import GateStep + + _force_gate_stdin(monkeypatch, tty=True) + inputs = iter(["²", "1"]) # superscript-two, then a real "1" + monkeypatch.setattr("builtins.input", lambda _prompt="": next(inputs)) + + choice = GateStep._prompt("Review the spec.", ["approve", "reject"]) + assert choice == "approve" + def test_interactive_prompt_missing_show_file_does_not_crash( self, tmp_path, monkeypatch, capsys ):