test_git_amend.py•32.1 kB
#!/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()