Files
clide/tests/unit/test_models.py
T
Jeroen SchweitzerandClaude Opus 4.5 1ed21d2a0f Restructure project and add user settings persistence
- 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>
2026-02-01 00:42:46 +01:00

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+`"