From 6cb84da93d7588ee12c64f36a0f2490b590c5466 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 12:46:35 +0100 Subject: [PATCH 01/23] build: Add pytest-mock --- requirements/requirements-dev.in | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/requirements-dev.in b/requirements/requirements-dev.in index a7bbf3aeaf2..e52d0cdc30a 100644 --- a/requirements/requirements-dev.in +++ b/requirements/requirements-dev.in @@ -1,6 +1,7 @@ pytest pytest-asyncio pytest-env +pytest-mock pip-tools lox matplotlib From 8256d68aa4cf9e930be500baebe4de3fee90133f Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 12:47:15 +0100 Subject: [PATCH 02/23] build: Add pytest-mock related updates to requirement files --- requirements/requirements-dev.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt index 2c21ad40b50..d1cebb9dff6 100644 --- a/requirements/requirements-dev.txt +++ b/requirements/requirements-dev.txt @@ -215,6 +215,7 @@ pytest==9.0.1 # -r requirements/requirements-dev.in # pytest-asyncio # pytest-env + # pytest-mock pytest-asyncio==1.3.0 # via # -c requirements/common-constraints.txt @@ -223,6 +224,10 @@ pytest-env==1.2.0 # via # -c requirements/common-constraints.txt # -r requirements/requirements-dev.in +pytest-mock==3.15.1 + # via + # -c requirements/common-constraints.txt + # -r requirements/requirements-dev.in python-dateutil==2.9.0.post0 # via # -c requirements/common-constraints.txt From 0c2c6d50a4a110c49a475eda2eb14bafa14c471b Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 12:21:53 +0100 Subject: [PATCH 03/23] test: add smoke tests to verify main entry points execute Added test_main_smoke.py with two smoke tests: - test_main_async_executes: Tests async entry point - test_main_executes: Tests sync entry point These tests expose a bug where Path object is passed to generate_search_path_list() which expects a string, causing AttributeError when trying to call lstrip() on Path object. --- tests/basic/test_main_smoke.py | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/basic/test_main_smoke.py diff --git a/tests/basic/test_main_smoke.py b/tests/basic/test_main_smoke.py new file mode 100644 index 00000000000..1b998b2d34c --- /dev/null +++ b/tests/basic/test_main_smoke.py @@ -0,0 +1,36 @@ +import os + +import pytest +from prompt_toolkit.input import DummyInput +from prompt_toolkit.output import DummyOutput + +from aider.main import main, main_async + + +@pytest.fixture(autouse=True) +def isolated_env(tmp_path, monkeypatch, mocker): + """Completely isolated test environment with no real API keys.""" + fake_home = tmp_path / "home" + fake_home.mkdir() + + clean_env = { + "OPENAI_API_KEY": "test-key", + "HOME": str(fake_home), + "AIDER_CHECK_UPDATE": "false", + "AIDER_ANALYTICS": "false", + } + + mocker.patch.dict(os.environ, clean_env, clear=True) + mocker.patch("aider.io.webbrowser.open", side_effect=AssertionError("Browser should not open during tests")) + mocker.patch("builtins.input", return_value=None) + monkeypatch.chdir(tmp_path) + + yield tmp_path + + +async def test_main_async_executes(): + await main_async(["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput()) + + +def test_main_executes(): + main(["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput()) From a01588810f493f7518eeec1c7c23c75a45867a70 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 13:36:21 +0100 Subject: [PATCH 04/23] refactor: convert test_main async tests to synchronous Convert 76 async test methods to synchronous by: - Removing 'async def' -> 'def' for test methods - Removing 'await' from main() calls (main() handles asyncio.run internally) 3 tests remain async (marked with TODO comments) as they call async helper functions (setup_git, check_gitignore, coder.allowed_to_edit) and will need further refactoring. This fixes the issue where async tests were never executing because unittest.TestCase doesn't support async test methods. --- tests/basic/test_main.py | 359 ++++++++++++++++++++------------------- 1 file changed, 181 insertions(+), 178 deletions(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 7ed6564e5c3..2bae0af46ff 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -45,43 +45,43 @@ def tearDown(self): self.input_patcher.stop() self.webbrowser_patcher.stop() - async def test_main_with_empty_dir_no_files_on_command(self): - await main(["--no-git", "--exit", "--yes"], input=DummyInput(), output=DummyOutput()) + def test_main_with_empty_dir_no_files_on_command(self): + main(["--no-git", "--exit", "--yes"], input=DummyInput(), output=DummyOutput()) - async def test_main_with_emptqy_dir_new_file(self): - await main( + def test_main_with_emptqy_dir_new_file(self): + main( ["foo.txt", "--yes", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput() ) self.assertTrue(os.path.exists("foo.txt")) @patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message") - async def test_main_with_empty_git_dir_new_file(self, _): + def test_main_with_empty_git_dir_new_file(self, _): make_repo() - await main(["--yes", "foo.txt", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes", "foo.txt", "--exit"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) @patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message") - async def test_main_with_empty_git_dir_new_files(self, _): + def test_main_with_empty_git_dir_new_files(self, _): make_repo() - await main( + main( ["--yes", "foo.txt", "bar.txt", "--exit"], input=DummyInput(), output=DummyOutput() ) self.assertTrue(os.path.exists("foo.txt")) self.assertTrue(os.path.exists("bar.txt")) - async def test_main_with_dname_and_fname(self): + def test_main_with_dname_and_fname(self): subdir = Path("subdir") subdir.mkdir() make_repo(str(subdir)) - res = await main(["subdir", "foo.txt"], input=DummyInput(), output=DummyOutput()) + res = main(["subdir", "foo.txt"], input=DummyInput(), output=DummyOutput()) self.assertNotEqual(res, None) @patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message") - async def test_main_with_subdir_repo_fnames(self, _): + def test_main_with_subdir_repo_fnames(self, _): subdir = Path("subdir") subdir.mkdir() make_repo(str(subdir)) - await main( + main( ["--yes", str(subdir / "foo.txt"), str(subdir / "bar.txt"), "--exit"], input=DummyInput(), output=DummyOutput(), @@ -89,9 +89,9 @@ async def test_main_with_subdir_repo_fnames(self, _): self.assertTrue((subdir / "foo.txt").exists()) self.assertTrue((subdir / "bar.txt").exists()) - async def test_main_copy_paste_model_overrides(self): + def test_main_copy_paste_model_overrides(self): overrides = json.dumps({"gpt-4o": {"fast": {"temperature": 0.42}}}) - coder = await main( + coder = main( [ "--no-git", "--exit", @@ -112,10 +112,10 @@ async def test_main_copy_paste_model_overrides(self): self.assertEqual(coder.main_model.override_kwargs, {"temperature": 0.42}) @patch("aider.main.ClipboardWatcher") - async def test_main_copy_paste_flag_sets_mode(self, mock_watcher): + def test_main_copy_paste_flag_sets_mode(self, mock_watcher): mock_watcher.return_value = MagicMock() - coder = await main( + coder = main( ["--no-git", "--exit", "--yes", "--copy-paste"], input=DummyInput(), output=DummyOutput(), @@ -128,22 +128,22 @@ async def test_main_copy_paste_flag_sets_mode(self, mock_watcher): self.assertTrue(coder.copy_paste_mode) self.assertFalse(coder.manual_copy_paste) - async def test_main_with_git_config_yml(self): + def test_main_with_git_config_yml(self): make_repo() Path(".aider.conf.yml").write_text("auto-commits: false\n") with patch("aider.coders.Coder.create") as MockCoder: - await main(["--yes"], input=DummyInput(), output=DummyOutput()) + main(["--yes"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is False Path(".aider.conf.yml").write_text("auto-commits: true\n") with patch("aider.coders.Coder.create") as MockCoder: - await main([], input=DummyInput(), output=DummyOutput()) + main([], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is True - async def test_main_with_empty_git_dir_new_subdir_file(self): + def test_main_with_empty_git_dir_new_subdir_file(self): make_repo() subdir = Path("subdir") subdir.mkdir() @@ -155,8 +155,9 @@ async def test_main_with_empty_git_dir_new_subdir_file(self): # This will throw a git error on windows if get_tracked_files doesn't # properly convert git/posix/paths to git\posix\paths. # Because aider will try and `git add` a file that's already in the repo. - await main(["--yes", str(fname), "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes", str(fname), "--exit"], input=DummyInput(), output=DummyOutput()) + # TODO: This test needs to be converted to call async functions via asyncio.run() or refactored async def test_setup_git(self): io = InputOutput(pretty=False, yes=True) git_root = await setup_git(None, io) @@ -169,6 +170,7 @@ async def test_setup_git(self): self.assertTrue(gitignore.exists()) self.assertEqual(".aider*", gitignore.read_text().splitlines()[0]) + # TODO: This test needs to be converted to call async functions via asyncio.run() or refactored async def test_check_gitignore(self): with GitTemporaryDirectory(): os.environ["GIT_CONFIG_GLOBAL"] = "globalgitconfig" @@ -195,7 +197,7 @@ async def test_check_gitignore(self): self.assertEqual("one\ntwo\n.aider*\n.env\n", gitignore.read_text()) del os.environ["GIT_CONFIG_GLOBAL"] - async def test_command_line_gitignore_files_flag(self): + def test_command_line_gitignore_files_flag(self): with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) @@ -211,7 +213,7 @@ async def test_command_line_gitignore_files_flag(self): abs_ignored_file = str(ignored_file.resolve()) # Test without the --add-gitignore-files flag (default: False) - coder = await main( + coder = main( ["--exit", "--yes", abs_ignored_file], input=DummyInput(), output=DummyOutput(), @@ -222,7 +224,7 @@ async def test_command_line_gitignore_files_flag(self): self.assertNotIn(abs_ignored_file, coder.abs_fnames) # Test with --add-gitignore-files set to True - coder = await main( + coder = main( ["--add-gitignore-files", "--exit", "--yes", abs_ignored_file], input=DummyInput(), output=DummyOutput(), @@ -233,7 +235,7 @@ async def test_command_line_gitignore_files_flag(self): self.assertIn(abs_ignored_file, coder.abs_fnames) # Test with --add-gitignore-files set to False - coder = await main( + coder = main( ["--no-add-gitignore-files", "--exit", "--yes", abs_ignored_file], input=DummyInput(), output=DummyOutput(), @@ -243,7 +245,7 @@ async def test_command_line_gitignore_files_flag(self): # Verify the ignored file is not in the chat self.assertNotIn(abs_ignored_file, coder.abs_fnames) - async def test_add_command_gitignore_files_flag(self): + def test_add_command_gitignore_files_flag(self): with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) @@ -260,7 +262,7 @@ async def test_add_command_gitignore_files_flag(self): rel_ignored_file = "ignored.txt" # Test without the --add-gitignore-files flag (default: False) - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -275,7 +277,7 @@ async def test_add_command_gitignore_files_flag(self): self.assertNotIn(abs_ignored_file, coder.abs_fnames) # Test with --add-gitignore-files set to True - coder = await main( + coder = main( ["--add-gitignore-files", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -289,7 +291,7 @@ async def test_add_command_gitignore_files_flag(self): self.assertIn(abs_ignored_file, coder.abs_fnames) # Test with --add-gitignore-files set to False - coder = await main( + coder = main( ["--no-add-gitignore-files", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -303,36 +305,36 @@ async def test_add_command_gitignore_files_flag(self): # Verify the ignored file is not in the chat self.assertNotIn(abs_ignored_file, coder.abs_fnames) - async def test_main_args(self): + def test_main_args(self): with patch("aider.coders.Coder.create") as MockCoder: # --yes will just ok the git repo without blocking on input # following calls to main will see the new repo already - await main(["--no-auto-commits", "--yes"], input=DummyInput()) + main(["--no-auto-commits", "--yes"], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is False with patch("aider.coders.Coder.create") as MockCoder: - await main(["--auto-commits"], input=DummyInput()) + main(["--auto-commits"], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is True with patch("aider.coders.Coder.create") as MockCoder: - await main([], input=DummyInput()) + main([], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["dirty_commits"] is True assert kwargs["auto_commits"] is True with patch("aider.coders.Coder.create") as MockCoder: - await main(["--no-dirty-commits"], input=DummyInput()) + main(["--no-dirty-commits"], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["dirty_commits"] is False with patch("aider.coders.Coder.create") as MockCoder: - await main(["--dirty-commits"], input=DummyInput()) + main(["--dirty-commits"], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["dirty_commits"] is True - async def test_env_file_override(self): + def test_env_file_override(self): with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) git_env = git_dir / ".env" @@ -356,7 +358,7 @@ async def test_env_file_override(self): named_env.write_text("A=named") with patch("pathlib.Path.home", return_value=fake_home): - await main(["--yes", "--exit", "--env-file", str(named_env)]) + main(["--yes", "--exit", "--env-file", str(named_env)]) self.assertEqual(os.environ["A"], "named") self.assertEqual(os.environ["B"], "cwd") @@ -364,7 +366,7 @@ async def test_env_file_override(self): self.assertEqual(os.environ["D"], "home") self.assertEqual(os.environ["E"], "existing") - async def test_message_file_flag(self): + def test_message_file_flag(self): message_file_content = "This is a test message from a file." message_file_path = tempfile.mktemp() with open(message_file_path, "w", encoding="utf-8") as message_file: @@ -380,7 +382,7 @@ async def mock_run(*args, **kwargs): mock_coder_instance.run = AsyncMock() MockCoder.return_value = mock_coder_instance - await main( + main( ["--yes", "--message-file", message_file_path], input=DummyInput(), output=DummyOutput(), @@ -390,7 +392,7 @@ async def mock_run(*args, **kwargs): os.remove(message_file_path) - async def test_encodings_arg(self): + def test_encodings_arg(self): fname = "foo.py" with GitTemporaryDirectory(): @@ -403,51 +405,51 @@ def side_effect(*args, **kwargs): MockSend.side_effect = side_effect - await main(["--yes", fname, "--encoding", "iso-8859-15"]) + main(["--yes", fname, "--encoding", "iso-8859-15"]) - async def test_main_exit_calls_version_check(self): + def test_main_exit_calls_version_check(self): with GitTemporaryDirectory(): with ( patch("aider.main.check_version") as mock_check_version, patch("aider.main.InputOutput") as mock_input_output, ): - await main(["--exit", "--check-update"], input=DummyInput(), output=DummyOutput()) + main(["--exit", "--check-update"], input=DummyInput(), output=DummyOutput()) mock_check_version.assert_called_once() mock_input_output.assert_called_once() @patch("aider.main.InputOutput") @patch("aider.coders.base_coder.Coder.run") - async def test_main_message_adds_to_input_history(self, mock_run, MockInputOutput): + def test_main_message_adds_to_input_history(self, mock_run, MockInputOutput): test_message = "test message" mock_io_instance = MockInputOutput.return_value - await main(["--message", test_message], input=DummyInput(), output=DummyOutput()) + main(["--message", test_message], input=DummyInput(), output=DummyOutput()) mock_io_instance.add_to_input_history.assert_called_once_with(test_message) @patch("aider.main.InputOutput") @patch("aider.coders.base_coder.Coder.run") - async def test_yes(self, mock_run, MockInputOutput): + def test_yes(self, mock_run, MockInputOutput): test_message = "test message" - await main(["--yes", "--message", test_message]) + main(["--yes", "--message", test_message]) args, kwargs = MockInputOutput.call_args self.assertTrue(args[1]) @patch("aider.main.InputOutput") @patch("aider.coders.base_coder.Coder.run") - async def test_default_yes(self, mock_run, MockInputOutput): + def test_default_yes(self, mock_run, MockInputOutput): test_message = "test message" - await main(["--message", test_message]) + main(["--message", test_message]) args, kwargs = MockInputOutput.call_args self.assertEqual(args[1], None) - async def test_dark_mode_sets_code_theme(self): + def test_dark_mode_sets_code_theme(self): # Mock InputOutput to capture the configuration with patch("aider.main.InputOutput") as MockInputOutput: MockInputOutput.return_value.get_input.return_value = None - await main( + main( ["--dark-mode", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput() ) # Ensure InputOutput was called @@ -456,11 +458,11 @@ async def test_dark_mode_sets_code_theme(self): _, kwargs = MockInputOutput.call_args self.assertEqual(kwargs["code_theme"], "monokai") - async def test_light_mode_sets_code_theme(self): + def test_light_mode_sets_code_theme(self): # Mock InputOutput to capture the configuration with patch("aider.main.InputOutput") as MockInputOutput: MockInputOutput.return_value.get_input.return_value = None - await main( + main( ["--light-mode", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput() ) # Ensure InputOutput was called @@ -474,12 +476,12 @@ async def create_env_file(self, file_name, content): env_file_path.write_text(content) return env_file_path - async def test_env_file_flag_sets_automatic_variable(self): + def test_env_file_flag_sets_automatic_variable(self): env_file_path = self.create_env_file(".env.test", "AIDER_DARK_MODE=True") with patch("aider.main.InputOutput") as MockInputOutput: MockInputOutput.return_value.get_input.return_value = None MockInputOutput.return_value.get_input.confirm_ask = True - await main( + main( ["--env-file", str(env_file_path), "--no-git", "--exit"], input=DummyInput(), output=DummyOutput(), @@ -489,35 +491,35 @@ async def test_env_file_flag_sets_automatic_variable(self): _, kwargs = MockInputOutput.call_args self.assertEqual(kwargs["code_theme"], "monokai") - async def test_default_env_file_sets_automatic_variable(self): + def test_default_env_file_sets_automatic_variable(self): self.create_env_file(".env", "AIDER_DARK_MODE=True") with patch("aider.main.InputOutput") as MockInputOutput: MockInputOutput.return_value.get_input.return_value = None MockInputOutput.return_value.get_input.confirm_ask = True - await main(["--no-git", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--no-git", "--exit"], input=DummyInput(), output=DummyOutput()) # Ensure InputOutput was called MockInputOutput.assert_called_once() # Check if the color settings are for dark mode _, kwargs = MockInputOutput.call_args self.assertEqual(kwargs["code_theme"], "monokai") - async def test_false_vals_in_env_file(self): + def test_false_vals_in_env_file(self): self.create_env_file(".env", "AIDER_SHOW_DIFFS=off") with patch("aider.coders.Coder.create") as MockCoder: - await main(["--no-git", "--yes"], input=DummyInput(), output=DummyOutput()) + main(["--no-git", "--yes"], input=DummyInput(), output=DummyOutput()) MockCoder.assert_called_once() _, kwargs = MockCoder.call_args self.assertEqual(kwargs["show_diffs"], False) - async def test_true_vals_in_env_file(self): + def test_true_vals_in_env_file(self): self.create_env_file(".env", "AIDER_SHOW_DIFFS=on") with patch("aider.coders.Coder.create") as MockCoder: - await main(["--no-git", "--yes"], input=DummyInput(), output=DummyOutput()) + main(["--no-git", "--yes"], input=DummyInput(), output=DummyOutput()) MockCoder.assert_called_once() _, kwargs = MockCoder.call_args self.assertEqual(kwargs["show_diffs"], True) - async def test_lint_option(self): + def test_lint_option(self): with GitTemporaryDirectory() as git_dir: # Create a dirty file in the root dirty_file = Path("dirty_file.py") @@ -541,7 +543,7 @@ async def test_lint_option(self): MockLinter.return_value = "" # Run main with --lint option - await main(["--lint", "--yes"]) + main(["--lint", "--yes"]) # Check if the Linter was called with a filename ending in "dirty_file.py" # but not ending in "subdir/dirty_file.py" @@ -550,10 +552,10 @@ async def test_lint_option(self): self.assertTrue(called_arg.endswith("dirty_file.py")) self.assertFalse(called_arg.endswith(f"subdir{os.path.sep}dirty_file.py")) - async def test_verbose_mode_lists_env_vars(self): + def test_verbose_mode_lists_env_vars(self): self.create_env_file(".env", "AIDER_DARK_MODE=on") with patch("sys.stdout", new_callable=StringIO) as mock_stdout: - await main( + main( ["--no-git", "--verbose", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -569,7 +571,7 @@ async def test_verbose_mode_lists_env_vars(self): self.assertRegex(relevant_output, r"AIDER_DARK_MODE:\s+on") self.assertRegex(relevant_output, r"dark_mode:\s+True") - async def test_yaml_config_file_loading(self): + def test_yaml_config_file_loading(self): with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) @@ -599,7 +601,7 @@ async def test_yaml_config_file_loading(self): patch("aider.coders.Coder.create") as MockCoder, ): # Test loading from specified config file - await main( + main( ["--yes", "--exit", "--config", str(named_config)], input=DummyInput(), output=DummyOutput(), @@ -609,7 +611,7 @@ async def test_yaml_config_file_loading(self): self.assertEqual(kwargs["map_tokens"], 8192) # Test loading from current working directory - await main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args print("kwargs:", kwargs) # Add this line for debugging self.assertIn("main_model", kwargs, "main_model key not found in kwargs") @@ -618,46 +620,46 @@ async def test_yaml_config_file_loading(self): # Test loading from git root cwd_config.unlink() - await main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args self.assertEqual(kwargs["main_model"].name, "gpt-4") self.assertEqual(kwargs["map_tokens"], 2048) # Test loading from home directory git_config.unlink() - await main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args self.assertEqual(kwargs["main_model"].name, "gpt-3.5-turbo") self.assertEqual(kwargs["map_tokens"], 1024) - async def test_map_tokens_option(self): + def test_map_tokens_option(self): with GitTemporaryDirectory(): with patch("aider.coders.base_coder.RepoMap") as MockRepoMap: MockRepoMap.return_value.max_map_tokens = 0 - await main( + main( ["--model", "gpt-4", "--map-tokens", "0", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), ) MockRepoMap.assert_not_called() - async def test_map_tokens_option_with_non_zero_value(self): + def test_map_tokens_option_with_non_zero_value(self): with GitTemporaryDirectory(): with patch("aider.coders.base_coder.RepoMap") as MockRepoMap: MockRepoMap.return_value.max_map_tokens = 1000 - await main( + main( ["--model", "gpt-4", "--map-tokens", "1000", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), ) MockRepoMap.assert_called_once() - async def test_read_option(self): + def test_read_option(self): with GitTemporaryDirectory(): test_file = "test_file.txt" Path(test_file).touch() - coder = await main( + coder = main( ["--read", test_file, "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -666,14 +668,14 @@ async def test_read_option(self): self.assertIn(str(Path(test_file).resolve()), coder.abs_read_only_fnames) - async def test_read_option_with_external_file(self): + def test_read_option_with_external_file(self): with tempfile.NamedTemporaryFile(mode="w", delete=False) as external_file: external_file.write("External file content") external_file_path = external_file.name try: with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--read", external_file_path, "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -685,7 +687,7 @@ async def test_read_option_with_external_file(self): finally: os.unlink(external_file_path) - async def test_model_metadata_file(self): + def test_model_metadata_file(self): # Re-init so we don't have old data lying around from earlier test cases from aider import models @@ -702,7 +704,7 @@ async def test_model_metadata_file(self): metadata_content = {"deepseek/deepseek-chat": {"max_input_tokens": 1234}} metadata_file.write_text(json.dumps(metadata_content)) - coder = await main( + coder = main( [ "--model", "deepseek/deepseek-chat", @@ -718,14 +720,14 @@ async def test_model_metadata_file(self): self.assertEqual(coder.main_model.info["max_input_tokens"], 1234) - async def test_sonnet_and_cache_options(self): + def test_sonnet_and_cache_options(self): with GitTemporaryDirectory(): with patch("aider.coders.base_coder.RepoMap") as MockRepoMap: mock_repo_map = MagicMock() mock_repo_map.max_map_tokens = 1000 # Set a specific value MockRepoMap.return_value = mock_repo_map - await main( + main( ["--sonnet", "--cache-prompts", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -737,9 +739,9 @@ async def test_sonnet_and_cache_options(self): call_kwargs.get("refresh"), "files" ) # Check the 'refresh' keyword argument - async def test_sonnet_and_cache_prompts_options(self): + def test_sonnet_and_cache_prompts_options(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--sonnet", "--cache-prompts", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -748,9 +750,9 @@ async def test_sonnet_and_cache_prompts_options(self): self.assertTrue(coder.add_cache_headers) - async def test_4o_and_cache_options(self): + def test_4o_and_cache_options(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--4o", "--cache-prompts", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -759,9 +761,9 @@ async def test_4o_and_cache_options(self): self.assertFalse(coder.add_cache_headers) - async def test_return_coder(self): + def test_return_coder(self): with GitTemporaryDirectory(): - result = await main( + result = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -769,7 +771,7 @@ async def test_return_coder(self): ) self.assertIsInstance(result, Coder) - result = await main( + result = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -777,9 +779,9 @@ async def test_return_coder(self): ) self.assertIsNone(result) - async def test_map_mul_option(self): + def test_map_mul_option(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--map-mul", "5", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -788,9 +790,9 @@ async def test_map_mul_option(self): self.assertIsInstance(coder, Coder) self.assertEqual(coder.repo_map.map_mul_no_files, 5) - async def test_suggest_shell_commands_default(self): + def test_suggest_shell_commands_default(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -798,9 +800,9 @@ async def test_suggest_shell_commands_default(self): ) self.assertTrue(coder.suggest_shell_commands) - async def test_suggest_shell_commands_disabled(self): + def test_suggest_shell_commands_disabled(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--no-suggest-shell-commands", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -808,9 +810,9 @@ async def test_suggest_shell_commands_disabled(self): ) self.assertFalse(coder.suggest_shell_commands) - async def test_suggest_shell_commands_enabled(self): + def test_suggest_shell_commands_enabled(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--suggest-shell-commands", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -818,9 +820,9 @@ async def test_suggest_shell_commands_enabled(self): ) self.assertTrue(coder.suggest_shell_commands) - async def test_detect_urls_default(self): + def test_detect_urls_default(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -828,9 +830,9 @@ async def test_detect_urls_default(self): ) self.assertTrue(coder.detect_urls) - async def test_detect_urls_disabled(self): + def test_detect_urls_disabled(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--no-detect-urls", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -838,9 +840,9 @@ async def test_detect_urls_disabled(self): ) self.assertFalse(coder.detect_urls) - async def test_detect_urls_enabled(self): + def test_detect_urls_enabled(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--detect-urls", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -848,7 +850,7 @@ async def test_detect_urls_enabled(self): ) self.assertTrue(coder.detect_urls) - async def test_accepts_settings_warnings(self): + def test_accepts_settings_warnings(self): # Test that appropriate warnings are shown based on accepts_settings configuration with GitTemporaryDirectory(): # Test model that accepts the thinking_tokens setting @@ -856,7 +858,7 @@ async def test_accepts_settings_warnings(self): patch("aider.io.InputOutput.tool_warning") as mock_warning, patch("aider.models.Model.set_thinking_tokens") as mock_set_thinking, ): - await main( + main( [ "--model", "anthropic/claude-3-7-sonnet-20250219", @@ -879,7 +881,7 @@ async def test_accepts_settings_warnings(self): patch("aider.io.InputOutput.tool_warning") as mock_warning, patch("aider.models.Model.set_thinking_tokens") as mock_set_thinking, ): - await main( + main( [ "--model", "gpt-4o", @@ -906,7 +908,7 @@ async def test_accepts_settings_warnings(self): patch("aider.io.InputOutput.tool_warning") as mock_warning, patch("aider.models.Model.set_reasoning_effort") as mock_set_reasoning, ): - await main( + main( ["--model", "o1", "--reasoning-effort", "3", "--yes", "--exit"], input=DummyInput(), output=DummyOutput(), @@ -922,7 +924,7 @@ async def test_accepts_settings_warnings(self): patch("aider.io.InputOutput.tool_warning") as mock_warning, patch("aider.models.Model.set_reasoning_effort") as mock_set_reasoning, ): - await main( + main( ["--model", "gpt-3.5-turbo", "--reasoning-effort", "3", "--yes", "--exit"], input=DummyInput(), output=DummyOutput(), @@ -937,7 +939,7 @@ async def test_accepts_settings_warnings(self): mock_set_reasoning.assert_not_called() @patch("aider.models.ModelInfoManager.set_verify_ssl") - async def test_no_verify_ssl_sets_model_info_manager(self, mock_set_verify_ssl): + def test_no_verify_ssl_sets_model_info_manager(self, mock_set_verify_ssl): with GitTemporaryDirectory(): # Mock Model class to avoid actual model initialization with patch("aider.models.Model") as mock_model: @@ -951,27 +953,27 @@ async def test_no_verify_ssl_sets_model_info_manager(self, mock_set_verify_ssl): # Mock fuzzy_match_models to avoid string operations on MagicMock with patch("aider.models.fuzzy_match_models", return_value=[]): - await main( + main( ["--no-verify-ssl", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), ) mock_set_verify_ssl.assert_called_once_with(False) - async def test_pytest_env_vars(self): + def test_pytest_env_vars(self): # Verify that environment variables from pytest.ini are properly set self.assertEqual(os.environ.get("AIDER_ANALYTICS"), "false") - async def test_set_env_single(self): + def test_set_env_single(self): # Test setting a single environment variable with GitTemporaryDirectory(): - await main(["--set-env", "TEST_VAR=test_value", "--exit", "--yes"]) + main(["--set-env", "TEST_VAR=test_value", "--exit", "--yes"]) self.assertEqual(os.environ.get("TEST_VAR"), "test_value") - async def test_set_env_multiple(self): + def test_set_env_multiple(self): # Test setting multiple environment variables with GitTemporaryDirectory(): - await main( + main( [ "--set-env", "TEST_VAR1=value1", @@ -984,40 +986,40 @@ async def test_set_env_multiple(self): self.assertEqual(os.environ.get("TEST_VAR1"), "value1") self.assertEqual(os.environ.get("TEST_VAR2"), "value2") - async def test_set_env_with_spaces(self): + def test_set_env_with_spaces(self): # Test setting env var with spaces in value with GitTemporaryDirectory(): - await main(["--set-env", "TEST_VAR=test value with spaces", "--exit", "--yes"]) + main(["--set-env", "TEST_VAR=test value with spaces", "--exit", "--yes"]) self.assertEqual(os.environ.get("TEST_VAR"), "test value with spaces") - async def test_set_env_invalid_format(self): + def test_set_env_invalid_format(self): # Test invalid format handling with GitTemporaryDirectory(): - result = await main(["--set-env", "INVALID_FORMAT", "--exit", "--yes"]) + result = main(["--set-env", "INVALID_FORMAT", "--exit", "--yes"]) self.assertEqual(result, 1) - async def test_api_key_single(self): + def test_api_key_single(self): # Test setting a single API key with GitTemporaryDirectory(): - await main(["--api-key", "anthropic=test-key", "--exit", "--yes"]) + main(["--api-key", "anthropic=test-key", "--exit", "--yes"]) self.assertEqual(os.environ.get("ANTHROPIC_API_KEY"), "test-key") - async def test_api_key_multiple(self): + def test_api_key_multiple(self): # Test setting multiple API keys with GitTemporaryDirectory(): - await main( + main( ["--api-key", "anthropic=key1", "--api-key", "openai=key2", "--exit", "--yes"] ) self.assertEqual(os.environ.get("ANTHROPIC_API_KEY"), "key1") self.assertEqual(os.environ.get("OPENAI_API_KEY"), "key2") - async def test_api_key_invalid_format(self): + def test_api_key_invalid_format(self): # Test invalid format handling with GitTemporaryDirectory(): - result = await main(["--api-key", "INVALID_FORMAT", "--exit", "--yes"]) + result = main(["--api-key", "INVALID_FORMAT", "--exit", "--yes"]) self.assertEqual(result, 1) - async def test_git_config_include(self): + def test_git_config_include(self): # Test that aider respects git config includes for user.name and user.email with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) @@ -1042,7 +1044,7 @@ async def test_git_config_include(self): git_config_content = git_config_path.read_text() # Run aider and verify it doesn't change the git config - await main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) # Check that the user settings are still the same using git command repo = git.Repo(git_dir) # Re-open repo to ensure we get fresh config @@ -1053,7 +1055,7 @@ async def test_git_config_include(self): git_config_content_after = git_config_path.read_text() self.assertEqual(git_config_content, git_config_content_after) - async def test_git_config_include_directive(self): + def test_git_config_include_directive(self): # Test that aider respects the include directive in git config with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) @@ -1083,7 +1085,7 @@ async def test_git_config_include_directive(self): self.assertEqual(repo.git.config("user.email"), "directive@example.com") # Run aider and verify it doesn't change the git config - await main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) # Check that the git config file wasn't modified config_after_aider = git_config.read_text() @@ -1094,7 +1096,7 @@ async def test_git_config_include_directive(self): self.assertEqual(repo.git.config("user.name"), "Directive User") self.assertEqual(repo.git.config("user.email"), "directive@example.com") - async def test_resolve_aiderignore_path(self): + def test_resolve_aiderignore_path(self): # Import the function directly to test it from aider.args import resolve_aiderignore_path @@ -1113,12 +1115,12 @@ async def test_resolve_aiderignore_path(self): rel_path = ".aiderignore" self.assertEqual(resolve_aiderignore_path(rel_path), rel_path) - async def test_invalid_edit_format(self): + def test_invalid_edit_format(self): with GitTemporaryDirectory(): # Suppress stderr for this test as argparse prints an error message with patch("sys.stderr", new_callable=StringIO) as mock_stderr: with self.assertRaises(SystemExit) as cm: - _ = await main( + _ = main( ["--edit-format", "not-a-real-format", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -1129,11 +1131,11 @@ async def test_invalid_edit_format(self): self.assertIn("invalid choice", stderr_output) self.assertIn("not-a-real-format", stderr_output) - async def test_default_model_selection(self): + def test_default_model_selection(self): with GitTemporaryDirectory(): # Test Anthropic API key os.environ["ANTHROPIC_API_KEY"] = "test-key" - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("sonnet", coder.main_model.name.lower()) @@ -1141,7 +1143,7 @@ async def test_default_model_selection(self): # Test DeepSeek API key os.environ["DEEPSEEK_API_KEY"] = "test-key" - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("deepseek", coder.main_model.name.lower()) @@ -1149,7 +1151,7 @@ async def test_default_model_selection(self): # Test OpenRouter API key os.environ["OPENROUTER_API_KEY"] = "test-key" - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("openrouter/", coder.main_model.name.lower()) @@ -1157,7 +1159,7 @@ async def test_default_model_selection(self): # Test OpenAI API key os.environ["OPENAI_API_KEY"] = "test-key" - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("gpt-4", coder.main_model.name.lower()) @@ -1165,7 +1167,7 @@ async def test_default_model_selection(self): # Test Gemini API key os.environ["GEMINI_API_KEY"] = "test-key" - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("gemini", coder.main_model.name.lower()) @@ -1174,23 +1176,23 @@ async def test_default_model_selection(self): # Test no API keys - should offer OpenRouter OAuth with patch("aider.onboarding.offer_openrouter_oauth") as mock_offer_oauth: mock_offer_oauth.return_value = None # Simulate user declining or failure - result = await main(["--exit", "--yes"], input=DummyInput(), output=DummyOutput()) + result = main(["--exit", "--yes"], input=DummyInput(), output=DummyOutput()) self.assertEqual(result, 1) # Expect failure since no model could be selected mock_offer_oauth.assert_called_once() - async def test_model_precedence(self): + def test_model_precedence(self): with GitTemporaryDirectory(): # Test that earlier API keys take precedence os.environ["ANTHROPIC_API_KEY"] = "test-key" os.environ["OPENAI_API_KEY"] = "test-key" - coder = await main( + coder = main( ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("sonnet", coder.main_model.name.lower()) del os.environ["ANTHROPIC_API_KEY"] del os.environ["OPENAI_API_KEY"] - async def test_model_overrides_suffix_applied(self): + def test_model_overrides_suffix_applied(self): with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) overrides_file = git_dir / ".aider.model.overrides.yml" @@ -1214,7 +1216,7 @@ async def test_model_overrides_suffix_applied(self): mock_instance.weak_model_name = None mock_instance.get_weak_model.return_value = None - await main( + main( ["--model", "gpt-4o:fast", "--exit", "--yes", "--no-git"], input=DummyInput(), output=DummyOutput(), @@ -1241,7 +1243,7 @@ async def test_model_overrides_suffix_applied(self): ), ) - async def test_model_overrides_no_match_preserves_model_name(self): + def test_model_overrides_no_match_preserves_model_name(self): with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) @@ -1265,7 +1267,7 @@ async def test_model_overrides_no_match_preserves_model_name(self): model_name = "hf:moonshotai/Kimi-K2-Thinking" - await main( + main( ["--model", model_name, "--exit", "--yes", "--no-git"], input=DummyInput(), output=DummyOutput(), @@ -1287,9 +1289,9 @@ async def test_model_overrides_no_match_preserves_model_name(self): ), ) - async def test_chat_language_spanish(self): + def test_chat_language_spanish(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--chat-language", "Spanish", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -1298,9 +1300,9 @@ async def test_chat_language_spanish(self): system_info = coder.get_platform_info() self.assertIn("Spanish", system_info) - async def test_commit_language_japanese(self): + def test_commit_language_japanese(self): with GitTemporaryDirectory(): - coder = await main( + coder = main( ["--commit-language", "japanese", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -1309,18 +1311,18 @@ async def test_commit_language_japanese(self): self.assertIn("japanese", coder.commit_language) @patch("git.Repo.init") - async def test_main_exit_with_git_command_not_found(self, mock_git_init): + def test_main_exit_with_git_command_not_found(self, mock_git_init): mock_git_init.side_effect = git.exc.GitCommandNotFound("git", "Command 'git' not found") try: - result = await main(["--exit", "--yes"], input=DummyInput(), output=DummyOutput()) + result = main(["--exit", "--yes"], input=DummyInput(), output=DummyOutput()) except Exception as e: - self.fail(f"await main() raised an unexpected exception: {e}") + self.fail(f"main() raised an unexpected exception: {e}") - self.assertIsNone(result, "await main() should return None when called with --exit") + self.assertIsNone(result, "main() should return None when called with --exit") - async def test_reasoning_effort_option(self): - coder = await main( + def test_reasoning_effort_option(self): + coder = main( ["--reasoning-effort", "3", "--no-check-model-accepts-settings", "--yes", "--exit"], input=DummyInput(), output=DummyOutput(), @@ -1330,8 +1332,8 @@ async def test_reasoning_effort_option(self): coder.main_model.extra_params.get("extra_body", {}).get("reasoning_effort"), "3" ) - async def test_thinking_tokens_option(self): - coder = await main( + def test_thinking_tokens_option(self): + coder = main( ["--model", "sonnet", "--thinking-tokens", "1000", "--yes", "--exit"], input=DummyInput(), output=DummyOutput(), @@ -1341,7 +1343,7 @@ async def test_thinking_tokens_option(self): coder.main_model.extra_params.get("thinking", {}).get("budget_tokens"), 1000 ) - async def test_list_models_includes_metadata_models(self): + def test_list_models_includes_metadata_models(self): # Test that models from model-metadata.json appear in list-models output with GitTemporaryDirectory(): # Create a temporary model-metadata.json with test models @@ -1362,7 +1364,7 @@ async def test_list_models_includes_metadata_models(self): # Capture stdout to check the output with patch("sys.stdout", new_callable=StringIO) as mock_stdout: - await main( + main( [ "--list-models", "unique-model", @@ -1379,7 +1381,7 @@ async def test_list_models_includes_metadata_models(self): # Check that the unique model name from our metadata file is listed self.assertIn("test-provider/unique-model-name", output) - async def test_list_models_includes_all_model_sources(self): + def test_list_models_includes_all_model_sources(self): # Test that models from both litellm.model_cost and model-metadata.json # appear in list-models with GitTemporaryDirectory(): @@ -1396,7 +1398,7 @@ async def test_list_models_includes_all_model_sources(self): # Capture stdout to check the output with patch("sys.stdout", new_callable=StringIO) as mock_stdout: - await main( + main( [ "--list-models", "metadata-only-model", @@ -1415,12 +1417,12 @@ async def test_list_models_includes_all_model_sources(self): # Check that both models appear in the output self.assertIn("test-provider/metadata-only-model", output) - async def test_check_model_accepts_settings_flag(self): + def test_check_model_accepts_settings_flag(self): # Test that --check-model-accepts-settings affects whether settings are applied with GitTemporaryDirectory(): # When flag is on, setting shouldn't be applied to non-supporting model with patch("aider.models.Model.set_thinking_tokens") as mock_set_thinking: - await main( + main( [ "--model", "gpt-4o", @@ -1436,7 +1438,7 @@ async def test_check_model_accepts_settings_flag(self): # Method should not be called because model doesn't support it and flag is on mock_set_thinking.assert_not_called() - async def test_list_models_with_direct_resource_patch(self): + def test_list_models_with_direct_resource_patch(self): # Test that models from resources/model-metadata.json are included in list-models output with GitTemporaryDirectory(): # Create a temporary file with test model metadata @@ -1461,7 +1463,7 @@ async def test_list_models_with_direct_resource_patch(self): with patch("aider.main.importlib_resources.files", return_value=mock_files): # Capture stdout to check the output with patch("sys.stdout", new_callable=StringIO) as mock_stdout: - await main( + main( ["--list-models", "special", "--yes", "--no-gitignore"], input=DummyInput(), output=DummyOutput(), @@ -1473,7 +1475,7 @@ async def test_list_models_with_direct_resource_patch(self): # When flag is off, setting should be applied regardless of support with patch("aider.models.Model.set_reasoning_effort") as mock_set_reasoning: - await main( + main( [ "--model", "gpt-3.5-turbo", @@ -1489,7 +1491,7 @@ async def test_list_models_with_direct_resource_patch(self): # Method should be called because flag is off mock_set_reasoning.assert_called_once_with("3") - async def test_model_accepts_settings_attribute(self): + def test_model_accepts_settings_attribute(self): with GitTemporaryDirectory(): # Test with a model where we override the accepts_settings attribute with patch("aider.models.Model") as MockModel: @@ -1506,7 +1508,7 @@ async def test_model_accepts_settings_attribute(self): mock_instance.get_weak_model.return_value = None # Run with both settings, but model only accepts reasoning_effort - await main( + main( [ "--model", "test-model", @@ -1527,10 +1529,10 @@ async def test_model_accepts_settings_attribute(self): mock_instance.set_thinking_tokens.assert_not_called() @patch("aider.main.InputOutput") - async def test_stream_and_cache_warning(self, MockInputOutput): + def test_stream_and_cache_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value with GitTemporaryDirectory(): - await main( + main( ["--stream", "--cache-prompts", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -1540,10 +1542,10 @@ async def test_stream_and_cache_warning(self, MockInputOutput): ) @patch("aider.main.InputOutput") - async def test_stream_without_cache_no_warning(self, MockInputOutput): + def test_stream_without_cache_no_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value with GitTemporaryDirectory(): - await main( + main( ["--stream", "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), @@ -1551,13 +1553,14 @@ async def test_stream_without_cache_no_warning(self, MockInputOutput): for call in mock_io_instance.tool_warning.call_args_list: self.assertNotIn("Cost estimates may be inaccurate", call[0][0]) + # TODO: This test needs to be converted to call async functions via asyncio.run() or refactored async def test_argv_file_respects_git(self): with GitTemporaryDirectory(): fname = Path("not_in_git.txt") fname.touch() with open(".gitignore", "w+") as f: f.write("not_in_git.txt") - coder = await main( + coder = main( argv=["--file", "not_in_git.txt"], input=DummyInput(), output=DummyOutput(), @@ -1626,10 +1629,10 @@ def test_load_dotenv_files_override(self): os.chdir(original_cwd) @patch("aider.main.InputOutput") - async def test_cache_without_stream_no_warning(self, MockInputOutput): + def test_cache_without_stream_no_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value with GitTemporaryDirectory(): - await main( + main( ["--cache-prompts", "--exit", "--yes", "--no-stream"], input=DummyInput(), output=DummyOutput(), @@ -1638,14 +1641,14 @@ async def test_cache_without_stream_no_warning(self, MockInputOutput): self.assertNotIn("Cost estimates may be inaccurate", call[0][0]) @patch("aider.coders.Coder.create") - async def test_mcp_servers_parsing(self, mock_coder_create): + def test_mcp_servers_parsing(self, mock_coder_create): # Setup mock coder mock_coder_instance = MagicMock() mock_coder_create.return_value = mock_coder_instance # Test with --mcp-servers option with GitTemporaryDirectory(): - await main( + main( [ "--mcp-servers", '{"mcpServers":{"git":{"command":"uvx","args":["mcp-server-git"]}}}', @@ -1675,7 +1678,7 @@ async def test_mcp_servers_parsing(self, mock_coder_create): mcp_content = {"mcpServers": {"git": {"command": "uvx", "args": ["mcp-server-git"]}}} mcp_file.write_text(json.dumps(mcp_content)) - await main( + main( ["--mcp-servers-file", str(mcp_file), "--exit", "--yes"], input=DummyInput(), output=DummyOutput(), From 1c37468255122ccda032491a61673e765655ec0c Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 14:24:41 +0100 Subject: [PATCH 05/23] fix: replace ambiguous --yes flag with --yes-always in tests The --yes flag is ambiguous as it matches both --yes-always and --yes-always-commands. Update all test cases to use the explicit --yes-always flag. --- tests/basic/test_main.py | 170 +++++++++++++++++++-------------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 2bae0af46ff..53bb85e1ea8 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -46,25 +46,25 @@ def tearDown(self): self.webbrowser_patcher.stop() def test_main_with_empty_dir_no_files_on_command(self): - main(["--no-git", "--exit", "--yes"], input=DummyInput(), output=DummyOutput()) + main(["--no-git", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput()) def test_main_with_emptqy_dir_new_file(self): main( - ["foo.txt", "--yes", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput() + ["foo.txt", "--yes-always", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput() ) self.assertTrue(os.path.exists("foo.txt")) @patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message") def test_main_with_empty_git_dir_new_file(self, _): make_repo() - main(["--yes", "foo.txt", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes-always", "foo.txt", "--exit"], input=DummyInput(), output=DummyOutput()) self.assertTrue(os.path.exists("foo.txt")) @patch("aider.repo.GitRepo.get_commit_message", return_value="mock commit message") def test_main_with_empty_git_dir_new_files(self, _): make_repo() main( - ["--yes", "foo.txt", "bar.txt", "--exit"], input=DummyInput(), output=DummyOutput() + ["--yes-always", "foo.txt", "bar.txt", "--exit"], input=DummyInput(), output=DummyOutput() ) self.assertTrue(os.path.exists("foo.txt")) self.assertTrue(os.path.exists("bar.txt")) @@ -82,7 +82,7 @@ def test_main_with_subdir_repo_fnames(self, _): subdir.mkdir() make_repo(str(subdir)) main( - ["--yes", str(subdir / "foo.txt"), str(subdir / "bar.txt"), "--exit"], + ["--yes-always", str(subdir / "foo.txt"), str(subdir / "bar.txt"), "--exit"], input=DummyInput(), output=DummyOutput(), ) @@ -95,7 +95,7 @@ def test_main_copy_paste_model_overrides(self): [ "--no-git", "--exit", - "--yes", + "--yes-always", "--model", "cp:gpt-4o:fast", "--model-overrides", @@ -116,7 +116,7 @@ def test_main_copy_paste_flag_sets_mode(self, mock_watcher): mock_watcher.return_value = MagicMock() coder = main( - ["--no-git", "--exit", "--yes", "--copy-paste"], + ["--no-git", "--exit", "--yes-always", "--copy-paste"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -133,7 +133,7 @@ def test_main_with_git_config_yml(self): Path(".aider.conf.yml").write_text("auto-commits: false\n") with patch("aider.coders.Coder.create") as MockCoder: - main(["--yes"], input=DummyInput(), output=DummyOutput()) + main(["--yes-always"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is False @@ -155,7 +155,7 @@ def test_main_with_empty_git_dir_new_subdir_file(self): # This will throw a git error on windows if get_tracked_files doesn't # properly convert git/posix/paths to git\posix\paths. # Because aider will try and `git add` a file that's already in the repo. - main(["--yes", str(fname), "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes-always", str(fname), "--exit"], input=DummyInput(), output=DummyOutput()) # TODO: This test needs to be converted to call async functions via asyncio.run() or refactored async def test_setup_git(self): @@ -214,7 +214,7 @@ def test_command_line_gitignore_files_flag(self): # Test without the --add-gitignore-files flag (default: False) coder = main( - ["--exit", "--yes", abs_ignored_file], + ["--exit", "--yes-always", abs_ignored_file], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -225,7 +225,7 @@ def test_command_line_gitignore_files_flag(self): # Test with --add-gitignore-files set to True coder = main( - ["--add-gitignore-files", "--exit", "--yes", abs_ignored_file], + ["--add-gitignore-files", "--exit", "--yes-always", abs_ignored_file], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -236,7 +236,7 @@ def test_command_line_gitignore_files_flag(self): # Test with --add-gitignore-files set to False coder = main( - ["--no-add-gitignore-files", "--exit", "--yes", abs_ignored_file], + ["--no-add-gitignore-files", "--exit", "--yes-always", abs_ignored_file], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -263,7 +263,7 @@ def test_add_command_gitignore_files_flag(self): # Test without the --add-gitignore-files flag (default: False) coder = main( - ["--exit", "--yes"], + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -278,7 +278,7 @@ def test_add_command_gitignore_files_flag(self): # Test with --add-gitignore-files set to True coder = main( - ["--add-gitignore-files", "--exit", "--yes"], + ["--add-gitignore-files", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -292,7 +292,7 @@ def test_add_command_gitignore_files_flag(self): # Test with --add-gitignore-files set to False coder = main( - ["--no-add-gitignore-files", "--exit", "--yes"], + ["--no-add-gitignore-files", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -309,7 +309,7 @@ def test_main_args(self): with patch("aider.coders.Coder.create") as MockCoder: # --yes will just ok the git repo without blocking on input # following calls to main will see the new repo already - main(["--no-auto-commits", "--yes"], input=DummyInput()) + main(["--no-auto-commits", "--yes-always"], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is False @@ -358,7 +358,7 @@ def test_env_file_override(self): named_env.write_text("A=named") with patch("pathlib.Path.home", return_value=fake_home): - main(["--yes", "--exit", "--env-file", str(named_env)]) + main(["--yes-always", "--exit", "--env-file", str(named_env)]) self.assertEqual(os.environ["A"], "named") self.assertEqual(os.environ["B"], "cwd") @@ -383,7 +383,7 @@ async def mock_run(*args, **kwargs): MockCoder.return_value = mock_coder_instance main( - ["--yes", "--message-file", message_file_path], + ["--yes-always", "--message-file", message_file_path], input=DummyInput(), output=DummyOutput(), ) @@ -405,7 +405,7 @@ def side_effect(*args, **kwargs): MockSend.side_effect = side_effect - main(["--yes", fname, "--encoding", "iso-8859-15"]) + main(["--yes-always", fname, "--encoding", "iso-8859-15"]) def test_main_exit_calls_version_check(self): with GitTemporaryDirectory(): @@ -432,7 +432,7 @@ def test_main_message_adds_to_input_history(self, mock_run, MockInputOutput): def test_yes(self, mock_run, MockInputOutput): test_message = "test message" - main(["--yes", "--message", test_message]) + main(["--yes-always", "--message", test_message]) args, kwargs = MockInputOutput.call_args self.assertTrue(args[1]) @@ -506,7 +506,7 @@ def test_default_env_file_sets_automatic_variable(self): def test_false_vals_in_env_file(self): self.create_env_file(".env", "AIDER_SHOW_DIFFS=off") with patch("aider.coders.Coder.create") as MockCoder: - main(["--no-git", "--yes"], input=DummyInput(), output=DummyOutput()) + main(["--no-git", "--yes-always"], input=DummyInput(), output=DummyOutput()) MockCoder.assert_called_once() _, kwargs = MockCoder.call_args self.assertEqual(kwargs["show_diffs"], False) @@ -514,7 +514,7 @@ def test_false_vals_in_env_file(self): def test_true_vals_in_env_file(self): self.create_env_file(".env", "AIDER_SHOW_DIFFS=on") with patch("aider.coders.Coder.create") as MockCoder: - main(["--no-git", "--yes"], input=DummyInput(), output=DummyOutput()) + main(["--no-git", "--yes-always"], input=DummyInput(), output=DummyOutput()) MockCoder.assert_called_once() _, kwargs = MockCoder.call_args self.assertEqual(kwargs["show_diffs"], True) @@ -543,7 +543,7 @@ def test_lint_option(self): MockLinter.return_value = "" # Run main with --lint option - main(["--lint", "--yes"]) + main(["--lint", "--yes-always"]) # Check if the Linter was called with a filename ending in "dirty_file.py" # but not ending in "subdir/dirty_file.py" @@ -556,7 +556,7 @@ def test_verbose_mode_lists_env_vars(self): self.create_env_file(".env", "AIDER_DARK_MODE=on") with patch("sys.stdout", new_callable=StringIO) as mock_stdout: main( - ["--no-git", "--verbose", "--exit", "--yes"], + ["--no-git", "--verbose", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) @@ -602,7 +602,7 @@ def test_yaml_config_file_loading(self): ): # Test loading from specified config file main( - ["--yes", "--exit", "--config", str(named_config)], + ["--yes-always", "--exit", "--config", str(named_config)], input=DummyInput(), output=DummyOutput(), ) @@ -611,7 +611,7 @@ def test_yaml_config_file_loading(self): self.assertEqual(kwargs["map_tokens"], 8192) # Test loading from current working directory - main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes-always", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args print("kwargs:", kwargs) # Add this line for debugging self.assertIn("main_model", kwargs, "main_model key not found in kwargs") @@ -620,14 +620,14 @@ def test_yaml_config_file_loading(self): # Test loading from git root cwd_config.unlink() - main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes-always", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args self.assertEqual(kwargs["main_model"].name, "gpt-4") self.assertEqual(kwargs["map_tokens"], 2048) # Test loading from home directory git_config.unlink() - main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes-always", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args self.assertEqual(kwargs["main_model"].name, "gpt-3.5-turbo") self.assertEqual(kwargs["map_tokens"], 1024) @@ -637,7 +637,7 @@ def test_map_tokens_option(self): with patch("aider.coders.base_coder.RepoMap") as MockRepoMap: MockRepoMap.return_value.max_map_tokens = 0 main( - ["--model", "gpt-4", "--map-tokens", "0", "--exit", "--yes"], + ["--model", "gpt-4", "--map-tokens", "0", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) @@ -648,7 +648,7 @@ def test_map_tokens_option_with_non_zero_value(self): with patch("aider.coders.base_coder.RepoMap") as MockRepoMap: MockRepoMap.return_value.max_map_tokens = 1000 main( - ["--model", "gpt-4", "--map-tokens", "1000", "--exit", "--yes"], + ["--model", "gpt-4", "--map-tokens", "1000", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) @@ -660,7 +660,7 @@ def test_read_option(self): Path(test_file).touch() coder = main( - ["--read", test_file, "--exit", "--yes"], + ["--read", test_file, "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -676,7 +676,7 @@ def test_read_option_with_external_file(self): try: with GitTemporaryDirectory(): coder = main( - ["--read", external_file_path, "--exit", "--yes"], + ["--read", external_file_path, "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -711,7 +711,7 @@ def test_model_metadata_file(self): "--model-metadata-file", str(metadata_file), "--exit", - "--yes", + "--yes-always", ], input=DummyInput(), output=DummyOutput(), @@ -728,7 +728,7 @@ def test_sonnet_and_cache_options(self): MockRepoMap.return_value = mock_repo_map main( - ["--sonnet", "--cache-prompts", "--exit", "--yes"], + ["--sonnet", "--cache-prompts", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) @@ -742,7 +742,7 @@ def test_sonnet_and_cache_options(self): def test_sonnet_and_cache_prompts_options(self): with GitTemporaryDirectory(): coder = main( - ["--sonnet", "--cache-prompts", "--exit", "--yes"], + ["--sonnet", "--cache-prompts", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -753,7 +753,7 @@ def test_sonnet_and_cache_prompts_options(self): def test_4o_and_cache_options(self): with GitTemporaryDirectory(): coder = main( - ["--4o", "--cache-prompts", "--exit", "--yes"], + ["--4o", "--cache-prompts", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -764,7 +764,7 @@ def test_4o_and_cache_options(self): def test_return_coder(self): with GitTemporaryDirectory(): result = main( - ["--exit", "--yes"], + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -772,7 +772,7 @@ def test_return_coder(self): self.assertIsInstance(result, Coder) result = main( - ["--exit", "--yes"], + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=False, @@ -782,7 +782,7 @@ def test_return_coder(self): def test_map_mul_option(self): with GitTemporaryDirectory(): coder = main( - ["--map-mul", "5", "--exit", "--yes"], + ["--map-mul", "5", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -793,7 +793,7 @@ def test_map_mul_option(self): def test_suggest_shell_commands_default(self): with GitTemporaryDirectory(): coder = main( - ["--exit", "--yes"], + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -803,7 +803,7 @@ def test_suggest_shell_commands_default(self): def test_suggest_shell_commands_disabled(self): with GitTemporaryDirectory(): coder = main( - ["--no-suggest-shell-commands", "--exit", "--yes"], + ["--no-suggest-shell-commands", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -813,7 +813,7 @@ def test_suggest_shell_commands_disabled(self): def test_suggest_shell_commands_enabled(self): with GitTemporaryDirectory(): coder = main( - ["--suggest-shell-commands", "--exit", "--yes"], + ["--suggest-shell-commands", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -823,7 +823,7 @@ def test_suggest_shell_commands_enabled(self): def test_detect_urls_default(self): with GitTemporaryDirectory(): coder = main( - ["--exit", "--yes"], + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -833,7 +833,7 @@ def test_detect_urls_default(self): def test_detect_urls_disabled(self): with GitTemporaryDirectory(): coder = main( - ["--no-detect-urls", "--exit", "--yes"], + ["--no-detect-urls", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -843,7 +843,7 @@ def test_detect_urls_disabled(self): def test_detect_urls_enabled(self): with GitTemporaryDirectory(): coder = main( - ["--detect-urls", "--exit", "--yes"], + ["--detect-urls", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -864,7 +864,7 @@ def test_accepts_settings_warnings(self): "anthropic/claude-3-7-sonnet-20250219", "--thinking-tokens", "1000", - "--yes", + "--yes-always", "--exit", ], input=DummyInput(), @@ -888,7 +888,7 @@ def test_accepts_settings_warnings(self): "--thinking-tokens", "1000", "--check-model-accepts-settings", - "--yes", + "--yes-always", "--exit", ], input=DummyInput(), @@ -909,7 +909,7 @@ def test_accepts_settings_warnings(self): patch("aider.models.Model.set_reasoning_effort") as mock_set_reasoning, ): main( - ["--model", "o1", "--reasoning-effort", "3", "--yes", "--exit"], + ["--model", "o1", "--reasoning-effort", "3", "--yes-always", "--exit"], input=DummyInput(), output=DummyOutput(), ) @@ -925,7 +925,7 @@ def test_accepts_settings_warnings(self): patch("aider.models.Model.set_reasoning_effort") as mock_set_reasoning, ): main( - ["--model", "gpt-3.5-turbo", "--reasoning-effort", "3", "--yes", "--exit"], + ["--model", "gpt-3.5-turbo", "--reasoning-effort", "3", "--yes-always", "--exit"], input=DummyInput(), output=DummyOutput(), ) @@ -954,7 +954,7 @@ def test_no_verify_ssl_sets_model_info_manager(self, mock_set_verify_ssl): # Mock fuzzy_match_models to avoid string operations on MagicMock with patch("aider.models.fuzzy_match_models", return_value=[]): main( - ["--no-verify-ssl", "--exit", "--yes"], + ["--no-verify-ssl", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) @@ -967,7 +967,7 @@ def test_pytest_env_vars(self): def test_set_env_single(self): # Test setting a single environment variable with GitTemporaryDirectory(): - main(["--set-env", "TEST_VAR=test_value", "--exit", "--yes"]) + main(["--set-env", "TEST_VAR=test_value", "--exit", "--yes-always"]) self.assertEqual(os.environ.get("TEST_VAR"), "test_value") def test_set_env_multiple(self): @@ -980,7 +980,7 @@ def test_set_env_multiple(self): "--set-env", "TEST_VAR2=value2", "--exit", - "--yes", + "--yes-always", ] ) self.assertEqual(os.environ.get("TEST_VAR1"), "value1") @@ -989,26 +989,26 @@ def test_set_env_multiple(self): def test_set_env_with_spaces(self): # Test setting env var with spaces in value with GitTemporaryDirectory(): - main(["--set-env", "TEST_VAR=test value with spaces", "--exit", "--yes"]) + main(["--set-env", "TEST_VAR=test value with spaces", "--exit", "--yes-always"]) self.assertEqual(os.environ.get("TEST_VAR"), "test value with spaces") def test_set_env_invalid_format(self): # Test invalid format handling with GitTemporaryDirectory(): - result = main(["--set-env", "INVALID_FORMAT", "--exit", "--yes"]) + result = main(["--set-env", "INVALID_FORMAT", "--exit", "--yes-always"]) self.assertEqual(result, 1) def test_api_key_single(self): # Test setting a single API key with GitTemporaryDirectory(): - main(["--api-key", "anthropic=test-key", "--exit", "--yes"]) + main(["--api-key", "anthropic=test-key", "--exit", "--yes-always"]) self.assertEqual(os.environ.get("ANTHROPIC_API_KEY"), "test-key") def test_api_key_multiple(self): # Test setting multiple API keys with GitTemporaryDirectory(): main( - ["--api-key", "anthropic=key1", "--api-key", "openai=key2", "--exit", "--yes"] + ["--api-key", "anthropic=key1", "--api-key", "openai=key2", "--exit", "--yes-always"] ) self.assertEqual(os.environ.get("ANTHROPIC_API_KEY"), "key1") self.assertEqual(os.environ.get("OPENAI_API_KEY"), "key2") @@ -1016,7 +1016,7 @@ def test_api_key_multiple(self): def test_api_key_invalid_format(self): # Test invalid format handling with GitTemporaryDirectory(): - result = main(["--api-key", "INVALID_FORMAT", "--exit", "--yes"]) + result = main(["--api-key", "INVALID_FORMAT", "--exit", "--yes-always"]) self.assertEqual(result, 1) def test_git_config_include(self): @@ -1044,7 +1044,7 @@ def test_git_config_include(self): git_config_content = git_config_path.read_text() # Run aider and verify it doesn't change the git config - main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes-always", "--exit"], input=DummyInput(), output=DummyOutput()) # Check that the user settings are still the same using git command repo = git.Repo(git_dir) # Re-open repo to ensure we get fresh config @@ -1085,7 +1085,7 @@ def test_git_config_include_directive(self): self.assertEqual(repo.git.config("user.email"), "directive@example.com") # Run aider and verify it doesn't change the git config - main(["--yes", "--exit"], input=DummyInput(), output=DummyOutput()) + main(["--yes-always", "--exit"], input=DummyInput(), output=DummyOutput()) # Check that the git config file wasn't modified config_after_aider = git_config.read_text() @@ -1121,7 +1121,7 @@ def test_invalid_edit_format(self): with patch("sys.stderr", new_callable=StringIO) as mock_stderr: with self.assertRaises(SystemExit) as cm: _ = main( - ["--edit-format", "not-a-real-format", "--exit", "--yes"], + ["--edit-format", "not-a-real-format", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) @@ -1136,7 +1136,7 @@ def test_default_model_selection(self): # Test Anthropic API key os.environ["ANTHROPIC_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("sonnet", coder.main_model.name.lower()) del os.environ["ANTHROPIC_API_KEY"] @@ -1144,7 +1144,7 @@ def test_default_model_selection(self): # Test DeepSeek API key os.environ["DEEPSEEK_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("deepseek", coder.main_model.name.lower()) del os.environ["DEEPSEEK_API_KEY"] @@ -1152,7 +1152,7 @@ def test_default_model_selection(self): # Test OpenRouter API key os.environ["OPENROUTER_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("openrouter/", coder.main_model.name.lower()) del os.environ["OPENROUTER_API_KEY"] @@ -1160,7 +1160,7 @@ def test_default_model_selection(self): # Test OpenAI API key os.environ["OPENAI_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("gpt-4", coder.main_model.name.lower()) del os.environ["OPENAI_API_KEY"] @@ -1168,7 +1168,7 @@ def test_default_model_selection(self): # Test Gemini API key os.environ["GEMINI_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("gemini", coder.main_model.name.lower()) del os.environ["GEMINI_API_KEY"] @@ -1176,7 +1176,7 @@ def test_default_model_selection(self): # Test no API keys - should offer OpenRouter OAuth with patch("aider.onboarding.offer_openrouter_oauth") as mock_offer_oauth: mock_offer_oauth.return_value = None # Simulate user declining or failure - result = main(["--exit", "--yes"], input=DummyInput(), output=DummyOutput()) + result = main(["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput()) self.assertEqual(result, 1) # Expect failure since no model could be selected mock_offer_oauth.assert_called_once() @@ -1186,7 +1186,7 @@ def test_model_precedence(self): os.environ["ANTHROPIC_API_KEY"] = "test-key" os.environ["OPENAI_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True ) self.assertIn("sonnet", coder.main_model.name.lower()) del os.environ["ANTHROPIC_API_KEY"] @@ -1217,7 +1217,7 @@ def test_model_overrides_suffix_applied(self): mock_instance.get_weak_model.return_value = None main( - ["--model", "gpt-4o:fast", "--exit", "--yes", "--no-git"], + ["--model", "gpt-4o:fast", "--exit", "--yes-always", "--no-git"], input=DummyInput(), output=DummyOutput(), force_git_root=git_dir, @@ -1268,7 +1268,7 @@ def test_model_overrides_no_match_preserves_model_name(self): model_name = "hf:moonshotai/Kimi-K2-Thinking" main( - ["--model", model_name, "--exit", "--yes", "--no-git"], + ["--model", model_name, "--exit", "--yes-always", "--no-git"], input=DummyInput(), output=DummyOutput(), force_git_root=git_dir, @@ -1292,7 +1292,7 @@ def test_model_overrides_no_match_preserves_model_name(self): def test_chat_language_spanish(self): with GitTemporaryDirectory(): coder = main( - ["--chat-language", "Spanish", "--exit", "--yes"], + ["--chat-language", "Spanish", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -1303,7 +1303,7 @@ def test_chat_language_spanish(self): def test_commit_language_japanese(self): with GitTemporaryDirectory(): coder = main( - ["--commit-language", "japanese", "--exit", "--yes"], + ["--commit-language", "japanese", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -1315,7 +1315,7 @@ def test_main_exit_with_git_command_not_found(self, mock_git_init): mock_git_init.side_effect = git.exc.GitCommandNotFound("git", "Command 'git' not found") try: - result = main(["--exit", "--yes"], input=DummyInput(), output=DummyOutput()) + result = main(["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput()) except Exception as e: self.fail(f"main() raised an unexpected exception: {e}") @@ -1323,7 +1323,7 @@ def test_main_exit_with_git_command_not_found(self, mock_git_init): def test_reasoning_effort_option(self): coder = main( - ["--reasoning-effort", "3", "--no-check-model-accepts-settings", "--yes", "--exit"], + ["--reasoning-effort", "3", "--no-check-model-accepts-settings", "--yes-always", "--exit"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -1334,7 +1334,7 @@ def test_reasoning_effort_option(self): def test_thinking_tokens_option(self): coder = main( - ["--model", "sonnet", "--thinking-tokens", "1000", "--yes", "--exit"], + ["--model", "sonnet", "--thinking-tokens", "1000", "--yes-always", "--exit"], input=DummyInput(), output=DummyOutput(), return_coder=True, @@ -1370,7 +1370,7 @@ def test_list_models_includes_metadata_models(self): "unique-model", "--model-metadata-file", str(metadata_file), - "--yes", + "--yes-always", "--no-gitignore", ], input=DummyInput(), @@ -1404,7 +1404,7 @@ def test_list_models_includes_all_model_sources(self): "metadata-only-model", "--model-metadata-file", str(metadata_file), - "--yes", + "--yes-always", "--no-gitignore", ], input=DummyInput(), @@ -1429,7 +1429,7 @@ def test_check_model_accepts_settings_flag(self): "--thinking-tokens", "1000", "--check-model-accepts-settings", - "--yes", + "--yes-always", "--exit", ], input=DummyInput(), @@ -1464,7 +1464,7 @@ def test_list_models_with_direct_resource_patch(self): # Capture stdout to check the output with patch("sys.stdout", new_callable=StringIO) as mock_stdout: main( - ["--list-models", "special", "--yes", "--no-gitignore"], + ["--list-models", "special", "--yes-always", "--no-gitignore"], input=DummyInput(), output=DummyOutput(), ) @@ -1482,7 +1482,7 @@ def test_list_models_with_direct_resource_patch(self): "--reasoning-effort", "3", "--no-check-model-accepts-settings", - "--yes", + "--yes-always", "--exit", ], input=DummyInput(), @@ -1517,7 +1517,7 @@ def test_model_accepts_settings_attribute(self): "--thinking-tokens", "1000", "--check-model-accepts-settings", - "--yes", + "--yes-always", "--exit", ], input=DummyInput(), @@ -1533,7 +1533,7 @@ def test_stream_and_cache_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value with GitTemporaryDirectory(): main( - ["--stream", "--cache-prompts", "--exit", "--yes"], + ["--stream", "--cache-prompts", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) @@ -1546,7 +1546,7 @@ def test_stream_without_cache_no_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value with GitTemporaryDirectory(): main( - ["--stream", "--exit", "--yes"], + ["--stream", "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) @@ -1633,7 +1633,7 @@ def test_cache_without_stream_no_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value with GitTemporaryDirectory(): main( - ["--cache-prompts", "--exit", "--yes", "--no-stream"], + ["--cache-prompts", "--exit", "--yes-always", "--no-stream"], input=DummyInput(), output=DummyOutput(), ) @@ -1653,7 +1653,7 @@ def test_mcp_servers_parsing(self, mock_coder_create): "--mcp-servers", '{"mcpServers":{"git":{"command":"uvx","args":["mcp-server-git"]}}}', "--exit", - "--yes", + "--yes-always", ], input=DummyInput(), output=DummyOutput(), @@ -1679,7 +1679,7 @@ def test_mcp_servers_parsing(self, mock_coder_create): mcp_file.write_text(json.dumps(mcp_content)) main( - ["--mcp-servers-file", str(mcp_file), "--exit", "--yes"], + ["--mcp-servers-file", str(mcp_file), "--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), ) From 3c97313be88a112ad333a1e4eac46526fe9708a4 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 14:42:09 +0100 Subject: [PATCH 06/23] test: Flag test_add_command_gitignore_files as xfail --- tests/basic/test_main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 53bb85e1ea8..dfde4c553c5 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import git +import pytest from prompt_toolkit.input import DummyInput from prompt_toolkit.output import DummyOutput @@ -245,6 +246,7 @@ def test_command_line_gitignore_files_flag(self): # Verify the ignored file is not in the chat self.assertNotIn(abs_ignored_file, coder.abs_fnames) + @pytest.mark.xfail(reason="TODO: Commands.cmd_add no longer exists, test needs refactoring") def test_add_command_gitignore_files_flag(self): with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) From 882145ba4965295d7716c23902288ee99991c182 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 15:00:49 +0100 Subject: [PATCH 07/23] fix: add autospec=True to InputOutput mocks and configure pretty attribute Add autospec=True to 6 InputOutput mock tests to handle async methods correctly. Configure 'pretty' attribute since autospec requires instance attributes to be set manually. Fixes 6 tests, bringing suite to 63 passed / 17 failed. --- tests/basic/test_main.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index dfde4c553c5..dff0a3ab735 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -419,29 +419,32 @@ def test_main_exit_calls_version_check(self): mock_check_version.assert_called_once() mock_input_output.assert_called_once() - @patch("aider.main.InputOutput") + @patch("aider.main.InputOutput", autospec=True) @patch("aider.coders.base_coder.Coder.run") def test_main_message_adds_to_input_history(self, mock_run, MockInputOutput): test_message = "test message" mock_io_instance = MockInputOutput.return_value + mock_io_instance.pretty = True main(["--message", test_message], input=DummyInput(), output=DummyOutput()) mock_io_instance.add_to_input_history.assert_called_once_with(test_message) - @patch("aider.main.InputOutput") + @patch("aider.main.InputOutput", autospec=True) @patch("aider.coders.base_coder.Coder.run") def test_yes(self, mock_run, MockInputOutput): test_message = "test message" + MockInputOutput.return_value.pretty = True main(["--yes-always", "--message", test_message]) args, kwargs = MockInputOutput.call_args self.assertTrue(args[1]) - @patch("aider.main.InputOutput") + @patch("aider.main.InputOutput", autospec=True) @patch("aider.coders.base_coder.Coder.run") def test_default_yes(self, mock_run, MockInputOutput): test_message = "test message" + MockInputOutput.return_value.pretty = True main(["--message", test_message]) args, kwargs = MockInputOutput.call_args @@ -1530,9 +1533,10 @@ def test_model_accepts_settings_attribute(self): mock_instance.set_reasoning_effort.assert_called_once_with("3") mock_instance.set_thinking_tokens.assert_not_called() - @patch("aider.main.InputOutput") + @patch("aider.main.InputOutput", autospec=True) def test_stream_and_cache_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value + mock_io_instance.pretty = True with GitTemporaryDirectory(): main( ["--stream", "--cache-prompts", "--exit", "--yes-always"], @@ -1543,9 +1547,10 @@ def test_stream_and_cache_warning(self, MockInputOutput): "Cost estimates may be inaccurate when using streaming and caching." ) - @patch("aider.main.InputOutput") + @patch("aider.main.InputOutput", autospec=True) def test_stream_without_cache_no_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value + mock_io_instance.pretty = True with GitTemporaryDirectory(): main( ["--stream", "--exit", "--yes-always"], @@ -1630,9 +1635,10 @@ def test_load_dotenv_files_override(self): # Restore CWD os.chdir(original_cwd) - @patch("aider.main.InputOutput") + @patch("aider.main.InputOutput", autospec=True) def test_cache_without_stream_no_warning(self, MockInputOutput): mock_io_instance = MockInputOutput.return_value + mock_io_instance.pretty = True with GitTemporaryDirectory(): main( ["--cache-prompts", "--exit", "--yes-always", "--no-stream"], From 9e6d24a86da86b8d6a4ba23a33b20fc865cad846 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 15:05:53 +0100 Subject: [PATCH 08/23] fix: remove incorrect async from create_env_file helper method The create_env_file method was marked as async but only performs synchronous file operations. This caused RuntimeWarning about unawaited coroutines in tests that call it. Fixes 2 tests, bringing suite to 65 passed / 15 failed. --- tests/basic/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index dff0a3ab735..a3f80dd1dae 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -476,7 +476,7 @@ def test_light_mode_sets_code_theme(self): _, kwargs = MockInputOutput.call_args self.assertEqual(kwargs["code_theme"], "default") - async def create_env_file(self, file_name, content): + def create_env_file(self, file_name, content): env_file_path = Path(self.tempdir) / file_name env_file_path.write_text(content) return env_file_path From 78786784c753994661e80a2f49bb62955c96de79 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 16:07:19 +0100 Subject: [PATCH 09/23] test: fix Coder.create mock to handle _autosave_future Add mock_autosave_future() helper that returns AsyncMock()() to create an awaitable coroutine for _autosave_future attribute. Apply fix to all tests that patch Coder.create to avoid "object AsyncMock can't be used in 'await' expression" errors. For tests calling main() multiple times, create fresh coroutine before each call since coroutines can only be awaited once. Fixes 9 tests. Remaining 5 failures are unrelated to mock issues. --- tests/basic/test_main.py | 44 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index a3f80dd1dae..4e3543945bb 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -1,3 +1,4 @@ +import asyncio import json import os import subprocess @@ -19,6 +20,15 @@ from aider.utils import GitTemporaryDirectory, IgnorantTemporaryDirectory, make_repo +def mock_autosave_future(): + """Create an awaitable mock for _autosave_future. + + Returns AsyncMock()() - the first call creates an async mock function, + the second call invokes it to get an awaitable coroutine object. + """ + return AsyncMock()() + + class TestMain(TestCase): def setUp(self): self.original_env = os.environ.copy() @@ -134,12 +144,16 @@ def test_main_with_git_config_yml(self): Path(".aider.conf.yml").write_text("auto-commits: false\n") with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() main(["--yes-always"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is False Path(".aider.conf.yml").write_text("auto-commits: true\n") with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() main([], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is True @@ -309,6 +323,8 @@ def test_add_command_gitignore_files_flag(self): def test_main_args(self): with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() # --yes will just ok the git repo without blocking on input # following calls to main will see the new repo already main(["--no-auto-commits", "--yes-always"], input=DummyInput()) @@ -316,22 +332,30 @@ def test_main_args(self): assert kwargs["auto_commits"] is False with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() main(["--auto-commits"], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["auto_commits"] is True with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() main([], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["dirty_commits"] is True assert kwargs["auto_commits"] is True with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() main(["--no-dirty-commits"], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["dirty_commits"] is False with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() main(["--dirty-commits"], input=DummyInput()) _, kwargs = MockCoder.call_args assert kwargs["dirty_commits"] is True @@ -382,6 +406,7 @@ async def mock_run(*args, **kwargs): # Create a mock coder instance with an async run method mock_coder_instance = MagicMock() mock_coder_instance.run = AsyncMock() + mock_coder_instance._autosave_future = mock_autosave_future() MockCoder.return_value = mock_coder_instance main( @@ -398,7 +423,9 @@ def test_encodings_arg(self): fname = "foo.py" with GitTemporaryDirectory(): - with patch("aider.coders.Coder.create") as MockCoder: # noqa: F841 + with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() with patch("aider.main.InputOutput") as MockSend: def side_effect(*args, **kwargs): @@ -510,7 +537,9 @@ def test_default_env_file_sets_automatic_variable(self): def test_false_vals_in_env_file(self): self.create_env_file(".env", "AIDER_SHOW_DIFFS=off") - with patch("aider.coders.Coder.create") as MockCoder: + with patch("aider.coders.Coder.create", autospec=True) as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() main(["--no-git", "--yes-always"], input=DummyInput(), output=DummyOutput()) MockCoder.assert_called_once() _, kwargs = MockCoder.call_args @@ -519,6 +548,8 @@ def test_false_vals_in_env_file(self): def test_true_vals_in_env_file(self): self.create_env_file(".env", "AIDER_SHOW_DIFFS=on") with patch("aider.coders.Coder.create") as MockCoder: + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() main(["--no-git", "--yes-always"], input=DummyInput(), output=DummyOutput()) MockCoder.assert_called_once() _, kwargs = MockCoder.call_args @@ -605,6 +636,8 @@ def test_yaml_config_file_loading(self): patch("pathlib.Path.home", return_value=fake_home), patch("aider.coders.Coder.create") as MockCoder, ): + mock_coder_instance = MockCoder.return_value + mock_coder_instance._autosave_future = mock_autosave_future() # Test loading from specified config file main( ["--yes-always", "--exit", "--config", str(named_config)], @@ -616,6 +649,7 @@ def test_yaml_config_file_loading(self): self.assertEqual(kwargs["map_tokens"], 8192) # Test loading from current working directory + mock_coder_instance._autosave_future = mock_autosave_future() main(["--yes-always", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args print("kwargs:", kwargs) # Add this line for debugging @@ -625,6 +659,7 @@ def test_yaml_config_file_loading(self): # Test loading from git root cwd_config.unlink() + mock_coder_instance._autosave_future = mock_autosave_future() main(["--yes-always", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args self.assertEqual(kwargs["main_model"].name, "gpt-4") @@ -632,6 +667,7 @@ def test_yaml_config_file_loading(self): # Test loading from home directory git_config.unlink() + mock_coder_instance._autosave_future = mock_autosave_future() main(["--yes-always", "--exit"], input=DummyInput(), output=DummyOutput()) _, kwargs = MockCoder.call_args self.assertEqual(kwargs["main_model"].name, "gpt-3.5-turbo") @@ -1208,6 +1244,7 @@ def test_model_overrides_suffix_applied(self): patch("aider.coders.Coder.create") as MockCoder, ): mock_coder_instance = MagicMock() + mock_coder_instance._autosave_future = mock_autosave_future() MockCoder.return_value = mock_coder_instance mock_instance = MockModel.return_value @@ -1257,6 +1294,7 @@ def test_model_overrides_no_match_preserves_model_name(self): patch("aider.coders.Coder.create") as MockCoder, ): mock_coder_instance = MagicMock() + mock_coder_instance._autosave_future = mock_autosave_future() MockCoder.return_value = mock_coder_instance mock_instance = MockModel.return_value @@ -1652,6 +1690,7 @@ def test_cache_without_stream_no_warning(self, MockInputOutput): def test_mcp_servers_parsing(self, mock_coder_create): # Setup mock coder mock_coder_instance = MagicMock() + mock_coder_instance._autosave_future = mock_autosave_future() mock_coder_create.return_value = mock_coder_instance # Test with --mcp-servers option @@ -1679,6 +1718,7 @@ def test_mcp_servers_parsing(self, mock_coder_create): # Test with --mcp-servers-file option mock_coder_create.reset_mock() + mock_coder_instance._autosave_future = mock_autosave_future() with GitTemporaryDirectory(): # Create a temporary MCP servers file From 5f592469b6dc361f9523e5a175f97aa0e56f42d4 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 16:12:29 +0100 Subject: [PATCH 10/23] test: fix InputOutput mock in test_encodings_arg Configure mock_io.confirm_ask as AsyncMock to handle async confirm_ask calls in check_gitignore. Fixes TypeError: object MagicMock can't be used in 'await' expression. --- tests/basic/test_main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 4e3543945bb..b18a595d6ac 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -430,7 +430,9 @@ def test_encodings_arg(self): def side_effect(*args, **kwargs): self.assertEqual(kwargs["encoding"], "iso-8859-15") - return MagicMock() + mock_io = MagicMock() + mock_io.confirm_ask = AsyncMock(return_value=True) + return mock_io MockSend.side_effect = side_effect From a366831fa996ce081d1e5702b5d399895e76cf77 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 16:14:39 +0100 Subject: [PATCH 11/23] test: fix InputOutput mock in test_main_exit_calls_version_check Configure confirm_ask as AsyncMock on InputOutput mock return_value. --- tests/basic/test_main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index b18a595d6ac..ac37f90a6c8 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -444,6 +444,7 @@ def test_main_exit_calls_version_check(self): patch("aider.main.check_version") as mock_check_version, patch("aider.main.InputOutput") as mock_input_output, ): + mock_input_output.return_value.confirm_ask = AsyncMock(return_value=True) main(["--exit", "--check-update"], input=DummyInput(), output=DummyOutput()) mock_check_version.assert_called_once() mock_input_output.assert_called_once() From 25701746b8264b0bf535b966a65ffaf7da29b2cc Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 16:16:24 +0100 Subject: [PATCH 12/23] test: mark test_lint_option as xfail Commands.cmd_lint no longer exists, similar to cmd_add. Mark as xfail for future refactoring. --- tests/basic/test_main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index ac37f90a6c8..5bf6f69546e 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -558,6 +558,7 @@ def test_true_vals_in_env_file(self): _, kwargs = MockCoder.call_args self.assertEqual(kwargs["show_diffs"], True) + @pytest.mark.xfail(reason="TODO: Commands.cmd_lint no longer exists, test needs refactoring") def test_lint_option(self): with GitTemporaryDirectory() as git_dir: # Create a dirty file in the root From f07020ebc8f5fc654f875b2fdf3e6e0b814961f5 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 16:27:04 +0100 Subject: [PATCH 13/23] test: update tests to expect exit code 0 instead of None Update test_return_coder and test_main_exit_with_git_command_not_found to expect exit code 0 (success) instead of None. This aligns with the graceful_exit() wrapper introduced in commit 7f813af which changed main() to always return integer exit codes for proper shell integration. Fixes final 2 test failures. --- tests/basic/test_main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 5bf6f69546e..e601551797a 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -172,7 +172,7 @@ def test_main_with_empty_git_dir_new_subdir_file(self): # Because aider will try and `git add` a file that's already in the repo. main(["--yes-always", str(fname), "--exit"], input=DummyInput(), output=DummyOutput()) - # TODO: This test needs to be converted to call async functions via asyncio.run() or refactored + @pytest.mark.skip(reason="TODO: Needs refactoring to call async helpers via asyncio.run()") async def test_setup_git(self): io = InputOutput(pretty=False, yes=True) git_root = await setup_git(None, io) @@ -185,7 +185,7 @@ async def test_setup_git(self): self.assertTrue(gitignore.exists()) self.assertEqual(".aider*", gitignore.read_text().splitlines()[0]) - # TODO: This test needs to be converted to call async functions via asyncio.run() or refactored + @pytest.mark.skip(reason="TODO: Needs refactoring to call async helpers via asyncio.run()") async def test_check_gitignore(self): with GitTemporaryDirectory(): os.environ["GIT_CONFIG_GLOBAL"] = "globalgitconfig" @@ -822,7 +822,7 @@ def test_return_coder(self): output=DummyOutput(), return_coder=False, ) - self.assertIsNone(result) + self.assertEqual(result, 0) def test_map_mul_option(self): with GitTemporaryDirectory(): @@ -1366,7 +1366,7 @@ def test_main_exit_with_git_command_not_found(self, mock_git_init): except Exception as e: self.fail(f"main() raised an unexpected exception: {e}") - self.assertIsNone(result, "main() should return None when called with --exit") + self.assertEqual(result, 0, "main() should return 0 (success) when called with --exit") def test_reasoning_effort_option(self): coder = main( @@ -1602,7 +1602,7 @@ def test_stream_without_cache_no_warning(self, MockInputOutput): for call in mock_io_instance.tool_warning.call_args_list: self.assertNotIn("Cost estimates may be inaccurate", call[0][0]) - # TODO: This test needs to be converted to call async functions via asyncio.run() or refactored + @pytest.mark.skip(reason="TODO: Needs refactoring to call async helpers via asyncio.run()") async def test_argv_file_respects_git(self): with GitTemporaryDirectory(): fname = Path("not_in_git.txt") From 9b1160c1daa88801730a10457bc0a02c1584ef1a Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 16:46:25 +0100 Subject: [PATCH 14/23] style: apply black formatting to test files --- tests/basic/test_main.py | 73 +++++++++++++++++++++++++--------- tests/basic/test_main_smoke.py | 5 ++- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index e601551797a..a42aeb1c0ce 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -1,4 +1,3 @@ -import asyncio import json import os import subprocess @@ -61,7 +60,9 @@ def test_main_with_empty_dir_no_files_on_command(self): def test_main_with_emptqy_dir_new_file(self): main( - ["foo.txt", "--yes-always", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput() + ["foo.txt", "--yes-always", "--no-git", "--exit"], + input=DummyInput(), + output=DummyOutput(), ) self.assertTrue(os.path.exists("foo.txt")) @@ -75,7 +76,9 @@ def test_main_with_empty_git_dir_new_file(self, _): def test_main_with_empty_git_dir_new_files(self, _): make_repo() main( - ["--yes-always", "foo.txt", "bar.txt", "--exit"], input=DummyInput(), output=DummyOutput() + ["--yes-always", "foo.txt", "bar.txt", "--exit"], + input=DummyInput(), + output=DummyOutput(), ) self.assertTrue(os.path.exists("foo.txt")) self.assertTrue(os.path.exists("bar.txt")) @@ -484,9 +487,7 @@ def test_dark_mode_sets_code_theme(self): # Mock InputOutput to capture the configuration with patch("aider.main.InputOutput") as MockInputOutput: MockInputOutput.return_value.get_input.return_value = None - main( - ["--dark-mode", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput() - ) + main(["--dark-mode", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput()) # Ensure InputOutput was called MockInputOutput.assert_called_once() # Check if the code_theme setting is for dark mode @@ -497,9 +498,7 @@ def test_light_mode_sets_code_theme(self): # Mock InputOutput to capture the configuration with patch("aider.main.InputOutput") as MockInputOutput: MockInputOutput.return_value.get_input.return_value = None - main( - ["--light-mode", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput() - ) + main(["--light-mode", "--no-git", "--exit"], input=DummyInput(), output=DummyOutput()) # Ensure InputOutput was called MockInputOutput.assert_called_once() # Check if the code_theme setting is for light mode @@ -970,7 +969,14 @@ def test_accepts_settings_warnings(self): patch("aider.models.Model.set_reasoning_effort") as mock_set_reasoning, ): main( - ["--model", "gpt-3.5-turbo", "--reasoning-effort", "3", "--yes-always", "--exit"], + [ + "--model", + "gpt-3.5-turbo", + "--reasoning-effort", + "3", + "--yes-always", + "--exit", + ], input=DummyInput(), output=DummyOutput(), ) @@ -1053,7 +1059,14 @@ def test_api_key_multiple(self): # Test setting multiple API keys with GitTemporaryDirectory(): main( - ["--api-key", "anthropic=key1", "--api-key", "openai=key2", "--exit", "--yes-always"] + [ + "--api-key", + "anthropic=key1", + "--api-key", + "openai=key2", + "--exit", + "--yes-always", + ] ) self.assertEqual(os.environ.get("ANTHROPIC_API_KEY"), "key1") self.assertEqual(os.environ.get("OPENAI_API_KEY"), "key2") @@ -1181,7 +1194,10 @@ def test_default_model_selection(self): # Test Anthropic API key os.environ["ANTHROPIC_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], + input=DummyInput(), + output=DummyOutput(), + return_coder=True, ) self.assertIn("sonnet", coder.main_model.name.lower()) del os.environ["ANTHROPIC_API_KEY"] @@ -1189,7 +1205,10 @@ def test_default_model_selection(self): # Test DeepSeek API key os.environ["DEEPSEEK_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], + input=DummyInput(), + output=DummyOutput(), + return_coder=True, ) self.assertIn("deepseek", coder.main_model.name.lower()) del os.environ["DEEPSEEK_API_KEY"] @@ -1197,7 +1216,10 @@ def test_default_model_selection(self): # Test OpenRouter API key os.environ["OPENROUTER_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], + input=DummyInput(), + output=DummyOutput(), + return_coder=True, ) self.assertIn("openrouter/", coder.main_model.name.lower()) del os.environ["OPENROUTER_API_KEY"] @@ -1205,7 +1227,10 @@ def test_default_model_selection(self): # Test OpenAI API key os.environ["OPENAI_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], + input=DummyInput(), + output=DummyOutput(), + return_coder=True, ) self.assertIn("gpt-4", coder.main_model.name.lower()) del os.environ["OPENAI_API_KEY"] @@ -1213,7 +1238,10 @@ def test_default_model_selection(self): # Test Gemini API key os.environ["GEMINI_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], + input=DummyInput(), + output=DummyOutput(), + return_coder=True, ) self.assertIn("gemini", coder.main_model.name.lower()) del os.environ["GEMINI_API_KEY"] @@ -1231,7 +1259,10 @@ def test_model_precedence(self): os.environ["ANTHROPIC_API_KEY"] = "test-key" os.environ["OPENAI_API_KEY"] = "test-key" coder = main( - ["--exit", "--yes-always"], input=DummyInput(), output=DummyOutput(), return_coder=True + ["--exit", "--yes-always"], + input=DummyInput(), + output=DummyOutput(), + return_coder=True, ) self.assertIn("sonnet", coder.main_model.name.lower()) del os.environ["ANTHROPIC_API_KEY"] @@ -1370,7 +1401,13 @@ def test_main_exit_with_git_command_not_found(self, mock_git_init): def test_reasoning_effort_option(self): coder = main( - ["--reasoning-effort", "3", "--no-check-model-accepts-settings", "--yes-always", "--exit"], + [ + "--reasoning-effort", + "3", + "--no-check-model-accepts-settings", + "--yes-always", + "--exit", + ], input=DummyInput(), output=DummyOutput(), return_coder=True, diff --git a/tests/basic/test_main_smoke.py b/tests/basic/test_main_smoke.py index 1b998b2d34c..9b071a52f2f 100644 --- a/tests/basic/test_main_smoke.py +++ b/tests/basic/test_main_smoke.py @@ -21,7 +21,10 @@ def isolated_env(tmp_path, monkeypatch, mocker): } mocker.patch.dict(os.environ, clean_env, clear=True) - mocker.patch("aider.io.webbrowser.open", side_effect=AssertionError("Browser should not open during tests")) + mocker.patch( + "aider.io.webbrowser.open", + side_effect=AssertionError("Browser should not open during tests"), + ) mocker.patch("builtins.input", return_value=None) monkeypatch.chdir(tmp_path) From 633cdd2f57b0b1121a4ee45494f2567e689abcfa Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 18:46:37 +0100 Subject: [PATCH 15/23] ci: Install pytest-mock in test actions --- .github/workflows/ubuntu-tests.yml | 1 + .github/workflows/windows-tests.yml | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ubuntu-tests.yml b/.github/workflows/ubuntu-tests.yml index e26127489a3..f84933f6b9d 100644 --- a/.github/workflows/ubuntu-tests.yml +++ b/.github/workflows/ubuntu-tests.yml @@ -50,6 +50,7 @@ jobs: uv pip install --system \ pytest \ pytest-asyncio \ + pytest-mock \ -r requirements/requirements.in \ -r requirements/requirements-help.in \ -r requirements/requirements-playwright.in \ diff --git a/.github/workflows/windows-tests.yml b/.github/workflows/windows-tests.yml index c5e3f12d988..8b809a17405 100644 --- a/.github/workflows/windows-tests.yml +++ b/.github/workflows/windows-tests.yml @@ -42,11 +42,10 @@ jobs: run: | python -m pip install --upgrade pip pip install uv - uv pip install --system pytest pytest-asyncio -r requirements/requirements.in -r requirements/requirements-help.in -r requirements/requirements-playwright.in '.[help,playwright]' + uv pip install --system pytest pytest-asyncio pytest-mock -r requirements/requirements.in -r requirements/requirements-help.in -r requirements/requirements-playwright.in '.[help,playwright]' - name: Run tests env: AIDER_ANALYTICS: false run: | pytest - From 8e148a621c4bea6b1d65602782f77c7d7bbbfbd4 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 19:02:03 +0100 Subject: [PATCH 16/23] fix: set USERPROFILE for Windows compatibility in smoke tests Windows uses USERPROFILE environment variable for Path.home() instead of HOME. Use platform.system() to conditionally set the appropriate environment variable for the platform. --- tests/basic/test_main_smoke.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/basic/test_main_smoke.py b/tests/basic/test_main_smoke.py index 9b071a52f2f..1586d88a3eb 100644 --- a/tests/basic/test_main_smoke.py +++ b/tests/basic/test_main_smoke.py @@ -1,4 +1,5 @@ import os +import platform import pytest from prompt_toolkit.input import DummyInput @@ -15,11 +16,15 @@ def isolated_env(tmp_path, monkeypatch, mocker): clean_env = { "OPENAI_API_KEY": "test-key", - "HOME": str(fake_home), "AIDER_CHECK_UPDATE": "false", "AIDER_ANALYTICS": "false", } + if platform.system() == "Windows": + clean_env["USERPROFILE"] = str(fake_home) + else: + clean_env["HOME"] = str(fake_home) + mocker.patch.dict(os.environ, clean_env, clear=True) mocker.patch( "aider.io.webbrowser.open", From 6b765305bbbc99b3ff16412f68692a9753a5be7c Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 19:48:46 +0100 Subject: [PATCH 17/23] test: convert async tests to sync using asyncio.run() - Convert test_setup_git from async def to def - Convert test_check_gitignore from async def to def - Convert test_argv_file_respects_git from async def to def - Wrap async function calls with asyncio.run() - Add asyncio import - Remove @pytest.mark.skip decorators All three tests now execute and pass (79 passed, 0 skipped, 2 xfailed) --- tests/basic/test_main.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index a42aeb1c0ce..7037c320790 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -1,3 +1,4 @@ +import asyncio import json import os import subprocess @@ -175,10 +176,9 @@ def test_main_with_empty_git_dir_new_subdir_file(self): # Because aider will try and `git add` a file that's already in the repo. main(["--yes-always", str(fname), "--exit"], input=DummyInput(), output=DummyOutput()) - @pytest.mark.skip(reason="TODO: Needs refactoring to call async helpers via asyncio.run()") - async def test_setup_git(self): + def test_setup_git(self): io = InputOutput(pretty=False, yes=True) - git_root = await setup_git(None, io) + git_root = asyncio.run(setup_git(None, io)) git_root = Path(git_root).resolve() self.assertEqual(git_root, Path(self.tempdir).resolve()) @@ -188,8 +188,7 @@ async def test_setup_git(self): self.assertTrue(gitignore.exists()) self.assertEqual(".aider*", gitignore.read_text().splitlines()[0]) - @pytest.mark.skip(reason="TODO: Needs refactoring to call async helpers via asyncio.run()") - async def test_check_gitignore(self): + def test_check_gitignore(self): with GitTemporaryDirectory(): os.environ["GIT_CONFIG_GLOBAL"] = "globalgitconfig" @@ -198,20 +197,20 @@ async def test_check_gitignore(self): gitignore = cwd / ".gitignore" self.assertFalse(gitignore.exists()) - await check_gitignore(cwd, io) + asyncio.run(check_gitignore(cwd, io)) self.assertTrue(gitignore.exists()) self.assertEqual(".aider*", gitignore.read_text().splitlines()[0]) # Test without .env file present gitignore.write_text("one\ntwo\n") - await check_gitignore(cwd, io) + asyncio.run(check_gitignore(cwd, io)) self.assertEqual("one\ntwo\n.aider*\n", gitignore.read_text()) # Test with .env file present env_file = cwd / ".env" env_file.touch() - await check_gitignore(cwd, io) + asyncio.run(check_gitignore(cwd, io)) self.assertEqual("one\ntwo\n.aider*\n.env\n", gitignore.read_text()) del os.environ["GIT_CONFIG_GLOBAL"] @@ -1639,8 +1638,7 @@ def test_stream_without_cache_no_warning(self, MockInputOutput): for call in mock_io_instance.tool_warning.call_args_list: self.assertNotIn("Cost estimates may be inaccurate", call[0][0]) - @pytest.mark.skip(reason="TODO: Needs refactoring to call async helpers via asyncio.run()") - async def test_argv_file_respects_git(self): + def test_argv_file_respects_git(self): with GitTemporaryDirectory(): fname = Path("not_in_git.txt") fname.touch() @@ -1653,7 +1651,7 @@ async def test_argv_file_respects_git(self): return_coder=True, ) self.assertNotIn("not_in_git.txt", str(coder.abs_fnames)) - self.assertFalse(await coder.allowed_to_edit("not_in_git.txt")) + self.assertFalse(asyncio.run(coder.allowed_to_edit("not_in_git.txt"))) def test_load_dotenv_files_override(self): with GitTemporaryDirectory() as git_dir: From 9682e6300981a00a4b3008191700a4586b26219d Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 20:01:43 +0100 Subject: [PATCH 18/23] test: update test_add_command_gitignore_files_flag for new command architecture - Remove @pytest.mark.xfail decorator - Replace coder.commands.cmd_add() with asyncio.run(coder.commands.do_run()) - Add try/except SwitchCoder blocks (AddCommand raises this to signal refresh) - Remove patch.object for confirm_ask (--yes-always handles this) - Add SwitchCoder import The add command was refactored from method-based to class-based in commit 82ba8977b. The test now uses the new Commands.do_run() API to execute the AddCommand.execute() method through the CommandRegistry. Test verifies gitignore behavior: - Default: gitignored files rejected - --add-gitignore-files: gitignored files accepted - --no-add-gitignore-files: gitignored files rejected --- tests/basic/test_main.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 7037c320790..00845b17648 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -14,6 +14,7 @@ from prompt_toolkit.output import DummyOutput from aider.coders import Coder, CopyPasteCoder +from aider.commands import SwitchCoder from aider.dump import dump # noqa: F401 from aider.io import InputOutput from aider.main import check_gitignore, load_dotenv_files, main, setup_git @@ -262,7 +263,6 @@ def test_command_line_gitignore_files_flag(self): # Verify the ignored file is not in the chat self.assertNotIn(abs_ignored_file, coder.abs_fnames) - @pytest.mark.xfail(reason="TODO: Commands.cmd_add no longer exists, test needs refactoring") def test_add_command_gitignore_files_flag(self): with GitTemporaryDirectory() as git_dir: git_dir = Path(git_dir) @@ -288,8 +288,10 @@ def test_add_command_gitignore_files_flag(self): force_git_root=git_dir, ) - with patch.object(coder.io, "confirm_ask", return_value=True): - coder.commands.cmd_add(rel_ignored_file) + try: + asyncio.run(coder.commands.do_run("add", rel_ignored_file)) + except SwitchCoder: + pass # Verify the ignored file is not in the chat self.assertNotIn(abs_ignored_file, coder.abs_fnames) @@ -302,8 +304,10 @@ def test_add_command_gitignore_files_flag(self): return_coder=True, force_git_root=git_dir, ) - with patch.object(coder.io, "confirm_ask", return_value=True): - coder.commands.cmd_add(rel_ignored_file) + try: + asyncio.run(coder.commands.do_run("add", rel_ignored_file)) + except SwitchCoder: + pass # Verify the ignored file is in the chat self.assertIn(abs_ignored_file, coder.abs_fnames) @@ -317,8 +321,10 @@ def test_add_command_gitignore_files_flag(self): force_git_root=git_dir, ) - with patch.object(coder.io, "confirm_ask", return_value=True): - coder.commands.cmd_add(rel_ignored_file) + try: + asyncio.run(coder.commands.do_run("add", rel_ignored_file)) + except SwitchCoder: + pass # Verify the ignored file is not in the chat self.assertNotIn(abs_ignored_file, coder.abs_fnames) From 6d0655ebf36ebc0eb5d9c178bd2ad96a9733c5a5 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 21:14:24 +0100 Subject: [PATCH 19/23] fix: update LintCommand to read files from system_args and support globs - Update LintCommand to read CLI files from system_args instead of kwargs - Add glob pattern expansion support for --lint flag - Move expand_glob_patterns from main.py to utils.py to avoid circular imports - Add tests for explicit files and glob patterns with --lint This fixes the --lint flag after the command system refactoring (82ba8977b) that moved commands to class-based architecture but didn't update callers. Tests: - test_lint_option: verifies --lint with no files (dirty file fallback) - test_lint_option_with_explicit_files: verifies --lint file1.py file2.py - test_lint_option_with_glob_pattern: verifies --lint *.py All 83 tests pass. --- aider/commands/lint.py | 14 ++++++++- aider/main.py | 23 ++------------- aider/utils.py | 20 +++++++++++++ tests/basic/test_main.py | 61 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/aider/commands/lint.py b/aider/commands/lint.py index fc6d45ead57..1e49d57c824 100644 --- a/aider/commands/lint.py +++ b/aider/commands/lint.py @@ -1,7 +1,9 @@ +from pathlib import Path from typing import List from aider.commands.utils.base_command import BaseCommand from aider.commands.utils.helpers import format_command_result +from aider.utils import expand_glob_patterns class LintCommand(BaseCommand): @@ -11,7 +13,17 @@ class LintCommand(BaseCommand): @classmethod async def execute(cls, io, coder, args, **kwargs): """Execute the lint command with given parameters.""" - fnames = kwargs.get("fnames", None) + fnames = None + + # Get files from CLI arguments if available + system_args = kwargs.get("system_args") + if system_args: + cli_files = getattr(system_args, 'files', []) or [] + cli_file_arg = getattr(system_args, 'file', []) or [] + all_cli_files = cli_files + cli_file_arg + if all_cli_files: + all_cli_files = expand_glob_patterns(all_cli_files) + fnames = [str(Path(f).resolve()) for f in all_cli_files] if not coder.repo: io.tool_error("No git repository found.") diff --git a/aider/main.py b/aider/main.py index a5b79e7137b..61d6a035fe0 100644 --- a/aider/main.py +++ b/aider/main.py @@ -515,23 +515,6 @@ async def sanity_check_repo(repo, io): return False -def expand_glob_patterns(patterns, root="."): - """Expand glob patterns in a list of file paths.""" - expanded_files = [] - for pattern in patterns: - # Check if the pattern contains glob characters - if any(c in pattern for c in "*?[]"): - # Use glob to expand the pattern - matches = glob.glob(pattern, recursive=True) - if matches: - expanded_files.extend(matches) - else: - # If no matches, keep the original pattern - expanded_files.append(pattern) - else: - # Not a glob pattern, keep as is - expanded_files.append(pattern) - return expanded_files PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) @@ -841,12 +824,12 @@ def get_io(pretty): # Expand glob patterns in files and file arguments all_files = args.files + (args.file or []) - all_files = expand_glob_patterns(all_files) + all_files = utils.expand_glob_patterns(all_files) fnames = [str(Path(fn).resolve()) for fn in all_files] # Expand glob patterns in read arguments read_patterns = args.read or [] - read_expanded = expand_glob_patterns(read_patterns) + read_expanded = utils.expand_glob_patterns(read_patterns) read_only_fnames = [] for fn in read_expanded: path = Path(fn).expanduser().resolve() @@ -1323,7 +1306,7 @@ def apply_model_overrides(model_name): return await graceful_exit(coder) if args.lint: - await coder.commands.cmd_lint(fnames=fnames) + await coder.commands.do_run("lint", "") if args.test: if not args.test_cmd: diff --git a/aider/utils.py b/aider/utils.py index a171ca8466d..7b04ed86b0a 100644 --- a/aider/utils.py +++ b/aider/utils.py @@ -1,3 +1,4 @@ +import glob import os import platform import shutil @@ -14,6 +15,25 @@ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".webp", ".pdf"} +def expand_glob_patterns(patterns, root="."): + """Expand glob patterns in a list of file paths.""" + expanded_files = [] + for pattern in patterns: + # Check if the pattern contains glob characters + if any(c in pattern for c in "*?[]"): + # Use glob to expand the pattern + matches = glob.glob(pattern, recursive=True) + if matches: + expanded_files.extend(matches) + else: + # If no matches, keep the original pattern + expanded_files.append(pattern) + else: + # Not a glob pattern, keep as is + expanded_files.append(pattern) + return expanded_files + + def _execute_fzf(input_data, multi=False): """ Runs fzf as a subprocess, feeding it input_data. diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 00845b17648..5b5153d5d81 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -562,7 +562,6 @@ def test_true_vals_in_env_file(self): _, kwargs = MockCoder.call_args self.assertEqual(kwargs["show_diffs"], True) - @pytest.mark.xfail(reason="TODO: Commands.cmd_lint no longer exists, test needs refactoring") def test_lint_option(self): with GitTemporaryDirectory() as git_dir: # Create a dirty file in the root @@ -587,7 +586,7 @@ def test_lint_option(self): MockLinter.return_value = "" # Run main with --lint option - main(["--lint", "--yes-always"]) + main(["--lint", "--yes-always"], input=DummyInput(), output=DummyOutput()) # Check if the Linter was called with a filename ending in "dirty_file.py" # but not ending in "subdir/dirty_file.py" @@ -596,6 +595,64 @@ def test_lint_option(self): self.assertTrue(called_arg.endswith("dirty_file.py")) self.assertFalse(called_arg.endswith(f"subdir{os.path.sep}dirty_file.py")) + def test_lint_option_with_explicit_files(self): + with GitTemporaryDirectory(): + # Create two files + file1 = Path("file1.py") + file1.write_text("def foo(): pass") + file2 = Path("file2.py") + file2.write_text("def bar(): pass") + + # Mock the Linter class + with patch("aider.linter.Linter.lint") as MockLinter: + MockLinter.return_value = "" + + # Run main with --lint and explicit files + main( + ["--lint", "file1.py", "file2.py", "--yes-always"], + input=DummyInput(), + output=DummyOutput(), + ) + + # Check if the Linter was called twice (once for each file) + self.assertEqual(MockLinter.call_count, 2) + + # Check that both files were linted + called_files = [call[0][0] for call in MockLinter.call_args_list] + self.assertTrue(any(f.endswith("file1.py") for f in called_files)) + self.assertTrue(any(f.endswith("file2.py") for f in called_files)) + + def test_lint_option_with_glob_pattern(self): + with GitTemporaryDirectory(): + # Create multiple Python files + file1 = Path("test1.py") + file1.write_text("def foo(): pass") + file2 = Path("test2.py") + file2.write_text("def bar(): pass") + file3 = Path("readme.txt") + file3.write_text("not a python file") + + # Mock the Linter class + with patch("aider.linter.Linter.lint") as MockLinter: + MockLinter.return_value = "" + + # Run main with --lint and glob pattern + main( + ["--lint", "test*.py", "--yes-always"], + input=DummyInput(), + output=DummyOutput(), + ) + + # Check if the Linter was called for Python files matching the glob + self.assertGreaterEqual(MockLinter.call_count, 2) + + # Check that Python files were linted + called_files = [call[0][0] for call in MockLinter.call_args_list] + self.assertTrue(any(f.endswith("test1.py") for f in called_files)) + self.assertTrue(any(f.endswith("test2.py") for f in called_files)) + # Check that non-Python file was not linted + self.assertFalse(any(f.endswith("readme.txt") for f in called_files)) + def test_verbose_mode_lists_env_vars(self): self.create_env_file(".env", "AIDER_DARK_MODE=on") with patch("sys.stdout", new_callable=StringIO) as mock_stdout: From 07bf35c7d0fbcd150874bb094fc0985a722c68a8 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 21:15:02 +0100 Subject: [PATCH 20/23] refactor: remove duplicate path resolution in LintCommand CLI files were being resolved to absolute paths twice: 1. At line 26: str(Path(f).resolve()) 2. At line 43: coder.abs_root_path(fname) Now all file paths (CLI, in-chat, dirty) are resolved uniformly at line 43. --- aider/commands/lint.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aider/commands/lint.py b/aider/commands/lint.py index 1e49d57c824..2599acbc8ec 100644 --- a/aider/commands/lint.py +++ b/aider/commands/lint.py @@ -22,8 +22,7 @@ async def execute(cls, io, coder, args, **kwargs): cli_file_arg = getattr(system_args, 'file', []) or [] all_cli_files = cli_files + cli_file_arg if all_cli_files: - all_cli_files = expand_glob_patterns(all_cli_files) - fnames = [str(Path(f).resolve()) for f in all_cli_files] + fnames = expand_glob_patterns(all_cli_files) if not coder.repo: io.tool_error("No git repository found.") From 45ee516d6a5c4ba0240d989d03c48bae1797d686 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 21:15:27 +0100 Subject: [PATCH 21/23] refactor: remove redundant coder.repo check in LintCommand The check 'if not fnames and coder.repo' is redundant because we already checked 'if not coder.repo' at line 27 and returned early. At line 35, coder.repo is guaranteed to be truthy. --- aider/commands/lint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/commands/lint.py b/aider/commands/lint.py index 2599acbc8ec..1e5db3c9560 100644 --- a/aider/commands/lint.py +++ b/aider/commands/lint.py @@ -32,7 +32,7 @@ async def execute(cls, io, coder, args, **kwargs): fnames = coder.get_inchat_relative_files() # If still no files, get all dirty files in the repo - if not fnames and coder.repo: + if not fnames: fnames = coder.repo.get_dirty_files() if not fnames: From 4172b39db9048ef949cc51e1a13666abadba0645 Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 21:16:25 +0100 Subject: [PATCH 22/23] refactor: remove unused root parameter from expand_glob_patterns The 'root' parameter was defined but never used in the function body. Verified that all 3 call sites (2 in main.py, 1 in lint.py) call it without this parameter. --- aider/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aider/utils.py b/aider/utils.py index 7b04ed86b0a..dbaede294a3 100644 --- a/aider/utils.py +++ b/aider/utils.py @@ -15,7 +15,7 @@ IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".webp", ".pdf"} -def expand_glob_patterns(patterns, root="."): +def expand_glob_patterns(patterns): """Expand glob patterns in a list of file paths.""" expanded_files = [] for pattern in patterns: From 96ddfb9820813f5e85e6d2440f02da2f1bb75caa Mon Sep 17 00:00:00 2001 From: Johannes Bornhold Date: Mon, 29 Dec 2025 21:21:38 +0100 Subject: [PATCH 23/23] style: fix linting issues - Remove unused Path import from lint.py - Remove unused glob import from main.py - Remove unused pytest import from test_main.py - Apply black formatting All pre-commit hooks now pass. --- aider/commands/lint.py | 5 ++--- aider/main.py | 3 --- tests/basic/test_main.py | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/aider/commands/lint.py b/aider/commands/lint.py index 1e5db3c9560..939bd6b5372 100644 --- a/aider/commands/lint.py +++ b/aider/commands/lint.py @@ -1,4 +1,3 @@ -from pathlib import Path from typing import List from aider.commands.utils.base_command import BaseCommand @@ -18,8 +17,8 @@ async def execute(cls, io, coder, args, **kwargs): # Get files from CLI arguments if available system_args = kwargs.get("system_args") if system_args: - cli_files = getattr(system_args, 'files', []) or [] - cli_file_arg = getattr(system_args, 'file', []) or [] + cli_files = getattr(system_args, "files", []) or [] + cli_file_arg = getattr(system_args, "file", []) or [] all_cli_files = cli_files + cli_file_arg if all_cli_files: fnames = expand_glob_patterns(all_cli_files) diff --git a/aider/main.py b/aider/main.py index 61d6a035fe0..41d9f92fb8b 100644 --- a/aider/main.py +++ b/aider/main.py @@ -10,7 +10,6 @@ pass import asyncio -import glob import json import os import re @@ -515,8 +514,6 @@ async def sanity_check_repo(repo, io): return False - - PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) log_file = None file_excludelist = { diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 5b5153d5d81..bc082a40590 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -9,7 +9,6 @@ from unittest.mock import AsyncMock, MagicMock, patch import git -import pytest from prompt_toolkit.input import DummyInput from prompt_toolkit.output import DummyOutput