fix(workflows): gate prompt uses isdecimal() so a superscript digit doesn't crash (#3624)

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) <noreply@anthropic.com>
This commit is contained in:
Ali jawwad
2026-07-22 17:29:50 +05:00
committed by GitHub
parent d39f8fd5e5
commit 9ef477167d
2 changed files with 18 additions and 1 deletions

View File

@@ -168,7 +168,11 @@ class GateStep(StepBase):
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]:

View File

@@ -2145,6 +2145,19 @@ class TestGateStep:
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
):