Skip to content

test: note the approval dialog when Textual pushes it - #471

Merged
hellices merged 1 commit into
mainfrom
fix/470-approval-dialog-signal
Oct 25, 2026
Merged

hellices merged 1 commit into
mainfrom
fix/470-approval-dialog-signal

Conversation

@hellices

Copy link
Copy Markdown
Owner

Closes #470

Summary

test_an_injected_short_timeout_expires_an_agent_write learned that the approval dialog had opened only by polling app.screen every 0.05 seconds. It failed four times on 2026-10-02, on four unrelated branches, with "agent approval dialog opened not met within 5.0s".

Its one-second window starts before the dialog is pushed. _await_user_approval re-checks the budget right after push_screen, so when mounting took the whole second, the request expired and the dialog was dismissed while it mounted, between two polls. A longer wait (#464, #465) cannot help here: by then the request has already resolved. #460 fixed the same race in the eval driver (#451).

  • The fix: the test now subscribes to the app's screen_change_signal (immediate=True, as in test: note each approval dialog when Textual pushes it #460) and notes each ConfirmScreen as Textual pushes it.
  • The new test: test_a_window_that_runs_out_while_the_dialog_mounts_still_expires_it pins the loaded-runner case. With a one-millisecond window the dialog always expires as it mounts; the test requires that the dialog was pushed, the request resolves as expired, nothing is written, and no dialog is left on screen.

Verification

  • RED: the new test, observing the dialog by polling app.screen as the old test did, failed five runs out of five with CI's exact message, "agent approval dialog opened not met within 5.0s".
  • GREEN: with the subscription, tests/ui/test_approval_timeout.py passed three runs in a row (7 passed each).
  • Mutation: removing the subscription fails both approval tests the same way.

Run locally on macOS with Python 3.13 and the frozen lock. Each step ran directly with the project's environment rather than through make, because this machine cannot sync from PyPI:

Command Result
pytest -p no:tach tests/ui/test_approval_timeout.py 7 passed
python scripts/check_source_size.py passed
ruff check src/ tests/ and ruff format --check src/ tests/ passed; 659 files already formatted
SKIP=mypy,source-size pre-commit run --files tests/ui/test_approval_timeout.py passed
mypy no issues in 659 source files
tach check all modules validated
pytest -n 8 -q 13,697 passed, 25 skipped

Checklist

  • This closes an accepted issue in this repository that is assigned to me.
  • Tests cover the change, written to fail first where behavior changed.
  • make check passes locally.
  • User-facing docs and the README match the change, or nothing user-facing changed.
  • No security invariant is weakened: writes still need a keystroke approval
    and a fail-closed audit entry, and sensitive reads are still masked.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 09:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused test-only change addresses the missed-dialog race, preserves safety assertions, and has no identified blocking issues.

No open issues 🎉

What changed in this PR

Addresses #470 by making approval-timeout tests record dialogs when Textual pushes them, rather than relying on polling that can miss short-lived dialogs. Production behavior is unchanged.

Changes:

  • Adds a signal-based helper to record approval dialogs and updates the existing timeout test to use it.
  • Adds a one-millisecond timeout regression test checking expiry, no writes, and dialog dismissal.
File Description
tests/​ui/​test_approval_timeout.py Records dialog pushes through Textual’s signal and adds short-window expiry coverage.

Review effort: Balanced

@my-reviewer-agent my-reviewer-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

검토 범위에서 보고할 이슈가 없습니다.

test_an_injected_short_timeout_expires_an_agent_write learned that the
dialog had opened only by polling app.screen every 0.05 seconds. Its
one-second window starts before the push, and _await_user_approval
re-checks the budget right after mounting, so on a loaded runner the
request expired and the dialog was dismissed while it mounted, between
two polls. The wait then failed with "agent approval dialog opened not
met within 5.0s", four times on 2026-10-02, and a longer wait could not
help because the request had already resolved (#470). #460 fixed the
same race in the eval driver.

The test now subscribes to the app's screen_change_signal and notes each
ConfirmScreen as it is pushed. A new test pins the case directly: with a
one-millisecond window the dialog always expires as it mounts, the
request resolves as expired, nothing is written, and no dialog is left
on screen. Polling app.screen failed that test five runs out of five;
without the subscription both approval tests fail the same way.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hellices
hellices force-pushed the fix/470-approval-dialog-signal branch from a67729a to a90ec1a Compare October 23, 2026 15:47

@my-reviewer-agent my-reviewer-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

검토 범위에서 보고할 이슈가 없습니다.

@hellices
hellices merged commit e02a52e into main Oct 25, 2026
32 of 38 checks passed
hellices added a commit that referenced this pull request Oct 25, 2026
…ecurity bumps (#482)

* test: drop the relay child-watcher race from stdio stderr checks

test_stdout_noise_is_diverted_and_wire_is_restored[True] failed on 3.12
because the stdio server's stderr carried "child process pid 3098 exit
status already read:  will report returncode 255" (#472). relay=True
forces the Windows-only relays onto POSIX. When the stdin relay exits on
its own as the test closes stdin, and _relay_stdin's cleanup runs before
the event loop has processed that exit, process.kill() goes through
Popen.send_signal, which polls first (bpo-38630). That waitpid reaps the
relay, and asyncio's child watcher then finds it gone, reports 255 and
logs the warning. On Windows, where the relays run in production, nothing
reaps.

The three forced-relay tests that compare the server's stderr exactly now
read it through _server_stderr, which drops whole lines matching either
watcher's warning (pidfd: "exit status already read", threaded: "Unknown
child process pid") and keeps everything else. Two tests pin both
directions: both warnings are dropped with the surrounding lines intact,
and a line that only contains one is kept.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: emit the relay watcher race in every forced-relay run

The race is rare on its own, so the first fix only covered the three
exact stderr checks it was seen against. Under four xdist workers on
korvid-runners it hit a fourth on 3.12, twice:
test_stdio_delivers_large_utf8_output_without_truncation[True] expected
an empty stderr log and got "child process pid ... exit status already
read:  will report returncode 255".

The forced-relay entrypoint now logs that exact asyncio warning at
startup, so every relay=True test that reads the server's stderr has to
filter it on every run instead of passing on a lucky one. With the line
emitted, exactly that test failed; it now reads its stderr log through
_server_stderr, which accepts text as well as bytes, and test_stdio.py
passes in full.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: drop agent events that arrive while the app shuts down

Textual's App._shutdown clears _running, removes every screen and widget,
and only then sends the App its Unmount, which is where KorvidApp marks
the agent session down. A turn the user interrupted just before quitting
can settle in between. _finish_interrupted_turn then still sees
_shutting_down as False and hands its TurnInterrupted to the panel port,
which looks the panel up on a pruned screen. The resulting NoMatches
fails the turn task and escapes app shutdown through wait_for_turn
(#476). On #471's CI it surfaced as "No nodes match '#agent-status' on
AgentPanel()".

AppAgentPanel.apply_event, the one place agent events reach the panel,
now drops an event once the app is no longer running. The new test
reproduces the ordering deterministically: the interrupted turn's
cleanup waits for the panel's Unmount, so it settles after Textual
pruned the panel and before the App's Unmount. Without the guard it
fails with NoMatches every run.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: wait for an approval dialog to compose before reading it

The operation-journey driver read `.confirm-title` as soon as app.screen
was a ConfirmScreen. Textual makes a pushed screen current before it
composes the screen's widgets, so on a loaded runner the poll landed in
between and the read raised "No nodes match '.confirm-title' on
ConfirmScreen()". That failed two journeys on main's 3.11 (#478); the
second NoMatches in that log came from the journey tearing down after
the first.

handle() now first waits, through _await_dialog_composed, until the
current dialog has its title or is no longer current; a dialog that
closes first falls through to the existing closed-before-a-decision
path. The new test delays ConfirmScreen's compose by 0.3 seconds and
still expects one matched dialog_observed and no hard failures; without
the wait it fails every run with CI's exact pair of errors.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build(deps-dev): bump gitpython from 3.1.62 to 3.2.1

Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.62 to 3.2.1.
- [Release notes](https://github.com/gitpython-developers/GitPython/releases)
- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/doc/source/changes.rst)
- [Commits](gitpython-developers/GitPython@3.1.62...3.2.1)

---
updated-dependencies:
- dependency-name: gitpython
  dependency-version: 3.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 3861683)

* build(deps): bump virtualenv from 21.7.13 to 21.14.9

Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.7.13 to 21.14.9.
- [Release notes](https://github.com/pypa/virtualenv/releases)
- [Changelog](https://github.com/pypa/virtualenv/blob/main/docs/changelog.rst)
- [Commits](pypa/virtualenv@21.7.13...21.14.9)

---
updated-dependencies:
- dependency-name: virtualenv
  dependency-version: 21.14.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 5e10f14)

* build: relock filelock

Since 2026-10-23 pip-audit fails every branch on filelock 3.32.0:
GHSA-v4x8-3wr5-3wq8 (high), GHSA-9vhv-jxww-xfm5 and GHSA-567q-5qvw-pf92,
all fixed in 4.2.1 (#480). The locked virtualenv 21.7.13 required
filelock<4; the virtualenv 21.14.9 bump in this branch allows <5, so
filelock can now move to 4.2.4.

These are the bytes the Relock workflow produced and verified on this
branch: run 40155755520, package=filelock, base 293d7a8. Its relock job
resolved the lock on a runner with direct PyPI access, confirmed every
url, registry and index names PyPI, and ran uv sync --locked, ruff, the
format check, mypy, tach and the test suite (13,753 passed). Its propose
job then could not open a branch, because the base's uv.lock differs from
main's (#481), so the relocked-lock artifact was committed by hand after
matching its SHA-256 to the digest the relock job recorded before any
resolved code ran (493ce1c4...4f96) and re-checking it with main's
scripts/check_lock_hosts.py.

uv 0.10.9, which the workflow pins, writes lock revision 3 and keeps the
httpcore2 and secretstorage markers, so this also returns those lines to
main's form: the branch's net lock change is the four version bumps.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* build: relock virtualenv

virtualenv 21.14.9, the version Dependabot proposed in #474, introduced
GHSA-cx6f-vjhg-g3cc / CVE-2026-115586 (high): the Bash activator leaves
the --prompt value unescaped in PS1 when promptvars is off, so prompt
expansion later executes it. The advisory was published on 2026-10-23,
after #474 passed its checks, and this branch's dependency-review check
failed on it. It is fixed in 21.14.13, which still allows filelock<5 and
needs packaging>=26.3, both already locked here.

These are the bytes the Relock workflow produced and verified on this
branch: run 40158406513, package=virtualenv, base cd253a2. Its relock
job resolved on a runner with direct PyPI access, confirmed every url,
registry and index names PyPI, and ran uv sync --locked, ruff, the format
check, mypy, tach and the test suite (13,753 passed). Its propose job hit
#481 again, so the relocked-lock artifact was committed by hand after
matching its SHA-256 to the digest the relock job recorded
(1de17a61...95da) and re-checking it with main's
scripts/check_lock_hosts.py. Only the virtualenv block changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.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.

UI test polls for an approval dialog that can open and expire between two polls

2 participants