From 2925927c494d429b239377153a777853401f632a Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Thu, 27 Mar 2025 19:43:43 +0800 Subject: [PATCH 1/2] Update [ghstack-poisoned] --- codemcp/file_utils.py | 8 +- e2e/test_trailing_whitespace.py | 179 ++++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 e2e/test_trailing_whitespace.py diff --git a/codemcp/file_utils.py b/codemcp/file_utils.py index 1a384532..1f520a92 100644 --- a/codemcp/file_utils.py +++ b/codemcp/file_utils.py @@ -164,6 +164,7 @@ async def write_text_content( line_endings: Optional[str] = None, ) -> None: """Write text content to a file with specified encoding and line endings. + Automatically strips trailing whitespace from each line. Args: file_path: The path to the file @@ -181,8 +182,13 @@ async def write_text_content( # First normalize content to LF line endings normalized_content = normalize_to_lf(content) + # Strip trailing whitespace from each line + stripped_content = "\n".join( + line.rstrip() for line in normalized_content.splitlines() + ) + # Apply the requested line ending - final_content = apply_line_endings(normalized_content, line_endings) + final_content = apply_line_endings(stripped_content, line_endings) # Ensure directory exists ensure_directory_exists(file_path) diff --git a/e2e/test_trailing_whitespace.py b/e2e/test_trailing_whitespace.py new file mode 100644 index 00000000..ad6823aa --- /dev/null +++ b/e2e/test_trailing_whitespace.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 + +"""Tests for trailing whitespace stripping in the WriteFile and EditFile subtools.""" + +import os +import unittest + +from codemcp.testing import MCPEndToEndTestCase + + +class TrailingWhitespaceTest(MCPEndToEndTestCase): + """Test that trailing whitespace is properly stripped when using WriteFile and EditFile.""" + + async def test_write_file_strips_trailing_whitespace(self): + """Test that the WriteFile subtool strips trailing whitespace from each line.""" + test_file_path = os.path.join(self.temp_dir.name, "write_whitespace.txt") + + # Content with trailing whitespace + content_with_whitespace = "Line 1 \nLine 2 \t \nLine 3\n Line 4 \n" + + # Expected content after whitespace stripping + # Note: The trailing newline is preserved + expected_content = "Line 1\nLine 2\nLine 3\n Line 4" + + async with self.create_client_session() as session: + # Initialize 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 trailing whitespace test", + "subject_line": "test: initialize for trailing whitespace test", + "reuse_head_chat_id": False, + }, + ) + + # Extract chat_id from the init result + chat_id = self.extract_chat_id_from_text(init_result_text) + + # Call the WriteFile tool with content containing trailing whitespace + result_text = await self.call_tool_assert_success( + session, + "codemcp", + { + "subtool": "WriteFile", + "path": test_file_path, + "content": content_with_whitespace, + "description": "Create file with trailing whitespace", + "chat_id": chat_id, + }, + ) + + # Verify the success message + self.assertIn("Successfully wrote to", result_text) + + # Verify the file was created with trailing whitespace removed + with open(test_file_path) as f: + file_content = f.read() + + self.assertEqual(file_content, expected_content) + + async def test_edit_file_strips_trailing_whitespace(self): + """Test that the EditFile subtool strips trailing whitespace from each line.""" + # Create a test file with multiple lines + test_file_path = os.path.join(self.temp_dir.name, "edit_whitespace.txt") + original_content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n" + with open(test_file_path, "w") as f: + f.write(original_content) + + # Add the file to git and commit it + await self.git_run(["add", "edit_whitespace.txt"], check=False) + await self.git_run( + ["commit", "-m", "Add file for editing with whitespace"], check=False + ) + + # Edit the file with content containing trailing whitespace + old_string = "Line 2\nLine 3\n" + new_string_with_whitespace = "Line 2 \nModified Line 3 \t \n" + + async with self.create_client_session() as session: + # Initialize 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 edit file whitespace test", + "subject_line": "test: initialize for edit file whitespace test", + "reuse_head_chat_id": False, + }, + ) + + # Extract chat_id from the init result + chat_id = self.extract_chat_id_from_text(init_result_text) + + # Call the EditFile tool with content containing trailing whitespace + result_text = await self.call_tool_assert_success( + session, + "codemcp", + { + "subtool": "EditFile", + "path": test_file_path, + "old_string": old_string, + "new_string": new_string_with_whitespace, + "description": "Modify content with trailing whitespace", + "chat_id": chat_id, + }, + ) + + # Verify the success message + self.assertIn("Successfully edited", result_text) + + # Verify the file was edited with trailing whitespace removed + with open(test_file_path) as f: + file_content = f.read() + + expected_content = "Line 1\nLine 2\nModified Line 3\nLine 4\nLine 5" + self.assertEqual(file_content, expected_content) + + async def test_empty_lines_preserved(self): + """Test that empty lines are preserved when stripping trailing whitespace.""" + test_file_path = os.path.join(self.temp_dir.name, "empty_lines.txt") + + # Content with empty lines and whitespace-only lines + content_with_empty_lines = "Line 1\n\n \t \nLine 2\n" + + # Expected content after whitespace stripping + # The whitespace-only line should become an empty line + expected_content = "Line 1\n\n\nLine 2" + + async with self.create_client_session() as session: + # Initialize 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 empty lines test", + "subject_line": "test: initialize for empty lines test", + "reuse_head_chat_id": False, + }, + ) + + # Extract chat_id from the init result + chat_id = self.extract_chat_id_from_text(init_result_text) + + # Call the WriteFile tool with content containing empty lines + result_text = await self.call_tool_assert_success( + session, + "codemcp", + { + "subtool": "WriteFile", + "path": test_file_path, + "content": content_with_empty_lines, + "description": "Create file with empty lines", + "chat_id": chat_id, + }, + ) + + # Verify the success message + self.assertIn("Successfully wrote to", result_text) + + # Verify the file was created with empty lines preserved + with open(test_file_path) as f: + file_content = f.read() + + self.assertEqual(file_content, expected_content) + + +class OutOfProcessTrailingWhitespaceTest(TrailingWhitespaceTest): + in_process = False + + +if __name__ == "__main__": + unittest.main() From 48201f8dda50da0eff7050fc08eb7ed5dd6ea81b Mon Sep 17 00:00:00 2001 From: "Edward Z. Yang" Date: Thu, 27 Mar 2025 19:51:14 +0800 Subject: [PATCH 2/2] Update [ghstack-poisoned] --- codemcp/file_utils.py | 7 ++++++- e2e/test_trailing_whitespace.py | 8 ++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/codemcp/file_utils.py b/codemcp/file_utils.py index 1f520a92..11c8a60c 100644 --- a/codemcp/file_utils.py +++ b/codemcp/file_utils.py @@ -164,7 +164,8 @@ async def write_text_content( line_endings: Optional[str] = None, ) -> None: """Write text content to a file with specified encoding and line endings. - Automatically strips trailing whitespace from each line. + Automatically strips trailing whitespace from each line and ensures + a trailing newline at the end of the file. Args: file_path: The path to the file @@ -187,6 +188,10 @@ async def write_text_content( line.rstrip() for line in normalized_content.splitlines() ) + # Ensure there's always a trailing newline + if not stripped_content.endswith("\n"): + stripped_content += "\n" + # Apply the requested line ending final_content = apply_line_endings(stripped_content, line_endings) diff --git a/e2e/test_trailing_whitespace.py b/e2e/test_trailing_whitespace.py index ad6823aa..8aada0cc 100644 --- a/e2e/test_trailing_whitespace.py +++ b/e2e/test_trailing_whitespace.py @@ -19,8 +19,8 @@ async def test_write_file_strips_trailing_whitespace(self): content_with_whitespace = "Line 1 \nLine 2 \t \nLine 3\n Line 4 \n" # Expected content after whitespace stripping - # Note: The trailing newline is preserved - expected_content = "Line 1\nLine 2\nLine 3\n Line 4" + # The trailing newline is preserved + expected_content = "Line 1\nLine 2\nLine 3\n Line 4\n" async with self.create_client_session() as session: # Initialize project to get chat_id @@ -117,7 +117,7 @@ async def test_edit_file_strips_trailing_whitespace(self): with open(test_file_path) as f: file_content = f.read() - expected_content = "Line 1\nLine 2\nModified Line 3\nLine 4\nLine 5" + expected_content = "Line 1\nLine 2\nModified Line 3\nLine 4\nLine 5\n" self.assertEqual(file_content, expected_content) async def test_empty_lines_preserved(self): @@ -129,7 +129,7 @@ async def test_empty_lines_preserved(self): # Expected content after whitespace stripping # The whitespace-only line should become an empty line - expected_content = "Line 1\n\n\nLine 2" + expected_content = "Line 1\n\n\nLine 2\n" async with self.create_client_session() as session: # Initialize project to get chat_id