Files
github-spec-kit/src/specify_cli
Noor ul ain afbb2c7b65 fix(workflows): validate prompt step 'timeout' like the shell step (#3847)
* fix(workflows): validate prompt step 'timeout' like the shell step

PR #3768 added a `timeout` to the prompt step and passed it straight into
`subprocess.run(timeout=...)`. Neither `validate()` nor `execute()` checks
it, so a bad value from a user-authored `workflow.yml` escapes as a raw
exception:

    steps:
      - id: first
        type: shell
        run: echo side-effect
      - id: ask
        type: prompt
        prompt: do it
        timeout: abc

    $ specify workflow run wf.yml
      > [first] shell ...
    Workflow failed: unsupported operand type(s) for +: 'float' and 'str'

The engine re-raises anything a step throws, so this takes down the whole
run — after `first` has already run its side effect — with a message that
names neither the step nor the field. `timeout: .nan` raises `ValueError:
cannot convert float NaN to integer` the same way, and a non-positive
`timeout` (`0`, `-5`) makes `subprocess.run` report an immediate
TimeoutExpired for a command that never got the time to run. `timeout:
true` silently becomes a 1-second limit, since bool is an int subclass.

The sibling shell step already rejects exactly these values via a
`_timeout_error()` helper shared by `execute()` and `validate()`, so the
same workflow failed validation cleanly as a shell step and crashed as a
prompt one. Mirrored that helper onto PromptStep: `validate()` reports the
contract error, and `execute()` re-checks it so an unvalidated run fails
just that step instead of aborting. Now:

    Workflow validation failed:
      - Prompt step 'ask': 'timeout' must be a positive number of seconds,
        got 'abc'.

caught before the first step runs. Positive int/float timeouts and an
absent `timeout` are unaffected.

Regression tests in `TestPromptStep` mirror the shell step's: validate
rejects "30"/True/inf/nan/0/-5/list/None, validate accepts 300/5/0.5 and
an absent field, and execute fails cleanly with `subprocess.run` patched
to assert it is never reached. With the source fix reverted, all 9
rejection tests fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Assisted-by: Claude Code (model: claude-opus-5, under direct human supervision)

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* test(workflows): cover the huge-int timeout OverflowError guard

The autofix commit wrapped the prompt step's `_timeout_error()` check in
`try/except OverflowError` but added no test, so nothing pins the
behaviour it introduced.

`math.isfinite(10**400)` raises `OverflowError: int too large to convert
to float` — the value is an `int`, is `> 0`, and is not a `bool`, so it
clears every other clause of the guard and reaches `isfinite()`. Without
the `except`, validating

```yaml
- id: ask
  type: prompt
  prompt: do it
  timeout: 1000...0   # 400 digits
```

raises that `OverflowError` out of `validate()`/`execute()` — exactly the
uncaught-crash failure mode this guard was added to prevent. The same
value raises `OverflowError` from `subprocess.run(timeout=...)`.

Add `10**400` to both parametrized rejection lists (`validate()` and the
`execute()` fails-cleanly loop). Test-the-test: reverting the `try/except`
fails both new cases with `OverflowError` and leaves the rest passing.

Assisted-by: Claude Opus 5 (model: claude-opus-5, autonomous)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-29 10:50:45 -05:00
..