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,6 @@
|
||||
"""Test harnesses for Clide testing."""
|
||||
|
||||
from tests.harnesses.app_harness import AppHarness
|
||||
from tests.harnesses.controller_harness import ControllerHarness
|
||||
|
||||
__all__ = ["AppHarness", "ControllerHarness"]
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Full application test harness for Clide."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from textual.pilot import Pilot
|
||||
|
||||
from clide.app import ClideApp
|
||||
from clide.extensions.manager import ExtensionManager
|
||||
from clide.models.config import ClideSettings
|
||||
|
||||
|
||||
class AppHarness:
|
||||
"""Test harness for running the full Clide application.
|
||||
|
||||
Provides a controlled environment for integration testing with
|
||||
mocked services and isolated file systems.
|
||||
|
||||
Usage:
|
||||
harness = AppHarness(workdir=tmp_path)
|
||||
app, pilot = await harness.start()
|
||||
await pilot.press("ctrl+q")
|
||||
await harness.stop()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
workdir: Path,
|
||||
settings: Optional[ClideSettings] = None,
|
||||
extension_manager: Optional[ExtensionManager] = None,
|
||||
) -> None:
|
||||
self.workdir = workdir
|
||||
self.settings = settings or ClideSettings()
|
||||
self.extension_manager = extension_manager or ExtensionManager()
|
||||
self._app: Optional[ClideApp] = None
|
||||
self._pilot: Optional[Pilot] = None
|
||||
|
||||
async def start(self) -> tuple[ClideApp, Pilot]:
|
||||
"""Start the application and return app and pilot for testing.
|
||||
|
||||
Returns:
|
||||
Tuple of (ClideApp instance, Pilot for simulating input)
|
||||
"""
|
||||
self._app = ClideApp(workdir=self.workdir)
|
||||
# Inject test dependencies
|
||||
self._app.extension_manager = self.extension_manager
|
||||
|
||||
# Start app in test mode
|
||||
async with self._app.run_test() as pilot:
|
||||
self._pilot = pilot
|
||||
return self._app, pilot
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Clean shutdown of the application."""
|
||||
if self._app:
|
||||
await self._app.action_quit()
|
||||
self._app = None
|
||||
self._pilot = None
|
||||
|
||||
@property
|
||||
def app(self) -> ClideApp:
|
||||
"""Get the running app instance."""
|
||||
if self._app is None:
|
||||
raise RuntimeError("App not started. Call start() first.")
|
||||
return self._app
|
||||
|
||||
@property
|
||||
def pilot(self) -> Pilot:
|
||||
"""Get the pilot for simulating user input."""
|
||||
if self._pilot is None:
|
||||
raise RuntimeError("App not started. Call start() first.")
|
||||
return self._pilot
|
||||
|
||||
async def press_keys(self, *keys: str) -> None:
|
||||
"""Simulate pressing a sequence of keys."""
|
||||
await self.pilot.press(*keys)
|
||||
|
||||
async def click(self, selector: str) -> None:
|
||||
"""Click on a widget by CSS selector."""
|
||||
await self.pilot.click(selector)
|
||||
|
||||
async def wait_for_animation(self) -> None:
|
||||
"""Wait for any running animations to complete."""
|
||||
await self.pilot.pause()
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Controller isolation test harness for Clide."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from textual.message import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from clide.controllers.base import BaseController
|
||||
|
||||
|
||||
class MockApp:
|
||||
"""Minimal mock of a Textual App for controller testing."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.messages: list[Message] = []
|
||||
self.post_message = MagicMock(side_effect=self._capture_message)
|
||||
|
||||
def _capture_message(self, message: Message) -> None:
|
||||
self.messages.append(message)
|
||||
|
||||
def get_messages(self, message_type: Optional[type] = None) -> list[Message]:
|
||||
"""Get captured messages, optionally filtered by type."""
|
||||
if message_type is None:
|
||||
return self.messages.copy()
|
||||
return [m for m in self.messages if isinstance(m, message_type)]
|
||||
|
||||
def clear_messages(self) -> None:
|
||||
"""Clear captured messages."""
|
||||
self.messages.clear()
|
||||
|
||||
|
||||
class ControllerHarness:
|
||||
"""Test harness for isolated controller testing.
|
||||
|
||||
Provides a mock app environment for testing controllers without
|
||||
the full Textual application overhead.
|
||||
|
||||
Usage:
|
||||
harness = ControllerHarness()
|
||||
controller = GitController(harness.mock_app)
|
||||
await controller.initialize()
|
||||
await controller.refresh_status()
|
||||
messages = harness.get_messages()
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._mock_app = MockApp()
|
||||
self._controllers: list["BaseController"] = []
|
||||
self._mocks: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def mock_app(self) -> MockApp:
|
||||
"""Get the mock app for controller injection."""
|
||||
return self._mock_app
|
||||
|
||||
def register_controller(self, controller: "BaseController") -> None:
|
||||
"""Register a controller for lifecycle management."""
|
||||
self._controllers.append(controller)
|
||||
|
||||
async def initialize_all(self) -> None:
|
||||
"""Initialize all registered controllers."""
|
||||
for controller in self._controllers:
|
||||
await controller.initialize()
|
||||
|
||||
async def shutdown_all(self) -> None:
|
||||
"""Shutdown all registered controllers."""
|
||||
for controller in self._controllers:
|
||||
await controller.shutdown()
|
||||
|
||||
def get_messages(self, message_type: Optional[type] = None) -> list[Message]:
|
||||
"""Get messages posted to the mock app."""
|
||||
return self._mock_app.get_messages(message_type)
|
||||
|
||||
def clear_messages(self) -> None:
|
||||
"""Clear all captured messages."""
|
||||
self._mock_app.clear_messages()
|
||||
|
||||
def add_mock(self, name: str, mock: Any) -> None:
|
||||
"""Add a named mock for dependency injection.
|
||||
|
||||
Args:
|
||||
name: Identifier for the mock
|
||||
mock: Mock object or AsyncMock
|
||||
"""
|
||||
self._mocks[name] = mock
|
||||
|
||||
def get_mock(self, name: str) -> Any:
|
||||
"""Retrieve a named mock."""
|
||||
return self._mocks.get(name)
|
||||
|
||||
def create_async_mock(self, return_value: Any = None) -> AsyncMock:
|
||||
"""Create an AsyncMock with optional return value."""
|
||||
mock = AsyncMock()
|
||||
if return_value is not None:
|
||||
mock.return_value = return_value
|
||||
return mock
|
||||
Reference in New Issue
Block a user