Deskaid

  • e2e
#!/usr/bin/env python3 """Tests for the Git amend functionality.""" import os import unittest from codemcp.testing import MCPEndToEndTestCase class GitAmendTest(MCPEndToEndTestCase): """Test the Git amend functionality for commits.""" async def test_amend_commit_same_chat_id(self): """Test that subsequent edits within the same chat session amend the previous commit.""" # Create a file to edit multiple times test_file_path = os.path.join(self.temp_dir.name, "amend_test.txt") initial_content = "Initial content\nLine 2" # Create the file with open(test_file_path, "w") as f: f.write(initial_content) # Add it to git await self.git_run(["add", test_file_path]) # Commit it await self.git_run(["commit", "-m", "Add file for amend test"]) # Get the current commit count log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) initial_commit_count = len(log_output.split("\n")) async with self.create_client_session() as session: # First initialize the project to get a chat_id init_result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for amend test", "subject_line": "test: initialize for amend commit test", "reuse_head_chat_id": False, }, ) # Extract chat_id from the init result chat_id = self.extract_chat_id_from_text(init_result_text) # First edit with our chat_id using our helper method result_text1 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Initial content\nLine 2", "new_string": "Modified content\nLine 2", "description": "First edit", "chat_id": chat_id, }, ) # Verify success message self.assertIn("Successfully edited", result_text1) # Get the current commit count after first edit log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) commit_count_after_first_edit = len(log_output.split("\n")) # Verify a new commit was created (initial + 1) self.assertEqual( commit_count_after_first_edit, initial_commit_count + 1, "A new commit should be created for the first edit", ) # Get the last commit message and check for chat_id metadata commit_msg1 = await self.git_run( ["log", "-1", "--pretty=%B"], capture_output=True, text=True ) self.assertIn(f"codemcp-id: {chat_id}", commit_msg1) self.assertIn("First edit", commit_msg1) # Second edit with the same chat_id using our helper method result_text2 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Modified content\nLine 2", "new_string": "Modified content\nLine 2\nLine 3", "description": "Second edit", "chat_id": chat_id, }, ) # Verify success message self.assertIn("Successfully edited", result_text2) # Get the commit count after second edit log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) commit_count_after_second_edit = len(log_output.split("\n")) # Verify the commit count remains the same (amend) self.assertEqual( commit_count_after_second_edit, commit_count_after_first_edit, "The second edit should amend the previous commit, not create a new one", ) # Get the last commit message commit_msg2 = await self.git_run( ["log", "-1", "--pretty=%B"], capture_output=True, text=True ) # Verify the commit message contains both edits and the chat ID self.assertIn(f"codemcp-id: {chat_id}", commit_msg2) self.assertIn("First edit", commit_msg2) self.assertIn("Second edit", commit_msg2) # Use more general regex patterns that don't depend on exact placement # Just check that both base revision and HEAD markers exist somewhere in the commit message import re base_revision_regex = r"[0-9a-f]{7}\s+\(Base revision\)" head_regex = r"HEAD\s+Second edit" self.assertTrue( re.search(base_revision_regex, commit_msg2, re.MULTILINE), f"Commit message doesn't contain base revision pattern. Got: {commit_msg2}", ) self.assertTrue( re.search(head_regex, commit_msg2, re.MULTILINE), f"Commit message doesn't contain HEAD pattern. Got: {commit_msg2}", ) async def test_new_commit_different_chat_id(self): """Test that edits with a different chat_id create a new commit rather than amending.""" # Create a file to edit test_file_path = os.path.join(self.temp_dir.name, "different_chat_test.txt") initial_content = "Initial content for different chat test" # Create the file with open(test_file_path, "w") as f: f.write(initial_content) # Add it to git await self.git_run(["add", test_file_path]) # Commit it await self.git_run(["commit", "-m", "Add file for different chat test"]) # Get the current commit count log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) initial_commit_count = len(log_output.split("\n")) async with self.create_client_session() as session: # First initialize the project to get first chat_id init_result_text1 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for different chat ID test 1", "subject_line": "test: initialize for different chat 1", "reuse_head_chat_id": False, }, ) # Extract first chat_id from the init result chat_id1 = self.extract_chat_id_from_text(init_result_text1) # First edit with chat_id1 result1_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Initial content for different chat test", "new_string": "Modified by chat 1", "description": "Edit from chat 1", "chat_id": chat_id1, }, ) # Check the result self.assertIn("Successfully edited", result1_text) # Get the commit count after first edit log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) commit_count_after_first_edit = len(log_output.split("\n")) # Verify a new commit was created self.assertEqual( commit_count_after_first_edit, initial_commit_count + 1, "A new commit should be created for the first chat", ) # Initialize the project to get second chat_id init_result_text2 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for different chat ID test 2", "subject_line": "test: initialize for different chat 2", "reuse_head_chat_id": False, }, ) # Extract second chat_id from the init result chat_id2 = self.extract_chat_id_from_text(init_result_text2) # Edit with chat_id2 result2_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Modified by chat 1", "new_string": "Modified by chat 2", "description": "Edit from chat 2", "chat_id": chat_id2, }, ) # Check the result self.assertIn("Successfully edited", result2_text) # Get the commit count after second edit log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) commit_count_after_second_edit = len(log_output.split("\n")) # Verify a new commit was created (not amended) self.assertEqual( commit_count_after_second_edit, commit_count_after_first_edit + 1, "Edit with different chat_id should create a new commit", ) # Get the last two commit messages commit_msgs = await self.git_run( ["log", "-2", "--pretty=%B"], capture_output=True, text=True ) # Verify the latest commit has chat_id2 self.assertIn(f"codemcp-id: {chat_id2}", commit_msgs) self.assertIn("Edit from chat 2", commit_msgs) # Verify the previous commit has chat_id1 self.assertIn(f"codemcp-id: {chat_id1}", commit_msgs) async def test_non_ai_commit_not_amended(self): """Test that a user (non-AI) generated commit isn't amended.""" # Create a file to edit test_file_path = os.path.join(self.temp_dir.name, "user_commit_test.txt") initial_content = "Initial content for user commit test" # Create the file with open(test_file_path, "w") as f: f.write(initial_content) # Add it to git await self.git_run(["add", test_file_path]) # Commit it (user-generated commit without a chat_id) await self.git_run(["commit", "-m", "User-generated commit"]) # Get the current commit count log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) initial_commit_count = len(log_output.split("\n")) async with self.create_client_session() as session: # Initialize the project to get chat_id init_result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for user commit test", "subject_line": "test: initialize for non-AI commit test", "reuse_head_chat_id": False, }, ) # Extract chat_id from the init result chat_id = self.extract_chat_id_from_text(init_result_text) # AI-generated edit result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Initial content for user commit test", "new_string": "Modified by AI", "description": "AI edit after user commit", "chat_id": chat_id, }, ) # Check the result self.assertIn("Successfully edited", result_text) # Get the commit count after AI edit log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) commit_count_after_edit = len(log_output.split("\n")) # Verify a new commit was created (not amended) self.assertEqual( commit_count_after_edit, initial_commit_count + 1, "AI edit after user commit should create a new commit, not amend", ) # Get the last two commit messages commit_msgs = await self.git_run( ["log", "-2", "--pretty=%B"], capture_output=True, text=True ) # Verify the latest commit has AI chat_id self.assertIn(f"codemcp-id: {chat_id}", commit_msgs) # Verify the user commit message is included self.assertIn("User-generated commit", commit_msgs) # Make sure there's only one codemcp-id in the output codemcp_id_count = commit_msgs.count("codemcp-id:") self.assertEqual( codemcp_id_count, 1, "Should be only one codemcp-id metadata tag" ) async def test_commit_history_with_nonhead_match(self): """Test behavior when HEAD~ has the same chat_id as current but HEAD doesn't.""" # Create a file to edit test_file_path = os.path.join(self.temp_dir.name, "history_test.txt") initial_content = "Initial content for history test" # Create the file with open(test_file_path, "w") as f: f.write(initial_content) # Add it to git await self.git_run(["add", test_file_path]) # Commit it await self.git_run(["commit", "-m", "Add file for history test"]) async with self.create_client_session() as session: # Initialize the project to get first chat_id init_result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for history test", "subject_line": "test: initialize for history test", "reuse_head_chat_id": False, }, ) # Extract chat_id from the init result chat_id1 = self.extract_chat_id_from_text(init_result_text) # Helper function to create a commit with chat ID async def create_chat_commit(content, message, chat_id): with open(test_file_path, "w") as f: f.write(content) await self.git_run(["add", test_file_path]) await self.git_run(["commit", "-m", f"{message}\n\ncodemcp-id: {chat_id}"]) # Create first AI commit with chat_id1 await create_chat_commit("Modified by chat 1", "First AI edit", chat_id1) # Create a user commit (different chat_id) await create_chat_commit("Modified by user", "User edit", "some-other-chat") # Get the current commit count log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) len(log_output.split("\n")) async with self.create_client_session() as session: # Initialize the project again with the same chat_id init_result_text2 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test second initialization for history test", "subject_line": "test: second initialize for history test", "reuse_head_chat_id": True, }, ) # Get a new chat_id for the second edit # We don't want to reuse the HEAD chat_id here since we're testing edits # when HEAD has a different chat ID second_chat_id = self.extract_chat_id_from_text(init_result_text2) # New edit with the second chat_id result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Modified by user", "new_string": "Modified again by chat 1", "description": "Second edit from chat 1", "chat_id": second_chat_id, }, ) # Check the result self.assertIn("Successfully edited", result_text) # Get the commit count after the new edit log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) len(log_output.split("\n")) # We should verify the edit was made successfully without asserting # a specific commit count, as that's not the main purpose of this test # Get the last commit message last_commit_msg = await self.git_run( ["log", "-1", "--pretty=%B"], capture_output=True, text=True ) # Verify the latest commit has the correct chat_id and message # The main point is that the edited content is correct and the commit has the # right chat_id and message self.assertIn(f"codemcp-id: {second_chat_id}", last_commit_msg) self.assertIn("Second edit from chat 1", last_commit_msg) async def test_write_with_no_chatid(self): """Test that WriteFile creates a new commit if HEAD has no chat ID.""" # Create a file to edit test_file_path = os.path.join(self.temp_dir.name, "no_chatid_test.txt") initial_content = "Initial content without chat ID" # Create the file with open(test_file_path, "w") as f: f.write(initial_content) # Add it to git await self.git_run(["add", test_file_path]) # Commit it without a chat ID await self.git_run(["commit", "-m", "Regular commit without chat ID"]) # Get the current commit count log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) initial_commit_count = len(log_output.split("\n")) async with self.create_client_session() as session: # Initialize the project to get AI chat_id init_result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for no chatid test", "subject_line": "test: initialize for no chatid test", "reuse_head_chat_id": False, }, ) # Extract AI chat_id from the init result ai_chat_id = self.extract_chat_id_from_text(init_result_text) # Write with an AI chat ID using our helper method result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "WriteFile", "path": test_file_path, "content": "Modified content with AI chat ID", "description": "Write with AI chat ID", "chat_id": ai_chat_id, }, ) # Verify success message self.assertIn("Successfully wrote to", result_text) # Get the commit count after the write log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) commit_count_after_write = len(log_output.split("\n")) # Verify a new commit was created (not amended) self.assertEqual( commit_count_after_write, initial_commit_count + 1, "Write after commit without chat_id should create a new commit", ) # Get the commit messages commit_msgs = await self.git_run( ["log", "-2", "--pretty=%B"], capture_output=True, text=True ) # Verify new commit has AI chat_id self.assertIn(f"codemcp-id: {ai_chat_id}", commit_msgs) self.assertIn("Write with AI chat ID", commit_msgs) # Verify previous commit message is included self.assertIn("Regular commit without chat ID", commit_msgs) # Make sure there's only one codemcp-id in the output codemcp_id_count = commit_msgs.count("codemcp-id:") self.assertEqual( codemcp_id_count, 1, "Should be only one codemcp-id metadata tag" ) async def test_write_with_different_chatid(self): """Test that WriteFile creates a new commit if HEAD has a different chat ID.""" # Create a file to edit test_file_path = os.path.join(self.temp_dir.name, "write_test.txt") initial_content = "Initial content for write test" # Create the file with open(test_file_path, "w") as f: f.write(initial_content) # Add it to git await self.git_run(["add", test_file_path]) # First initialize the project to get first chat_id async with self.create_client_session() as session: init_result_text1 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for write with different chat id", "subject_line": "test: initialize for different chat id write test", "reuse_head_chat_id": False, }, ) # Extract first chat_id from the init result first_chat_id = self.extract_chat_id_from_text(init_result_text1) # Create a simple file to simulate an AI commit with the first chat_id await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Initial content for write test", "new_string": "Modified by first chat", "description": "First edit", "chat_id": first_chat_id, }, ) # Get the current commit count log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) initial_commit_count = len(log_output.split("\n")) # Initialize the project again to get a second chat_id async with self.create_client_session() as session: init_result_text2 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for second chat id", "subject_line": "test: initialize for second chat id", "reuse_head_chat_id": False, }, ) # Extract second chat_id from the init result second_chat_id = self.extract_chat_id_from_text(init_result_text2) # Write to the file with a different chat ID using our helper method result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "WriteFile", "path": test_file_path, "content": "Modified content for write test", "description": "Write with different chat ID", "chat_id": second_chat_id, }, ) # Verify success message self.assertIn("Successfully wrote to", result_text) # Get the commit count after the write log_output = await self.git_run( ["log", "--oneline"], capture_output=True, text=True ) commit_count_after_write = len(log_output.split("\n")) # Verify a new commit was created (not amended) self.assertEqual( commit_count_after_write, initial_commit_count + 1, "Write with different chat_id should create a new commit", ) # Get the commit messages commit_msgs = await self.git_run( ["log", "-2", "--pretty=%B"], capture_output=True, text=True ) # Verify both chat IDs are in the commit history self.assertIn(f"codemcp-id: {second_chat_id}", commit_msgs) self.assertIn(f"codemcp-id: {first_chat_id}", commit_msgs) # Make sure there are exactly two codemcp-id tags in the output codemcp_id_count = commit_msgs.count("codemcp-id:") self.assertEqual( codemcp_id_count, 2, "Should be exactly two codemcp-id metadata tags" ) async def test_commit_hash_in_message(self): """Test that the commit hash appears in the commit message when amending.""" # Create a file to edit multiple times test_file_path = os.path.join(self.temp_dir.name, "hash_test.txt") initial_content = "Hash test content" # Create the file with open(test_file_path, "w") as f: f.write(initial_content) # Add it to git await self.git_run(["add", test_file_path]) # Commit it await self.git_run(["commit", "-m", "Add file for hash test"]) async with self.create_client_session() as session: # Initialize the project to get a chat_id init_result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization for hash test", "subject_line": "test: initialize for hash test", "reuse_head_chat_id": False, }, ) # Extract chat_id from the init result chat_id = self.extract_chat_id_from_text(init_result_text) # First edit with our chat_id result1_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Hash test content", "new_string": "Modified hash test content", "description": "First hash test edit", "chat_id": chat_id, }, ) # Check the result self.assertIn("Successfully edited", result1_text) # Get the commit hash for the first edit await self.git_run( ["rev-parse", "--short", "HEAD"], capture_output=True, text=True ) # Second edit with the same chat_id result2_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Modified hash test content", "new_string": "Twice modified hash test content", "description": "Second hash test edit", "chat_id": chat_id, }, ) # Check in the response text for the commit hash pattern in the result import re hash_pattern = r"previous commit was [0-9a-f]{7}" self.assertTrue( re.search(hash_pattern, result2_text), f"Result text doesn't mention previous commit hash. Got: {result2_text}", ) # Get the last commit message commit_msg = await self.git_run( ["log", "-1", "--pretty=%B"], capture_output=True, text=True ) # Verify the commit hash format in the message with base revision and HEAD import re base_revision_regex = r"[0-9a-f]{7}\s+\(Base revision\)" head_regex = r"HEAD\s+Second hash test edit" self.assertTrue( re.search(base_revision_regex, commit_msg, re.MULTILINE), f"Commit message doesn't contain base revision pattern. Got: {commit_msg}", ) self.assertTrue( re.search(head_regex, commit_msg, re.MULTILINE), f"Commit message doesn't contain HEAD pattern. Got: {commit_msg}", ) # Third edit to check multiple hash entries await self.call_tool_assert_success( session, "codemcp", { "subtool": "EditFile", "path": test_file_path, "old_string": "Twice modified hash test content", "new_string": "Thrice modified hash test content", "description": "Third hash test edit", "chat_id": chat_id, }, ) # Get the second commit hash await self.git_run( ["rev-parse", "--short", "HEAD"], capture_output=True, text=True ) # Get the updated commit message final_commit_msg = await self.git_run( ["log", "-1", "--pretty=%B"], capture_output=True, text=True ) # Verify both commit hashes appear in the correct format import re # Check for base revision and head format base_revision_regex = r"[0-9a-f]{7}\s+\(Base revision\)" hash_edit_regex = r"[0-9a-f]{7}\s+Second hash test edit" head_regex = r"HEAD\s+Third hash test edit" self.assertTrue( re.search(base_revision_regex, final_commit_msg, re.MULTILINE), f"Commit message doesn't contain base revision pattern. Got: {final_commit_msg}", ) self.assertTrue( re.search(hash_edit_regex, final_commit_msg, re.MULTILINE), f"Commit message doesn't contain hash pattern for second edit. Got: {final_commit_msg}", ) self.assertTrue( re.search(head_regex, final_commit_msg, re.MULTILINE), f"Commit message doesn't contain HEAD pattern. Got: {final_commit_msg}", ) if __name__ == "__main__": unittest.main()