mirror of
https://github.com/Aider-AI/aider.git
synced 2025-05-19 20:05:00 +00:00
1438 lines
54 KiB
Python
1438 lines
54 KiB
Python
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 of a pure basename skips files with duplicate basenames
|
|
mentioned = coder.get_file_mentions(f"Check {fname2.name} 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("/", "\\")
|
|
win_path3 = test_files[3].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 {win_path3}"
|
|
),
|
|
{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 **{win_path3}** 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_multiline_backticks(self):
|
|
with GitTemporaryDirectory():
|
|
io = InputOutput(pretty=False, yes=True)
|
|
coder = Coder.create(self.GPT35, None, io)
|
|
|
|
# Create test files
|
|
test_files = [
|
|
"swebench/harness/test_spec/python.py",
|
|
"swebench/harness/test_spec/javascript.py",
|
|
]
|
|
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))
|
|
|
|
# Input text with multiline backticked filenames
|
|
content = """
|
|
Could you please **add the following files to the chat**?
|
|
|
|
1. `swebench/harness/test_spec/python.py`
|
|
2. `swebench/harness/test_spec/javascript.py`
|
|
|
|
Once I have these, I can show you precisely how to do the thing.
|
|
"""
|
|
expected_mentions = {
|
|
"swebench/harness/test_spec/python.py",
|
|
"swebench/harness/test_spec/javascript.py",
|
|
}
|
|
|
|
mentioned_files = coder.get_file_mentions(content)
|
|
self.assertEqual(
|
|
mentioned_files,
|
|
expected_mentions,
|
|
f"Failed to extract mentions from multiline backticked content: {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, user_language=None):
|
|
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, user_language=None):
|
|
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, user_language=None):
|
|
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_skip_gitignored_files_on_init(self):
|
|
with GitTemporaryDirectory() as _:
|
|
repo_path = Path(".")
|
|
repo = git.Repo.init(repo_path)
|
|
|
|
ignored_file = repo_path / "ignored_by_git.txt"
|
|
ignored_file.write_text("This file should be ignored by git.")
|
|
|
|
regular_file = repo_path / "regular_file.txt"
|
|
regular_file.write_text("This is a regular file.")
|
|
|
|
gitignore_content = "ignored_by_git.txt\n"
|
|
(repo_path / ".gitignore").write_text(gitignore_content)
|
|
|
|
repo.index.add([str(regular_file), ".gitignore"])
|
|
repo.index.commit("Initial commit with gitignore and regular file")
|
|
|
|
mock_io = MagicMock()
|
|
mock_io.tool_warning = MagicMock()
|
|
|
|
fnames_to_add = [str(ignored_file), str(regular_file)]
|
|
|
|
coder = Coder.create(self.GPT35, None, mock_io, fnames=fnames_to_add)
|
|
|
|
self.assertNotIn(str(ignored_file.resolve()), coder.abs_fnames)
|
|
self.assertIn(str(regular_file.resolve()), coder.abs_fnames)
|
|
mock_io.tool_warning.assert_any_call(
|
|
f"Skipping {ignored_file.name} that matches gitignore spec."
|
|
)
|
|
|
|
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_normalize_language(self):
|
|
coder = Coder.create(self.GPT35, None, io=InputOutput())
|
|
|
|
# Test None and empty
|
|
self.assertIsNone(coder.normalize_language(None))
|
|
self.assertIsNone(coder.normalize_language(""))
|
|
|
|
# Test "C" and "POSIX"
|
|
self.assertIsNone(coder.normalize_language("C"))
|
|
self.assertIsNone(coder.normalize_language("POSIX"))
|
|
|
|
# Test already formatted names
|
|
self.assertEqual(coder.normalize_language("English"), "English")
|
|
self.assertEqual(coder.normalize_language("French"), "French")
|
|
|
|
# Test common locale codes (fallback map, assuming babel is not installed or fails)
|
|
with patch("aider.coders.base_coder.Locale", None):
|
|
self.assertEqual(coder.normalize_language("en_US"), "English")
|
|
self.assertEqual(coder.normalize_language("fr_FR"), "French")
|
|
self.assertEqual(coder.normalize_language("es"), "Spanish")
|
|
self.assertEqual(coder.normalize_language("de_DE.UTF-8"), "German")
|
|
self.assertEqual(
|
|
coder.normalize_language("zh-CN"), "Chinese"
|
|
) # Test hyphen in fallback
|
|
self.assertEqual(coder.normalize_language("ja"), "Japanese")
|
|
self.assertEqual(
|
|
coder.normalize_language("unknown_code"), "unknown_code"
|
|
) # Fallback to original
|
|
|
|
# Test with babel.Locale mocked (available)
|
|
mock_babel_locale = MagicMock()
|
|
mock_locale_instance = MagicMock()
|
|
mock_babel_locale.parse.return_value = mock_locale_instance
|
|
|
|
with patch("aider.coders.base_coder.Locale", mock_babel_locale):
|
|
mock_locale_instance.get_display_name.return_value = "english" # For en_US
|
|
self.assertEqual(coder.normalize_language("en_US"), "English")
|
|
mock_babel_locale.parse.assert_called_with("en_US")
|
|
mock_locale_instance.get_display_name.assert_called_with("en")
|
|
|
|
mock_locale_instance.get_display_name.return_value = "french" # For fr-FR
|
|
self.assertEqual(coder.normalize_language("fr-FR"), "French") # Test with hyphen
|
|
mock_babel_locale.parse.assert_called_with("fr_FR") # Hyphen replaced
|
|
mock_locale_instance.get_display_name.assert_called_with("en")
|
|
|
|
# Test with babel.Locale raising an exception (simulating parse failure)
|
|
mock_babel_locale_error = MagicMock()
|
|
mock_babel_locale_error.parse.side_effect = Exception("Babel parse error")
|
|
with patch("aider.coders.base_coder.Locale", mock_babel_locale_error):
|
|
self.assertEqual(coder.normalize_language("en_US"), "English") # Falls back to map
|
|
|
|
def test_get_user_language(self):
|
|
io = InputOutput()
|
|
coder = Coder.create(self.GPT35, None, io=io)
|
|
|
|
# 1. Test with self.chat_language set
|
|
coder.chat_language = "fr_CA"
|
|
with patch.object(coder, "normalize_language", return_value="French Canadian") as mock_norm:
|
|
self.assertEqual(coder.get_user_language(), "French Canadian")
|
|
mock_norm.assert_called_once_with("fr_CA")
|
|
coder.chat_language = None # Reset
|
|
|
|
# 2. Test with locale.getlocale()
|
|
with patch("locale.getlocale", return_value=("en_GB", "UTF-8")) as mock_getlocale:
|
|
with patch.object(
|
|
coder, "normalize_language", return_value="British English"
|
|
) as mock_norm:
|
|
self.assertEqual(coder.get_user_language(), "British English")
|
|
mock_getlocale.assert_called_once()
|
|
mock_norm.assert_called_once_with("en_GB")
|
|
|
|
# Test with locale.getlocale() returning None or empty
|
|
with patch("locale.getlocale", return_value=(None, None)) as mock_getlocale:
|
|
with patch("os.environ.get") as mock_env_get: # Ensure env vars are not used yet
|
|
mock_env_get.return_value = None
|
|
self.assertIsNone(coder.get_user_language()) # Should be None if nothing found
|
|
|
|
# 3. Test with environment variables: LANG
|
|
with patch(
|
|
"locale.getlocale", side_effect=Exception("locale error")
|
|
): # Mock locale to fail
|
|
with patch("os.environ.get") as mock_env_get:
|
|
mock_env_get.side_effect = lambda key: "de_DE.UTF-8" if key == "LANG" else None
|
|
with patch.object(coder, "normalize_language", return_value="German") as mock_norm:
|
|
self.assertEqual(coder.get_user_language(), "German")
|
|
mock_env_get.assert_any_call("LANG")
|
|
mock_norm.assert_called_once_with("de_DE")
|
|
|
|
# Test LANGUAGE (takes precedence over LANG if both were hypothetically checked
|
|
# by os.environ.get, but our code checks in order, so we mock the first one it finds)
|
|
with patch("locale.getlocale", side_effect=Exception("locale error")):
|
|
with patch("os.environ.get") as mock_env_get:
|
|
mock_env_get.side_effect = lambda key: "es_ES" if key == "LANGUAGE" else None
|
|
with patch.object(coder, "normalize_language", return_value="Spanish") as mock_norm:
|
|
self.assertEqual(coder.get_user_language(), "Spanish")
|
|
mock_env_get.assert_any_call("LANGUAGE") # LANG would be called first
|
|
mock_norm.assert_called_once_with("es_ES")
|
|
|
|
# 4. Test priority: chat_language > locale > env
|
|
coder.chat_language = "it_IT"
|
|
with patch("locale.getlocale", return_value=("en_US", "UTF-8")) as mock_getlocale:
|
|
with patch("os.environ.get", return_value="de_DE") as mock_env_get:
|
|
with patch.object(
|
|
coder, "normalize_language", side_effect=lambda x: x.upper()
|
|
) as mock_norm:
|
|
self.assertEqual(coder.get_user_language(), "IT_IT") # From chat_language
|
|
mock_norm.assert_called_once_with("it_IT")
|
|
mock_getlocale.assert_not_called()
|
|
mock_env_get.assert_not_called()
|
|
coder.chat_language = None
|
|
|
|
# 5. Test when no language is found
|
|
with patch("locale.getlocale", side_effect=Exception("locale error")):
|
|
with patch("os.environ.get", return_value=None) as mock_env_get:
|
|
self.assertIsNone(coder.get_user_language())
|
|
|
|
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()
|