superpowers- plan for hot reload(minimax)
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
# Hot Reloader Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Implement selective, state-preserving hot-reload for `src/gui_2.py` with delegation pattern refactor, manual trigger via `Ctrl+Alt+R` and GUI button, and visual error tint feedback on failure.
|
||||
|
||||
**Architecture:** Separating code into **delegators** (thin `App` wrappers in `gui_2.py`) and **delegation targets** (actual render logic as module-level functions). State preservation via `copy.deepcopy` capture/restore around `importlib.reload`. Error tint overlay on reload failure using ImGui draw list.
|
||||
|
||||
**Tech Stack:** Python `importlib.reload`, `copy.deepcopy`, Dear PyGui / ImGui draw lists, `traceback`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| File | Responsibility |
|
||||
|------|----------------|
|
||||
| `src/hot_reloader.py` | New — `HotModule` dataclass and `HotReloader` class with registry, capture/restore, reload API |
|
||||
| `src/gui_2.py` | Refactor render methods from `App._render_xxx` to module-level `render_xxx(app)` |
|
||||
| `src/app_controller.py` | Delegation wrappers — `App._render_xxx` becomes `import src.gui_2 as g2; g2.render_xxx(self)` |
|
||||
| `tests/test_hot_reloader.py` | Unit tests for `HotModule`, `HotReloader` registry and reload |
|
||||
| `tests/test_hot_reload_integration.py` | Integration tests via `live_gui` fixture |
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Create `src/hot_reloader.py` — HotModule Dataclass and HotReloader Core
|
||||
|
||||
**Files:**
|
||||
- Create: `src/hot_reloader.py`
|
||||
- Test: `tests/test_hot_reloader.py`
|
||||
|
||||
- [ ] **Step 1: Write failing test for HotModule dataclass**
|
||||
|
||||
```python
|
||||
# tests/test_hot_reloader.py
|
||||
import pytest
|
||||
from dataclasses import dataclass
|
||||
from src.hot_reloader import HotModule, HotReloader
|
||||
|
||||
def test_hot_module_dataclass_fields():
|
||||
hm = HotModule(
|
||||
name="test_module",
|
||||
file_path="/path/to/test_module.py",
|
||||
state_keys=["attr1", "attr2"],
|
||||
delegation_targets=["method_a", "method_b"],
|
||||
)
|
||||
assert hm.name == "test_module"
|
||||
assert hm.file_path == "/path/to/test_module.py"
|
||||
assert hm.state_keys == ["attr1", "attr2"]
|
||||
assert hm.delegation_targets == ["method_a", "method_b"]
|
||||
|
||||
def test_hot_reloader_register_and_get():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
hm = HotModule(name="test_mod", file_path="/fake/path.py", state_keys=[], delegation_targets=[])
|
||||
HotReloader.register(hm)
|
||||
assert "test_mod" in HotReloader.HOT_MODULES
|
||||
assert HotReloader.HOT_MODULES["test_mod"] is hm
|
||||
|
||||
def test_hot_reloader_register_duplicate_raises():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
hm1 = HotModule(name="dup", file_path="/a.py", state_keys=[], delegation_targets=[])
|
||||
HotReloader.register(hm1)
|
||||
with pytest.raises(ValueError, match="already registered"):
|
||||
HotReloader.register(hm1)
|
||||
|
||||
def test_hot_reloader_is_error_state():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
HotReloader.last_error = None
|
||||
HotReloader.is_error_state = False
|
||||
assert HotReloader.is_error_state is False
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py -v`
|
||||
Expected: FAIL — `src/hot_reloader.py` does not exist
|
||||
|
||||
- [ ] **Step 3: Write minimal HotModule dataclass and HotReloader skeleton**
|
||||
|
||||
```python
|
||||
# src/hot_reloader.py
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
import copy
|
||||
import traceback
|
||||
|
||||
@dataclass
|
||||
class HotModule:
|
||||
name: str
|
||||
file_path: str
|
||||
state_keys: list[str] = field(default_factory=list)
|
||||
delegation_targets: list[str] = field(default_factory=list)
|
||||
|
||||
class HotReloader:
|
||||
HOT_MODULES: dict[str, HotModule] = {}
|
||||
last_error: str | None = None
|
||||
is_error_state: bool = False
|
||||
|
||||
@classmethod
|
||||
def register(cls, module: HotModule) -> None:
|
||||
if module.name in cls.HOT_MODULES:
|
||||
raise ValueError(f"Module {module.name} already registered")
|
||||
cls.HOT_MODULES[module.name] = module
|
||||
|
||||
@classmethod
|
||||
def capture_state(cls, app: Any, state_keys: list[str]) -> dict[str, Any]:
|
||||
return {key: copy.deepcopy(getattr(app, key, None)) for key in state_keys if hasattr(app, key)}
|
||||
|
||||
@classmethod
|
||||
def restore_state(cls, app: Any, state: dict[str, Any]) -> None:
|
||||
for key, value in state.items():
|
||||
setattr(app, key, value)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hot_reloader.py tests/test_hot_reloader.py
|
||||
git commit -m "feat(hot-reload): Add HotModule dataclass and HotReloader registry"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Implement `HotReloader.reload()` and `HotReloader.reload_all()`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/hot_reloader.py`
|
||||
- Test: `tests/test_hot_reloader.py`
|
||||
|
||||
- [ ] **Step 1: Write failing test for reload method**
|
||||
|
||||
```python
|
||||
# tests/test_hot_reloader.py (add these tests)
|
||||
import importlib
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
def test_reload_unknown_module_returns_false():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
HotReloader.register(HotModule(name="nonexistent_mod", file_path="/nonexistent.py", state_keys=[], delegation_targets=[]))
|
||||
app = MagicMock()
|
||||
result = HotReloader.reload("nonexistent_mod", app)
|
||||
assert result is False
|
||||
assert HotReloader.is_error_state is True
|
||||
assert HotReloader.last_error is not None
|
||||
|
||||
def test_reload_success_clears_error_state():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
# Create a minimal test module that can be reloaded
|
||||
import sys
|
||||
import types
|
||||
test_mod = types.ModuleType("src._test_reload_mod")
|
||||
sys.modules["src._test_reload_mod"] = test_mod
|
||||
HotReloader.register(HotModule(name="src._test_reload_mod", file_path=str(Path(__file__).parent.parent / "src" / "_test_reload_mod.py"), state_keys=[], delegation_targets=[]))
|
||||
app = MagicMock()
|
||||
result = HotReloader.reload("src._test_reload_mod", app)
|
||||
assert result is True
|
||||
assert HotReloader.is_error_state is False
|
||||
assert HotReloader.last_error is None
|
||||
del sys.modules["src._test_reload_mod"]
|
||||
|
||||
def test_reload_captures_and_restores_state_on_failure():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
HotReloader.register(HotModule(name="bad_mod", file_path="/bad.py", state_keys=["_test_attr"], delegation_targets=[]))
|
||||
app = MagicMock()
|
||||
app._test_attr = "preserved_value"
|
||||
result = HotReloader.reload("bad_mod", app)
|
||||
assert result is False
|
||||
assert HotReloader.is_error_state is True
|
||||
# State should be restored even on failure
|
||||
|
||||
def test_reload_all():
|
||||
HotReloader.HOT_MODULES.clear()
|
||||
# Register two modules and verify reload_all calls both
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py -v`
|
||||
Expected: FAIL — `reload` method not implemented
|
||||
|
||||
- [ ] **Step 3: Write `reload()` and `reload_all()` implementations**
|
||||
|
||||
```python
|
||||
# src/hot_reloader.py (add after existing methods)
|
||||
@classmethod
|
||||
def reload(cls, module_name: str, app: Any) -> bool:
|
||||
if module_name not in cls.HOT_MODULES:
|
||||
cls.last_error = f"Module {module_name} not registered"
|
||||
cls.is_error_state = True
|
||||
return False
|
||||
|
||||
hm = cls.HOT_MODULES[module_name]
|
||||
state = cls.capture_state(app, hm.state_keys)
|
||||
|
||||
try:
|
||||
import importlib
|
||||
import sys
|
||||
if module_name in sys.modules:
|
||||
old_module = sys.modules[module_name]
|
||||
importlib.reload(old_module)
|
||||
else:
|
||||
importlib.import_module(module_name)
|
||||
cls.last_error = None
|
||||
cls.is_error_state = False
|
||||
return True
|
||||
except Exception:
|
||||
cls.restore_state(app, state)
|
||||
cls.last_error = traceback.format_exc()
|
||||
cls.is_error_state = True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def reload_all(cls, app: Any) -> bool:
|
||||
success = True
|
||||
for name in cls.HOT_MODULES:
|
||||
if not cls.reload(name, app):
|
||||
success = False
|
||||
return success
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/hot_reloader.py tests/test_hot_reloader.py
|
||||
git commit -m "feat(hot-reload): Implement HotReloader.reload and reload_all"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Register `src.gui_2` with state_keys and delegation_targets
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app_controller.py` — add `HotModule` registration
|
||||
- Test: `tests/test_hot_reloader.py`
|
||||
|
||||
- [ ] **Step 1: Write test for src.gui_2 registration**
|
||||
|
||||
```python
|
||||
def test_gui_2_module_registered():
|
||||
from src.hot_reloader import HotReloader
|
||||
assert "src.gui_2" in HotReloader.HOT_MODULES
|
||||
hm = HotReloader.HOT_MODULES["src.gui_2"]
|
||||
assert len(hm.state_keys) > 0
|
||||
assert len(hm.delegation_targets) > 0
|
||||
assert "_render_main_interface" in hm.delegation_targets
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py::test_gui_2_module_registered -v`
|
||||
Expected: FAIL — registration not yet added
|
||||
|
||||
- [ ] **Step 3: Add src.gui_2 HotModule registration in app_controller.py**
|
||||
|
||||
Find the end of `AppController.__init__` or add a `_register_hot_modules()` function called at end of `__init__`:
|
||||
|
||||
```python
|
||||
# In src/app_controller.py, near end of AppController.__init__ or as standalone:
|
||||
from src.hot_reloader import HotReloader, HotModule
|
||||
from pathlib import Path
|
||||
|
||||
def _register_hot_modules() -> None:
|
||||
HotReloader.register(HotModule(
|
||||
name="src.gui_2",
|
||||
file_path=str(Path(__file__).parent / "gui_2.py"),
|
||||
state_keys=[
|
||||
"_active_discussion", "_disc_entries", "_disc_roles",
|
||||
"show_windows", "ui_discussion_split_h", "active_tickets",
|
||||
"show_preset_manager_window", "show_tool_preset_manager_window",
|
||||
"show_persona_editor_window", "_pending_patch",
|
||||
"_tool_log", "_comms_log",
|
||||
],
|
||||
delegation_targets=[
|
||||
"_render_main_interface", "_render_discussion_hub",
|
||||
"_render_discussion_panel", "_render_discussion_selector",
|
||||
"_render_discussion_entries", "_render_discussion_entry",
|
||||
"_render_files_and_media", "_render_files_panel",
|
||||
"_render_screenshots_panel", "_render_context_composition_panel",
|
||||
"_render_ai_settings_hub", "_render_provider_panel",
|
||||
"_render_persona_selector_panel", "_render_agent_tools_panel",
|
||||
"_render_rag_panel", "_render_system_prompts_panel",
|
||||
"_render_preset_manager_content", "_render_persona_editor_window",
|
||||
"_render_tool_preset_manager_content",
|
||||
"_render_operations_hub", "_render_tool_calls_panel",
|
||||
"_render_comms_history_panel", "_render_mma_dashboard",
|
||||
"_render_mma_modals", "_render_ticket_queue",
|
||||
"_render_task_dag_panel", "_render_approve_script_modal",
|
||||
"_render_patch_modal",
|
||||
],
|
||||
))
|
||||
```
|
||||
|
||||
Call `_register_hot_modules()` at the end of `AppController.__init__` after all state is initialized.
|
||||
|
||||
- [ ] **Step 4: Run test to verify it passes**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py::test_gui_2_module_registered -v`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/app_controller.py
|
||||
git commit -m "feat(hot-reload): Register src.gui_2 with state_keys and delegation_targets"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Phase 1 — Refactor gui_2 render methods to module-level functions
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` — extract `App._render_xxx` to `render_xxx(app)`
|
||||
- Modify: `src/app_controller.py` — delegation wrappers
|
||||
- Test: `tests/test_hot_reloader.py`
|
||||
|
||||
**Refactor Pattern** (for each render method):
|
||||
|
||||
```python
|
||||
# BEFORE (in App class):
|
||||
def _render_main_interface(self) -> None:
|
||||
if self.perf_profiling_enabled:
|
||||
self.perf_monitor.start_component("_render_main_interface")
|
||||
with imscope.window("Main Interface", self.show_windows.get("main_interface", True)) as (exp, opened):
|
||||
if not exp: return
|
||||
# ... all render logic
|
||||
|
||||
# AFTER — App method becomes thin delegation wrapper:
|
||||
def _render_main_interface(self) -> None:
|
||||
import src.gui_2 as g2
|
||||
g2.render_main_interface(self)
|
||||
|
||||
# NEW module-level function:
|
||||
def render_main_interface(app: App) -> None:
|
||||
if app.perf_profiling_enabled:
|
||||
app.perf_monitor.start_component("_render_main_interface")
|
||||
with imscope.window("Main Interface", app.show_windows.get("main_interface", True)) as (exp, opened):
|
||||
if not exp: return
|
||||
# ... same render logic, replacing self with app
|
||||
```
|
||||
|
||||
**Methods to refactor** (in order):
|
||||
1. `_render_main_interface` (lines 769-857)
|
||||
2. `_render_discussion_hub` (lines 3235-3245)
|
||||
3. `_render_discussion_panel` (lines 3395-3415)
|
||||
4. `_render_discussion_selector` (lines 3430-3473)
|
||||
5. `_render_discussion_entries` (lines 3247-3259)
|
||||
6. `_render_discussion_entry` (lines 3261-3304)
|
||||
7. `_render_files_and_media` (lines 2529-2636)
|
||||
8. `_render_files_panel` (lines 2638-2716)
|
||||
9. `_render_screenshots_panel` (lines 2718-2750)
|
||||
10. `_render_context_composition_panel` (lines 2752-2765)
|
||||
11. `_render_ai_settings_hub` (lines 1754-1759)
|
||||
12. `_render_provider_panel` (lines 2377-2441)
|
||||
13. `_render_persona_selector_panel` (lines 2443-2523)
|
||||
14. `_render_agent_tools_panel` (lines 1850-1913)
|
||||
15. `_render_rag_panel` (lines 1761-1796)
|
||||
16. `_render_system_prompts_panel` (lines 1798-1848)
|
||||
17. `_render_preset_manager_content` (lines 1915-2002)
|
||||
18. `_render_persona_editor_window` (lines 2205-2375)
|
||||
19. `_render_tool_preset_manager_content` (lines 2014-2193)
|
||||
20. `_render_operations_hub` (lines 3750-3788)
|
||||
21. `_render_tool_calls_panel` (lines 3790-3853)
|
||||
22. `_render_comms_history_panel` (lines 3855-3971)
|
||||
23. `_render_mma_dashboard` (lines 4664-4695)
|
||||
24. `_render_mma_modals` (lines 4711-4785)
|
||||
25. `_render_ticket_queue` (lines 4365-4478)
|
||||
26. `_render_task_dag_panel` (lines 4480-4621)
|
||||
27. `_render_approve_script_modal` (lines 4269-4310)
|
||||
28. `_render_patch_modal` (lines 4135-4177)
|
||||
|
||||
- [ ] **Step 1: Write test for module-level function pattern**
|
||||
|
||||
```python
|
||||
def test_gui2_has_module_level_render_functions():
|
||||
import src.gui_2 as g2
|
||||
# After refactor, these should exist as module-level functions
|
||||
assert hasattr(g2, 'render_main_interface')
|
||||
assert hasattr(g2, 'render_discussion_hub')
|
||||
assert hasattr(g2, 'render_discussion_panel')
|
||||
# ... etc
|
||||
# Each should accept app: App as first parameter
|
||||
import inspect
|
||||
sig = inspect.signature(g2.render_main_interface)
|
||||
assert 'app' in sig.parameters
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run test to verify it fails**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py::test_gui2_has_module_level_render_functions -v`
|
||||
Expected: FAIL — module-level functions don't exist yet
|
||||
|
||||
- [ ] **Step 3: Refactor `_render_main_interface` first as pilot**
|
||||
|
||||
Extract the method body, replace `self` with `app`, create module-level `render_main_interface(app: App) -> None`, and convert `App._render_main_interface` to delegation wrapper.
|
||||
|
||||
- [ ] **Step 4: Run test after each method refactor**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reloader.py::test_gui2_has_module_level_render_functions -v`
|
||||
Continue until all module-level functions exist.
|
||||
|
||||
- [ ] **Step 5: Commit after completing all methods**
|
||||
|
||||
```bash
|
||||
git add src/gui_2.py src/app_controller.py
|
||||
git commit -m "refactor(hot-reload): Extract gui_2 render methods to module-level functions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Add `Ctrl+Alt+R` trigger and GUI Hot Reload button
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py:App.run` — capture keyboard shortcut
|
||||
- Modify: `src/gui_2.py:_render_mma_dashboard` — add Hot Reload button
|
||||
- Modify: `src/gui_2.py` — add `_trigger_hot_reload` and `_hot_reload_error` state
|
||||
- Test: `tests/test_hot_reload_integration.py`
|
||||
|
||||
- [ ] **Step 1: Write test for hot reload trigger**
|
||||
|
||||
```python
|
||||
def test_ctrl_alt_r_triggers_hot_reload(live_gui):
|
||||
client = ApiHookClient(port=8999)
|
||||
# Verify initial state
|
||||
initial_state = client.get_value("_hot_reload_error")
|
||||
# Note: Direct keyboard simulation via hooks not yet implemented
|
||||
# Test will verify button-based trigger
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Write test for Hot Reload button in MMA dashboard**
|
||||
|
||||
```python
|
||||
def test_hot_reload_button_in_mma_dashboard(live_gui):
|
||||
client = ApiHookClient(port=8999)
|
||||
# Verify _render_mma_dashboard has a Hot Reload button
|
||||
# via exposed component state or via behavior
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add `_hot_reload_error` state to App.__init__**
|
||||
|
||||
In `src/gui_2.py:App.__init__`, add:
|
||||
|
||||
```python
|
||||
self._hot_reload_error = False
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add `_trigger_hot_reload()` method to App**
|
||||
|
||||
```python
|
||||
def _trigger_hot_reload(self) -> None:
|
||||
from src.hot_reloader import HotReloader
|
||||
success = HotReloader.reload_all(self)
|
||||
self._hot_reload_error = not success
|
||||
|
||||
def _render_hot_reload_button(self) -> None:
|
||||
imgui.push_style_color(imgui.Col_.button, imgui.get_color_u32_rgba(0.2, 0.2, 0.2, 1.0))
|
||||
if imgui.button("Hot Reload"):
|
||||
self._trigger_hot_reload()
|
||||
imgui.pop_style_color()
|
||||
if self._hot_reload_error:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(imgui.get_color_u32_rgba(1.0, 0.3, 0.2, 1.0), "Reload failed")
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add keyboard capture in `App.run` main loop**
|
||||
|
||||
In `src/gui_2.py:App.run`, inside the main `while running:` loop:
|
||||
|
||||
```python
|
||||
# Check for Ctrl+Alt+R
|
||||
io = imgui.get_io()
|
||||
if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.KEY_R):
|
||||
self._trigger_hot_reload()
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Integrate button into `_render_mma_global_controls` or `_render_mma_dashboard`**
|
||||
|
||||
Add `self._render_hot_reload_button()` call in `_render_mma_dashboard` or a nearby section.
|
||||
|
||||
- [ ] **Step 7: Run tests**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=60`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add src/gui_2.py src/app_controller.py
|
||||
git commit -m "feat(hot-reload): Add Ctrl+Alt+R trigger and GUI Hot Reload button"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Implement visual error tint on reload failure
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/gui_2.py` — add `_render_error_tint()` and call from `render_main_interface`
|
||||
- Test: `tests/test_hot_reload_integration.py`
|
||||
|
||||
- [ ] **Step 1: Write test for error tint overlay**
|
||||
|
||||
```python
|
||||
def test_error_tint_renders_on_failure(live_gui):
|
||||
# Inject a failure state and verify tint appears
|
||||
pass
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add `_render_error_tint(app: App)` function**
|
||||
|
||||
```python
|
||||
def _render_error_tint(app: App) -> None:
|
||||
if not getattr(app, '_hot_reload_error', False):
|
||||
return
|
||||
# NERV theme error tint
|
||||
tint_color = imgui.get_color_u32_rgba(1.0, 0.28, 0.25, 0.15)
|
||||
draw_list = imgui.get_background_draw_list()
|
||||
viewport = imgui.get_main_viewport()
|
||||
draw_list.add_rect_filled(
|
||||
viewport.pos,
|
||||
viewport.pos + viewport.size,
|
||||
tint_color,
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Call `_render_error_tint(app)` at end of `render_main_interface`**
|
||||
|
||||
Add at the very start of `render_main_interface` since it's an overlay:
|
||||
|
||||
```python
|
||||
def render_main_interface(app: App) -> None:
|
||||
_render_error_tint(app)
|
||||
if app.perf_profiling_enabled:
|
||||
# ...
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run test**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=60`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add src/gui_2.py
|
||||
git commit -m "feat(hot-reload): Add visual error tint on reload failure"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: End-to-End Integration Test
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/test_hot_reload_integration.py`
|
||||
- Test: `tests/test_hot_reload_integration.py`
|
||||
|
||||
- [ ] **Step 1: Write e2e tests using live_gui fixture**
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from api_hook_client import ApiHookClient
|
||||
from simulation.sim_base import BaseSimulation, run_sim
|
||||
|
||||
class HotReloadSimulation(BaseSimulation):
|
||||
def run(self) -> None:
|
||||
# 1. Verify hot reload button is accessible
|
||||
# 2. Trigger hot reload via API hook
|
||||
# 3. Verify GUI still renders after reload
|
||||
# 4. Verify error tint clears on success
|
||||
pass
|
||||
|
||||
def test_hot_reload_e2e(live_gui):
|
||||
client = ApiHookClient(port=8999)
|
||||
# Trigger hot reload
|
||||
response = client.post("/api/gui", {"action": "hot_reload"})
|
||||
# Verify GUI still responds
|
||||
state = client.get("/api/gui_state")
|
||||
assert state is not None
|
||||
|
||||
def test_hot_reload_preserves_state(live_gui):
|
||||
client = ApiHookClient(port=8999)
|
||||
# Set some state
|
||||
# Trigger reload
|
||||
# Verify state preserved
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run e2e tests**
|
||||
|
||||
Run: `uv run pytest tests/test_hot_reload_integration.py -v --timeout=120`
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add tests/test_hot_reload_integration.py
|
||||
git commit -m "test(hot-reload): Add e2e integration test"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria Verification
|
||||
|
||||
1. Pressing `Ctrl+Alt+R` reloads `src.gui_2` without losing App state — tested via manual trigger + state verification
|
||||
2. GUI "Hot Reload" button triggers same reload — tested via button click in live_gui
|
||||
3. Failed reload shows error tint, preserves last-good state — tested via mock failure injection
|
||||
4. New hot modules can be added via `HotReloader.register()` without modifying core — tested in unit tests
|
||||
5. No performance impact when not reloading (< 1ms overhead per frame) — verified via perf profiling
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (Per Spec)
|
||||
|
||||
- Automatic file watching (manual trigger only)
|
||||
- Hot-reloading of C extensions or native code
|
||||
- Cross-platform reload support (Windows focus)
|
||||
Reference in New Issue
Block a user