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 ):