mcp-wcgw

by rusiaaman
Verified
import os import tempfile from typing import Generator import pytest from wcgw.client.bash_state.bash_state import BashState from wcgw.client.file_ops.diff_edit import SearchReplaceMatchError from wcgw.client.file_ops.search_replace import SearchReplaceSyntaxError from wcgw.client.tools import ( Context, FileWriteOrEdit, Initialize, default_enc, get_tool_output, ) from wcgw.types_ import Console class TestConsole(Console): def __init__(self): self.logs = [] self.prints = [] def log(self, msg: str) -> None: self.logs.append(msg) def print(self, msg: str) -> None: self.prints.append(msg) @pytest.fixture def temp_dir() -> Generator[str, None, None]: """Provides a temporary directory for testing.""" with tempfile.TemporaryDirectory() as td: yield td @pytest.fixture def context(temp_dir: str) -> Generator[Context, None, None]: """Provides a test context with temporary directory and handles cleanup.""" console = TestConsole() bash_state = BashState( console=console, working_dir=temp_dir, bash_command_mode=None, file_edit_mode=None, write_if_empty_mode=None, mode=None, use_screen=False, ) ctx = Context( bash_state=bash_state, console=console, ) yield ctx # Cleanup after each test bash_state.cleanup() def test_file_edit(context: Context, temp_dir: str) -> None: """Test the FileWriteOrEdit tool.""" # First initialize init_args = Initialize( any_workspace_path=temp_dir, initial_files_to_read=[], task_id_to_resume="", mode_name="wcgw", code_writer_config=None, type="first_call", ) get_tool_output(context, init_args, default_enc, 1.0, lambda x, y: ("", 0.0), None) # Create a test file test_file = os.path.join(temp_dir, "test.py") with open(test_file, "w") as f: f.write("def hello():\n print('hello')\n") # Test editing the file edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=10, file_content_or_search_replace_blocks="""<<<<<<< SEARCH def hello(): print('hello') ======= def hello(): print('hello world') >>>>>>> REPLACE""", ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) assert len(outputs) == 1 # Verify the change with open(test_file) as f: content = f.read() assert "hello world" in content # Test indentation match edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=100, file_content_or_search_replace_blocks="""<<<<<<< SEARCH def hello(): print('hello world') ======= def hello(): print('ok') >>>>>>> REPLACE""", ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) assert len(outputs) == 1 assert "Warning: matching without considering indentation" in outputs[0] # Verify the change with open(test_file) as f: content = f.read() assert "print('ok')" in content # Test no match with partial edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=50, file_content_or_search_replace_blocks="""<<<<<<< SEARCH def hello(): print('no match') ======= def hello(): print('no match replace') >>>>>>> REPLACE""", ) with pytest.raises(SearchReplaceMatchError) as e: outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) assert """def hello(): print('ok')""" in str(e) with open(test_file) as f: content = f.read() assert "print('ok')" in content # Test syntax error edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=10, file_content_or_search_replace_blocks="""<<<<<<< SEARCH def hello(): print('ok') ======= def hello(): print('ok") >>>>>>> REPLACE""", ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) assert len(outputs) == 1 assert "tree-sitter reported syntax errors" in outputs[0] # Verify the change with open(test_file) as f: content = f.read() assert "print('ok\")" in content with pytest.raises(SearchReplaceSyntaxError) as e: edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=50, file_content_or_search_replace_blocks="""<<<<<<< SEARCH def hello(): print('ok') ======= def hello(): print('ok") >>>>>>> REPLACE >>>>>>> REPLACE """, ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) with pytest.raises(SearchReplaceSyntaxError) as e: edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=10, file_content_or_search_replace_blocks="""<<<<<<< SEARCH def hello(): print('ok') ======= def hello(): print('ok") """, ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) # Test multiple matches with open(test_file, "w") as f: f.write(""" def hello(): print('ok') # Comment def hello(): print('ok') """) with pytest.raises(SearchReplaceMatchError) as e: edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=1, file_content_or_search_replace_blocks="""<<<<<<< SEARCH def hello(): print('ok') ======= def hello(): print('hello world') >>>>>>> REPLACE """, ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) # Grounding should pass even when duplicate found edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=10, file_content_or_search_replace_blocks="""<<<<<<< SEARCH # Comment ======= # New Comment >>>>>>> REPLACE <<<""" + """<<<< SEARCH def hello(): print('ok') ======= def hello(): print('hello world') >>>>>>> REPLACE """, ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) with open(test_file) as f: content = f.read() assert ( content == """ def hello(): print('ok') # New Comment def hello(): print('hello world') """ ) import re def fix_indentation( matched_lines: list[str], searched_lines: list[str], replaced_lines: list[str] ) -> list[str]: if not matched_lines or not searched_lines or not replaced_lines: return replaced_lines def get_indentation(line: str) -> str: match = re.match(r"^(\s*)", line) assert match return match.group(0) matched_indents = [get_indentation(line) for line in matched_lines if line.strip()] searched_indents = [ get_indentation(line) for line in searched_lines if line.strip() ] if len(matched_indents) != len(searched_indents): return replaced_lines diffs: list[int] = [ len(searched) - len(matched) for matched, searched in zip(matched_indents, searched_indents) ] if not diffs: return replaced_lines if not all(diff == diffs[0] for diff in diffs): return replaced_lines if diffs[0] == 0: return replaced_lines def adjust_indentation(line: str, diff: int) -> str: if diff < 0: # Need to add -diff spaces return matched_indents[0][:-diff] + line # Need to remove diff spaces return line[diff:] if diffs[0] > 0: # Check if replaced_lines have enough leading spaces to remove if not all(not line[: diffs[0]].strip() for line in replaced_lines): return replaced_lines return [adjust_indentation(line, diffs[0]) for line in replaced_lines] def test_empty_inputs(): assert fix_indentation([], [" foo"], [" bar"]) == [" bar"] assert fix_indentation([" foo"], [], [" bar"]) == [" bar"] assert fix_indentation([" foo"], [" foo"], []) == [] def test_no_non_empty_lines_in_matched_or_searched(): # All lines in matched_lines/searched_lines are blank or just spaces matched_lines = [" ", " "] searched_lines = [" ", "\t "] replaced_lines = [" Some text", " Another text"] # Because matched_lines / searched_lines effectively have 0 non-empty lines, # the function returns replaced_lines as is assert ( fix_indentation(matched_lines, searched_lines, replaced_lines) == replaced_lines ) def test_same_indentation_no_change(): # The non-empty lines have the same indentation => diff=0 => no changes matched_lines = [" foo", " bar"] searched_lines = [" baz", " qux"] replaced_lines = [" spam", " ham"] # Should return replaced_lines unchanged assert ( fix_indentation(matched_lines, searched_lines, replaced_lines) == replaced_lines ) def test_positive_indentation_difference(): # matched_lines have fewer spaces than searched_lines => diff > 0 => remove indentation from replaced_lines matched_lines = [" foo", " bar"] searched_lines = [" foo", " bar"] replaced_lines = [" spam", " ham"] # diff is 2 => remove 2 spaces from the start of each replaced line expected = [" spam", " ham"] assert fix_indentation(matched_lines, searched_lines, replaced_lines) == expected def test_positive_indentation_not_enough_spaces(): # We want to remove 2 spaces, but replaced_lines do not have that many leading spaces matched_lines = ["foo", "bar"] searched_lines = [" foo", " bar"] replaced_lines = [" spam", " ham"] # only 1 leading space # The function should detect there's not enough indentation to remove => return replaced_lines unchanged assert ( fix_indentation(matched_lines, searched_lines, replaced_lines) == replaced_lines ) def test_negative_indentation_difference(): # matched_lines have more spaces than searched_lines => diff < 0 => add indentation to replaced_lines matched_lines = [" foo", " bar"] searched_lines = [" foo", " bar"] replaced_lines = ["spam", "ham"] # diff is -2 => add 2 spaces from matched_indents[0] to each line # matched_indents[0] = ' ' => matched_indents[0][:-diff] => ' '[:2] => ' ' expected = [" spam", " ham"] assert fix_indentation(matched_lines, searched_lines, replaced_lines) == expected def test_different_number_of_non_empty_lines(): # matched_indents and searched_indents have different lengths => return replaced_lines matched_lines = [ " foo", " ", " baz", ] # effectively 2 non-empty lines searched_lines = [" foo", " bar", " baz"] # 3 non-empty lines replaced_lines = [" spam", " ham"] assert ( fix_indentation(matched_lines, searched_lines, replaced_lines) == replaced_lines ) def test_inconsistent_indentation_difference(): # The diffs are not all the same => return replaced_lines matched_lines = [" foo", " bar"] searched_lines = [" foo", " bar"] replaced_lines = ["spam", "ham"] # For the first pair, diff = len(" ") - len(" ") = 2 - 4 = -2 # For the second pair, diff = len(" ") - len(" ") = 4 - 8 = -4 # Not all diffs are equal => should return replaced_lines assert ( fix_indentation(matched_lines, searched_lines, replaced_lines) == replaced_lines ) def test_realistic_fix_indentation_scenario(): matched_lines = [ " class Example:", " def method(self):", " print('hello')", ] searched_lines = [ "class Example:", " def method(self):", " print('world')", ] replaced_lines = [ "class Example:", " def another_method(self):", " print('world')", ] expected = [ " class Example:", " def another_method(self):", " print('world')", ] assert fix_indentation(matched_lines, searched_lines, replaced_lines) == expected def test_realistic_nonfix_indentation_scenario(): matched_lines = [ " class Example:", " def method(self):", " print('hello')", ] searched_lines = [ "class Example:", " def method(self):", " print('world')", ] replaced_lines = [ "class Example:", " def another_method(self):", " print('world')", ] assert ( fix_indentation(matched_lines, searched_lines, replaced_lines) == replaced_lines ) def test_context_based_matching(context: Context, temp_dir: str) -> None: """Test using past and future context to uniquely identify search blocks.""" # First initialize init_args = Initialize( any_workspace_path=temp_dir, initial_files_to_read=[], task_id_to_resume="", mode_name="wcgw", code_writer_config=None, type="first_call", ) get_tool_output(context, init_args, default_enc, 1.0, lambda x, y: ("", 0.0), None) # Create a test file with repeating pattern test_file = os.path.join(temp_dir, "test_context.py") with open(test_file, "w") as f: f.write("A\nB\nC\nB\n") # Test case 1: Using future context to uniquely identify a block # The search "A" followed by "B" followed by "C" uniquely determines the first B edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=10, file_content_or_search_replace_blocks="""<<<<<<< SEARCH A ======= A >>>>>>> REPLACE <<<<<<< SEARCH B ======= B_MODIFIED_FIRST >>>>>>> REPLACE <<<<<<< SEARCH C ======= C >>>>>>> REPLACE""", ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) # Verify the change - first B should be modified with open(test_file) as f: content = f.read() assert content == "A\nB_MODIFIED_FIRST\nC\nB\n" # Test case 2: Using past context to uniquely identify a block # Reset the file with open(test_file, "w") as f: f.write("A\nB\nC\nB\n") # The search "C" followed by "B" uniquely determines the second B edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=10, file_content_or_search_replace_blocks="""<<<<<<< SEARCH C ======= C >>>>>>> REPLACE <<<<<<< SEARCH B ======= B_MODIFIED_SECOND >>>>>>> REPLACE""", ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) # Verify the change - second B should be modified with open(test_file) as f: content = f.read() assert content == "A\nB\nC\nB_MODIFIED_SECOND\n" def test_unordered(context: Context, temp_dir: str) -> None: # First initialize init_args = Initialize( any_workspace_path=temp_dir, initial_files_to_read=[], task_id_to_resume="", mode_name="wcgw", code_writer_config=None, type="first_call", ) get_tool_output(context, init_args, default_enc, 1.0, lambda x, y: ("", 0.0), None) # Create a test file with repeating pattern test_file = os.path.join(temp_dir, "test_context.py") with open(test_file, "w") as f: f.write("A\nB\nC\nB\n") # Test case 1: Using future context to uniquely identify a block # The search "A" followed by "B" followed by "C" uniquely determines the first B edit_args = FileWriteOrEdit( file_path=test_file, percentage_to_change=10, file_content_or_search_replace_blocks="""<<<<<<< SEARCH C ======= CPrime >>>>>>> REPLACE <<<<<<< SEARCH A ======= A_MODIFIED >>>>>>> REPLACE """, ) outputs, _ = get_tool_output( context, edit_args, default_enc, 1.0, lambda x, y: ("", 0.0), None ) # Verify the change - first B should be modified with open(test_file) as f: content = f.read() assert content == "A_MODIFIED\nB\nCPrime\nB\n"