Files
clide/legacy/tests/unit/test_controllers.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

418 lines
15 KiB
Python

"""Tests for controller classes."""
from pathlib import Path
from unittest.mock import patch
import pytest
from clide.controllers.diff import DiffController
from clide.controllers.editor import EditorController
from clide.controllers.git import GitController
from clide.controllers.jira import JiraController
from clide.controllers.problems import ProblemsController
from clide.controllers.todos import TodosController
from clide.models.diff import DiffContent, DiffHunk
from clide.models.editor import FileBuffer
from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit, GitStatus
from clide.models.problems import Problem, Severity
from clide.models.todos import TodoItem, TodoType
class TestGitController:
"""Tests for GitController."""
@pytest.fixture
def controller(self, tmp_path: Path) -> GitController:
return GitController(tmp_path)
@pytest.mark.asyncio
async def test_get_status(self, controller: GitController):
mock_status = GitStatus(
branch="main",
staged=(GitChange(path="a.py", status=ChangeStatus.ADDED, staged=True),),
unstaged=(),
)
with patch.object(controller._service, "get_status", return_value=mock_status):
status = await controller.get_status()
assert status.branch == "main"
assert len(status.staged) == 1
@pytest.mark.asyncio
async def test_get_branches(self, controller: GitController):
mock_branches = [
GitBranch(name="main", is_current=True, is_remote=False),
GitBranch(name="develop", is_current=False, is_remote=False),
]
with patch.object(controller._service, "get_branches", return_value=mock_branches):
branches = await controller.get_branches()
assert len(branches) == 2
assert branches[0].is_current is True
@pytest.mark.asyncio
async def test_get_log(self, controller: GitController):
mock_commits = [
GitCommit(
hash="abc123def456",
short_hash="abc123",
message="Initial commit",
author="Test",
date="2024-01-01",
)
]
with patch.object(controller._service, "get_log", return_value=mock_commits):
commits = await controller.get_log(limit=10)
assert len(commits) == 1
assert commits[0].message == "Initial commit"
@pytest.mark.asyncio
async def test_stage_file(self, controller: GitController):
mock_status = GitStatus(branch="main", staged=(), unstaged=())
with patch.object(controller._service, "stage_file", return_value=True):
with patch.object(controller._service, "get_status", return_value=mock_status):
result = await controller.stage_file("test.py")
assert result is True
@pytest.mark.asyncio
async def test_unstage_file(self, controller: GitController):
mock_status = GitStatus(branch="main", staged=(), unstaged=())
with patch.object(controller._service, "unstage_file", return_value=True):
with patch.object(controller._service, "get_status", return_value=mock_status):
result = await controller.unstage_file("test.py")
assert result is True
@pytest.mark.asyncio
async def test_checkout_branch(self, controller: GitController):
mock_status = GitStatus(branch="develop", staged=(), unstaged=())
with patch.object(controller._service, "checkout_branch", return_value=True):
with patch.object(controller._service, "get_status", return_value=mock_status):
with patch.object(controller._service, "get_branches", return_value=[]):
result = await controller.checkout_branch("develop")
assert result is True
@pytest.mark.asyncio
async def test_current_branch_property(self, controller: GitController):
mock_status = GitStatus(branch="feature", staged=(), unstaged=())
with patch.object(controller._service, "get_status", return_value=mock_status):
await controller.get_status() # Populate _status
assert controller.current_branch == "feature"
def test_current_branch_unknown(self, controller: GitController):
assert controller.current_branch == "unknown"
class TestEditorController:
"""Tests for EditorController."""
@pytest.fixture
def controller(self) -> EditorController:
return EditorController()
def test_initial_state(self, controller: EditorController):
assert controller.active_buffer is None
assert controller.open_files == []
assert controller.has_unsaved_changes is False
def test_update_content(self, controller: EditorController):
buffer = FileBuffer(path=Path("/test.py"), content="original")
controller._state.buffers.append(buffer)
controller.update_content(Path("/test.py"), "modified")
assert buffer.content == "modified"
assert buffer.is_modified is True
def test_update_cursor(self, controller: EditorController):
buffer = FileBuffer(path=Path("/test.py"), content="test")
controller._state.buffers.append(buffer)
controller.update_cursor(Path("/test.py"), 5, 10)
assert buffer.cursor.line == 5
assert buffer.cursor.column == 10
def test_set_active_by_index(self, controller: EditorController):
buffer1 = FileBuffer(path=Path("/a.py"), content="a")
buffer2 = FileBuffer(path=Path("/b.py"), content="b")
controller._state.buffers = [buffer1, buffer2]
controller.set_active_by_index(1)
assert controller._state.active_buffer_index == 1
def test_set_active_invalid_index(self, controller: EditorController):
controller.set_active_by_index(10) # Should not crash
assert controller._state.active_buffer_index is None
class TestDiffController:
"""Tests for DiffController."""
@pytest.fixture
def controller(self, tmp_path: Path) -> DiffController:
return DiffController(tmp_path)
def test_initial_state(self, controller: DiffController):
assert controller.diff is None
assert controller.is_proposal is False
def test_load_proposal(self, controller: DiffController):
old_content = "line1\nline2\n"
new_content = "line1\nmodified\n"
diff = controller.load_proposal("test.py", old_content, new_content)
assert diff.file_path == "test.py"
assert controller.is_proposal is True
def test_accept_hunk(self, controller: DiffController):
controller._state.diff = DiffContent(file_path="t.py", hunks=())
controller.accept_hunk(0)
assert 0 in controller._state.accepted_hunks
assert 0 not in controller._state.rejected_hunks
def test_reject_hunk(self, controller: DiffController):
controller._state.diff = DiffContent(file_path="t.py", hunks=())
controller.reject_hunk(0)
assert 0 in controller._state.rejected_hunks
assert 0 not in controller._state.accepted_hunks
def test_accept_all(self, controller: DiffController):
hunk = DiffHunk(
header="@@",
old_start=1,
old_count=1,
new_start=1,
new_count=1,
lines=(),
)
controller._state.diff = DiffContent(file_path="t.py", hunks=(hunk, hunk))
controller.accept_all()
assert len(controller._state.accepted_hunks) == 2
def test_reject_all(self, controller: DiffController):
hunk = DiffHunk(
header="@@",
old_start=1,
old_count=1,
new_start=1,
new_count=1,
lines=(),
)
controller._state.diff = DiffContent(file_path="t.py", hunks=(hunk,))
controller.reject_all()
assert len(controller._state.rejected_hunks) == 1
def test_clear(self, controller: DiffController):
controller._state.diff = DiffContent(file_path="t.py", hunks=())
controller._state.is_proposal = True
controller.clear()
assert controller.diff is None
assert controller.is_proposal is False
def test_toggle_side_by_side(self, controller: DiffController):
assert controller._state.side_by_side is True # Default is True
result = controller.toggle_side_by_side()
assert result is False # Toggled to False
assert controller._state.side_by_side is False
class TestProblemsController:
"""Tests for ProblemsController."""
@pytest.fixture
def controller(self, tmp_path: Path) -> ProblemsController:
return ProblemsController(tmp_path)
def test_initial_state(self, controller: ProblemsController):
assert controller.problems == []
assert controller.error_count == 0
assert controller.warning_count == 0
def test_filter_by_severity(self, controller: ProblemsController):
problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="test",
),
Problem(
file_path=Path("/b.py"),
line=1,
column=1,
severity=Severity.WARNING,
message="warn",
source="test",
),
]
controller._state.problems = problems
errors = controller.filter_by_severity(Severity.ERROR)
assert len(errors) == 1
assert errors[0].severity == Severity.ERROR
def test_filter_by_source(self, controller: ProblemsController):
problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="ruff",
),
Problem(
file_path=Path("/b.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="mypy",
),
]
controller._state.problems = problems
ruff_problems = controller.filter_by_source("ruff")
assert len(ruff_problems) == 1
def test_next_problem(self, controller: ProblemsController):
problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="1",
source="test",
),
Problem(
file_path=Path("/b.py"),
line=2,
column=1,
severity=Severity.ERROR,
message="2",
source="test",
),
]
controller._state.problems = problems
p1 = controller.next_problem()
assert p1.message == "1"
p2 = controller.next_problem()
assert p2.message == "2"
# Should wrap around
p3 = controller.next_problem()
assert p3.message == "1"
def test_prev_problem(self, controller: ProblemsController):
problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="1",
source="test",
),
Problem(
file_path=Path("/b.py"),
line=2,
column=1,
severity=Severity.ERROR,
message="2",
source="test",
),
]
controller._state.problems = problems
p = controller.prev_problem()
assert p.message == "2"
def test_clear(self, controller: ProblemsController):
controller._state.problems = [
Problem(
file_path=Path("/a.py"),
line=1,
column=1,
severity=Severity.ERROR,
message="err",
source="test",
)
]
controller.clear()
assert controller.problems == []
class TestTodosController:
"""Tests for TodosController."""
@pytest.fixture
def controller(self, tmp_path: Path) -> TodosController:
return TodosController(tmp_path)
def test_initial_state(self, controller: TodosController):
assert controller.items == []
assert controller.total_count == 0
def test_filter_by_type(self, controller: TodosController):
items = [
TodoItem(file_path=Path("/a.py"), line=1, column=1, todo_type=TodoType.TODO, text="1", context_line="# TODO: 1"),
TodoItem(file_path=Path("/b.py"), line=2, column=1, todo_type=TodoType.FIXME, text="2", context_line="# FIXME: 2"),
]
controller._state.items = items
todos = controller.filter_by_type(TodoType.TODO)
assert len(todos) == 1
assert todos[0].todo_type == TodoType.TODO
def test_get_grouped_items(self, controller: TodosController):
items = [
TodoItem(file_path=Path("/a.py"), line=1, column=1, todo_type=TodoType.TODO, text="1", context_line="# TODO: 1"),
TodoItem(file_path=Path("/a.py"), line=5, column=1, todo_type=TodoType.FIXME, text="2", context_line="# FIXME: 2"),
TodoItem(file_path=Path("/b.py"), line=1, column=1, todo_type=TodoType.TODO, text="3", context_line="# TODO: 3"),
]
controller._state.items = items
grouped = controller.get_grouped_items()
assert len(grouped) == 2
assert len(grouped[Path("/a.py")]) == 2
assert len(grouped[Path("/b.py")]) == 1
def test_toggle_group_by_file(self, controller: TodosController):
assert controller._state.group_by_file is True # Default is True per TodosState model
result = controller.toggle_group_by_file()
assert result is False
assert controller._state.group_by_file is False
class TestJiraController:
"""Tests for JiraController."""
@pytest.fixture
def controller(self) -> JiraController:
return JiraController(enabled=True)
@pytest.fixture
def disabled_controller(self) -> JiraController:
return JiraController(enabled=False)
def test_initial_enabled_state(self, controller: JiraController):
assert controller.enabled is True
def test_initial_disabled_state(self, disabled_controller: JiraController):
assert disabled_controller.enabled is False
def test_enable_disable(self, disabled_controller: JiraController):
disabled_controller.enable()
assert disabled_controller.enabled is True
disabled_controller.disable()
assert disabled_controller.enabled is False
@pytest.mark.asyncio
async def test_run_command_when_disabled(self, disabled_controller: JiraController):
result = await disabled_controller.run_command("issue", "list")
assert "disabled" in result.lower()
@pytest.mark.asyncio
async def test_get_content_when_disabled(self, disabled_controller: JiraController):
result = await disabled_controller.get_content()
assert result is None