# Section 07: Tests

## Overview

This section creates the complete test suite for the Python bridge layer. All tests use mocked WebSocket connections (no real server needed) and follow project conventions: `pytest` with `asyncio_mode = "auto"`, class-based organization, `unittest.mock.AsyncMock` for async mocking.

## Dependencies

- **Section 04** (protocol.py, connection.py) -- under test
- **Section 06** (screenshot.py, run.py, perf_snapshot.py, editor_context.py) -- under test

## Files to Create

| File | Purpose |
|------|---------|
| `tests/test_bridge/__init__.py` | Empty package init |
| `tests/test_bridge/test_protocol.py` | Protocol encode/decode and timeout tests |
| `tests/test_bridge/test_connection.py` | Connection constructor, state, events, error types |
| `tests/test_bridge/test_tools.py` | Tool functions with mocked bridge |

---

## File 1: `tests/test_bridge/__init__.py`

Empty file. Package marker for pytest discovery.

---

## File 2: `tests/test_bridge/test_protocol.py`

### Imports

```python
import json
import pytest
from godotiq.bridge.protocol import (
    BridgeRequest, BridgeResponse, BridgeEvent,
    parse_message, get_timeout, TOOL_TIMEOUTS,
)
```

### TestBridgeRequest (2 tests)

- **test_to_json_minimal**: `BridgeRequest(id="r1", method="ping")` produces valid JSON with `id`, `method`, empty `params`
- **test_to_json_with_params**: `BridgeRequest(id="r2", method="screenshot", params={"scale": 0.5, "format": "webp"})` preserves param values

### TestParseMessage (7 tests)

- **test_parse_ok_response**: Input `{"id": "r1", "status": "ok", "result": {"pong": true}}`. Returns `BridgeResponse` with `is_ok == True`, correct result
- **test_parse_error_response**: Input with `status: "error"`. Returns `BridgeResponse` with `error_code` and `error_message`
- **test_parse_event**: Input `{"event": "game_started", "data": {}}`. Returns `BridgeEvent`
- **test_parse_event_with_data**: Input with nested data. Returns `BridgeEvent` with preserved data
- **test_parse_invalid_json**: Input `"not json"`. Raises `ValueError` matching "Invalid JSON"
- **test_parse_missing_id_and_event**: Input `{"status": "ok"}`. Raises `ValueError`
- **test_parse_non_object**: Input `'"just a string"'`. Raises `ValueError`

### TestTimeouts (3 tests)

- **test_known_timeouts**: `get_timeout("screenshot") == 30.0`, `get_timeout("ping") == 2.0`
- **test_default_timeout**: `get_timeout("unknown") == 5.0`
- **test_all_positive**: All values in `TOOL_TIMEOUTS` are `> 0`

---

## File 3: `tests/test_bridge/test_connection.py`

### Imports

```python
import pytest
from unittest.mock import MagicMock
from godotiq.bridge.connection import (
    BridgeConnection, BridgeConnectionError, BridgeError,
)
from godotiq.bridge.protocol import BridgeEvent
```

### TestBridgeConnectionInit (3 tests)

- **test_default_params**: `BridgeConnection()` has host `127.0.0.1`, port `6007`
- **test_custom_params**: `BridgeConnection(host="192.168.1.1", port=9999)` stores correctly
- **test_initial_state**: `is_connected == False`, `is_game_running == False`

### TestBridgeConnectionRequest (1 test)

- **test_request_without_connect**: `await conn.request("ping")` raises `BridgeConnectionError`

### TestBridgeConnectionEvents (4 tests)

- **test_game_started**: `_on_game_started({})` sets `is_game_running = True`
- **test_game_stopped**: `_on_game_stopped({})` sets `is_game_running = False`
- **test_event_handler_called**: Register handler, dispatch event, handler called with correct data
- **test_multiple_handlers**: Two handlers on same event, both called

### TestBridgeConnectionErrorTypes (2 tests)

- **test_bridge_error_attributes**: `BridgeError("CODE", "msg")` has `code` and `error_message`
- **test_bridge_error_str**: `str(err)` includes the error code

---

## File 4: `tests/test_bridge/test_tools.py`

### Imports

```python
import pytest
from unittest.mock import AsyncMock, patch
from godotiq.tools.bridge.screenshot import screenshot
from godotiq.tools.bridge.run import run
from godotiq.tools.bridge.perf_snapshot import perf_snapshot
from godotiq.tools.bridge.editor_context import editor_context
```

### Mocking Pattern

All tool tests create an `AsyncMock` for the bridge and patch `get_bridge` or `get_bridge_or_none` at the module where it is imported (e.g., `"godotiq.tools.bridge.screenshot.get_bridge"`).

### TestScreenshotTool (3 tests)

- **test_default_params**: Mock `request_with_retry` returning `{image, format, width, height}`. Call `await screenshot()`. Assert called with `method="screenshot"`, `params={"viewport": "game", "scale": 0.5, "format": "webp"}`, `max_retries=3`, `retry_delay=0.5`, `timeout=30.0`
- **test_invalid_scale_clamped**: Call with `scale=-1.0`. Assert params have `scale=0.5`
- **test_invalid_format_fallback**: Call with `format="bmp"`. Assert params have `format="webp"`

### TestRunTool (2 tests)

- **test_play_main**: Call `await run(action="play", scene="main")`. Assert `request("run", params={"scene": "main"})` called
- **test_stop**: Call `await run(action="stop")`. Assert `request("stop")` called

### TestPerfSnapshotTool (1 test)

- **test_basic_call**: Mock `request` returning `{fps: 60, ...}`. Call `await perf_snapshot()`. Assert `request("perf_snapshot")` called

### TestEditorContextTool (2 tests)

- **test_connected**: Patch `get_bridge_or_none` returning mock. Mock `request` returning editor data. Assert result has `addon_connected == True`
- **test_disconnected**: Patch `get_bridge_or_none` returning `None`. Assert result has `addon_connected == False` and `"note"` key

---

## Conventions

- File header: `"""Tests for ..."""` docstring
- `from __future__ import annotations`
- Class-based: `class TestSomething:` (no base class)
- Method naming: `test_descriptive_name`
- Async tests: `async def test_...` (auto-detected by asyncio_mode="auto")
- Direct `assert` statements, no `self.assertEqual`
- Patch target: where the name is looked up, not where defined

## Verification

```bash
pytest tests/test_bridge/ -v          # All new tests pass
pytest tests/ -v                       # No Phase 1 regressions
```

---

## Post-Implementation Notes

### Files Created
- `tests/test_bridge/test_tools.py` — 9 tests for the 4 bridge tool functions

### Pre-existing Files (from Section 04)
- `tests/test_bridge/__init__.py` — already existed
- `tests/test_bridge/test_protocol.py` — 13 tests, already existed
- `tests/test_bridge/test_connection.py` — 10 tests, already existed

### Deviations from Plan
1. **Added boundary test**: `test_zero_scale_clamped` for scale=0.0 (off-by-one boundary not in plan)
2. **Added request method assertion**: `editor_context` connected test now asserts `request("editor_context")` was called
3. **Added project_path assertion**: `editor_context` disconnected test verifies `project_path == ""`
4. **Style alignment**: Added type annotations, class docstrings, and pytest import to match existing test file conventions

### Test Results
- 32 bridge tests pass (13 protocol + 10 connection + 9 tools)
- 492 total tests pass (460 Phase 1 + 32 bridge)
