diff --git a/contributors/emails/mehmet.kar@std.yildiz.edu.tr b/contributors/emails/mehmet.kar@std.yildiz.edu.tr new file mode 100644 index 00000000000..0f28e31662c --- /dev/null +++ b/contributors/emails/mehmet.kar@std.yildiz.edu.tr @@ -0,0 +1 @@ +mehmetkr-31 diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py new file mode 100644 index 00000000000..f3afb042764 --- /dev/null +++ b/tests/cli/conftest.py @@ -0,0 +1,50 @@ +"""Shared fixtures for CLI tests. + +prompt_toolkit / capsys isolation +--------------------------------- +``cli._cprint`` renders through ``prompt_toolkit.print_formatted_text``, +which — when called with no explicit ``output=`` — lazily creates an +``Output`` from ``sys.stdout`` **and caches it on the process-global default +``AppSession``** (``prompt_toolkit.application.current._current_app_session``, +a ``ContextVar`` with a module-level default). The cache is keyed to nothing +and never re-reads ``sys.stdout``. + +Under pytest, ``capsys`` swaps ``sys.stdout`` for a fresh buffer per test. +So the first CLI test that emits through ``_cprint`` (e.g. one exercising +``/queue``, which prints a "Queued: …" line) locks prompt_toolkit's cached +output onto *its* captured stdout. Every later ``capsys`` test that asserts +on ``_cprint`` output then reads an empty buffer, because the render went to +the first test's now-dead capture target. That is the mechanism behind the +order-dependent ``test_resume_quiet_stderr`` failure: it passes in isolation +and in its own file, but fails in a full ``tests/cli`` run. + +Reset the cached output before every CLI test so each one re-creates a fresh +prompt_toolkit ``Output`` bound to its own ``sys.stdout`` on first use. This +is a no-op when prompt_toolkit isn't importable and cheap otherwise (the +property re-creates lazily). +""" + +import pytest + + +@pytest.fixture(autouse=True) +def _reset_prompt_toolkit_output_cache(): + """Clear prompt_toolkit's cached AppSession output around each CLI test. + + See the module docstring for the capsys/prompt_toolkit interaction this + guards against. + """ + + def _clear() -> None: + try: + from prompt_toolkit.application.current import get_app_session + + get_app_session()._output = None + except Exception: + # prompt_toolkit not importable / internal shape changed — the + # tests that rely on this simply keep their prior behavior. + pass + + _clear() + yield + _clear() diff --git a/tests/cli/test_cli_init.py b/tests/cli/test_cli_init.py index d6c06bda14e..76757c0858c 100644 --- a/tests/cli/test_cli_init.py +++ b/tests/cli/test_cli_init.py @@ -44,13 +44,28 @@ def _make_cli(env_overrides=None, config_overrides=None, **kwargs): "prompt_toolkit.formatted_text": MagicMock(), "prompt_toolkit.auto_suggest": MagicMock(), } - with patch.dict(sys.modules, prompt_toolkit_stubs), \ - patch.dict("os.environ", clean_env, clear=False): - import cli as _cli_mod - _cli_mod = importlib.reload(_cli_mod) - with patch.object(_cli_mod, "get_tool_definitions", return_value=[]), \ - patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}): - return _cli_mod.HermesCLI(**kwargs) + try: + with patch.dict(sys.modules, prompt_toolkit_stubs), \ + patch.dict("os.environ", clean_env, clear=False): + import cli as _cli_mod + _cli_mod = importlib.reload(_cli_mod) + with patch.object(_cli_mod, "get_tool_definitions", return_value=[]), \ + patch.dict(_cli_mod.__dict__, {"CLI_CONFIG": _clean_config}): + return _cli_mod.HermesCLI(**kwargs) + finally: + # The reload above re-executed cli.py while prompt_toolkit was stubbed + # with MagicMocks, permanently rebinding cli's module globals + # (``_pt_print``, ``_PT_ANSI``, …) to those mocks. ``patch.dict`` + # restores ``sys.modules`` on exit, but NOT the names the reloaded + # module already bound — so ``sys.modules["cli"]`` is left with a + # mock ``_pt_print``, and ``cli._cprint`` then silently no-ops for + # every later test (one half of the order-dependent + # ``test_resume_quiet_stderr`` full-suite failure; the other half is + # the prompt_toolkit output cache reset in this dir's conftest). + # Reload once more with the real modules visible so cli's globals + # rebind cleanly. + import cli as _cli_restore + importlib.reload(_cli_restore) class TestMaxTurnsResolution: