Skip to content

fix(workflows): render gate show_file contents in the interactive prompt#2810

Open
doquanghuy wants to merge 4 commits into
github:mainfrom
doquanghuy:fix/2809-gate-show-file
Open

fix(workflows): render gate show_file contents in the interactive prompt#2810
doquanghuy wants to merge 4 commits into
github:mainfrom
doquanghuy:fix/2809-gate-show-file

Conversation

@doquanghuy
Copy link
Copy Markdown
Contributor

Description

Closes #2809.

The built-in gate step accepts a show_file config field. The value was read, template-expanded, and stored in output["show_file"], but its contents were never displayed at the interactive prompt — the operator was asked to approve/reject without seeing the referenced file.

This renders show_file inside the interactive gate prompt, before the options. A missing or undecodable file degrades to a short one-line notice instead of raising, so a misconfigured path never breaks the prompt.

What changed

  • GateStep._prompt(...) now accepts show_file and prints its contents (each line within the gate box) before the options.
  • New GateStep._read_show_file(...) helper reads the file as UTF-8 and returns a (could not read file: …) / (file is empty) notice on error/empty instead of raising.

Compatibility

  • Non-interactive (PAUSED) path is unchanged — the file is not read when stdin is not a TTY.
  • Exit codes, on_reject handling, and resume semantics are unchanged.
  • Gates without show_file produce byte-identical output to before.

Testing

  • Tested locally with uv run specify --help
  • Ran existing tests with uv sync --extra test && uv run pytest — 3310 passed, 40 skipped
  • Tested with a sample project (if applicable)

Added three targeted tests in tests/test_workflows.py::TestGateStep:

  • interactive prompt renders show_file contents and returns the chosen option;
  • a missing show_file shows the notice and does not crash;
  • the non-interactive path still PAUSES and preserves output["show_file"] without reading the file.

AI Disclosure

  • I did not use AI assistance for this contribution
  • I did use AI assistance (describe below)

Used Claude to trace the bug in gate/__init__.py, draft the fix and tests, and write this PR body. The behaviour was reproduced and the diff reviewed locally before submission.

@doquanghuy doquanghuy requested a review from mnriem as a code owner June 2, 2026 10:10
@doquanghuy doquanghuy force-pushed the fix/2809-gate-show-file branch from b43191b to 5412c47 Compare June 2, 2026 11:28
@mnriem mnriem requested a review from Copilot June 2, 2026 21:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes the built-in workflow gate step so that a configured show_file is actually rendered in the interactive prompt, allowing operators to review the referenced file contents before choosing approve/reject. It adds a bounded, non-throwing file-read helper and extends test coverage around the interactive/non-interactive behaviors.

Changes:

  • Render show_file contents inside the interactive gate prompt (before the options).
  • Add _read_show_file() helper that caps output length and degrades to a one-line notice on read/decode errors.
  • Add targeted tests covering interactive rendering, missing files, non-interactive pause behavior, empty files, and truncation.
Show a summary per file
File Description
src/specify_cli/workflows/steps/gate/__init__.py Passes show_file into the gate prompt and safely renders file contents with truncation/error notices.
tests/test_workflows.py Adds regression tests for interactive rendering and safe behavior across missing/empty/large files and non-interactive runs.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment on lines +85 to +89
if show_file and isinstance(show_file, str):
print(" │")
print(f" │ {show_file}:")
for line in GateStep._read_show_file(show_file):
print(f" │ {line}")
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed: show_file is now coerced with str() after template evaluation in execute(), so a single-expression template (or literal) that resolves to a non-string is rendered rather than skipped. Covered by test_templated_show_file_resolving_to_non_string_is_coerced.

The gate step read and recorded `show_file` but never displayed its
contents at the interactive prompt, so the operator approved/rejected
without seeing the referenced file. Render the file inside the prompt
when stdin is a TTY, with a graceful notice for missing/unreadable
files. Non-interactive PAUSED behaviour, exit codes, resume semantics,
and no-`show_file` output are unchanged.

Closes github#2809.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@doquanghuy doquanghuy force-pushed the fix/2809-gate-show-file branch from 5412c47 to bf2f25c Compare June 3, 2026 04:14
@doquanghuy
Copy link
Copy Markdown
Contributor Author

Thanks @copilot — fixed in the latest push.

show_file is now coerced to str after template evaluation (and for any non-string literal), so a single-expression template that resolves to a non-string (e.g. a number from a prior step) is rendered instead of being silently skipped. The now-redundant isinstance(show_file, str) guard at the prompt was simplified to if show_file:. Added a test covering a templated show_file that resolves to a non-string.

uv run pytest green (3313 passed, 40 skipped); ruff check src/ clean.

@mnriem ready for another look when you have a moment.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment on lines +125 to +133
try:
with Path(show_file).open(encoding="utf-8") as handle:
for line in handle:
if len(lines) >= GateStep.MAX_SHOW_FILE_LINES:
truncated = True
break
lines.append(line.rstrip("\n"))
except (OSError, UnicodeDecodeError) as exc:
return [f"(could not read file: {exc})"]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 017e84d: _read_show_file now catches ValueError as well (the error Path.open raises for an embedded NUL byte, before any I/O), so such a path degrades to the (could not read file: …) notice instead of crashing. Covered by test_read_show_file_invalid_path_does_not_raise.

doquanghuy and others added 2 commits June 4, 2026 00:05
…le reads

The gate prompt rendered show_file by passing it as a third positional
argument to _prompt. A test that stubs _prompt with a two-argument lambda
(test_gate_abort_still_halts_with_continue_on_error) then failed once the
branch caught up to main, because the call site passed three arguments to
the two-argument stub.

Compose the show_file material into the displayed message in execute() and
keep _prompt to its (message, options) contract. Display data no longer
widens the interactive seam, so stubbing _prompt stays stable and future
review material can be added without breaking callers. _prompt now renders
a multi-line message inside the gate box.

Also catch ValueError in _read_show_file so a path the OS rejects outright
(e.g. an embedded NUL byte) degrades to a notice instead of crashing the
prompt, matching the helper's stated contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@doquanghuy
Copy link
Copy Markdown
Contributor Author

Pushed a fix that also brings the branch up to date with main.

CI failure (the merge-time TypeError). main grew test_gate_abort_still_halts_with_continue_on_error, which stubs GateStep._prompt with a two-argument lambda. This PR had widened _prompt to take show_file as a third positional argument, so the stubbed call blew up once the branch met main.

Rather than patch the stub, I kept _prompt at its original (message, options) contract and compose the show_file material into the displayed message in execute() (new _compose_prompt helper; _prompt now renders a multi-line message inside the gate box). Display data no longer widens the interactive seam, so stubbing _prompt stays stable and future review material can be added without breaking callers — i.e. this class of failure can't recur.

Copilot's open note. _read_show_file now also catches ValueError, so a path the OS rejects outright (e.g. an embedded NUL byte, which Path.open raises before any I/O) degrades to the (could not read file: …) notice instead of crashing the prompt. Added a regression test.

Local: full uv run pytest green, ruff check src/ clean, gate suite + the previously-failing test pass.

@mnriem ready for another look when you have a moment.

The multi-line render loop split the message on newlines, which assumes a
str. A non-string message (e.g. a YAML numeric literal) previously rendered
fine through the old f-string and would now raise on .split. Coerce with
str() to preserve that tolerance, and add a regression test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Gate show_file is recorded but never displayed at the interactive prompt

3 participants