- Create engine/interfaces/ module with centralized re-exports of all ABCs/Protocols - Remove duplicate Display protocol from websocket.py - Remove unnecessary pass statements in exception classes - Skip flaky websocket test that fails in CI due to port binding
222 lines
5.6 KiB
Markdown
222 lines
5.6 KiB
Markdown
# Agent Development Guide
|
|
|
|
## Development Environment
|
|
|
|
This project uses:
|
|
- **mise** (mise.jdx.dev) - tool version manager and task runner
|
|
- **uv** - fast Python package installer
|
|
- **ruff** - linter and formatter (line-length 88, target Python 3.10)
|
|
- **pytest** - test runner with strict marker enforcement
|
|
|
|
### Setup
|
|
|
|
```bash
|
|
mise run install # Install dependencies
|
|
# Or: uv sync --all-extras # includes mic, websocket, sixel support
|
|
```
|
|
|
|
### Available Commands
|
|
|
|
```bash
|
|
# Testing
|
|
mise run test # Run all tests
|
|
mise run test-cov # Run tests with coverage report
|
|
pytest tests/test_foo.py::TestClass::test_method # Run single test
|
|
|
|
# Linting & Formatting
|
|
mise run lint # Run ruff linter
|
|
mise run lint-fix # Run ruff with auto-fix
|
|
mise run format # Run ruff formatter
|
|
|
|
# CI
|
|
mise run ci # Full CI pipeline (topics-init + lint + test-cov)
|
|
```
|
|
|
|
### Running a Single Test
|
|
|
|
```bash
|
|
# Run a specific test function
|
|
pytest tests/test_eventbus.py::TestEventBusInit::test_init_creates_empty_subscribers
|
|
|
|
# Run all tests in a file
|
|
pytest tests/test_eventbus.py
|
|
|
|
# Run tests matching a pattern
|
|
pytest -k "test_subscribe"
|
|
```
|
|
|
|
### Git Hooks
|
|
|
|
Install hooks at start of session:
|
|
```bash
|
|
ls -la .git/hooks/pre-commit # Verify installed
|
|
hk init --mise # Install if missing
|
|
mise run pre-commit # Run manually
|
|
```
|
|
|
|
## Code Style Guidelines
|
|
|
|
### Imports (three sections, alphabetical within each)
|
|
|
|
```python
|
|
# 1. Standard library
|
|
import os
|
|
import threading
|
|
from collections import defaultdict
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from typing import Any
|
|
|
|
# 2. Third-party
|
|
from abc import ABC, abstractmethod
|
|
|
|
# 3. Local project
|
|
from engine.events import EventType
|
|
```
|
|
|
|
### Type Hints
|
|
|
|
- Use type hints for all function signatures (parameters and return)
|
|
- Use `|` for unions (Python 3.10+): `EventType | None`
|
|
- Use `dict[K, V]`, `list[V]` (generic syntax): `dict[str, list[int]]`
|
|
- Use `Callable[[ArgType], ReturnType]` for callbacks
|
|
|
|
```python
|
|
def subscribe(self, event_type: EventType, callback: Callable[[Any], None]) -> None:
|
|
...
|
|
|
|
def get_sensor_value(self, sensor_name: str) -> float | None:
|
|
return self._state.get(f"sensor.{sensor_name}")
|
|
```
|
|
|
|
### Naming Conventions
|
|
|
|
- **Classes**: `PascalCase` (e.g., `EventBus`, `EffectPlugin`)
|
|
- **Functions/methods**: `snake_case` (e.g., `get_event_bus`, `process_partial`)
|
|
- **Constants**: `SCREAMING_SNAKE_CASE` (e.g., `CURSOR_OFF`)
|
|
- **Private methods**: `_snake_case` prefix (e.g., `_initialize`)
|
|
- **Type variables**: `PascalCase` (e.g., `T`, `EffectT`)
|
|
|
|
### Dataclasses
|
|
|
|
Use `@dataclass` for simple data containers:
|
|
|
|
```python
|
|
@dataclass
|
|
class EffectContext:
|
|
terminal_width: int
|
|
terminal_height: int
|
|
scroll_cam: int
|
|
ticker_height: int = 0
|
|
_state: dict[str, Any] = field(default_factory=dict, repr=False)
|
|
```
|
|
|
|
### Abstract Base Classes
|
|
|
|
Use ABC for interface enforcement:
|
|
|
|
```python
|
|
class EffectPlugin(ABC):
|
|
name: str
|
|
config: EffectConfig
|
|
|
|
@abstractmethod
|
|
def process(self, buf: list[str], ctx: EffectContext) -> list[str]:
|
|
...
|
|
|
|
@abstractmethod
|
|
def configure(self, config: EffectConfig) -> None:
|
|
...
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
- Catch specific exceptions, not bare `Exception`
|
|
- Use `try/except` with fallbacks for optional features
|
|
- Silent pass in event callbacks to prevent one handler from breaking others
|
|
|
|
```python
|
|
# Good: specific exception
|
|
try:
|
|
term_size = os.get_terminal_size()
|
|
except OSError:
|
|
term_width = 80
|
|
|
|
# Good: silent pass in callbacks
|
|
for callback in callbacks:
|
|
try:
|
|
callback(event)
|
|
except Exception:
|
|
pass
|
|
```
|
|
|
|
### Thread Safety
|
|
|
|
Use locks for shared state:
|
|
|
|
```python
|
|
class EventBus:
|
|
def __init__(self):
|
|
self._lock = threading.Lock()
|
|
|
|
def publish(self, event_type: EventType, event: Any = None) -> None:
|
|
with self._lock:
|
|
callbacks = list(self._subscribers.get(event_type, []))
|
|
```
|
|
|
|
### Comments
|
|
|
|
- **DO NOT ADD comments** unless explicitly required
|
|
- Let code be self-documenting with good naming
|
|
- Use docstrings only for public APIs or complex logic
|
|
|
|
### Testing Patterns
|
|
|
|
Follow pytest conventions:
|
|
|
|
```python
|
|
class TestEventBusSubscribe:
|
|
"""Tests for EventBus.subscribe method."""
|
|
|
|
def test_subscribe_adds_callback(self):
|
|
"""subscribe() adds a callback for an event type."""
|
|
bus = EventBus()
|
|
def callback(e):
|
|
return None
|
|
bus.subscribe(EventType.NTFY_MESSAGE, callback)
|
|
assert bus.subscriber_count(EventType.NTFY_MESSAGE) == 1
|
|
```
|
|
|
|
- Use classes to group related tests (`Test<ClassName>`, `Test<method_name>`)
|
|
- Test docstrings follow `"<method>() <action>"` pattern
|
|
- Use descriptive assertion messages via pytest behavior
|
|
|
|
## Workflow Rules
|
|
|
|
### Before Committing
|
|
|
|
1. Run tests: `mise run test`
|
|
2. Run linter: `mise run lint`
|
|
3. Review changes: `git diff`
|
|
|
|
### On Failing Tests
|
|
|
|
- **Out-of-date test**: Update test to match new expected behavior
|
|
- **Correctly failing test**: Fix implementation, not the test
|
|
|
|
**Never** modify a test to make it pass without understanding why it failed.
|
|
|
|
## Architecture Overview
|
|
|
|
- **Pipeline**: source → render → effects → display
|
|
- **EffectPlugin**: ABC with `process()` and `configure()` methods
|
|
- **Display backends**: terminal, websocket, sixel, null (for testing)
|
|
- **EventBus**: thread-safe pub/sub messaging
|
|
- **Presets**: TOML format in `engine/presets.toml`
|
|
|
|
Key files:
|
|
- `engine/pipeline/core.py` - Stage base class
|
|
- `engine/effects/types.py` - EffectPlugin ABC and dataclasses
|
|
- `engine/display/backends/` - Display backend implementations
|
|
- `engine/eventbus.py` - Thread-safe event system
|