From 595bcc89fc8c0e0891193180df27939c2d1ccd2d Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 2 May 2026 16:48:10 -0700 Subject: [PATCH] test(update): patch isatty on real streams to fix xdist-flaky --yes tests Two CI tests for the new `--yes` update flag (#18261) flaked under `pytest-xdist` on Linux/Python 3.11 even though they passed every local run on macOS/Python 3.14.4: FAILED tests/hermes_cli/test_update_yes_flag.py ::TestUpdateYesConfigMigration::test_no_yes_flag_still_prompts_in_tty `AssertionError: assert .called is False` FAILED tests/hermes_cli/test_update_yes_flag.py ::TestUpdateYesStashRestore::test_yes_restores_stash_without_prompting `AssertionError: assert .called is False` Captured stdout for the first failure shows `cmd_update` taking the "Non-interactive session \u2014 skipping config migration prompt." branch \u2014 i.e. the `sys.stdin.isatty() and sys.stdout.isatty()` check at `hermes_cli/main.py:7118` evaluated to `False` despite the test doing: with patch("hermes_cli.main.sys") as mock_sys: mock_sys.stdin.isatty.return_value = True mock_sys.stdout.isatty.return_value = True The whole-module mock is fragile under xdist worker reuse: a sibling test that imports `hermes_cli.main` first can leave another `sys` reference resolved inside the function (re-import in a helper, etc.), and the wholesale module replacement never gets consulted. Switch to `patch.object(_sys.stdin, "isatty", return_value=True)` (and the same for `stdout`). That patches the *attribute on the real stream object* \u2014 every call site, no matter how it reached `sys.stdin`, hits the patched method. Same fix applied to the stash-restore test (it took the "non-TTY \u2192 skip restore prompt" branch for the same reason). Validation: $ pytest tests/hermes_cli/test_update_yes_flag.py -q 3 passed in 5.47s No production code change. Fixes the two failures observed on `main` (run 25250051126): `tests/hermes_cli/test_update_yes_flag.py::TestUpdateYesConfigMigration::test_no_yes_flag_still_prompts_in_tty` `tests/hermes_cli/test_update_yes_flag.py::TestUpdateYesStashRestore::test_yes_restores_stash_without_prompting` Refs: #18261 (added the `--yes` flag + these tests). --- tests/hermes_cli/test_update_yes_flag.py | 28 +++++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/hermes_cli/test_update_yes_flag.py b/tests/hermes_cli/test_update_yes_flag.py index e36cc5142e..66060b10aa 100644 --- a/tests/hermes_cli/test_update_yes_flag.py +++ b/tests/hermes_cli/test_update_yes_flag.py @@ -113,11 +113,18 @@ class TestUpdateYesConfigMigration: args = SimpleNamespace(yes=False) - with patch("builtins.input", return_value="n") as mock_input, patch( - "hermes_cli.main.sys" - ) as mock_sys: - mock_sys.stdin.isatty.return_value = True - mock_sys.stdout.isatty.return_value = True + # Patch ``sys.stdin.isatty`` and ``sys.stdout.isatty`` directly on the + # real ``sys`` module instead of replacing ``hermes_cli.main.sys`` with + # a MagicMock. The MagicMock approach was flaky under ``pytest-xdist`` + # — a sibling test that imported ``hermes_cli.main`` first could leave + # a different ``sys`` reference resolved inside the function and the + # mock would never be consulted, with CI then taking the + # "Non-interactive session" branch instead of prompting. + import sys as _sys + + with patch("builtins.input", return_value="n") as mock_input, patch.object( + _sys.stdin, "isatty", return_value=True + ), patch.object(_sys.stdout, "isatty", return_value=True): cmd_update(args) # The user was actually prompted. assert mock_input.called @@ -156,7 +163,16 @@ class TestUpdateYesStashRestore: args = SimpleNamespace(yes=True) - cmd_update(args) + # Force a TTY-shaped session so the autostash-restore branch is + # reachable in CI workers regardless of inherited stdio (matches the + # isatty patching strategy in ``test_no_yes_flag_still_prompts_in_tty`` + # — ``patch.object`` on the real streams is robust under xdist). + import sys as _sys + + with patch.object(_sys.stdin, "isatty", return_value=True), patch.object( + _sys.stdout, "isatty", return_value=True + ): + cmd_update(args) # _restore_stashed_changes was called, and called with prompt_user=False # every time (so the user never sees "Restore local changes now?").