Add Clide project structure and initial implementation
Set up the TUI IDE wrapper for Claude Code CLI with: - Core app structure using Textual framework - Panel architecture (sidebar, workspace, claude, context) - Theme system with 22 built-in themes (Summer Night default) - Pydantic models for configuration and data - Makefile for development commands - Project documentation and specs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
"""Tests for service classes."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from clide.models.git import GitBranch, GitChange, GitCommit, GitStatus
|
||||
from clide.models.problems import Problem, Severity
|
||||
from clide.models.todos import TodoItem, 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(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(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(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")) == "tsx"
|
||||
|
||||
@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, 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, 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
|
||||
Reference in New Issue
Block a user