From cd566a93b688b29448bea7ed0e3aa56c3edfb761 Mon Sep 17 00:00:00 2001 From: Peter Etelej Date: Fri, 18 Jul 2025 00:36:21 +0300 Subject: [PATCH 1/6] ci: use platform ci tests > python version --- .github/workflows/ci.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a724713..30d9de3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,17 +1,18 @@ name: CI on: - pull_request: - branches: [ main ] push: branches: [ main ] + pull_request: + branches: [ main ] jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ["3.10", "3.11", "3.12"] + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.11"] steps: - name: Checkout code @@ -28,16 +29,16 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies - run: uv sync --all-extras --dev + run: uv sync --dev - name: Run tests run: uv run pytest -v --tb=short - name: Run type checking - run: uv run mypy src/ + run: uv run mypy src - name: Run linting - run: uv run ruff check + run: uv run ruff check . - name: Check formatting run: uv run ruff format --check @@ -56,7 +57,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: uv sync --dev @@ -89,7 +90,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" - name: Install dependencies run: uv sync --dev From d24b82006e3a6ff5fdc72a762a8184731dfec889 Mon Sep 17 00:00:00 2001 From: Peter Etelej Date: Fri, 18 Jul 2025 00:53:42 +0300 Subject: [PATCH 2/6] delete unnecessary entrypoint --- main.py | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 main.py diff --git a/main.py b/main.py deleted file mode 100644 index 60e3ab2..0000000 --- a/main.py +++ /dev/null @@ -1,6 +0,0 @@ -def main(): - print("Hello from diffchunk!") - - -if __name__ == "__main__": - main() From d3493d0ffaeca7df44adf5d263c4518e5eda4a4b Mon Sep 17 00:00:00 2001 From: Peter Etelej Date: Fri, 18 Jul 2025 00:53:58 +0300 Subject: [PATCH 3/6] Windows test fixes --- src/tools.py | 8 +++++--- tests/test_cli_integration.py | 9 +++++++-- tests/test_filesystem_edge_cases.py | 2 +- tests/test_integration.py | 4 +++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/tools.py b/src/tools.py index cd0182d..19729ec 100644 --- a/src/tools.py +++ b/src/tools.py @@ -64,12 +64,14 @@ def _load_diff_internal( resolved_file_path = os.path.realpath(os.path.expanduser(absolute_file_path)) # Validate file exists and is readable + if os.path.exists(resolved_file_path) and not os.path.isfile( + resolved_file_path + ): + raise ValueError(f"Path is not a file: {resolved_file_path}") + if not os.path.exists(resolved_file_path): raise ValueError(f"Diff file not found: {absolute_file_path}") - if not os.path.isfile(resolved_file_path): - raise ValueError(f"Path is not a file: {resolved_file_path}") - if not os.access(resolved_file_path, os.R_OK): raise ValueError(f"Cannot read file: {resolved_file_path}") diff --git a/tests/test_cli_integration.py b/tests/test_cli_integration.py index 774cf88..62c5eab 100644 --- a/tests/test_cli_integration.py +++ b/tests/test_cli_integration.py @@ -1,5 +1,7 @@ """CLI integration tests for main.py entry point.""" +import platform +import signal import subprocess import sys import time @@ -100,8 +102,11 @@ def test_cli_keyboard_interrupt_handling(self): # Give it a moment to start time.sleep(1) - # Send interrupt signal - process.send_signal(subprocess.signal.SIGINT) + # Send interrupt signal (Windows doesn't support SIGINT) + if platform.system() == "Windows": + process.terminate() + else: + process.send_signal(signal.SIGINT) # Wait for graceful shutdown stdout, stderr = process.communicate(timeout=3) diff --git a/tests/test_filesystem_edge_cases.py b/tests/test_filesystem_edge_cases.py index 09522ba..fdc6bda 100644 --- a/tests/test_filesystem_edge_cases.py +++ b/tests/test_filesystem_edge_cases.py @@ -38,7 +38,7 @@ def test_load_diff_nonexistent_file(self, tools): def test_load_diff_directory_instead_of_file(self, tools): """Test loading directory instead of file.""" with pytest.raises(ValueError, match="not a file"): - tools.load_diff("/tmp") + tools.load_diff(tempfile.gettempdir()) def test_load_diff_empty_file_path(self, tools): """Test loading with empty file path.""" diff --git a/tests/test_integration.py b/tests/test_integration.py index 4dfd2f1..5a1e856 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,5 +1,7 @@ """Integration tests using real diff files from test_data.""" +import tempfile + import pytest from pathlib import Path @@ -200,7 +202,7 @@ def test_invalid_file_errors(self, tools): # Directory instead of file with pytest.raises(ValueError, match="not a file"): - tools.load_diff("/tmp") + tools.load_diff(tempfile.gettempdir()) def test_chunk_size_consistency(self, tools, test_data_dir): """Test that chunk sizes are respected reasonably.""" From 68837d5904cb6c58cdad0db303c5617ff774aafa Mon Sep 17 00:00:00 2001 From: Peter Etelej Date: Fri, 18 Jul 2025 01:08:45 +0300 Subject: [PATCH 4/6] Skip flaky Windows signal test --- pyproject.toml | 2 +- tests/test_cli_integration.py | 12 ++++++------ uv.lock | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 047d3a7..cecffd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "diffchunk" -version = "0.1.5" +version = "0.1.6" description = "MCP server for navigating large diff files with intelligent chunking" readme = "README.md" requires-python = ">=3.10" diff --git a/tests/test_cli_integration.py b/tests/test_cli_integration.py index 62c5eab..9c934bb 100644 --- a/tests/test_cli_integration.py +++ b/tests/test_cli_integration.py @@ -1,7 +1,6 @@ """CLI integration tests for main.py entry point.""" import platform -import signal import subprocess import sys import time @@ -88,6 +87,10 @@ def test_cli_server_startup_basic(self): process.kill() process.wait() + @pytest.mark.skipif( + platform.system() == "Windows", + reason="Signal handling unreliable on Windows CI", + ) def test_cli_keyboard_interrupt_handling(self): """Test CLI handles KeyboardInterrupt gracefully.""" process = subprocess.Popen( @@ -102,11 +105,8 @@ def test_cli_keyboard_interrupt_handling(self): # Give it a moment to start time.sleep(1) - # Send interrupt signal (Windows doesn't support SIGINT) - if platform.system() == "Windows": - process.terminate() - else: - process.send_signal(signal.SIGINT) + # Send interrupt signal + process.send_signal(subprocess.signal.SIGINT) # Wait for graceful shutdown stdout, stderr = process.communicate(timeout=3) diff --git a/uv.lock b/uv.lock index f136d39..afef845 100644 --- a/uv.lock +++ b/uv.lock @@ -136,7 +136,7 @@ toml = [ [[package]] name = "diffchunk" -version = "0.1.5" +version = "0.1.6" source = { editable = "." } dependencies = [ { name = "click" }, From 6113cf1622c3cca3047514d54c027125ab2f4cc1 Mon Sep 17 00:00:00 2001 From: Peter Etelej Date: Fri, 18 Jul 2025 01:14:12 +0300 Subject: [PATCH 5/6] ci cache deps --- .github/workflows/ci.yml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30d9de3..42a166c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,9 +19,10 @@ jobs: uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v6 with: - version: "latest" + enable-cache: true + cache-dependency-glob: "uv.lock" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 @@ -52,7 +53,10 @@ jobs: uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" - name: Set up Python uses: actions/setup-python@v5 @@ -85,7 +89,10 @@ jobs: uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v3 + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" - name: Set up Python uses: actions/setup-python@v5 From a1ebfb37bceb002a2df40460c47830e30af2d3fc Mon Sep 17 00:00:00 2001 From: Peter Etelej Date: Fri, 18 Jul 2025 01:19:22 +0300 Subject: [PATCH 6/6] Add call tool handler tests --- tests/test_handle_call_tool.py | 90 ++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests/test_handle_call_tool.py diff --git a/tests/test_handle_call_tool.py b/tests/test_handle_call_tool.py new file mode 100644 index 0000000..b11a86a --- /dev/null +++ b/tests/test_handle_call_tool.py @@ -0,0 +1,90 @@ +"""Test handle_call_tool for code coverage.""" + +import json +from pathlib import Path + +import pytest +from mcp.types import CallToolRequest + +from src.server import DiffChunkServer + + +class TestHandleCallTool: + """Test handle_call_tool for coverage.""" + + @pytest.fixture + def server(self): + return DiffChunkServer() + + @pytest.fixture + def react_diff_file(self): + diff_file = Path(__file__).parent / "test_data" / "react_18.0_to_18.3.diff" + if not diff_file.exists(): + pytest.skip("React test diff not found") + return str(diff_file) + + @pytest.mark.asyncio + async def test_handle_call_tool_coverage(self, server, react_diff_file): + """Test all paths in handle_call_tool for coverage.""" + handler = server.app.request_handlers[CallToolRequest] + + # Test load_diff + request = CallToolRequest( + method="tools/call", + params={ + "name": "load_diff", + "arguments": {"absolute_file_path": react_diff_file}, + }, + ) + result = await handler(request) + data = json.loads(result.root.content[0].text) + assert data["chunks"] > 0 + + # Test list_chunks + request = CallToolRequest( + method="tools/call", + params={ + "name": "list_chunks", + "arguments": {"absolute_file_path": react_diff_file}, + }, + ) + result = await handler(request) + chunks = json.loads(result.root.content[0].text) + assert len(chunks) > 0 + + # Test get_chunk + request = CallToolRequest( + method="tools/call", + params={ + "name": "get_chunk", + "arguments": {"absolute_file_path": react_diff_file, "chunk_number": 1}, + }, + ) + result = await handler(request) + assert "=== Chunk 1 of" in result.root.content[0].text + + # Test find_chunks_for_files + request = CallToolRequest( + method="tools/call", + params={ + "name": "find_chunks_for_files", + "arguments": {"absolute_file_path": react_diff_file, "pattern": "*"}, + }, + ) + result = await handler(request) + chunk_nums = json.loads(result.root.content[0].text) + assert isinstance(chunk_nums, list) + + # Test unknown tool + request = CallToolRequest( + method="tools/call", params={"name": "unknown_tool", "arguments": {}} + ) + result = await handler(request) + assert "Unknown tool: unknown_tool" in result.root.content[0].text + + # Test None arguments (validation error from MCP layer) + request = CallToolRequest( + method="tools/call", params={"name": "load_diff", "arguments": None} + ) + result = await handler(request) + assert "Input validation error" in result.root.content[0].text