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>
438 lines
13 KiB
Python
438 lines
13 KiB
Python
"""Tests for Pydantic models."""
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from clide.models.config import ClideSettings, KeybindingsConfig, PanelConfig
|
|
from clide.models.diff import ChangeType, DiffContent, DiffHunk, DiffLine
|
|
from clide.models.editor import CursorPosition, EditorState, FileBuffer
|
|
from clide.models.git import ChangeStatus, GitBranch, GitChange, GitCommit, GitStatus
|
|
from clide.models.problems import Problem, ProblemsSummary, Severity
|
|
from clide.models.theme import ThemeColors, ThemeDefinition, ThemeMetadata
|
|
from clide.models.todos import TodoItem, TodosSummary, TodoType
|
|
|
|
|
|
class TestCursorPosition:
|
|
"""Tests for CursorPosition model."""
|
|
|
|
def test_create_cursor(self):
|
|
cursor = CursorPosition(line=10, column=5)
|
|
assert cursor.line == 10
|
|
assert cursor.column == 5
|
|
|
|
def test_cursor_is_frozen(self):
|
|
cursor = CursorPosition(line=0, column=0)
|
|
with pytest.raises(ValidationError):
|
|
cursor.line = 1
|
|
|
|
|
|
class TestFileBuffer:
|
|
"""Tests for FileBuffer model."""
|
|
|
|
def test_create_buffer(self):
|
|
buffer = FileBuffer(
|
|
path=Path("/test/file.py"),
|
|
content="print('hello')",
|
|
language="python",
|
|
)
|
|
assert buffer.path == Path("/test/file.py")
|
|
assert buffer.content == "print('hello')"
|
|
assert buffer.language == "python"
|
|
assert buffer.is_modified is False
|
|
|
|
def test_buffer_display_name(self):
|
|
buffer = FileBuffer(path=Path("/test/file.py"), content="")
|
|
assert buffer.display_name == "file.py"
|
|
|
|
def test_buffer_modified_display_name(self):
|
|
buffer = FileBuffer(path=Path("/test/file.py"), content="", is_modified=True)
|
|
assert buffer.display_name == "● file.py"
|
|
|
|
def test_buffer_filename(self):
|
|
buffer = FileBuffer(path=Path("/some/deep/path/script.js"), content="")
|
|
assert buffer.filename == "script.js"
|
|
|
|
|
|
class TestEditorState:
|
|
"""Tests for EditorState model."""
|
|
|
|
def test_empty_state(self):
|
|
state = EditorState()
|
|
assert state.buffers == []
|
|
assert state.active_buffer_index is None
|
|
assert state.active_buffer is None
|
|
|
|
def test_active_buffer(self):
|
|
buffer1 = FileBuffer(path=Path("/a.py"), content="a")
|
|
buffer2 = FileBuffer(path=Path("/b.py"), content="b")
|
|
state = EditorState(buffers=[buffer1, buffer2], active_buffer_index=1)
|
|
assert state.active_buffer == buffer2
|
|
|
|
def test_get_buffer_by_path(self):
|
|
buffer = FileBuffer(path=Path("/test.py"), content="test")
|
|
state = EditorState(buffers=[buffer])
|
|
assert state.get_buffer_by_path(Path("/test.py")) == buffer
|
|
assert state.get_buffer_by_path(Path("/other.py")) is None
|
|
|
|
|
|
class TestGitChange:
|
|
"""Tests for GitChange model."""
|
|
|
|
def test_create_change(self):
|
|
from clide.models.git import ChangeStatus
|
|
change = GitChange(path="src/main.py", status=ChangeStatus.MODIFIED, staged=True)
|
|
assert change.path == "src/main.py"
|
|
assert change.status == ChangeStatus.MODIFIED
|
|
assert change.staged is True
|
|
|
|
def test_change_is_frozen(self):
|
|
from clide.models.git import ChangeStatus
|
|
change = GitChange(path="f", status=ChangeStatus.ADDED, staged=False)
|
|
with pytest.raises(ValidationError):
|
|
change.path = "new.py"
|
|
|
|
|
|
class TestGitStatus:
|
|
"""Tests for GitStatus model."""
|
|
|
|
def test_create_status(self):
|
|
staged = (GitChange(path="a.py", status=ChangeStatus.ADDED, staged=True),)
|
|
unstaged = (GitChange(path="b.py", status=ChangeStatus.MODIFIED, staged=False),)
|
|
status = GitStatus(branch="main", staged=staged, unstaged=unstaged)
|
|
assert status.branch == "main"
|
|
assert len(status.staged) == 1
|
|
assert len(status.unstaged) == 1
|
|
|
|
def test_empty_status(self):
|
|
status = GitStatus(branch="main", staged=(), unstaged=())
|
|
assert status.branch == "main"
|
|
assert len(status.staged) == 0
|
|
assert len(status.unstaged) == 0
|
|
|
|
|
|
class TestGitBranch:
|
|
"""Tests for GitBranch model."""
|
|
|
|
def test_create_branch(self):
|
|
branch = GitBranch(name="feature/test", is_current=True, is_remote=False)
|
|
assert branch.name == "feature/test"
|
|
assert branch.is_current is True
|
|
assert branch.is_remote is False
|
|
|
|
|
|
class TestGitCommit:
|
|
"""Tests for GitCommit model."""
|
|
|
|
def test_create_commit(self):
|
|
commit = GitCommit(
|
|
hash="abc123def456789",
|
|
short_hash="abc123",
|
|
message="Test commit",
|
|
author="Test Author",
|
|
date="2024-01-01",
|
|
)
|
|
assert commit.hash == "abc123def456789"
|
|
assert commit.short_hash == "abc123"
|
|
assert commit.message == "Test commit"
|
|
|
|
|
|
class TestDiffLine:
|
|
"""Tests for DiffLine model."""
|
|
|
|
def test_added_line(self):
|
|
line = DiffLine(change_type=ChangeType.ADDED, content="new line", new_line_num=10)
|
|
assert line.change_type == ChangeType.ADDED
|
|
assert line.content == "new line"
|
|
|
|
def test_removed_line(self):
|
|
line = DiffLine(change_type=ChangeType.REMOVED, content="old line", old_line_num=5)
|
|
assert line.change_type == ChangeType.REMOVED
|
|
|
|
def test_context_line(self):
|
|
line = DiffLine(
|
|
change_type=ChangeType.CONTEXT,
|
|
content="unchanged",
|
|
old_line_num=5,
|
|
new_line_num=5,
|
|
)
|
|
assert line.change_type == ChangeType.CONTEXT
|
|
|
|
|
|
class TestDiffHunk:
|
|
"""Tests for DiffHunk model."""
|
|
|
|
def test_create_hunk(self):
|
|
lines = (
|
|
DiffLine(change_type=ChangeType.REMOVED, content="old", old_line_num=1),
|
|
DiffLine(change_type=ChangeType.ADDED, content="new", new_line_num=1),
|
|
)
|
|
hunk = DiffHunk(
|
|
header="@@ -1,1 +1,1 @@",
|
|
old_start=1,
|
|
old_count=1,
|
|
new_start=1,
|
|
new_count=1,
|
|
lines=lines,
|
|
)
|
|
assert len(hunk.lines) == 2
|
|
|
|
|
|
class TestDiffContent:
|
|
"""Tests for DiffContent model."""
|
|
|
|
def test_create_diff(self):
|
|
hunk = DiffHunk(
|
|
header="@@ -1 +1 @@",
|
|
old_start=1,
|
|
old_count=1,
|
|
new_start=1,
|
|
new_count=1,
|
|
lines=(),
|
|
)
|
|
diff = DiffContent(file_path="test.py", hunks=(hunk,))
|
|
assert diff.file_path == "test.py"
|
|
assert len(diff.hunks) == 1
|
|
|
|
|
|
class TestProblem:
|
|
"""Tests for Problem model."""
|
|
|
|
def test_create_problem(self):
|
|
problem = Problem(
|
|
file_path=Path("/test.py"),
|
|
line=10,
|
|
column=5,
|
|
severity=Severity.ERROR,
|
|
message="Syntax error",
|
|
source="ruff",
|
|
code="E999",
|
|
)
|
|
assert problem.line == 10
|
|
assert problem.severity == Severity.ERROR
|
|
|
|
def test_severity_icon(self):
|
|
error = Problem(
|
|
file_path=Path("/t.py"),
|
|
line=1,
|
|
column=1,
|
|
severity=Severity.ERROR,
|
|
message="err",
|
|
source="test",
|
|
)
|
|
assert error.severity_icon == "✖"
|
|
|
|
warning = Problem(
|
|
file_path=Path("/t.py"),
|
|
line=1,
|
|
column=1,
|
|
severity=Severity.WARNING,
|
|
message="warn",
|
|
source="test",
|
|
)
|
|
assert warning.severity_icon == "⚠"
|
|
|
|
|
|
class TestProblemsSummary:
|
|
"""Tests for ProblemsSummary model."""
|
|
|
|
def test_create_summary(self):
|
|
summary = ProblemsSummary(errors=5, warnings=3, infos=1, hints=0)
|
|
assert summary.total == 9
|
|
|
|
|
|
class TestTodoItem:
|
|
"""Tests for TodoItem model."""
|
|
|
|
def test_create_todo(self):
|
|
todo = TodoItem(
|
|
file_path=Path("/src/main.py"),
|
|
line=42,
|
|
column=5,
|
|
todo_type=TodoType.TODO,
|
|
text="Implement this feature",
|
|
context_line="# TODO: Implement this feature",
|
|
)
|
|
assert todo.line == 42
|
|
assert todo.todo_type == TodoType.TODO
|
|
|
|
def test_type_icon(self):
|
|
todo = TodoItem(
|
|
file_path=Path("/t.py"),
|
|
line=1,
|
|
column=1,
|
|
todo_type=TodoType.TODO,
|
|
text="todo",
|
|
context_line="# TODO: todo",
|
|
)
|
|
assert todo.type_icon == "☐"
|
|
|
|
fixme = TodoItem(
|
|
file_path=Path("/t.py"),
|
|
line=1,
|
|
column=1,
|
|
todo_type=TodoType.FIXME,
|
|
text="fixme",
|
|
context_line="# FIXME: fixme",
|
|
)
|
|
assert fixme.type_icon == "🔧"
|
|
|
|
|
|
class TestTodosSummary:
|
|
"""Tests for TodosSummary model."""
|
|
|
|
def test_create_summary(self):
|
|
summary = TodosSummary(
|
|
todo_count=5,
|
|
fixme_count=3,
|
|
hack_count=2,
|
|
other_count=0,
|
|
)
|
|
assert summary.total == 10
|
|
assert summary.todo_count == 5
|
|
|
|
|
|
class TestThemeColors:
|
|
"""Tests for ThemeColors model."""
|
|
|
|
def test_valid_colors(self):
|
|
colors = ThemeColors(
|
|
primary="#00a3d2",
|
|
secondary="#00a9b9",
|
|
accent="#fa5f8b",
|
|
background="#21262f",
|
|
surface="#393e48",
|
|
panel="#292e38",
|
|
foreground="#e2e8f5",
|
|
success="#00ab9a",
|
|
warning="#d08447",
|
|
error="#f06c6f",
|
|
)
|
|
assert colors.primary == "#00a3d2"
|
|
|
|
def test_invalid_color_rejected(self):
|
|
with pytest.raises(ValidationError):
|
|
ThemeColors(
|
|
primary="invalid",
|
|
secondary="#00a9b9",
|
|
accent="#fa5f8b",
|
|
background="#21262f",
|
|
surface="#393e48",
|
|
panel="#292e38",
|
|
foreground="#e2e8f5",
|
|
success="#00ab9a",
|
|
warning="#d08447",
|
|
error="#f06c6f",
|
|
)
|
|
|
|
def test_color_normalized_to_lowercase(self):
|
|
colors = ThemeColors(
|
|
primary="#00A3D2",
|
|
secondary="#00A9B9",
|
|
accent="#FA5F8B",
|
|
background="#21262F",
|
|
surface="#393E48",
|
|
panel="#292E38",
|
|
foreground="#E2E8F5",
|
|
success="#00AB9A",
|
|
warning="#D08447",
|
|
error="#F06C6F",
|
|
)
|
|
assert colors.primary == "#00a3d2"
|
|
|
|
|
|
class TestThemeDefinition:
|
|
"""Tests for ThemeDefinition model."""
|
|
|
|
def test_create_theme(self):
|
|
colors = ThemeColors(
|
|
primary="#00a3d2",
|
|
secondary="#00a9b9",
|
|
accent="#fa5f8b",
|
|
background="#21262f",
|
|
surface="#393e48",
|
|
panel="#292e38",
|
|
foreground="#e2e8f5",
|
|
success="#00ab9a",
|
|
warning="#d08447",
|
|
error="#f06c6f",
|
|
)
|
|
theme = ThemeDefinition(
|
|
name="test-theme",
|
|
display_name="Test Theme",
|
|
dark=True,
|
|
colors=colors,
|
|
)
|
|
assert theme.name == "test-theme"
|
|
assert theme.dark is True
|
|
|
|
def test_to_textual_theme(self):
|
|
colors = ThemeColors(
|
|
primary="#00a3d2",
|
|
secondary="#00a9b9",
|
|
accent="#fa5f8b",
|
|
background="#21262f",
|
|
surface="#393e48",
|
|
panel="#292e38",
|
|
foreground="#e2e8f5",
|
|
success="#00ab9a",
|
|
warning="#d08447",
|
|
error="#f06c6f",
|
|
)
|
|
theme_def = ThemeDefinition(
|
|
name="test",
|
|
display_name="Test",
|
|
dark=True,
|
|
colors=colors,
|
|
)
|
|
textual_theme = theme_def.to_textual_theme()
|
|
assert textual_theme.name == "test"
|
|
|
|
|
|
class TestThemeMetadata:
|
|
"""Tests for ThemeMetadata model."""
|
|
|
|
def test_create_metadata(self):
|
|
meta = ThemeMetadata(
|
|
name="summer-night",
|
|
display_name="Summer Night",
|
|
dark=True,
|
|
category="core",
|
|
)
|
|
assert meta.name == "summer-night"
|
|
assert meta.category == "core"
|
|
|
|
|
|
class TestClideSettings:
|
|
"""Tests for ClideSettings model."""
|
|
|
|
def test_default_settings(self):
|
|
settings = ClideSettings()
|
|
assert settings.theme == "summer-night"
|
|
assert settings.jira_enabled is False
|
|
|
|
def test_custom_settings(self):
|
|
settings = ClideSettings(theme="dracula", jira_enabled=True)
|
|
assert settings.theme == "dracula"
|
|
assert settings.jira_enabled is True
|
|
|
|
|
|
class TestPanelConfig:
|
|
"""Tests for PanelConfig model."""
|
|
|
|
def test_default_panel_config(self):
|
|
config = PanelConfig()
|
|
assert config.sidebar_visible is True
|
|
assert config.context_visible is True
|
|
assert config.workspace_visible is False
|
|
|
|
|
|
class TestKeybindingsConfig:
|
|
"""Tests for KeybindingsConfig model."""
|
|
|
|
def test_default_keybindings(self):
|
|
config = KeybindingsConfig()
|
|
assert config.toggle_sidebar == "ctrl+b"
|
|
assert config.toggle_terminal == "ctrl+`"
|