Files
clide/legacy/tests/unit/test_services.py
T
jpmschweitzerandClaude Opus 4.7 a355751437 move python clide to legacy/
Clide is being rebuilt as a Flutter desktop app. The Python Textual
implementation moves wholesale into legacy/ rather than being deleted:
its pane model, panel set, git skills, and panel communication design
are real thought that should remain readable next to the new code
while the rebuild finds its shape. Git's rename tracking preserves
history, so `git log -- legacy/` still works.

The Flutter rebuild lives at the repo root alongside a Go sidecar
(the architecture claudian was heading toward, which folds into
clide as a core component rather than a separate plugin project).
Bootstrap of the new shape lands in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:30:51 +02:00

287 lines
9.8 KiB
Python

"""Tests for service classes."""
from pathlib import Path
from unittest.mock import patch
import pytest
from clide.models.todos import TodoType
from clide.services.file_service import FileService
from clide.services.git_service import GitService
from clide.services.linter_service import LinterService
from clide.services.process_service import CommandResult, ProcessService
from clide.services.todo_scanner import TodoScanner
class TestProcessService:
"""Tests for ProcessService."""
@pytest.fixture
def service(self, tmp_path: Path) -> ProcessService:
return ProcessService(cwd=tmp_path)
def test_run_sync_success(self, service: ProcessService):
result = service.run_sync("echo", "hello")
assert result.success is True
assert "hello" in result.stdout
def test_run_sync_failure(self, service: ProcessService):
result = service.run_sync("false") # Unix command that always fails
assert result.success is False
@pytest.mark.asyncio
async def test_run_async_success(self, service: ProcessService):
result = await service.run("echo", "world")
assert result.success is True
assert "world" in result.stdout
@pytest.mark.asyncio
async def test_run_async_with_timeout(self, service: ProcessService):
# This should complete quickly
result = await service.run("echo", "fast", timeout=5.0)
assert result.success is True
def test_command_result(self):
result = CommandResult(
returncode=0,
stdout="output",
stderr="",
)
assert result.success is True
assert result.stdout == "output"
def test_command_result_failure(self):
result = CommandResult(
returncode=1,
stdout="",
stderr="error",
)
assert result.success is False
class TestFileService:
"""Tests for FileService."""
@pytest.mark.asyncio
async def test_read_file(self, tmp_path: Path):
test_file = tmp_path / "test.txt"
test_file.write_text("Hello, World!")
service = FileService(tmp_path)
content = await service.read_file_async(test_file)
assert content == "Hello, World!"
@pytest.mark.asyncio
async def test_read_file_not_found(self, tmp_path: Path):
service = FileService(tmp_path)
with pytest.raises(FileNotFoundError):
await service.read_file_async(tmp_path / "nonexistent.txt")
@pytest.mark.asyncio
async def test_write_file(self, tmp_path: Path):
test_file = tmp_path / "output.txt"
service = FileService(tmp_path)
await service.write_file_async(test_file, "Test content")
assert test_file.read_text() == "Test content"
@pytest.mark.asyncio
async def test_get_language_python(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("test.py")) == "python"
@pytest.mark.asyncio
async def test_get_language_javascript(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("app.js")) == "javascript"
@pytest.mark.asyncio
async def test_get_language_typescript(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("component.tsx")) == "typescript"
@pytest.mark.asyncio
async def test_get_language_unknown(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("file.xyz")) is None
@pytest.mark.asyncio
async def test_get_language_markdown(self, tmp_path: Path):
service = FileService(tmp_path)
assert await service.get_language(Path("README.md")) == "markdown"
class TestGitService:
"""Tests for GitService."""
@pytest.fixture
def git_repo(self, tmp_path: Path) -> Path:
"""Create a minimal git repo for testing."""
git_dir = tmp_path / ".git"
git_dir.mkdir()
(git_dir / "HEAD").write_text("ref: refs/heads/main")
(git_dir / "config").write_text("")
return tmp_path
@pytest.fixture
def service(self, git_repo: Path) -> GitService:
return GitService(git_repo)
@pytest.mark.asyncio
async def test_get_status(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
async def mock_status(*args, **_kwargs):
if "status" in args:
return CommandResult(returncode=0, stdout="", stderr="")
elif "branch" in args:
return CommandResult(returncode=0, stdout="main\n", stderr="")
return CommandResult(returncode=0, stdout="0\t0", stderr="")
mock_run.side_effect = mock_status
status = await service.get_status()
assert status is not None
assert status.branch == "main"
@pytest.mark.asyncio
async def test_get_branches(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="*main|origin/main|abc1234|Test\n feature|origin/feature|def5678|Test2\n",
stderr="",
)
branches = await service.get_branches()
assert len(branches) == 2
assert branches[0].name == "main"
assert branches[0].is_current is True
@pytest.mark.asyncio
async def test_get_log(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="abc1234567890|abc1234|Test commit|Author|2024-01-01||HEAD -> main\n",
stderr="",
)
commits = await service.get_log(max_count=10)
assert len(commits) == 1
assert commits[0].message == "Test commit"
@pytest.mark.asyncio
async def test_stage_file(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="",
stderr="",
)
result = await service.stage_file("test.py")
assert result is True
@pytest.mark.asyncio
async def test_unstage_file(self, service: GitService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="",
stderr="",
)
result = await service.unstage_file("test.py")
assert result is True
class TestLinterService:
"""Tests for LinterService."""
@pytest.fixture
def service(self, tmp_path: Path) -> LinterService:
return LinterService(tmp_path)
@pytest.mark.asyncio
async def test_run_ruff_no_issues(self, service: LinterService):
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=0,
stdout="[]",
stderr="",
)
problems = await service.run_ruff()
assert problems == []
@pytest.mark.asyncio
async def test_run_ruff_with_issues(self, service: LinterService):
ruff_output = """[
{
"filename": "/test/file.py",
"location": {"row": 10, "column": 5},
"code": "E501",
"message": "Line too long"
}
]"""
with patch.object(service._process, "run") as mock_run:
mock_run.return_value = CommandResult(
returncode=1,
stdout=ruff_output,
stderr="",
)
problems = await service.run_ruff()
assert len(problems) == 1
assert problems[0].code == "E501"
class TestTodoScanner:
"""Tests for TodoScanner."""
@pytest.fixture
def scanner(self, tmp_path: Path) -> TodoScanner:
return TodoScanner(tmp_path)
@pytest.fixture
def project_with_todos(self, tmp_path: Path) -> Path:
"""Create a project with TODO comments."""
src = tmp_path / "src"
src.mkdir()
(src / "main.py").write_text("""
# TODO: Implement feature
def main():
pass # FIXME: Handle errors
# HACK: Temporary workaround
""")
return tmp_path
@pytest.mark.asyncio
async def test_scan_finds_todos(self, project_with_todos: Path):
scanner = TodoScanner(project_with_todos)
items, project_items, summary = await scanner.scan()
# Should find TODO, FIXME, and HACK
assert len(items) >= 1
assert summary.total >= 1
@pytest.mark.asyncio
async def test_scan_empty_project(self, tmp_path: Path):
scanner = TodoScanner(tmp_path)
items, project_items, summary = await scanner.scan()
assert items == []
assert summary.total == 0
def test_parse_ripgrep_output_todo(self, scanner: TodoScanner):
output = "test.py:10:# TODO: Fix this"
items = scanner._parse_ripgrep_output(output)
assert len(items) == 1
assert items[0].todo_type == TodoType.TODO
assert "Fix this" in items[0].text
def test_parse_ripgrep_output_fixme(self, scanner: TodoScanner):
output = "test.py:20:// FIXME: Broken code"
items = scanner._parse_ripgrep_output(output)
assert len(items) == 1
assert items[0].todo_type == TodoType.FIXME
def test_parse_ripgrep_output_hack(self, scanner: TodoScanner):
output = "test.py:30:/* HACK: Workaround */"
items = scanner._parse_ripgrep_output(output)
assert len(items) == 1
assert items[0].todo_type == TodoType.HACK