- Move src/clide/ to clide/ (flatten directory structure) - Add SettingsService for persistent user preferences - Settings stored in ~/.clide/settings.json - Persist theme, panel visibility, and compact mode state - Fix terminal rendering glitch with throttled refresh (~60fps) - Update pyproject.toml, Makefile, and tests for new structure Co-Authored-By: Claude Opus 4.5 <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+`"
|