* auto-claude: subtask-1-1 - Remove validation_strategy backward compatibility shim - Delete apps/backend/validation_strategy.py shim file that re-exported from spec.validation_strategy - Update docstring in spec/validation_strategy.py to show correct import path Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-1-2 - Remove service_orchestrator shim - Deleted apps/backend/service_orchestrator.py backward compatibility shim file - Updated docstring in services/orchestrator.py to use correct import path (from services.orchestrator import instead of from service_orchestrator import) - Verified no external imports of the shim remain in the codebase - Import from services.orchestrator works correctly Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-2-1 - Remove Chunk/ChunkStatus aliases from implementation_plan Removed backwards compatibility aliases as part of cleaning up outdated compatibility shims: - Removed ChunkStatus = SubtaskStatus from enums.py - Removed Chunk = Subtask from subtask.py - Removed Chunk/ChunkStatus exports from __init__.py - Updated all test files to use canonical names (Subtask, SubtaskStatus) This completes subtasks 2-1, 2-2, and 2-3 together since the test files depend on all three changes being made atomically. Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-2-4 - Remove deprecated use_orchestrator_review field from GitHub runner models * auto-claude: subtask-3-1 - Update apps/frontend/src/main/index.ts to use platform imports * auto-claude: subtask-3-2 - Update python-detector.ts to use platform imports - Import isWindows from ./platform module - Replace all process.platform === 'win32' checks with isWindows() calls - Remove redundant local isWindows variable declarations * auto-claude: subtask-3-3 - Update apps/frontend/src/main/claude-cli-utils.ts to use platform imports * auto-claude: subtask-3-4 - Update apps/frontend/src/main/config-paths.ts to use platform imports * auto-claude: subtask-3-5 - Update apps/frontend/src/main/memory-service.ts to use platform imports * auto-claude: subtask-4-1 - Update claude-code-handlers.ts to use platform imports Replace all direct process.platform checks with centralized platform abstraction functions from ../platform module: - isWindows() for Windows platform checks - isMacOS() for macOS/Darwin platform checks - isLinux() for Linux platform checks This removes 6 instances of process.platform === '...' comparisons and 1 local isWindows variable assignment, replacing them with the platform abstraction layer for better cross-platform consistency. * auto-claude: subtask-4-2 - Update apps/frontend/src/main/ipc-handlers/mcp-handlers.ts to use platform imports * auto-claude: subtask-4-3 - Update apps/frontend/src/main/ipc-handlers/memory-handlers.ts to use platform imports - Replace process.platform checks with platform module functions - Add getOllamaExecutablePaths(), getOllamaInstallCommand(), and getWhichCommand() to platform/paths.ts - Export new functions from platform/index.ts - Migrate checkOllamaInstalled() to use platform module for path resolution - Migrate getOllamaInstallCommand() to delegate to platform module - Update debug log to use getCurrentOS() instead of process.platform Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-4-4 - Update apps/frontend/src/main/ipc-handlers/termina * auto-claude: subtask-4-5 - Update apps/frontend/src/main/ipc-handlers/github/ - Replace direct process.platform check with getWhichCommand() from platform abstraction - Import getWhichCommand from ../../platform for cross-platform which/where command * auto-claude: subtask-4-6 - Update apps/frontend/src/main/ipc-handlers/github/utils/subprocess-runner.ts to use platform imports * auto-claude: subtask-5-1 - Update apps/frontend/src/main/agent/agent-process.ts Replace direct process.platform checks with platform abstraction: - Import isWindows from ../platform module - Replace `process.platform !== 'win32'` with `!isWindows()` - Replace `process.platform === 'win32'` with `isWindows()` This ensures consistent platform detection using the centralized platform abstraction layer. Co-Authored-By: Claude Opus 4.5 <[email protected]> * auto-claude: subtask-5-2 - Update apps/frontend/src/main/agent/agent-queue.ts * auto-claude: subtask-5-3 - Update apps/frontend/src/main/terminal/pty-daemon.ts to use platform imports * auto-claude: subtask-5-4 - Update pty-daemon-client.ts to use platform imports Replace direct process.platform === 'win32' check with isWindows() from the platform abstraction layer for consistent cross-platform handling of socket paths. * auto-claude: subtask-5-5 - Update apps/frontend/src/main/insights/config.ts to use platform imports - Import isWindows() from '../platform' - Replace process.platform === 'win32' checks with isWindows() - Maintains case-insensitive path comparison on Windows * auto-claude: subtask-5-6 - Update apps/frontend/src/main/changelog/version-suggester.ts to use platform imports * auto-claude: subtask-5-7 - Update apps/frontend/src/main/changelog/generator. * fix: Remove unused import and fix test import paths - Remove unused `isWindows` import from memory-handlers.ts - Fix test_service_orchestrator.py to import from services.orchestrator instead of the removed service_orchestrator shim - Fix case sensitivity in path ("Apps" -> "apps") Co-Authored-By: Claude Opus 4.5 <[email protected]> * fix: Fix test import paths for case sensitivity and removed shims - Fix path case sensitivity: "Apps" -> "apps" in 21 test files - Update test_validation_strategy.py to import from spec.validation_strategy Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
257 lines
8.7 KiB
Python
257 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tests for Parallel Merge Infrastructure
|
|
========================================
|
|
|
|
Tests data structures and async merge runner for parallel merging.
|
|
|
|
Covers:
|
|
- ParallelMergeTask data structure
|
|
- ParallelMergeResult data structure (success, auto-merge, failure)
|
|
- Parallel merge runner with empty and populated task lists
|
|
- Base content handling (optional for new files)
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# Add auto-claude directory to path for imports
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "apps" / "backend"))
|
|
|
|
from workspace import ParallelMergeTask, ParallelMergeResult
|
|
from core.workspace import _run_parallel_merges
|
|
|
|
|
|
class TestParallelMergeDataclasses:
|
|
"""Tests for parallel merge data structures."""
|
|
|
|
def test_parallel_merge_task_creation(self, tmp_path):
|
|
"""ParallelMergeTask can be created with required fields."""
|
|
task = ParallelMergeTask(
|
|
file_path="src/App.tsx",
|
|
main_content="const main = 1;",
|
|
worktree_content="const main = 2;",
|
|
base_content="const main = 0;",
|
|
spec_name="001-test",
|
|
project_dir=tmp_path,
|
|
)
|
|
|
|
assert task.file_path == "src/App.tsx"
|
|
assert task.main_content == "const main = 1;"
|
|
assert task.worktree_content == "const main = 2;"
|
|
assert task.base_content == "const main = 0;"
|
|
assert task.spec_name == "001-test"
|
|
assert task.project_dir == tmp_path
|
|
|
|
def test_parallel_merge_task_optional_base(self, tmp_path):
|
|
"""ParallelMergeTask works with None base_content."""
|
|
task = ParallelMergeTask(
|
|
file_path="src/new-file.tsx",
|
|
main_content="// main version",
|
|
worktree_content="// worktree version",
|
|
base_content=None, # New file, no common ancestor
|
|
spec_name="001-new-feature",
|
|
project_dir=tmp_path,
|
|
)
|
|
|
|
assert task.base_content is None
|
|
assert task.file_path == "src/new-file.tsx"
|
|
|
|
def test_parallel_merge_result_success(self):
|
|
"""ParallelMergeResult can represent successful merge."""
|
|
result = ParallelMergeResult(
|
|
file_path="src/App.tsx",
|
|
merged_content="const main = 'merged';",
|
|
success=True,
|
|
was_auto_merged=False,
|
|
)
|
|
|
|
assert result.success is True
|
|
assert result.merged_content == "const main = 'merged';"
|
|
assert result.was_auto_merged is False
|
|
assert result.error is None
|
|
|
|
def test_parallel_merge_result_auto_merged(self):
|
|
"""ParallelMergeResult can indicate auto-merge (no AI)."""
|
|
result = ParallelMergeResult(
|
|
file_path="src/utils.py",
|
|
merged_content="# Auto-merged content",
|
|
success=True,
|
|
was_auto_merged=True,
|
|
)
|
|
|
|
assert result.success is True
|
|
assert result.was_auto_merged is True
|
|
|
|
def test_parallel_merge_result_failure(self):
|
|
"""ParallelMergeResult can represent failed merge."""
|
|
result = ParallelMergeResult(
|
|
file_path="src/complex.ts",
|
|
merged_content=None,
|
|
success=False,
|
|
error="AI could not resolve conflict",
|
|
)
|
|
|
|
assert result.success is False
|
|
assert result.merged_content is None
|
|
assert result.error == "AI could not resolve conflict"
|
|
|
|
|
|
class TestParallelMergeRunner:
|
|
"""Tests for the parallel merge runner."""
|
|
|
|
def test_run_parallel_merges_empty_list(self, tmp_path):
|
|
"""Running with empty task list returns empty results."""
|
|
import asyncio
|
|
results = asyncio.run(_run_parallel_merges([], tmp_path))
|
|
assert results == []
|
|
|
|
def test_parallel_merge_task_with_data(self, tmp_path):
|
|
"""ParallelMergeTask holds merge data correctly."""
|
|
task = ParallelMergeTask(
|
|
file_path="src/test.py",
|
|
main_content="def main(): pass",
|
|
worktree_content="def main():\n print('hi')",
|
|
base_content="def main(): pass",
|
|
spec_name="001-feature",
|
|
project_dir=tmp_path,
|
|
)
|
|
|
|
assert "main" in task.main_content
|
|
assert "hi" in task.worktree_content
|
|
assert task.spec_name == "001-feature"
|
|
|
|
|
|
class TestSimple3WayMerge:
|
|
"""Tests for the simple 3-way merge logic."""
|
|
|
|
def test_identical_files_merge(self, tmp_path):
|
|
"""When both versions are identical, return that version."""
|
|
import asyncio
|
|
|
|
task = ParallelMergeTask(
|
|
file_path="src/test.py",
|
|
main_content="def main(): pass",
|
|
worktree_content="def main(): pass", # Same as main
|
|
base_content="def main(): pass", # Same as both
|
|
spec_name="001-no-change",
|
|
project_dir=tmp_path,
|
|
)
|
|
|
|
results = asyncio.run(_run_parallel_merges([task], tmp_path))
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].was_auto_merged is True
|
|
assert results[0].merged_content == "def main(): pass"
|
|
|
|
def test_only_worktree_changed(self, tmp_path):
|
|
"""When only worktree changed, take worktree version."""
|
|
import asyncio
|
|
|
|
task = ParallelMergeTask(
|
|
file_path="src/test.py",
|
|
main_content="def main(): pass", # Same as base
|
|
worktree_content="def main():\n print('new')", # Changed
|
|
base_content="def main(): pass",
|
|
spec_name="001-worktree-only",
|
|
project_dir=tmp_path,
|
|
)
|
|
|
|
results = asyncio.run(_run_parallel_merges([task], tmp_path))
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].was_auto_merged is True
|
|
assert "print('new')" in results[0].merged_content
|
|
|
|
def test_only_main_changed(self, tmp_path):
|
|
"""When only main changed, take main version."""
|
|
import asyncio
|
|
|
|
task = ParallelMergeTask(
|
|
file_path="src/test.py",
|
|
main_content="def main():\n print('main')", # Changed
|
|
worktree_content="def main(): pass", # Same as base
|
|
base_content="def main(): pass",
|
|
spec_name="001-main-only",
|
|
project_dir=tmp_path,
|
|
)
|
|
|
|
results = asyncio.run(_run_parallel_merges([task], tmp_path))
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].was_auto_merged is True
|
|
assert "print('main')" in results[0].merged_content
|
|
|
|
def test_no_base_but_identical(self, tmp_path):
|
|
"""When no base and both identical, return that version."""
|
|
import asyncio
|
|
|
|
task = ParallelMergeTask(
|
|
file_path="src/new.py",
|
|
main_content="# Same content",
|
|
worktree_content="# Same content",
|
|
base_content=None, # New file, no base
|
|
spec_name="001-new-identical",
|
|
project_dir=tmp_path,
|
|
)
|
|
|
|
results = asyncio.run(_run_parallel_merges([task], tmp_path))
|
|
assert len(results) == 1
|
|
assert results[0].success is True
|
|
assert results[0].was_auto_merged is True
|
|
|
|
|
|
class TestParallelMergeIntegration:
|
|
"""Integration tests for parallel merge flow."""
|
|
|
|
def test_multiple_file_merge_structure(self, tmp_path):
|
|
"""Multiple ParallelMergeTasks can be created."""
|
|
tasks = [
|
|
ParallelMergeTask(
|
|
file_path=f"src/file{i}.py",
|
|
main_content=f"# File {i} main",
|
|
worktree_content=f"# File {i} feature",
|
|
base_content=f"# File {i} base",
|
|
spec_name="001-multi",
|
|
project_dir=tmp_path,
|
|
)
|
|
for i in range(3)
|
|
]
|
|
|
|
assert len(tasks) == 3
|
|
assert tasks[0].file_path == "src/file0.py"
|
|
assert tasks[2].file_path == "src/file2.py"
|
|
|
|
def test_result_collection(self):
|
|
"""ParallelMergeResults can be collected."""
|
|
results = [
|
|
ParallelMergeResult(
|
|
file_path=f"file{i}.py",
|
|
merged_content=f"# Merged {i}",
|
|
success=True,
|
|
was_auto_merged=i % 2 == 0,
|
|
)
|
|
for i in range(5)
|
|
]
|
|
|
|
assert len(results) == 5
|
|
# Check auto-merge pattern
|
|
assert results[0].was_auto_merged is True
|
|
assert results[1].was_auto_merged is False
|
|
assert results[2].was_auto_merged is True
|
|
|
|
def test_error_result_handling(self):
|
|
"""Error results are properly structured."""
|
|
error_result = ParallelMergeResult(
|
|
file_path="problematic.py",
|
|
merged_content=None,
|
|
success=False,
|
|
error="Complex conflict requires manual review",
|
|
)
|
|
|
|
assert error_result.success is False
|
|
assert error_result.error is not None
|
|
assert "manual review" in error_result.error
|