move python clide to legacy/
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>
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,83 @@
|
||||
"""Full application test harness for Clide."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
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: ClideSettings | None = None,
|
||||
extension_manager: ExtensionManager | None = None,
|
||||
) -> None:
|
||||
self.workdir = workdir
|
||||
self.settings = settings or ClideSettings()
|
||||
self.extension_manager = extension_manager or ExtensionManager()
|
||||
self._app: ClideApp | None = None
|
||||
self._pilot: Pilot | None = 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
|
||||
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
|
||||
Reference in New Issue
Block a user