# REQ-007: TUI Integration Tests with Textual Pilot

## Metadata
- **Status:** review
- **Priority:** 2
- **Dependencies:** REQ-006
- **Created:** 2026-03-03
- **Implemented:** 2026-03-03

## Context

The iEvo TUI dashboard (`src/ievo/tui/app.py`) is the primary user interface. It uses Textual's `App` with 4 tabs (Agents, Pipeline, Evolution, Files) and a `SelectionList` for agent toggle.

**Current problem**: the TUI is not interactable — users cannot navigate tabs, select agents, or toggle checkboxes. All existing TUI tests use unit-level mocks (`MagicMock`, `patch`) and never test real widget interaction. Bugs in focus, keyboard handling, and widget rendering are invisible to the test suite.

**Textual provides a testing framework**: `app.run_test()` returns an `async with` context with a `Pilot` object that simulates real user input — `pilot.press()`, `pilot.click()`, keyboard navigation. This is the standard way to test Textual apps.

### Current TUI architecture

- `IevoApp(App)` — main app class
- `compose()` yields: `Header`, `Static#logo`, `Static#project-info`, `TabbedContent` with 4 `TabPane`s, `Static#help-text`, `Footer`
- Agents tab: `SelectionList[str](id="agents-list")` — shows all 12 bundled agents with checkboxes
- Keyboard bindings: `a`=Agents, `p`=Pipeline, `e`=Evolution, `f`=Files, `r`=Refresh, `q`=Quit
- `on_mount()` calls `find_project_root()` → `watch_project_root()` loads data
- `on_selection_list_selection_toggled()` handles checkbox toggle → install/uninstall agent

### Known issues to investigate

1. **Focus**: SelectionList may not receive focus on mount, making keyboard navigation impossible
2. **Key conflicts**: App-level single-letter bindings (`a`, `p`, `e`, `f`) may capture keys before widgets receive them
3. **Tab switching**: `action_tab()` sets `TabbedContent.active` but may not actually switch visible content
4. **SelectionList toggle**: `Space`/`Enter` to toggle may not work if focus or event routing is broken

## Acceptance Criteria

### Navigation

- [x] On app mount with a project, Agents tab is visible and `SelectionList` is rendered with 12 agent entries
- [x] Pressing `p` switches to Pipeline tab — `#pipeline-view` content is visible
- [x] Pressing `e` switches to Evolution tab — `#evo-log` is visible
- [x] Pressing `f` switches to Files tab — `#project-tree` is visible
- [x] Pressing `a` returns to Agents tab
- [x] `Tab` key moves focus between widgets; `SelectionList` is focusable

### Agent selection

- [x] When SelectionList has focus, `Up`/`Down` arrows navigate between agent entries
- [x] Pressing `Space` on an unchecked agent toggles it ON — agent `.md` file created in `.claude/agents/`, manifest updated
- [x] Pressing `Space` on a checked agent toggles it OFF — agent `.md` file removed, manifest updated
- [x] Toggling a mandatory agent OFF shows a warning notification (toast)
- [x] Pipeline tab content updates after agent toggle (reflects new agent set)

### Edge cases

- [x] App with no project (root=None): shows "No project found" message, no crash on interaction
- [x] Pressing `q` exits the app cleanly
- [x] Pressing `r` refreshes data (calls `find_project_root` again)

## Negative Acceptance Criteria

- This does NOT add new TUI features — only tests for existing functionality
- This does NOT change the TUI layout or behavior unless a bug is found during testing
- This does NOT test Docker sandbox, Claude auth, or network operations — only TUI widget interaction
- Bug fixes discovered during testing are IN SCOPE (fix + test together)

## Technical Notes

- Use Textual's `app.run_test(size=(120, 40))` + `Pilot` API for all interaction tests
- Tests must be `async` — use `pytest-asyncio` or `asyncio.run()`
- Mock `find_project_root()` to return a `tmp_path` with pre-created project structure
- Mock `Registry.load_cached()` to avoid filesystem side effects from `~/.ievo/cache/`
- Each test should set up a minimal project in `tmp_path`: `.ievo/manifest.yaml`, optionally `.claude/agents/*.md`
- Use `await pilot.pause()` after actions to let Textual process events
- Query widgets with `app.query_one("#id", WidgetType)` to verify state

## Examples

### Example 1: Tab navigation test

```python
async def test_tab_switching():
    async with app.run_test(size=(120, 40)) as pilot:
        # Start on Agents tab
        tabbed = app.query_one(TabbedContent)
        assert tabbed.active == "agents"

        # Press 'p' → Pipeline tab
        await pilot.press("p")
        assert tabbed.active == "pipeline"
```

### Example 2: Agent toggle test

```python
async def test_agent_toggle_on():
    async with app.run_test(size=(120, 40)) as pilot:
        # Focus SelectionList, navigate to unchecked agent, press Space
        agents_list = app.query_one("#agents-list", SelectionList)
        await pilot.press("space")  # toggle first agent

        # Verify file created
        assert (tmp_path / ".claude/agents/architect.md").exists()
```

### Example 3: No-project state

```python
async def test_no_project_shows_message():
    # find_project_root returns None
    async with app.run_test(size=(120, 40)) as pilot:
        info = app.query_one("#project-info", Static)
        assert "No project found" in info.renderable
```
