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>
98 lines
3.1 KiB
Python
98 lines
3.1 KiB
Python
"""Controller isolation test harness for Clide."""
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
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: type | None = 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: type | None = 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
|