import os import tempfile import unittest from pathlib import Path from unittest.mock import MagicMock, patch import git from aider.coders import Coder from aider.coders.base_coder import FinishReasonLength, UnknownEditFormat from aider.dump import dump # noqa: F401 from aider.io import InputOutput from aider.models import Model from aider.repo import GitRepo from aider.sendchat import sanity_check_messages from aider.utils import GitTemporaryDirectory class TestCoder(unittest.TestCase): def setUp(self): self.GPT35 = Model("gpt-3.5-turbo") self.webbrowser_patcher = patch("aider.io.webbrowser.open") self.mock_webbrowser = self.webbrowser_patcher.start() def test_allowed_to_edit(self): with GitTemporaryDirectory(): repo = git.Repo() fname = Path("added.txt") fname.touch() repo.git.add(str(fname)) fname = Path("repo.txt") fname.touch() repo.git.add(str(fname)) repo.git.commit("-m", "init") # YES! # Use a completely mocked IO object instead of a real one io = MagicMock() io.confirm_ask = MagicMock(return_value=True) coder = Coder.create(self.GPT35, None, io, fnames=["added.txt"]) self.assertTrue(coder.allowed_to_edit("added.txt")) self.assertTrue(coder.allowed_to_edit("repo.txt")) self.assertTrue(coder.allowed_to_edit("new.txt")) self.assertIn("repo.txt", str(coder.abs_fnames)) self.assertIn("new.txt", str(coder.abs_fnames)) self.assertFalse(coder.need_commit_before_edits) def test_allowed_to_edit_no(self): with GitTemporaryDirectory(): repo = git.Repo() fname = Path("added.txt") fname.touch() repo.git.add(str(fname)) fname = Path("repo.txt") fname.touch() repo.git.add(str(fname)) repo.git.commit("-m", "init") # say NO io = InputOutput(yes=False) coder = Coder.create(self.GPT35, None, io, fnames=["added.txt"]) self.assertTrue(coder.allowed_to_edit("added.txt")) self.assertFalse(coder.allowed_to_edit("repo.txt")) self.assertFalse(coder.allowed_to_edit("new.txt")) self.assertNotIn("repo.txt", str(coder.abs_fnames)) self.assertNotIn("new.txt", str(coder.abs_fnames)) self.assertFalse(coder.need_commit_before_edits) def test_allowed_to_edit_dirty(self): with GitTemporaryDirectory(): repo = git.Repo() fname = Path("added.txt") fname.touch() repo.git.add(str(fname)) repo.git.commit("-m", "init") # say NO io = InputOutput(yes=False) coder = Coder.create(self.GPT35, None, io, fnames=["added.txt"]) self.assertTrue(coder.allowed_to_edit("added.txt")) self.assertFalse(coder.need_commit_before_edits) fname.write_text("dirty!") self.assertTrue(coder.allowed_to_edit("added.txt")) self.assertTrue(coder.need_commit_before_edits) def test_get_files_content(self): tempdir = Path(tempfile.mkdtemp()) file1 = tempdir / "file1.txt" file2 = tempdir / "file2.txt" file1.touch() file2.touch() files = [file1, file2] # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files) content = coder.get_files_content().splitlines() self.assertIn("file1.txt", content) self.assertIn("file2.txt", content) def test_check_for_filename_mentions(self): with GitTemporaryDirectory(): repo = git.Repo() mock_io = MagicMock() fname1 = Path("file1.txt") fname2 = Path("file2.py") fname1.write_text("one\n") fname2.write_text("two\n") repo.git.add(str(fname1)) repo.git.add(str(fname2)) repo.git.commit("-m", "new") # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create(self.GPT35, None, mock_io) # Call the check_for_file_mentions method coder.check_for_file_mentions("Please check file1.txt and file2.py") # Check if coder.abs_fnames contains both files expected_files = set( [ str(Path(coder.root) / fname1), str(Path(coder.root) / fname2), ] ) self.assertEqual(coder.abs_fnames, expected_files) def test_check_for_ambiguous_filename_mentions_of_longer_paths(self): with GitTemporaryDirectory(): io = InputOutput(pretty=False, yes=True) coder = Coder.create(self.GPT35, None, io) fname = Path("file1.txt") fname.touch() other_fname = Path("other") / "file1.txt" other_fname.parent.mkdir(parents=True, exist_ok=True) other_fname.touch() mock = MagicMock() mock.return_value = set([str(fname), str(other_fname)]) coder.repo.get_tracked_files = mock # Call the check_for_file_mentions method coder.check_for_file_mentions(f"Please check {fname}!") self.assertEqual(coder.abs_fnames, set([str(fname.resolve())])) def test_skip_duplicate_basename_mentions(self): with GitTemporaryDirectory(): io = InputOutput(pretty=False, yes=True) coder = Coder.create(self.GPT35, None, io) # Create files with same basename in different directories fname1 = Path("dir1") / "file.txt" fname2 = Path("dir2") / "file.txt" fname3 = Path("dir3") / "unique.txt" for fname in [fname1, fname2, fname3]: fname.parent.mkdir(parents=True, exist_ok=True) fname.touch() # Add one file to chat coder.add_rel_fname(str(fname1)) # Mock get_tracked_files to return all files mock = MagicMock() mock.return_value = set([str(fname1), str(fname2), str(fname3)]) coder.repo.get_tracked_files = mock # Check that file mentions skip files with duplicate basenames mentioned = coder.get_file_mentions(f"Check {fname2} and {fname3}") self.assertEqual(mentioned, {str(fname3)}) # Add a read-only file with same basename coder.abs_read_only_fnames.add(str(fname2.resolve())) mentioned = coder.get_file_mentions(f"Check {fname1} and {fname3}") self.assertEqual(mentioned, {str(fname3)}) def test_check_for_file_mentions_read_only(self): with GitTemporaryDirectory(): io = InputOutput( pretty=False, yes=True, ) coder = Coder.create(self.GPT35, None, io) fname = Path("readonly_file.txt") fname.touch() coder.abs_read_only_fnames.add(str(fname.resolve())) # Mock the get_tracked_files method mock = MagicMock() mock.return_value = set([str(fname)]) coder.repo.get_tracked_files = mock # Call the check_for_file_mentions method result = coder.check_for_file_mentions(f"Please check {fname}!") # Assert that the method returns None (user not asked to add the file) self.assertIsNone(result) # Assert that abs_fnames is still empty (file not added) self.assertEqual(coder.abs_fnames, set()) def test_check_for_file_mentions_with_mocked_confirm(self): with GitTemporaryDirectory(): io = InputOutput(pretty=False) coder = Coder.create(self.GPT35, None, io) # Mock get_file_mentions to return two file names coder.get_file_mentions = MagicMock(return_value=set(["file1.txt", "file2.txt"])) # Mock confirm_ask to return False for the first call and True for the second io.confirm_ask = MagicMock(side_effect=[False, True, True]) # First call to check_for_file_mentions coder.check_for_file_mentions("Please check file1.txt for the info") # Assert that confirm_ask was called twice self.assertEqual(io.confirm_ask.call_count, 2) # Assert that only file2.txt was added to abs_fnames self.assertEqual(len(coder.abs_fnames), 1) self.assertIn("file2.txt", str(coder.abs_fnames)) # Reset the mock io.confirm_ask.reset_mock() # Second call to check_for_file_mentions coder.check_for_file_mentions("Please check file1.txt and file2.txt again") # Assert that confirm_ask was called only once (for file1.txt) self.assertEqual(io.confirm_ask.call_count, 1) # Assert that abs_fnames still contains only file2.txt self.assertEqual(len(coder.abs_fnames), 1) self.assertIn("file2.txt", str(coder.abs_fnames)) # Assert that file1.txt is in ignore_mentions self.assertIn("file1.txt", coder.ignore_mentions) def test_check_for_subdir_mention(self): with GitTemporaryDirectory(): io = InputOutput(pretty=False, yes=True) coder = Coder.create(self.GPT35, None, io) fname = Path("other") / "file1.txt" fname.parent.mkdir(parents=True, exist_ok=True) fname.touch() mock = MagicMock() mock.return_value = set([str(fname)]) coder.repo.get_tracked_files = mock # Call the check_for_file_mentions method coder.check_for_file_mentions(f"Please check `{fname}`") self.assertEqual(coder.abs_fnames, set([str(fname.resolve())])) def test_get_file_mentions_various_formats(self): with GitTemporaryDirectory(): io = InputOutput(pretty=False, yes=True) coder = Coder.create(self.GPT35, None, io) # Create test files test_files = [ "file1.txt", "file2.py", "dir/nested_file.js", "dir/subdir/deep_file.html", "file99.txt", "special_chars!@#.md", ] # Pre-format the Windows path to avoid backslash issues in f-string expressions windows_path = test_files[2].replace("/", "\\") for fname in test_files: fpath = Path(fname) fpath.parent.mkdir(parents=True, exist_ok=True) fpath.touch() # Mock get_addable_relative_files to return our test files coder.get_addable_relative_files = MagicMock(return_value=set(test_files)) # Test different mention formats test_cases = [ # Simple plain text mentions (f"You should edit {test_files[0]} first", {test_files[0]}), # Multiple files in plain text (f"Edit both {test_files[0]} and {test_files[1]}", {test_files[0], test_files[1]}), # Files in backticks (f"Check the file `{test_files[2]}`", {test_files[2]}), # Files in code blocks (f"```\n{test_files[3]}\n```", {test_files[3]}), # Files in code blocks with language specifier # ( # f"```python\nwith open('{test_files[1]}', 'r') as f:\n" # f" data = f.read()\n```", # {test_files[1]}, # ), # Files with Windows-style paths (f"Edit the file {windows_path}", {test_files[2]}), # Files with different quote styles (f'Check "{test_files[5]}" now', {test_files[5]}), # All files in one complex message ( ( f"First, edit `{test_files[0]}`. Then modify {test_files[1]}.\n" f"```js\n// Update this file\nconst file = '{test_files[2]}';\n```\n" f"Finally check {test_files[3].replace('/', '\\')}" ), {test_files[0], test_files[1], test_files[2], test_files[3]}, ), # Files mentioned in markdown bold format (f"You should check **{test_files[0]}** for issues", {test_files[0]}), ( f"Look at both **{test_files[1]}** and **{test_files[2]}**", {test_files[1], test_files[2]}, ), ( f"The file **{test_files[3].replace('/', '\\')}** needs updating", {test_files[3]}, ), ( f"Files to modify:\n- **{test_files[0]}**\n- **{test_files[4]}**", {test_files[0], test_files[4]}, ), ] for content, expected_mentions in test_cases: with self.subTest(content=content): mentioned_files = coder.get_file_mentions(content) self.assertEqual( mentioned_files, expected_mentions, f"Failed to extract mentions from: {content}", ) def test_get_file_mentions_path_formats(self): with GitTemporaryDirectory(): io = InputOutput(pretty=False, yes=True) coder = Coder.create(self.GPT35, None, io) # Test cases with different path formats test_cases = [ # Unix paths in content, Unix paths in get_addable_relative_files ("Check file1.txt and dir/file2.txt", ["file1.txt", "dir/file2.txt"]), # Windows paths in content, Windows paths in get_addable_relative_files ("Check file1.txt and dir\\file2.txt", ["file1.txt", "dir\\file2.txt"]), # Unix paths in content, Windows paths in get_addable_relative_files ("Check file1.txt and dir/file2.txt", ["file1.txt", "dir\\file2.txt"]), # Windows paths in content, Unix paths in get_addable_relative_files ("Check file1.txt and dir\\file2.txt", ["file1.txt", "dir/file2.txt"]), # Mixed paths in content, Unix paths in get_addable_relative_files ( "Check file1.txt, dir/file2.txt, and other\\file3.txt", ["file1.txt", "dir/file2.txt", "other/file3.txt"], ), # Mixed paths in content, Windows paths in get_addable_relative_files ( "Check file1.txt, dir/file2.txt, and other\\file3.txt", ["file1.txt", "dir\\file2.txt", "other\\file3.txt"], ), ] for content, addable_files in test_cases: with self.subTest(content=content, addable_files=addable_files): coder.get_addable_relative_files = MagicMock(return_value=set(addable_files)) mentioned_files = coder.get_file_mentions(content) expected_files = set(addable_files) self.assertEqual( mentioned_files, expected_files, f"Failed for content: {content}, addable_files: {addable_files}", ) def test_run_with_file_deletion(self): # Create a few temporary files tempdir = Path(tempfile.mkdtemp()) file1 = tempdir / "file1.txt" file2 = tempdir / "file2.txt" file1.touch() file2.touch() files = [file1, file2] # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files) def mock_send(*args, **kwargs): coder.partial_response_content = "ok" coder.partial_response_function_call = dict() return [] coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") self.assertEqual(len(coder.abs_fnames), 2) file1.unlink() # Call the run method again with a message coder.run(with_message="hi") self.assertEqual(len(coder.abs_fnames), 1) def test_run_with_file_unicode_error(self): # Create a few temporary files _, file1 = tempfile.mkstemp() _, file2 = tempfile.mkstemp() files = [file1, file2] # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files) def mock_send(*args, **kwargs): coder.partial_response_content = "ok" coder.partial_response_function_call = dict() return [] coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") self.assertEqual(len(coder.abs_fnames), 2) # Write some non-UTF8 text into the file with open(file1, "wb") as f: f.write(b"\x80abc") # Call the run method again with a message coder.run(with_message="hi") self.assertEqual(len(coder.abs_fnames), 1) def test_choose_fence(self): # Create a few temporary files _, file1 = tempfile.mkstemp() with open(file1, "wb") as f: f.write(b"this contains\n```\nbackticks") files = [file1] # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create(self.GPT35, None, io=InputOutput(), fnames=files) def mock_send(*args, **kwargs): coder.partial_response_content = "ok" coder.partial_response_function_call = dict() return [] coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") self.assertNotEqual(coder.fence[0], "```") def test_run_with_file_utf_unicode_error(self): "make sure that we honor InputOutput(encoding) and don't just assume utf-8" # Create a few temporary files _, file1 = tempfile.mkstemp() _, file2 = tempfile.mkstemp() files = [file1, file2] encoding = "utf-16" # Initialize the Coder object with the mocked IO and mocked repo coder = Coder.create( self.GPT35, None, io=InputOutput(encoding=encoding), fnames=files, ) def mock_send(*args, **kwargs): coder.partial_response_content = "ok" coder.partial_response_function_call = dict() return [] coder.send = mock_send # Call the run method with a message coder.run(with_message="hi") self.assertEqual(len(coder.abs_fnames), 2) some_content_which_will_error_if_read_with_encoding_utf8 = "ÅÍÎÏ".encode(encoding) with open(file1, "wb") as f: f.write(some_content_which_will_error_if_read_with_encoding_utf8) coder.run(with_message="hi") # both files should still be here self.assertEqual(len(coder.abs_fnames), 2) def test_new_file_edit_one_commit(self): """A new file should get pre-committed before the GPT edit commit""" with GitTemporaryDirectory(): repo = git.Repo() fname = Path("file.txt") io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)]) self.assertTrue(fname.exists()) # make sure it was not committed with self.assertRaises(git.exc.GitCommandError): list(repo.iter_commits(repo.active_branch.name)) def mock_send(*args, **kwargs): coder.partial_response_content = f""" Do this: {str(fname)} <<<<<<< SEARCH ======= new >>>>>>> REPLACE """ coder.partial_response_function_call = dict() return [] coder.send = mock_send coder.repo.get_commit_message = MagicMock() coder.repo.get_commit_message.return_value = "commit message" coder.run(with_message="hi") content = fname.read_text() self.assertEqual(content, "new\n") num_commits = len(list(repo.iter_commits(repo.active_branch.name))) self.assertEqual(num_commits, 2) def test_only_commit_gpt_edited_file(self): """ Only commit file that gpt edits, not other dirty files. Also ensure commit msg only depends on diffs from the GPT edited file. """ with GitTemporaryDirectory(): repo = git.Repo() fname1 = Path("file1.txt") fname2 = Path("file2.txt") fname1.write_text("one\n") fname2.write_text("two\n") repo.git.add(str(fname1)) repo.git.add(str(fname2)) repo.git.commit("-m", "new") # DIRTY! fname1.write_text("ONE\n") io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname1), str(fname2)]) def mock_send(*args, **kwargs): coder.partial_response_content = f""" Do this: {str(fname2)} <<<<<<< SEARCH two ======= TWO >>>>>>> REPLACE """ coder.partial_response_function_call = dict() return [] def mock_get_commit_message(diffs, context): self.assertNotIn("one", diffs) self.assertNotIn("ONE", diffs) return "commit message" coder.send = mock_send coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message) coder.run(with_message="hi") content = fname2.read_text() self.assertEqual(content, "TWO\n") self.assertTrue(repo.is_dirty(path=str(fname1))) def test_gpt_edit_to_dirty_file(self): """A dirty file should be committed before the GPT edits are committed""" with GitTemporaryDirectory(): repo = git.Repo() fname = Path("file.txt") fname.write_text("one\n") repo.git.add(str(fname)) fname2 = Path("other.txt") fname2.write_text("other\n") repo.git.add(str(fname2)) repo.git.commit("-m", "new") # dirty fname.write_text("two\n") fname2.write_text("OTHER\n") io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)]) def mock_send(*args, **kwargs): coder.partial_response_content = f""" Do this: {str(fname)} <<<<<<< SEARCH two ======= three >>>>>>> REPLACE """ coder.partial_response_function_call = dict() return [] saved_diffs = [] def mock_get_commit_message(diffs, context): saved_diffs.append(diffs) return "commit message" coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message) coder.send = mock_send coder.run(with_message="hi") content = fname.read_text() self.assertEqual(content, "three\n") num_commits = len(list(repo.iter_commits(repo.active_branch.name))) self.assertEqual(num_commits, 3) diff = repo.git.diff(["HEAD~2", "HEAD~1"]) self.assertIn("one", diff) self.assertIn("two", diff) self.assertNotIn("three", diff) self.assertNotIn("other", diff) self.assertNotIn("OTHER", diff) diff = saved_diffs[0] self.assertIn("one", diff) self.assertIn("two", diff) self.assertNotIn("three", diff) self.assertNotIn("other", diff) self.assertNotIn("OTHER", diff) diff = repo.git.diff(["HEAD~1", "HEAD"]) self.assertNotIn("one", diff) self.assertIn("two", diff) self.assertIn("three", diff) self.assertNotIn("other", diff) self.assertNotIn("OTHER", diff) diff = saved_diffs[1] self.assertNotIn("one", diff) self.assertIn("two", diff) self.assertIn("three", diff) self.assertNotIn("other", diff) self.assertNotIn("OTHER", diff) self.assertEqual(len(saved_diffs), 2) def test_gpt_edit_to_existing_file_not_in_repo(self): with GitTemporaryDirectory(): repo = git.Repo() fname = Path("file.txt") fname.write_text("one\n") fname2 = Path("other.txt") fname2.write_text("other\n") repo.git.add(str(fname2)) repo.git.commit("-m", "initial") io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)]) def mock_send(*args, **kwargs): coder.partial_response_content = f""" Do this: {str(fname)} <<<<<<< SEARCH one ======= two >>>>>>> REPLACE """ coder.partial_response_function_call = dict() return [] saved_diffs = [] def mock_get_commit_message(diffs, context): saved_diffs.append(diffs) return "commit message" coder.repo.get_commit_message = MagicMock(side_effect=mock_get_commit_message) coder.send = mock_send coder.run(with_message="hi") content = fname.read_text() self.assertEqual(content, "two\n") diff = saved_diffs[0] self.assertIn("file.txt", diff) def test_skip_aiderignored_files(self): with GitTemporaryDirectory(): repo = git.Repo() fname1 = "ignoreme1.txt" fname2 = "ignoreme2.txt" fname3 = "dir/ignoreme3.txt" Path(fname2).touch() repo.git.add(str(fname2)) repo.git.commit("-m", "initial") io = InputOutput(yes=True) fnames = [fname1, fname2, fname3] aignore = Path(".aiderignore") aignore.write_text(f"{fname1}\n{fname2}\ndir\n") repo = GitRepo( io, fnames, None, aider_ignore_file=str(aignore), ) coder = Coder.create( self.GPT35, None, io, fnames=fnames, repo=repo, ) self.assertNotIn(fname1, str(coder.abs_fnames)) self.assertNotIn(fname2, str(coder.abs_fnames)) self.assertNotIn(fname3, str(coder.abs_fnames)) def test_check_for_urls(self): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, None, io=io) coder.commands.scraper = MagicMock() coder.commands.scraper.scrape = MagicMock(return_value="some content") # Test various URL formats test_cases = [ ("Check http://example.com, it's cool", "http://example.com"), ("Visit https://www.example.com/page and see stuff", "https://www.example.com/page"), ( "Go to http://subdomain.example.com:8080/path?query=value, or not", "http://subdomain.example.com:8080/path?query=value", ), ( "See https://example.com/path#fragment for example", "https://example.com/path#fragment", ), ("Look at http://localhost:3000", "http://localhost:3000"), ("View https://example.com/setup#whatever", "https://example.com/setup#whatever"), ("Open http://127.0.0.1:8000/api/v1/", "http://127.0.0.1:8000/api/v1/"), ( "Try https://example.com/path/to/page.html?param1=value1¶m2=value2", "https://example.com/path/to/page.html?param1=value1¶m2=value2", ), ("Access http://user:password@example.com", "http://user:password@example.com"), ( "Use https://example.com/path_(with_parentheses)", "https://example.com/path_(with_parentheses)", ), ] for input_text, expected_url in test_cases: with self.subTest(input_text=input_text): result = coder.check_for_urls(input_text) self.assertIn(expected_url, result) # Test cases from the GitHub issue issue_cases = [ ("check http://localhost:3002, there is an error", "http://localhost:3002"), ( "can you check out https://example.com/setup#whatever", "https://example.com/setup#whatever", ), ] for input_text, expected_url in issue_cases: with self.subTest(input_text=input_text): result = coder.check_for_urls(input_text) self.assertIn(expected_url, result) # Test case with multiple URLs multi_url_input = "Check http://example1.com and https://example2.com/page" result = coder.check_for_urls(multi_url_input) self.assertIn("http://example1.com", result) self.assertIn("https://example2.com/page", result) # Test case with no URL no_url_input = "This text contains no URL" result = coder.check_for_urls(no_url_input) self.assertEqual(result, no_url_input) # Test case with the same URL appearing multiple times repeated_url_input = ( "Check https://example.com, then https://example.com again, and https://example.com one" " more time" ) result = coder.check_for_urls(repeated_url_input) # the original 3 in the input text, plus 1 more for the scraped text self.assertEqual(result.count("https://example.com"), 4) self.assertIn("https://example.com", result) def test_coder_from_coder_with_subdir(self): with GitTemporaryDirectory() as root: repo = git.Repo.init(root) # Create a file in a subdirectory subdir = Path(root) / "subdir" subdir.mkdir() test_file = subdir / "test_file.txt" test_file.write_text("Test content") repo.git.add(str(test_file)) repo.git.commit("-m", "Add test file") # Change directory to the subdirectory os.chdir(subdir.resolve()) # Create the first coder io = InputOutput(yes=True) coder1 = Coder.create(self.GPT35, None, io=io, fnames=[test_file.name]) # Create a new coder from the first coder coder2 = Coder.create(from_coder=coder1) # Check if both coders have the same set of abs_fnames self.assertEqual(coder1.abs_fnames, coder2.abs_fnames) # Ensure the abs_fnames contain the correct absolute path expected_abs_path = os.path.realpath(str(test_file)) coder1_abs_fnames = set(os.path.realpath(path) for path in coder1.abs_fnames) self.assertIn(expected_abs_path, coder1_abs_fnames) self.assertIn(expected_abs_path, coder2.abs_fnames) # Check that the abs_fnames do not contain duplicate or incorrect paths self.assertEqual(len(coder1.abs_fnames), 1) self.assertEqual(len(coder2.abs_fnames), 1) def test_suggest_shell_commands(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io) def mock_send(*args, **kwargs): coder.partial_response_content = """Here's a shell command to run: ```bash echo "Hello, World!" ``` This command will print 'Hello, World!' to the console.""" coder.partial_response_function_call = dict() return [] coder.send = mock_send # Mock the handle_shell_commands method to check if it's called coder.handle_shell_commands = MagicMock() # Run the coder with a message coder.run(with_message="Suggest a shell command") # Check if the shell command was added to the list self.assertEqual(len(coder.shell_commands), 1) self.assertEqual(coder.shell_commands[0].strip(), 'echo "Hello, World!"') # Check if handle_shell_commands was called with the correct argument coder.handle_shell_commands.assert_called_once() def test_no_suggest_shell_commands(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io, suggest_shell_commands=False) self.assertFalse(coder.suggest_shell_commands) def test_detect_urls_enabled(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io, detect_urls=True) coder.commands.scraper = MagicMock() coder.commands.scraper.scrape = MagicMock(return_value="some content") # Test with a message containing a URL message = "Check out https://example.com" coder.check_for_urls(message) coder.commands.scraper.scrape.assert_called_once_with("https://example.com") def test_detect_urls_disabled(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io, detect_urls=False) coder.commands.scraper = MagicMock() coder.commands.scraper.scrape = MagicMock(return_value="some content") # Test with a message containing a URL message = "Check out https://example.com" result = coder.check_for_urls(message) self.assertEqual(result, message) coder.commands.scraper.scrape.assert_not_called() def test_unknown_edit_format_exception(self): # Test the exception message format invalid_format = "invalid_format" valid_formats = ["diff", "whole", "map"] exc = UnknownEditFormat(invalid_format, valid_formats) expected_msg = ( f"Unknown edit format {invalid_format}. Valid formats are: {', '.join(valid_formats)}" ) self.assertEqual(str(exc), expected_msg) def test_unknown_edit_format_creation(self): # Test that creating a Coder with invalid edit format raises the exception io = InputOutput(yes=True) invalid_format = "invalid_format" with self.assertRaises(UnknownEditFormat) as cm: Coder.create(self.GPT35, invalid_format, io=io) exc = cm.exception self.assertEqual(exc.edit_format, invalid_format) self.assertIsInstance(exc.valid_formats, list) self.assertTrue(len(exc.valid_formats) > 0) def test_system_prompt_prefix(self): # Test that system_prompt_prefix is properly set and used io = InputOutput(yes=True) test_prefix = "Test prefix. " # Create a model with system_prompt_prefix model = Model("gpt-3.5-turbo") model.system_prompt_prefix = test_prefix coder = Coder.create(model, None, io=io) # Get the formatted messages chunks = coder.format_messages() messages = chunks.all_messages() # Check if the system message contains our prefix system_message = next(msg for msg in messages if msg["role"] == "system") self.assertTrue(system_message["content"].startswith(test_prefix)) def test_coder_create_with_new_file_oserror(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) new_file = "new_file.txt" # Mock Path.touch() to raise OSError with patch("pathlib.Path.touch", side_effect=OSError("Permission denied")): # Create the coder with a new file coder = Coder.create(self.GPT35, "diff", io=io, fnames=[new_file]) # Check if the coder was created successfully self.assertIsInstance(coder, Coder) # Check if the new file is not in abs_fnames self.assertNotIn(new_file, [os.path.basename(f) for f in coder.abs_fnames]) def test_show_exhausted_error(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io) # Set up some real done_messages and cur_messages coder.done_messages = [ {"role": "user", "content": "Hello, can you help me with a Python problem?"}, { "role": "assistant", "content": "Of course! I'd be happy to help. What's the problem you're facing?", }, { "role": "user", "content": ( "I need to write a function that calculates the factorial of a number." ), }, { "role": "assistant", "content": ( "Sure, I can help you with that. Here's a simple Python function to" " calculate the factorial of a number:" ), }, ] coder.cur_messages = [ {"role": "user", "content": "Can you optimize this function for large numbers?"}, ] # Set up real values for the main model coder.main_model.info = { "max_input_tokens": 4000, "max_output_tokens": 1000, } coder.partial_response_content = ( "Here's an optimized version of the factorial function:" ) coder.io.tool_error = MagicMock() # Call the method coder.show_exhausted_error() # Check if tool_error was called with the expected message coder.io.tool_error.assert_called() error_message = coder.io.tool_error.call_args[0][0] # Assert that the error message contains the expected information self.assertIn("Model gpt-3.5-turbo has hit a token limit!", error_message) self.assertIn("Input tokens:", error_message) self.assertIn("Output tokens:", error_message) self.assertIn("Total tokens:", error_message) def test_keyboard_interrupt_handling(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io) # Simulate keyboard interrupt during message processing def mock_send(*args, **kwargs): coder.partial_response_content = "Partial response" coder.partial_response_function_call = dict() raise KeyboardInterrupt() coder.send = mock_send # Initial valid state sanity_check_messages(coder.cur_messages) # Process message that will trigger interrupt list(coder.send_message("Test message")) # Verify messages are still in valid state sanity_check_messages(coder.cur_messages) self.assertEqual(coder.cur_messages[-1]["role"], "assistant") def test_token_limit_error_handling(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io) # Simulate token limit error def mock_send(*args, **kwargs): coder.partial_response_content = "Partial response" coder.partial_response_function_call = dict() raise FinishReasonLength() coder.send = mock_send # Initial valid state sanity_check_messages(coder.cur_messages) # Process message that hits token limit list(coder.send_message("Long message")) # Verify messages are still in valid state sanity_check_messages(coder.cur_messages) self.assertEqual(coder.cur_messages[-1]["role"], "assistant") def test_message_sanity_after_partial_response(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) coder = Coder.create(self.GPT35, "diff", io=io) # Simulate partial response then interrupt def mock_send(*args, **kwargs): coder.partial_response_content = "Partial response" coder.partial_response_function_call = dict() raise KeyboardInterrupt() coder.send = mock_send list(coder.send_message("Test")) # Verify message structure remains valid sanity_check_messages(coder.cur_messages) self.assertEqual(coder.cur_messages[-1]["role"], "assistant") def test_architect_coder_auto_accept_true(self): with GitTemporaryDirectory(): io = InputOutput(yes=True) io.confirm_ask = MagicMock(return_value=True) # Create an ArchitectCoder with auto_accept_architect=True with patch("aider.coders.architect_coder.AskCoder.__init__", return_value=None): from aider.coders.architect_coder import ArchitectCoder coder = ArchitectCoder() coder.io = io coder.main_model = self.GPT35 coder.auto_accept_architect = True coder.verbose = False coder.total_cost = 0 coder.cur_messages = [] coder.done_messages = [] coder.summarizer = MagicMock() coder.summarizer.too_big.return_value = False # Mock editor_coder creation and execution mock_editor = MagicMock() with patch("aider.coders.architect_coder.Coder.create", return_value=mock_editor): # Set partial response content coder.partial_response_content = "Make these changes to the code" # Call reply_completed coder.reply_completed() # Verify that confirm_ask was not called (auto-accepted) io.confirm_ask.assert_not_called() # Verify that editor coder was created and run mock_editor.run.assert_called_once() def test_architect_coder_auto_accept_false_confirmed(self): with GitTemporaryDirectory(): io = InputOutput(yes=False) io.confirm_ask = MagicMock(return_value=True) # Create an ArchitectCoder with auto_accept_architect=False with patch("aider.coders.architect_coder.AskCoder.__init__", return_value=None): from aider.coders.architect_coder import ArchitectCoder coder = ArchitectCoder() coder.io = io coder.main_model = self.GPT35 coder.auto_accept_architect = False coder.verbose = False coder.total_cost = 0 coder.cur_messages = [] coder.done_messages = [] coder.summarizer = MagicMock() coder.summarizer.too_big.return_value = False coder.cur_messages = [] coder.done_messages = [] coder.summarizer = MagicMock() coder.summarizer.too_big.return_value = False # Mock editor_coder creation and execution mock_editor = MagicMock() with patch("aider.coders.architect_coder.Coder.create", return_value=mock_editor): # Set partial response content coder.partial_response_content = "Make these changes to the code" # Call reply_completed coder.reply_completed() # Verify that confirm_ask was called io.confirm_ask.assert_called_once_with("Edit the files?") # Verify that editor coder was created and run mock_editor.run.assert_called_once() def test_architect_coder_auto_accept_false_rejected(self): with GitTemporaryDirectory(): io = InputOutput(yes=False) io.confirm_ask = MagicMock(return_value=False) # Create an ArchitectCoder with auto_accept_architect=False with patch("aider.coders.architect_coder.AskCoder.__init__", return_value=None): from aider.coders.architect_coder import ArchitectCoder coder = ArchitectCoder() coder.io = io coder.main_model = self.GPT35 coder.auto_accept_architect = False coder.verbose = False coder.total_cost = 0 # Mock editor_coder creation and execution mock_editor = MagicMock() with patch("aider.coders.architect_coder.Coder.create", return_value=mock_editor): # Set partial response content coder.partial_response_content = "Make these changes to the code" # Call reply_completed coder.reply_completed() # Verify that confirm_ask was called io.confirm_ask.assert_called_once_with("Edit the files?") # Verify that editor coder was NOT created or run # (because user rejected the changes) mock_editor.run.assert_not_called() if __name__ == "__main__": unittest.main()