mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-02 02:34:59 +00:00
Merge branch 'main' into feature/litellm-mcp
This commit is contained in:
commit
c1a5e8d0d5
76 changed files with 3366 additions and 1403 deletions
|
@ -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)
|
||||
|
|
|
@ -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"}])
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():
|
||||
|
|
73
tests/basic/test_openrouter.py
Normal file
73
tests/basic/test_openrouter.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from aider.scrape import Scraper
|
||||
|
||||
from aider.scrape import install_playwright, Scraper
|
||||
|
||||
class DummyIO:
|
||||
def __init__(self):
|
||||
|
@ -21,17 +19,21 @@ class DummyIO:
|
|||
|
||||
def test_scraper_disable_playwright_flag(monkeypatch):
|
||||
io = DummyIO()
|
||||
# Simulate that playwright is not available (disable_playwright just means playwright_available=False)
|
||||
# Simulate that playwright is not available
|
||||
# (disable_playwright just means playwright_available=False)
|
||||
scraper = Scraper(print_error=io.tool_error, playwright_available=False)
|
||||
# Patch scrape_with_httpx to check it is called
|
||||
called = {}
|
||||
|
||||
def fake_httpx(url):
|
||||
called['called'] = True
|
||||
called["called"] = True
|
||||
return "plain text", "text/plain"
|
||||
|
||||
scraper.scrape_with_httpx = fake_httpx
|
||||
content = scraper.scrape("http://example.com")
|
||||
assert content == "plain text"
|
||||
assert called['called']
|
||||
assert called["called"]
|
||||
|
||||
|
||||
def test_scraper_enable_playwright(monkeypatch):
|
||||
io = DummyIO()
|
||||
|
@ -39,13 +41,16 @@ def test_scraper_enable_playwright(monkeypatch):
|
|||
scraper = Scraper(print_error=io.tool_error, playwright_available=True)
|
||||
# Patch scrape_with_playwright to check it is called
|
||||
called = {}
|
||||
|
||||
def fake_playwright(url):
|
||||
called['called'] = True
|
||||
called["called"] = True
|
||||
return "<html>hi</html>", "text/html"
|
||||
|
||||
scraper.scrape_with_playwright = fake_playwright
|
||||
content = scraper.scrape("http://example.com")
|
||||
assert content.startswith("hi") or "<html>" in content
|
||||
assert called['called']
|
||||
assert called["called"]
|
||||
|
||||
|
||||
def test_commands_web_disable_playwright(monkeypatch):
|
||||
"""
|
||||
|
@ -59,16 +64,22 @@ def test_commands_web_disable_playwright(monkeypatch):
|
|||
self.outputs = []
|
||||
self.warnings = []
|
||||
self.errors = []
|
||||
|
||||
def tool_output(self, msg, *a, **k):
|
||||
self.outputs.append(msg)
|
||||
|
||||
def tool_warning(self, msg, *a, **k):
|
||||
self.warnings.append(msg)
|
||||
|
||||
def tool_error(self, msg, *a, **k):
|
||||
self.errors.append(msg)
|
||||
|
||||
def read_text(self, filename, silent=False):
|
||||
return ""
|
||||
|
||||
def confirm_ask(self, *a, **k):
|
||||
return True
|
||||
|
||||
def print(self, *a, **k):
|
||||
pass
|
||||
|
||||
|
@ -77,18 +88,25 @@ def test_commands_web_disable_playwright(monkeypatch):
|
|||
def __init__(self):
|
||||
self.cur_messages = []
|
||||
self.main_model = type("M", (), {"edit_format": "code", "name": "dummy", "info": {}})
|
||||
|
||||
def get_rel_fname(self, fname):
|
||||
return fname
|
||||
|
||||
def get_inchat_relative_files(self):
|
||||
return []
|
||||
|
||||
def abs_root_path(self, fname):
|
||||
return fname
|
||||
|
||||
def get_all_abs_files(self):
|
||||
return []
|
||||
|
||||
def get_announcements(self):
|
||||
return []
|
||||
|
||||
def format_chat_chunks(self):
|
||||
return type("Chunks", (), {"repo": [], "readonly_files": [], "chat_files": []})()
|
||||
|
||||
def event(self, *a, **k):
|
||||
pass
|
||||
|
||||
|
@ -99,6 +117,7 @@ def test_commands_web_disable_playwright(monkeypatch):
|
|||
class DummyScraper:
|
||||
def __init__(self, **kwargs):
|
||||
self.called = False
|
||||
|
||||
def scrape(self, url):
|
||||
self.called = True
|
||||
return "dummy content"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue