From 73595245804c0401bf6d84b2b49e6a499c2b7680 Mon Sep 17 00:00:00 2001 From: Greg Holmes Date: Fri, 14 Aug 2026 15:00:23 +0100 Subject: [PATCH 1/3] test(core): stop output-format global leaking across tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit output_result reads the format from the process-global deepctl_core.output._output_config (via get_output_format), not from config.get(). But the five TestBaseCommand.test_output_result_* JSON tests only stubbed config.get.return_value = "json" — a no-op the code never reads — so they depended on an earlier CLI-invoking test having left the global on "json". They passed in the full suite by collection-order luck and failed in isolation (default format "default" → output_result returns early → _output_json / print_json never called). Fix both halves: - Patch deepctl_core.output._output_config in each JSON test (matching the yaml/table/csv tests) so they set the format the code actually reads. - Add an autouse fixture in packages/deepctl-core/tests/conftest.py that restores the pristine _output_config around every core test, so a format set by one test can no longer leak into the next. test_base.py in isolation: 5 failed -> 88 passed. Full core suite: 354 passed. --- packages/deepctl-core/tests/conftest.py | 31 +++++++ packages/deepctl-core/tests/unit/test_base.py | 82 ++++++++++++------- 2 files changed, 84 insertions(+), 29 deletions(-) create mode 100644 packages/deepctl-core/tests/conftest.py diff --git a/packages/deepctl-core/tests/conftest.py b/packages/deepctl-core/tests/conftest.py new file mode 100644 index 0000000..7104364 --- /dev/null +++ b/packages/deepctl-core/tests/conftest.py @@ -0,0 +1,31 @@ +"""Shared fixtures for deepctl-core tests.""" + +from __future__ import annotations + +import pytest + +from deepctl_core import output as _output + +# Snapshot the pristine output config at import time, before any test (or a +# test that invokes the CLI) mutates it. +_OUTPUT_DEFAULTS = dict(_output._output_config) + + +@pytest.fixture(autouse=True) +def _reset_output_config(): + """Isolate the process-global output config per test. + + ``deepctl_core.output._output_config`` is mutable module state that + ``setup_output`` (called by the CLI entrypoint and some tests) overwrites, + with nothing resetting it afterwards. That let a format set by one test leak + into the next — e.g. the ``output_result`` JSON tests only passed because an + earlier CLI-invoking test left the global on ``"json"`` (they fail in + isolation). Restore the pristine defaults around every test so + ``output_result`` — which reads this global via ``get_output_format`` — sees + a deterministic format regardless of ordering. + """ + _output._output_config.clear() + _output._output_config.update(_OUTPUT_DEFAULTS) + yield + _output._output_config.clear() + _output._output_config.update(_OUTPUT_DEFAULTS) diff --git a/packages/deepctl-core/tests/unit/test_base.py b/packages/deepctl-core/tests/unit/test_base.py index 358b731..42356b9 100644 --- a/packages/deepctl-core/tests/unit/test_base.py +++ b/packages/deepctl-core/tests/unit/test_base.py @@ -468,11 +468,14 @@ def test_output_result_none(self, mock_command_class): @pytest.mark.unit @patch("deepctl_core.base_command.console") + @patch( + "deepctl_core.output._output_config", + {"format": "json", "quiet": False, "verbose": False, "color": True}, + ) def test_output_result_json_dict(self, mock_console, mock_command_class): """Test output_result with JSON format and dict result.""" command = mock_command_class() config = Mock(spec=Config) - config.get.return_value = "json" # output.format returns json result = {"key": "value", "number": 42} command.output_result(result, config) @@ -484,11 +487,14 @@ def test_output_result_json_dict(self, mock_console, mock_command_class): @pytest.mark.unit @patch("deepctl_core.base_command.console") + @patch( + "deepctl_core.output._output_config", + {"format": "json", "quiet": False, "verbose": False, "color": True}, + ) def test_output_result_json_list(self, mock_console, mock_command_class): """Test output_result with JSON format and list result.""" command = mock_command_class() config = Mock(spec=Config) - config.get.return_value = "json" result = [{"id": 1}, {"id": 2}] command.output_result(result, config) @@ -499,11 +505,14 @@ def test_output_result_json_list(self, mock_console, mock_command_class): @pytest.mark.unit @patch("deepctl_core.base_command.console") + @patch( + "deepctl_core.output._output_config", + {"format": "json", "quiet": False, "verbose": False, "color": True}, + ) def test_output_result_json_string(self, mock_console, mock_command_class): """Test output_result with JSON format and string result.""" command = mock_command_class() config = Mock(spec=Config) - config.get.return_value = "json" result = "simple string" command.output_result(result, config) @@ -517,6 +526,10 @@ def test_output_result_json_string(self, mock_console, mock_command_class): assert json.loads(actual_output) == expected_data @pytest.mark.unit + @patch( + "deepctl_core.output._output_config", + {"format": "json", "quiet": False, "verbose": False, "color": True}, + ) def test_output_result_pydantic_model(self, mock_command_class): """Test output_result with Pydantic model.""" from pydantic import BaseModel @@ -527,7 +540,6 @@ class TestModel(BaseModel): command = mock_command_class() config = Mock(spec=Config) - config.get.return_value = "json" result = TestModel(name="test", value=123) @@ -538,6 +550,10 @@ class TestModel(BaseModel): mock_output_json.assert_called_once_with({"name": "test", "value": 123}) @pytest.mark.unit + @patch( + "deepctl_core.output._output_config", + {"format": "json", "quiet": False, "verbose": False, "color": True}, + ) def test_output_result_list_of_pydantic_models(self, mock_command_class): """Test output_result with list of Pydantic models.""" from pydantic import BaseModel @@ -548,7 +564,6 @@ class TestModel(BaseModel): command = mock_command_class() config = Mock(spec=Config) - config.get.return_value = "json" result = [ TestModel(name="test1", value=1), @@ -1049,27 +1064,30 @@ def handle(self, *args, **kwargs): @pytest.mark.unit def test_confirm_calls_click_when_guided_and_not_agentic(self, command): command._guided = True - with patch("deepctl_core.base_command._agentic", False), patch( - "deepctl_core.base_command.click.confirm", return_value=True - ) as mock: + with ( + patch("deepctl_core.base_command._agentic", False), + patch("deepctl_core.base_command.click.confirm", return_value=True) as mock, + ): assert command.confirm("OK?", default=False) is True mock.assert_called_once() @pytest.mark.unit def test_confirm_returns_default_when_not_guided(self, command): command._guided = False - with patch("deepctl_core.base_command._agentic", False), patch( - "deepctl_core.base_command.click.confirm" - ) as mock: + with ( + patch("deepctl_core.base_command._agentic", False), + patch("deepctl_core.base_command.click.confirm") as mock, + ): assert command.confirm("OK?", default=False) is False mock.assert_not_called() @pytest.mark.unit def test_confirm_returns_default_when_agentic(self, command): command._guided = True - with patch("deepctl_core.base_command._agentic", True), patch( - "deepctl_core.base_command.click.confirm" - ) as mock: + with ( + patch("deepctl_core.base_command._agentic", True), + patch("deepctl_core.base_command.click.confirm") as mock, + ): assert command.confirm("OK?", default=True) is True mock.assert_not_called() @@ -1077,27 +1095,32 @@ def test_confirm_returns_default_when_agentic(self, command): def test_confirm_returns_default_when_not_ci_friendly(self, command): command.ci_friendly = False command._guided = True - with patch("deepctl_core.base_command._agentic", False), patch( - "deepctl_core.base_command.click.confirm" - ) as mock: + with ( + patch("deepctl_core.base_command._agentic", False), + patch("deepctl_core.base_command.click.confirm") as mock, + ): assert command.confirm("OK?", default=False) is False mock.assert_not_called() @pytest.mark.unit def test_prompt_calls_click_when_guided(self, command): command._guided = True - with patch("deepctl_core.base_command._agentic", False), patch( - "deepctl_core.base_command.click.prompt", return_value="user-input" - ) as mock: + with ( + patch("deepctl_core.base_command._agentic", False), + patch( + "deepctl_core.base_command.click.prompt", return_value="user-input" + ) as mock, + ): assert command.prompt("Name?", default="alice") == "user-input" mock.assert_called_once() @pytest.mark.unit def test_prompt_returns_default_when_not_guided(self, command): command._guided = False - with patch("deepctl_core.base_command._agentic", False), patch( - "deepctl_core.base_command.click.prompt" - ) as mock: + with ( + patch("deepctl_core.base_command._agentic", False), + patch("deepctl_core.base_command.click.prompt") as mock, + ): assert command.prompt("Name?", default="alice") == "alice" mock.assert_not_called() @@ -1105,9 +1128,12 @@ def test_prompt_returns_default_when_not_guided(self, command): def test_prompt_with_no_default_still_prompts_when_not_guided(self, command): # Pre-existing safety: prompt() only short-circuits when default is not None command._guided = False - with patch("deepctl_core.base_command._agentic", False), patch( - "deepctl_core.base_command.click.prompt", return_value="typed" - ) as mock: + with ( + patch("deepctl_core.base_command._agentic", False), + patch( + "deepctl_core.base_command.click.prompt", return_value="typed" + ) as mock, + ): assert command.prompt("Name?", default=None) == "typed" mock.assert_called_once() @@ -1161,9 +1187,7 @@ def test_any_commandline_arg_breaks_guided(self, command): @pytest.mark.unit def test_env_var_breaks_guided(self, command): - ctx = self._ctx_with_sources( - {"foo": "DEFAULT", "bar": "ENVIRONMENT"} - ) + ctx = self._ctx_with_sources({"foo": "DEFAULT", "bar": "ENVIRONMENT"}) with patch("deepctl_core.base_command._agentic", False): assert command.is_guided(ctx) is False From e1c81903f6ea1eb6be2343f2f4879ce3d2824b53 Mon Sep 17 00:00:00 2001 From: Greg Holmes Date: Fri, 14 Aug 2026 15:11:32 +0100 Subject: [PATCH 2/3] style(core): fix fixture import order --- packages/deepctl-core/tests/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/deepctl-core/tests/conftest.py b/packages/deepctl-core/tests/conftest.py index 7104364..447a6e0 100644 --- a/packages/deepctl-core/tests/conftest.py +++ b/packages/deepctl-core/tests/conftest.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from deepctl_core import output as _output # Snapshot the pristine output config at import time, before any test (or a From f1469c1cf2a6787322742bec455b204c1787c2b4 Mon Sep 17 00:00:00 2001 From: Greg Holmes Date: Fri, 14 Aug 2026 17:13:25 +0100 Subject: [PATCH 3/3] refactor(core): mirror full _output_config key set in output-format tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 11 format-test patch dicts stood in for the process-global deepctl_core.output._output_config but carried only 4 of its 5 keys (agentic was missing). Safe on today's path — output_result -> get_output_format reads only ["format"] and _output_json prints directly — but other output.py helpers read _output_config["agentic"], so a future refactor routing output through one of them would KeyError every 4-key-dict test at once. Add a shared _OUTPUT_CONFIG_DEFAULTS constant mirroring the real global's full key set and spread it at each site ({**_OUTPUT_CONFIG_DEFAULTS, "format": "json"}), removing both the drift risk and the duplication. No behavior change: test_base.py 88 passed in isolation, full suite 976. --- packages/deepctl-core/tests/unit/test_base.py | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/packages/deepctl-core/tests/unit/test_base.py b/packages/deepctl-core/tests/unit/test_base.py index 42356b9..f2d3086 100644 --- a/packages/deepctl-core/tests/unit/test_base.py +++ b/packages/deepctl-core/tests/unit/test_base.py @@ -8,6 +8,17 @@ from click.testing import CliRunner from deepctl_core import AuthManager, BaseCommand, Config, DeepgramClient +# Mirrors the full key set of deepctl_core.output._output_config so a patched +# stand-in can't drift from the real global. Spread with a format override, +# e.g. {**_OUTPUT_CONFIG_DEFAULTS, "format": "json"}, at each patch site. +_OUTPUT_CONFIG_DEFAULTS = { + "format": "default", + "quiet": False, + "verbose": False, + "color": True, + "agentic": False, +} + class TestBaseCommand: """Test suite for BaseCommand class.""" @@ -470,7 +481,7 @@ def test_output_result_none(self, mock_command_class): @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "json", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "json"}, ) def test_output_result_json_dict(self, mock_console, mock_command_class): """Test output_result with JSON format and dict result.""" @@ -489,7 +500,7 @@ def test_output_result_json_dict(self, mock_console, mock_command_class): @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "json", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "json"}, ) def test_output_result_json_list(self, mock_console, mock_command_class): """Test output_result with JSON format and list result.""" @@ -507,7 +518,7 @@ def test_output_result_json_list(self, mock_console, mock_command_class): @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "json", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "json"}, ) def test_output_result_json_string(self, mock_console, mock_command_class): """Test output_result with JSON format and string result.""" @@ -528,7 +539,7 @@ def test_output_result_json_string(self, mock_console, mock_command_class): @pytest.mark.unit @patch( "deepctl_core.output._output_config", - {"format": "json", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "json"}, ) def test_output_result_pydantic_model(self, mock_command_class): """Test output_result with Pydantic model.""" @@ -552,7 +563,7 @@ class TestModel(BaseModel): @pytest.mark.unit @patch( "deepctl_core.output._output_config", - {"format": "json", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "json"}, ) def test_output_result_list_of_pydantic_models(self, mock_command_class): """Test output_result with list of Pydantic models.""" @@ -582,7 +593,7 @@ class TestModel(BaseModel): @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "yaml", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "yaml"}, ) def test_output_result_yaml(self, mock_console, mock_command_class): """Test output_result with YAML format.""" @@ -602,7 +613,7 @@ def test_output_result_yaml(self, mock_console, mock_command_class): @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "table", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "table"}, ) def test_output_result_table_list_of_dicts(self, mock_console, mock_command_class): """Test output_result with table format and list of dicts.""" @@ -621,7 +632,7 @@ def test_output_result_table_list_of_dicts(self, mock_console, mock_command_clas @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "table", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "table"}, ) def test_output_result_table_dict(self, mock_console, mock_command_class): """Test output_result with table format and dict.""" @@ -638,7 +649,7 @@ def test_output_result_table_dict(self, mock_console, mock_command_class): @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "csv", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "csv"}, ) def test_output_result_csv_list_of_dicts(self, mock_console, mock_command_class): """Test output_result with CSV format and list of dicts.""" @@ -659,7 +670,7 @@ def test_output_result_csv_list_of_dicts(self, mock_console, mock_command_class) @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "csv", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "csv"}, ) def test_output_result_csv_dict(self, mock_console, mock_command_class): """Test output_result with CSV format and dict.""" @@ -680,7 +691,7 @@ def test_output_result_csv_dict(self, mock_console, mock_command_class): @patch("deepctl_core.base_command.console") @patch( "deepctl_core.output._output_config", - {"format": "unknown", "quiet": False, "verbose": False, "color": True}, + {**_OUTPUT_CONFIG_DEFAULTS, "format": "unknown"}, ) def test_output_result_unknown_format(self, mock_console, mock_command_class): """Test output_result with unknown format falls back to JSON."""