Merge branch 'main' into feature/litellm-mcp

This commit is contained in:
Quinlan Jager 2025-05-12 08:08:55 -07:00
commit c1a5e8d0d5
76 changed files with 3366 additions and 1403 deletions

View file

@ -650,7 +650,7 @@ TWO
coder.partial_response_function_call = dict()
return []
def mock_get_commit_message(diffs, context):
def mock_get_commit_message(diffs, context, user_language=None):
self.assertNotIn("one", diffs)
self.assertNotIn("ONE", diffs)
return "commit message"
@ -705,7 +705,7 @@ three
saved_diffs = []
def mock_get_commit_message(diffs, context):
def mock_get_commit_message(diffs, context, user_language=None):
saved_diffs.append(diffs)
return "commit message"
@ -783,7 +783,7 @@ two
saved_diffs = []
def mock_get_commit_message(diffs, context):
def mock_get_commit_message(diffs, context, user_language=None):
saved_diffs.append(diffs)
return "commit message"
@ -1182,6 +1182,122 @@ This command will print 'Hello, World!' to the console."""
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)

View file

@ -2105,3 +2105,95 @@ class TestCommands(TestCase):
mock_tool_error.assert_any_call(
"Command '/model gpt-4' is only supported in interactive mode, skipping."
)
def test_reset_after_coder_clone_preserves_original_read_only_files(self):
with GitTemporaryDirectory() as _:
repo_dir = str(".")
io = InputOutput(pretty=False, fancy_input=False, yes=True)
orig_ro_path = Path(repo_dir) / "orig_ro.txt"
orig_ro_path.write_text("original read only")
editable_path = Path(repo_dir) / "editable.txt"
editable_path.write_text("editable content")
other_ro_path = Path(repo_dir) / "other_ro.txt"
other_ro_path.write_text("other read only")
original_read_only_fnames_set = {str(orig_ro_path)}
# Create the initial Coder
orig_coder = Coder.create(main_model=self.GPT35, io=io, fnames=[], repo=None)
orig_coder.root = repo_dir # Set root for path operations
# Replace its commands object with one that has the original_read_only_fnames
orig_coder.commands = Commands(
io, orig_coder, original_read_only_fnames=list(original_read_only_fnames_set)
)
orig_coder.commands.coder = orig_coder
# Populate coder's file sets
orig_coder.abs_read_only_fnames.add(str(orig_ro_path))
orig_coder.abs_fnames.add(str(editable_path))
orig_coder.abs_read_only_fnames.add(str(other_ro_path))
# Simulate SwitchCoder by creating a new coder from the original one
new_coder = Coder.create(from_coder=orig_coder)
new_commands = new_coder.commands
# Perform /reset
new_commands.cmd_reset("")
# Assertions for /reset
self.assertEqual(len(new_coder.abs_fnames), 0)
self.assertEqual(len(new_coder.abs_read_only_fnames), 1)
# self.assertIn(str(orig_ro_path), new_coder.abs_read_only_fnames)
self.assertTrue(
any(os.path.samefile(p, str(orig_ro_path)) for p in new_coder.abs_read_only_fnames),
f"File {str(orig_ro_path)} not found in {new_coder.abs_read_only_fnames}",
)
self.assertEqual(len(new_coder.done_messages), 0)
self.assertEqual(len(new_coder.cur_messages), 0)
def test_drop_bare_after_coder_clone_preserves_original_read_only_files(self):
with GitTemporaryDirectory() as _:
repo_dir = str(".")
io = InputOutput(pretty=False, fancy_input=False, yes=True)
orig_ro_path = Path(repo_dir) / "orig_ro.txt"
orig_ro_path.write_text("original read only")
editable_path = Path(repo_dir) / "editable.txt"
editable_path.write_text("editable content")
other_ro_path = Path(repo_dir) / "other_ro.txt"
other_ro_path.write_text("other read only")
original_read_only_fnames_set = {str(orig_ro_path)}
orig_coder = Coder.create(main_model=self.GPT35, io=io, fnames=[], repo=None)
orig_coder.root = repo_dir
orig_coder.commands = Commands(
io, orig_coder, original_read_only_fnames=list(original_read_only_fnames_set)
)
orig_coder.commands.coder = orig_coder
orig_coder.abs_read_only_fnames.add(str(orig_ro_path))
orig_coder.abs_fnames.add(str(editable_path))
orig_coder.abs_read_only_fnames.add(str(other_ro_path))
orig_coder.done_messages = [{"role": "user", "content": "d1"}]
orig_coder.cur_messages = [{"role": "user", "content": "c1"}]
new_coder = Coder.create(from_coder=orig_coder)
new_commands = new_coder.commands
new_commands.cmd_drop("")
self.assertEqual(len(new_coder.abs_fnames), 0)
self.assertEqual(len(new_coder.abs_read_only_fnames), 1)
# self.assertIn(str(orig_ro_path), new_coder.abs_read_only_fnames)
self.assertTrue(
any(os.path.samefile(p, str(orig_ro_path)) for p in new_coder.abs_read_only_fnames),
f"File {str(orig_ro_path)} not found in {new_coder.abs_read_only_fnames}",
)
self.assertEqual(new_coder.done_messages, [{"role": "user", "content": "d1"}])
self.assertEqual(new_coder.cur_messages, [{"role": "user", "content": "c1"}])

View file

@ -5,6 +5,7 @@ from unittest.mock import MagicMock, patch
from prompt_toolkit.completion import CompleteEvent
from prompt_toolkit.document import Document
from rich.text import Text
from aider.dump import dump # noqa: F401
from aider.io import AutoCompleter, ConfirmGroup, InputOutput
@ -451,8 +452,6 @@ class TestInputOutputMultilineMode(unittest.TestCase):
"""Test that tool_output correctly handles hex colors without # prefix"""
from unittest.mock import patch
from rich.text import Text
# Create IO with hex color without # for tool_output_color
io = InputOutput(tool_output_color="FFA500", pretty=True)
@ -476,5 +475,136 @@ class TestInputOutputMultilineMode(unittest.TestCase):
mock_print.assert_called_once()
@patch("aider.io.is_dumb_terminal", return_value=False)
@patch.dict(os.environ, {"NO_COLOR": ""})
class TestInputOutputFormatFiles(unittest.TestCase):
def test_format_files_for_input_pretty_false(self, mock_is_dumb_terminal):
io = InputOutput(pretty=False, fancy_input=False)
rel_fnames = ["file1.txt", "file[markup].txt", "ro_file.txt"]
rel_read_only_fnames = ["ro_file.txt"]
expected_output = "file1.txt\nfile[markup].txt\nro_file.txt (read only)\n"
# Sort the expected lines because the order of editable vs read-only might vary
# depending on internal sorting, but the content should be the same.
# The method sorts editable_files and read_only_files separately.
# The final output joins sorted(read_only_files) + sorted(editable_files)
# Based on current implementation:
# read_only_files = ["ro_file.txt (read only)"]
# editable_files = ["file1.txt", "file[markup].txt"]
# output = "\n".join(read_only_files + editable_files) + "\n"
# Correct expected output based on implementation:
expected_output_lines = sorted(
[
"ro_file.txt (read only)",
"file1.txt",
"file[markup].txt",
]
)
expected_output = "\n".join(expected_output_lines) + "\n"
actual_output = io.format_files_for_input(rel_fnames, rel_read_only_fnames)
# Normalizing actual output by splitting, sorting, and rejoining
actual_output_lines = sorted(filter(None, actual_output.splitlines()))
normalized_actual_output = "\n".join(actual_output_lines) + "\n"
self.assertEqual(normalized_actual_output, expected_output)
@patch("aider.io.Columns")
@patch("os.path.abspath")
@patch("os.path.join")
def test_format_files_for_input_pretty_true_no_files(
self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal
):
io = InputOutput(pretty=True, root="test_root")
io.format_files_for_input([], [])
mock_columns.assert_not_called()
@patch("aider.io.Columns")
@patch("os.path.abspath")
@patch("os.path.join")
def test_format_files_for_input_pretty_true_editable_only(
self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal
):
io = InputOutput(pretty=True, root="test_root")
rel_fnames = ["edit1.txt", "edit[markup].txt"]
io.format_files_for_input(rel_fnames, [])
mock_columns.assert_called_once()
args, _ = mock_columns.call_args
renderables = args[0]
self.assertEqual(len(renderables), 2)
self.assertIsInstance(renderables[0], Text)
self.assertEqual(renderables[0].plain, "edit1.txt")
self.assertIsInstance(renderables[1], Text)
self.assertEqual(renderables[1].plain, "edit[markup].txt")
@patch("aider.io.Columns")
@patch("os.path.abspath")
@patch("os.path.join")
def test_format_files_for_input_pretty_true_readonly_only(
self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal
):
io = InputOutput(pretty=True, root="test_root")
# Mock path functions to ensure rel_path is chosen by the shortener logic
mock_join.side_effect = lambda *args: "/".join(args)
mock_abspath.side_effect = lambda p: "/ABS_PREFIX_VERY_LONG/" + os.path.normpath(p)
rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"]
# When all files in chat are read-only
rel_fnames = list(rel_read_only_fnames)
io.format_files_for_input(rel_fnames, rel_read_only_fnames)
self.assertEqual(mock_columns.call_count, 2)
args, _ = mock_columns.call_args
renderables = args[0]
self.assertEqual(len(renderables), 3) # Readonly: + 2 files
self.assertIsInstance(renderables[0], Text)
self.assertEqual(renderables[0].plain, "Readonly:")
self.assertIsInstance(renderables[1], Text)
self.assertEqual(renderables[1].plain, "ro1.txt")
self.assertIsInstance(renderables[2], Text)
self.assertEqual(renderables[2].plain, "ro[markup].txt")
@patch("aider.io.Columns")
@patch("os.path.abspath")
@patch("os.path.join")
def test_format_files_for_input_pretty_true_mixed_files(
self, mock_join, mock_abspath, mock_columns, mock_is_dumb_terminal
):
io = InputOutput(pretty=True, root="test_root")
mock_join.side_effect = lambda *args: "/".join(args)
mock_abspath.side_effect = lambda p: "/ABS_PREFIX_VERY_LONG/" + os.path.normpath(p)
rel_fnames = ["edit1.txt", "edit[markup].txt", "ro1.txt", "ro[markup].txt"]
rel_read_only_fnames = ["ro1.txt", "ro[markup].txt"]
io.format_files_for_input(rel_fnames, rel_read_only_fnames)
self.assertEqual(mock_columns.call_count, 4)
# Check arguments for the first rendering of read-only files (call 0)
args_ro, _ = mock_columns.call_args_list[0]
renderables_ro = args_ro[0]
self.assertEqual(
renderables_ro, [Text("Readonly:"), Text("ro1.txt"), Text("ro[markup].txt")]
)
# Check arguments for the first rendering of editable files (call 2)
args_ed, _ = mock_columns.call_args_list[2]
renderables_ed = args_ed[0]
self.assertEqual(
renderables_ed, [Text("Editable:"), Text("edit1.txt"), Text("edit[markup].txt")]
)
if __name__ == "__main__":
unittest.main()

View file

@ -949,16 +949,19 @@ class TestMain(TestCase):
def test_invalid_edit_format(self):
with GitTemporaryDirectory():
with patch("aider.io.InputOutput.offer_url") as mock_offer_url:
result = main(
["--edit-format", "not-a-real-format", "--exit", "--yes"],
input=DummyInput(),
output=DummyOutput(),
)
self.assertEqual(result, 1) # main() should return 1 on error
mock_offer_url.assert_called_once()
args, _ = mock_offer_url.call_args
self.assertEqual(args[0], "https://aider.chat/docs/more/edit-formats.html")
# 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:
_ = main(
["--edit-format", "not-a-real-format", "--exit", "--yes"],
input=DummyInput(),
output=DummyOutput(),
)
# argparse.ArgumentParser.exit() is called with status 2 for invalid choice
self.assertEqual(cm.exception.code, 2)
stderr_output = mock_stderr.getvalue()
self.assertIn("invalid choice", stderr_output)
self.assertIn("not-a-real-format", stderr_output)
def test_default_model_selection(self):
with GitTemporaryDirectory():

View file

@ -0,0 +1,73 @@
from pathlib import Path
from aider.models import ModelInfoManager
from aider.openrouter import OpenRouterModelManager
class DummyResponse:
"""Minimal stand-in for requests.Response used in tests."""
def __init__(self, json_data):
self.status_code = 200
self._json_data = json_data
def json(self):
return self._json_data
def test_openrouter_get_model_info_from_cache(monkeypatch, tmp_path):
"""
OpenRouterModelManager should return correct metadata taken from the
downloaded (and locally cached) models JSON payload.
"""
payload = {
"data": [
{
"id": "mistralai/mistral-medium-3",
"context_length": 32768,
"pricing": {"prompt": "100", "completion": "200"},
"top_provider": {"context_length": 32768},
}
]
}
# Fake out the network call and the HOME directory used for the cache file
monkeypatch.setattr("requests.get", lambda *a, **k: DummyResponse(payload))
monkeypatch.setattr(Path, "home", staticmethod(lambda: tmp_path))
manager = OpenRouterModelManager()
info = manager.get_model_info("openrouter/mistralai/mistral-medium-3")
assert info["max_input_tokens"] == 32768
assert info["input_cost_per_token"] == 0.0001
assert info["output_cost_per_token"] == 0.0002
assert info["litellm_provider"] == "openrouter"
def test_model_info_manager_uses_openrouter_manager(monkeypatch):
"""
ModelInfoManager should delegate to OpenRouterModelManager when litellm
provides no data for an OpenRouter-prefixed model.
"""
# Ensure litellm path returns no info so that fallback logic triggers
monkeypatch.setattr("aider.models.litellm.get_model_info", lambda *a, **k: {})
stub_info = {
"max_input_tokens": 512,
"max_tokens": 512,
"max_output_tokens": 512,
"input_cost_per_token": 0.0001,
"output_cost_per_token": 0.0002,
"litellm_provider": "openrouter",
}
# Force OpenRouterModelManager to return our stub info
monkeypatch.setattr(
"aider.models.OpenRouterModelManager.get_model_info",
lambda self, model: stub_info,
)
mim = ModelInfoManager()
info = mim.get_model_info("openrouter/fake/model")
assert info == stub_info

View file

@ -206,13 +206,15 @@ class TestRepo(unittest.TestCase):
self.assertEqual(commit.committer.name, "Test User (aider)")
# Now test with explicit False
git_repo_explicit_false = GitRepo(io, None, None, attribute_author=False, attribute_committer=False)
git_repo_explicit_false = GitRepo(
io, None, None, attribute_author=False, attribute_committer=False
)
fname.write_text("explicit false content")
commit_result = git_repo_explicit_false.commit(fnames=[str(fname)], aider_edits=True)
self.assertIsNotNone(commit_result)
commit = raw_repo.head.commit
self.assertEqual(commit.author.name, "Test User") # Explicit False
self.assertEqual(commit.committer.name, "Test User") # Explicit False
self.assertEqual(commit.author.name, "Test User") # Explicit False
self.assertEqual(commit.committer.name, "Test User") # Explicit False
# check that the original committer name is restored
original_committer_name = os.environ.get("GIT_COMMITTER_NAME")
@ -223,11 +225,21 @@ class TestRepo(unittest.TestCase):
# Test user commit with explicit no-committer attribution
git_repo_user_no_committer = GitRepo(io, None, None, attribute_committer=False)
fname.write_text("user no committer content")
commit_result = git_repo_user_no_committer.commit(fnames=[str(fname)], aider_edits=False)
commit_result = git_repo_user_no_committer.commit(
fnames=[str(fname)], aider_edits=False
)
self.assertIsNotNone(commit_result)
commit = raw_repo.head.commit
self.assertEqual(commit.author.name, "Test User", msg="Author name should not be modified for user commits")
self.assertEqual(commit.committer.name, "Test User", msg="Committer name should not be modified when attribute_committer=False")
self.assertEqual(
commit.author.name,
"Test User",
msg="Author name should not be modified for user commits",
)
self.assertEqual(
commit.committer.name,
"Test User",
msg="Committer name should not be modified when attribute_committer=False",
)
@unittest.skipIf(platform.system() == "Windows", "Git env var behavior differs on Windows")
def test_commit_with_co_authored_by(self):
@ -246,21 +258,22 @@ class TestRepo(unittest.TestCase):
# Mock coder args: Co-authored-by enabled, author/committer use default (None)
mock_coder = MagicMock()
mock_coder.args.attribute_co_authored_by = True
mock_coder.args.attribute_author = None # Default
mock_coder.args.attribute_committer = None # Default
mock_coder.args.attribute_author = None # Default
mock_coder.args.attribute_committer = None # Default
mock_coder.args.attribute_commit_message_author = False
mock_coder.args.attribute_commit_message_committer = False
# The code uses coder.main_model.name for the co-authored-by line
mock_coder.main_model = MagicMock()
mock_coder.main_model.name = "gpt-test"
io = InputOutput()
git_repo = GitRepo(io, None, None)
# commit a change with aider_edits=True and co-authored-by flag
fname.write_text("new content")
commit_result = git_repo.commit(fnames=[str(fname)], aider_edits=True, coder=mock_coder, message="Aider edit")
commit_result = git_repo.commit(
fnames=[str(fname)], aider_edits=True, coder=mock_coder, message="Aider edit"
)
self.assertIsNotNone(commit_result)
# check the commit message and author/committer
@ -268,12 +281,21 @@ class TestRepo(unittest.TestCase):
self.assertIn("Co-authored-by: aider (gpt-test) <noreply@aider.chat>", commit.message)
self.assertEqual(commit.message.splitlines()[0], "Aider edit")
# With default (None), co-authored-by takes precedence
self.assertEqual(commit.author.name, "Test User", msg="Author name should not be modified when co-authored-by takes precedence")
self.assertEqual(commit.committer.name, "Test User", msg="Committer name should not be modified when co-authored-by takes precedence")
self.assertEqual(
commit.author.name,
"Test User",
msg="Author name should not be modified when co-authored-by takes precedence",
)
self.assertEqual(
commit.committer.name,
"Test User",
msg="Committer name should not be modified when co-authored-by takes precedence",
)
@unittest.skipIf(platform.system() == "Windows", "Git env var behavior differs on Windows")
def test_commit_co_authored_by_with_explicit_name_modification(self):
# Test scenario where Co-authored-by is true AND author/committer modification are explicitly True
# Test scenario where Co-authored-by is true AND
# author/committer modification are explicitly True
with GitTemporaryDirectory():
# Setup repo...
# new repo
@ -287,32 +309,45 @@ class TestRepo(unittest.TestCase):
raw_repo.git.add(str(fname))
raw_repo.git.commit("-m", "initial commit")
# Mock coder args: Co-authored-by enabled, author/committer modification explicitly enabled
# Mock coder args: Co-authored-by enabled,
# author/committer modification explicitly enabled
mock_coder = MagicMock()
mock_coder.args.attribute_co_authored_by = True
mock_coder.args.attribute_author = True # Explicitly enable
mock_coder.args.attribute_committer = True # Explicitly enable
mock_coder.args.attribute_author = True # Explicitly enable
mock_coder.args.attribute_committer = True # Explicitly enable
mock_coder.args.attribute_commit_message_author = False
mock_coder.args.attribute_commit_message_committer = False
mock_coder.main_model = MagicMock()
mock_coder.main_model.name = "gpt-test-combo"
io = InputOutput()
git_repo = GitRepo(io, None, None)
# commit a change with aider_edits=True and combo flags
fname.write_text("new content combo")
commit_result = git_repo.commit(fnames=[str(fname)], aider_edits=True, coder=mock_coder, message="Aider combo edit")
commit_result = git_repo.commit(
fnames=[str(fname)], aider_edits=True, coder=mock_coder, message="Aider combo edit"
)
self.assertIsNotNone(commit_result)
# check the commit message and author/committer
commit = raw_repo.head.commit
self.assertIn("Co-authored-by: aider (gpt-test-combo) <noreply@aider.chat>", commit.message)
self.assertIn(
"Co-authored-by: aider (gpt-test-combo) <noreply@aider.chat>", commit.message
)
self.assertEqual(commit.message.splitlines()[0], "Aider combo edit")
# When co-authored-by is true BUT author/committer are explicit True, modification SHOULD happen
self.assertEqual(commit.author.name, "Test User (aider)", msg="Author name should be modified when explicitly True, even with co-author")
self.assertEqual(commit.committer.name, "Test User (aider)", msg="Committer name should be modified when explicitly True, even with co-author")
# When co-authored-by is true BUT author/committer are explicit True,
# modification SHOULD happen
self.assertEqual(
commit.author.name,
"Test User (aider)",
msg="Author name should be modified when explicitly True, even with co-author",
)
self.assertEqual(
commit.committer.name,
"Test User (aider)",
msg="Committer name should be modified when explicitly True, even with co-author",
)
@unittest.skipIf(platform.system() == "Windows", "Git env var behavior differs on Windows")
def test_commit_ai_edits_no_coauthor_explicit_false(self):
@ -333,8 +368,8 @@ class TestRepo(unittest.TestCase):
# Case 1: attribute_author = False, attribute_committer = None (default True)
mock_coder_no_author = MagicMock()
mock_coder_no_author.args.attribute_co_authored_by = False
mock_coder_no_author.args.attribute_author = False # Explicit False
mock_coder_no_author.args.attribute_committer = None # Default True
mock_coder_no_author.args.attribute_author = False # Explicit False
mock_coder_no_author.args.attribute_committer = None # Default True
mock_coder_no_author.args.attribute_commit_message_author = False
mock_coder_no_author.args.attribute_commit_message_committer = False
mock_coder_no_author.main_model = MagicMock()
@ -342,18 +377,23 @@ class TestRepo(unittest.TestCase):
git_repo_no_author = GitRepo(io, None, None)
fname.write_text("no author content")
commit_result = git_repo_no_author.commit(fnames=[str(fname)], aider_edits=True, coder=mock_coder_no_author, message="Aider no author")
commit_result = git_repo_no_author.commit(
fnames=[str(fname)],
aider_edits=True,
coder=mock_coder_no_author,
message="Aider no author",
)
self.assertIsNotNone(commit_result)
commit = raw_repo.head.commit
self.assertNotIn("Co-authored-by:", commit.message)
self.assertEqual(commit.author.name, "Test User") # Explicit False
self.assertEqual(commit.committer.name, "Test User (aider)") # Default True
self.assertEqual(commit.author.name, "Test User") # Explicit False
self.assertEqual(commit.committer.name, "Test User (aider)") # Default True
# Case 2: attribute_author = None (default True), attribute_committer = False
mock_coder_no_committer = MagicMock()
mock_coder_no_committer.args.attribute_co_authored_by = False
mock_coder_no_committer.args.attribute_author = None # Default True
mock_coder_no_committer.args.attribute_committer = False # Explicit False
mock_coder_no_committer.args.attribute_author = None # Default True
mock_coder_no_committer.args.attribute_committer = False # Explicit False
mock_coder_no_committer.args.attribute_commit_message_author = False
mock_coder_no_committer.args.attribute_commit_message_committer = False
mock_coder_no_committer.main_model = MagicMock()
@ -361,12 +401,25 @@ class TestRepo(unittest.TestCase):
git_repo_no_committer = GitRepo(io, None, None)
fname.write_text("no committer content")
commit_result = git_repo_no_committer.commit(fnames=[str(fname)], aider_edits=True, coder=mock_coder_no_committer, message="Aider no committer")
commit_result = git_repo_no_committer.commit(
fnames=[str(fname)],
aider_edits=True,
coder=mock_coder_no_committer,
message="Aider no committer",
)
self.assertIsNotNone(commit_result)
commit = raw_repo.head.commit
self.assertNotIn("Co-authored-by:", commit.message)
self.assertEqual(commit.author.name, "Test User (aider)", msg="Author name should be modified (default True) when co-author=False")
self.assertEqual(commit.committer.name, "Test User", msg="Committer name should not be modified (explicit False) when co-author=False")
self.assertEqual(
commit.author.name,
"Test User (aider)",
msg="Author name should be modified (default True) when co-author=False",
)
self.assertEqual(
commit.committer.name,
"Test User",
msg="Committer name should not be modified (explicit False) when co-author=False",
)
def test_get_tracked_files(self):
# Create a temporary directory

View file

@ -155,7 +155,7 @@ def test_ai_comment_pattern():
assert (
question_js_has_bang == "?"
), "Expected at least one bang (!) comment in watch_question.js fixture"
# Test Lisp fixture
lisp_path = fixtures_dir / "watch.lisp"
lisp_lines, lisp_comments, lisp_has_bang = watcher.get_ai_comments(str(lisp_path))