Deskaid

  • e2e
#!/usr/bin/env python3 """End-to-end tests for InitProject subtool.""" import os import unittest from codemcp.testing import MCPEndToEndTestCase class InitProjectTest(MCPEndToEndTestCase): """Test the InitProject subtool functionality.""" async def test_reuse_head_chat_id(self): """Test that reuse_head_chat_id=True reuses the chat ID from the HEAD commit.""" # Set up a git repository in the temp dir from codemcp.git import ( commit_changes, get_head_commit_chat_id, get_ref_commit_chat_id, ) # Create a simple codemcp.toml file toml_path = os.path.join(self.temp_dir.name, "codemcp.toml") with open(toml_path, "w") as f: f.write(""" project_prompt = "Test reuse chat ID" [commands] test = ["./run_test.sh"] """) # Set up a git repository await self.git_run(["init"]) await self.git_run(["config", "user.email", "test@example.com"]) await self.git_run(["config", "user.name", "Test User"]) # Create an initial commit to have a HEAD reference test_file = os.path.join(self.temp_dir.name, "test_file.txt") with open(test_file, "w") as f: f.write("Initial content") await self.git_run(["add", "."]) await self.git_run(["commit", "-m", "Initial commit"]) # First InitProject call to create a reference with a chat ID async with self.create_client_session() as session: result_text1 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "First initialization", "subject_line": "feat: first commit", }, ) # Extract the chat ID from the result original_chat_id = self.extract_chat_id_from_text(result_text1) # Verify the reference contains the chat ID ref_name = f"refs/codemcp/{original_chat_id}" ref_chat_id = await get_ref_commit_chat_id(self.temp_dir.name, ref_name) self.assertEqual( original_chat_id, ref_chat_id, "Chat ID not found in reference" ) # The HEAD commit doesn't have the chat ID yet since it's only in the reference head_chat_id = await get_head_commit_chat_id(self.temp_dir.name) self.assertNotEqual( original_chat_id, head_chat_id, "HEAD shouldn't have the chat ID yet" ) # Make a change and commit it to add the chat ID to HEAD with open(test_file, "a") as f: f.write("\nAdded some content") await commit_changes( self.temp_dir.name, description="Adding content", chat_id=original_chat_id, ) # Now verify the HEAD has the chat ID head_chat_id = await get_head_commit_chat_id(self.temp_dir.name) self.assertEqual( original_chat_id, head_chat_id, "Chat ID should be in HEAD commit after changes", ) # Second InitProject call with reuse_head_chat_id=True async with self.create_client_session() as session: result_text2 = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Continue working on the same feature", "subject_line": "feat: continue working", "reuse_head_chat_id": True, }, ) # Extract the chat ID from the result reused_chat_id = self.extract_chat_id_from_text(result_text2) # Verify the chat ID is the same as the original self.assertEqual(original_chat_id, reused_chat_id, "Chat ID not reused") async def test_init_project_basic(self): """Test basic InitProject functionality with simple TOML file.""" # The basic codemcp.toml file is already created in the base test setup # We'll test that InitProject can read it correctly async with self.create_client_session() as session: # Call the InitProject tool with our new helper method result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialization", "subject_line": "feat: test basic functionality", "reuse_head_chat_id": False, }, ) # Verify the result contains expected system prompt elements self.assertIn("You are an AI assistant", result_text) async def test_init_project_complex_toml(self): """Test InitProject with a more complex TOML file that exercises all parsing features.""" # Create a more complex codemcp.toml file with various data types complex_toml_path = os.path.join(self.temp_dir.name, "codemcp.toml") with open(complex_toml_path, "w") as f: f.write("""# Complex TOML file with various data types project_prompt = \"\"\" This is a multiline string with multiple lines of text. \"\"\" [commands] format = ["./run_format.sh"] lint = ["./run_lint.sh"] [commands.test] command = ["./run_test.sh"] doc = "Run tests with optional arguments" """) async with self.create_client_session() as session: # Call the InitProject tool with our new helper method result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test with complex TOML", "subject_line": "feat: test complex TOML parsing", "reuse_head_chat_id": False, }, ) # Verify the result contains expected elements from the complex TOML self.assertIn("This is a multiline string", result_text) self.assertIn("format", result_text) self.assertIn("lint", result_text) self.assertIn("Run tests with optional arguments", result_text) async def test_init_project_with_binary_characters(self): """Test InitProject with TOML containing binary/non-ASCII characters to ensure proper handling.""" # Create a TOML file with non-ASCII characters and binary data binary_toml_path = os.path.join(self.temp_dir.name, "codemcp.toml") with open(binary_toml_path, "wb") as f: f.write(b"""project_prompt = "Testing binary data handling \xc2\xa9\xe2\x84\xa2\xf0\x9f\x98\x8a" [commands] format = ["./run_format.sh"] """) async with self.create_client_session() as session: # Call the InitProject tool with our new helper method result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test with binary characters", "subject_line": "feat: test binary character handling", "reuse_head_chat_id": False, }, ) # Verify the result contains expected elements, ensuring binary data was handled properly self.assertIn("format", result_text) async def test_chat_id_from_subject_line(self): """Test that the chat ID uses the subject line for the human-readable part.""" # Create a simple codemcp.toml file toml_path = os.path.join(self.temp_dir.name, "codemcp.toml") with open(toml_path, "w") as f: f.write(""" project_prompt = "Test project" [commands] test = ["./run_test.sh"] """) # Set up a git repository from codemcp.git import get_head_commit_hash, get_ref_commit_chat_id await self.git_run(["init"]) await self.git_run(["config", "user.email", "test@example.com"]) await self.git_run(["config", "user.name", "Test User"]) # Create an initial commit to have a HEAD reference test_file = os.path.join(self.temp_dir.name, "test_file.txt") with open(test_file, "w") as f: f.write("Initial content") await self.git_run(["add", "."]) await self.git_run(["commit", "-m", "Initial commit"]) # Get the hash of the initial commit initial_hash = await get_head_commit_hash(self.temp_dir.name, short=False) async with self.create_client_session() as session: # Call InitProject with a conventional commit style subject line result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": "Test initialize with subject line", "subject_line": "feat: add new feature with spaces!", }, ) # Verify that the chat ID contains the slugified subject line # The chat ID should be something like "1-feat-add-new-feature-with-spaces" # Check that it's not using "untitled" self.assertNotIn("untitled", result_text) # Check that a slugified version of the subject line is in the chat ID self.assertIn("feat-add-new-feature-with-spaces", result_text) # Extract the chat ID from the result chat_id = self.extract_chat_id_from_text(result_text) # Verify that HEAD hasn't changed - it should still point to the initial commit head_hash_after = await get_head_commit_hash( self.temp_dir.name, short=False ) self.assertEqual( initial_hash, head_hash_after, "HEAD should not change after InitProject", ) # Verify the reference was created with the chat ID ref_name = f"refs/codemcp/{chat_id}" ref_chat_id = await get_ref_commit_chat_id(self.temp_dir.name, ref_name) self.assertEqual( chat_id, ref_chat_id, f"Chat ID {chat_id} should be in reference {ref_name}", ) async def test_cherry_pick_reference_commit(self): """Test that commit_changes cherry-picks the reference commit when needed.""" from codemcp.git import ( commit_changes, get_head_commit_chat_id, get_head_commit_hash, get_head_commit_message, ) # Set up a git repository await self.git_run(["init"]) await self.git_run(["config", "user.email", "test@example.com"]) await self.git_run(["config", "user.name", "Test User"]) # Create an initial commit initial_file = os.path.join(self.temp_dir.name, "initial.txt") with open(initial_file, "w") as f: f.write("Initial content") await self.git_run(["add", "."]) await self.git_run(["commit", "-m", "Initial commit"]) # Get the hash of the initial commit initial_hash = await get_head_commit_hash(self.temp_dir.name, short=False) # Define the test subject and body for InitProject test_subject = "feat: test reference cherry-pick" test_body = "Test initialize for cherry-pick test" # Call InitProject which should create a reference without changing HEAD async with self.create_client_session() as session: result_text = await self.call_tool_assert_success( session, "codemcp", { "subtool": "InitProject", "path": self.temp_dir.name, "user_prompt": test_body, "subject_line": test_subject, }, ) # Extract the chat ID from the result chat_id = self.extract_chat_id_from_text(result_text) # Verify HEAD is unchanged head_hash_after_init = await get_head_commit_hash( self.temp_dir.name, short=False ) self.assertEqual( initial_hash, head_hash_after_init, "HEAD should not change after InitProject", ) # Now make a change and commit it test_file = os.path.join(self.temp_dir.name, "test_file.txt") with open(test_file, "w") as f: f.write("Test content for cherry-pick test") # Commit the changes - this should cherry-pick the reference commit first success, message = await commit_changes( path=self.temp_dir.name, description="Testing cherry-pick", chat_id=chat_id, ) self.assertTrue(success, f"Commit failed: {message}") # Verify HEAD has a new commit with the right chat ID head_chat_id = await get_head_commit_chat_id(self.temp_dir.name) self.assertEqual( chat_id, head_chat_id, "HEAD commit should have the correct chat ID after cherry-pick", ) # Verify the commit message contains the original subject and body from InitProject head_commit_msg = await get_head_commit_message(self.temp_dir.name) self.assertIsNotNone(head_commit_msg, "Commit message should not be None") # Check that both the subject and body are in the commit message self.assertIn( test_subject, head_commit_msg, "Commit message should contain the original subject line", ) self.assertIn( test_body, head_commit_msg, "Commit message should contain the original body", ) # Check that the new description is also included since this is an amended commit self.assertIn( "Testing cherry-pick", head_commit_msg, "Commit message should contain the new change description", ) # Get commit count to verify we have more than just the initial commit commit_count_output = await self.git_run( ["rev-list", "--count", "HEAD"], capture_output=True, text=True ) commit_count = int(commit_count_output.strip()) self.assertGreater( commit_count, 1, "Should have more than one commit after changes" ) if __name__ == "__main__": unittest.main()