Compare commits

..

8 Commits

6 changed files with 98 additions and 34 deletions

View File

@@ -8,7 +8,7 @@ This file tracks all major tracks for the project. Each track has its own detail
*The following tracks MUST be executed in this exact order to safely resolve tech debt before feature development.*
1. [ ] **Track: Hook API UI State Verification**
1. [x] **Track: Hook API UI State Verification**
*Link: [./tracks/hook_api_ui_state_verification_20260302/](./tracks/hook_api_ui_state_verification_20260302/)*
2. [ ] **Track: Asyncio Decoupling & Queue Refactor**

View File

@@ -2,37 +2,37 @@
> **TEST DEBT FIX:** This track replaces fragile `time.sleep()` and string-matching assertions in simulations (like `test_visual_sim_mma_v2.py`) with deterministic UI state queries. This is critical for stabilizing the test suite after the GUI decoupling.
## Phase 1: API Endpoint Implementation
- [ ] Task: Initialize MMA Environment `activate_skill mma-orchestrator`
- [ ] Task: Implement `/api/gui/state` GET Endpoint
- [ ] WHERE: `gui_2.py` (or `app_controller.py` if decoupled), inside `create_api()`.
- [ ] WHAT: Add a FastAPI route that serializes allowed UI state variables into JSON.
- [ ] HOW: Define a set of safe keys (e.g., `_gettable_fields`) and extract them from the App instance.
- [ ] SAFETY: Use thread-safe reads or deepcopies if accessing complex dictionaries.
- [ ] Task: Update `ApiHookClient`
- [ ] WHERE: `api_hook_client.py`
- [ ] WHAT: Add a `get_gui_state(self)` method that hits the new endpoint.
- [ ] HOW: Standard `requests.get`.
- [ ] SAFETY: Include error handling/timeouts.
- [ ] Task: Conductor - User Manual Verification 'Phase 1: API Endpoint' (Protocol in workflow.md)
## Phase 1: API Endpoint Implementation [checkpoint: 9967fbd]
- [x] Task: Initialize MMA Environment `activate_skill mma-orchestrator` [6b4c626]
- [x] Task: Implement `/api/gui/state` GET Endpoint [a783ee5]
- [x] WHERE: `gui_2.py` (or `app_controller.py` if decoupled), inside `create_api()`.
- [x] WHAT: Add a FastAPI route that serializes allowed UI state variables into JSON.
- [x] HOW: Define a set of safe keys (e.g., `_gettable_fields`) and extract them from the App instance.
- [x] SAFETY: Use thread-safe reads or deepcopies if accessing complex dictionaries.
- [x] Task: Update `ApiHookClient` [a783ee5]
- [x] WHERE: `api_hook_client.py`
- [x] WHAT: Add a `get_gui_state(self)` method that hits the new endpoint.
- [x] HOW: Standard `requests.get`.
- [x] SAFETY: Include error handling/timeouts.
- [x] Task: Conductor - User Manual Verification 'Phase 1: API Endpoint' (Protocol in workflow.md) [9967fbd]
## Phase 2: State Wiring & Integration Tests
- [ ] Task: Wire Critical UI States
- [ ] WHERE: `gui_2.py`
- [ ] WHAT: Ensure fields like `ui_focus_agent`, `active_discussion`, `_track_discussion_active` are included in the exposed state.
- [ ] HOW: Update the mapping definition.
- [ ] SAFETY: None.
- [ ] Task: Write `live_gui` Integration Tests
- [ ] WHERE: `tests/test_live_gui_integration.py`
- [ ] WHAT: Add a test that changes the provider/model or focus agent via actions, then asserts `client.get_gui_state()` reflects the change.
- [ ] HOW: Use `pytest` and `live_gui` fixture.
- [ ] SAFETY: Ensure robust wait conditions for GUI updates.
- [ ] Task: Conductor - User Manual Verification 'Phase 2: State Wiring & Tests' (Protocol in workflow.md)
## Phase 2: State Wiring & Integration Tests [checkpoint: 9967fbd]
- [x] Task: Wire Critical UI States [a783ee5]
- [x] WHERE: `gui_2.py`
- [x] WHAT: Ensure fields like `ui_focus_agent`, `active_discussion`, `_track_discussion_active` are included in the exposed state.
- [x] HOW: Update the mapping definition.
- [x] SAFETY: None.
- [x] Task: Write `live_gui` Integration Tests [a783ee5]
- [x] WHERE: `tests/test_live_gui_integration.py`
- [x] WHAT: Add a test that changes the provider/model or focus agent via actions, then asserts `client.get_gui_state()` reflects the change.
- [x] HOW: Use `pytest` and `live_gui` fixture.
- [x] SAFETY: Ensure robust wait conditions for GUI updates.
- [x] Task: Conductor - User Manual Verification 'Phase 2: State Wiring & Tests' (Protocol in workflow.md) [9967fbd]
## Phase 3: Final Validation
- [ ] Task: Full Suite Validation & Warning Cleanup
- [ ] WHERE: Project root
- [ ] WHAT: `uv run pytest`
- [ ] HOW: Ensure 100% pass rate.
- [ ] SAFETY: Ensure the hook server gracefully stops.
- [ ] Task: Conductor - User Manual Verification 'Phase 3: Final Validation' (Protocol in workflow.md)
## Phase 3: Final Validation [checkpoint: f42bee3]
- [x] Task: Full Suite Validation & Warning Cleanup [f42bee3]
- [x] WHERE: Project root
- [x] WHAT: `uv run pytest`
- [x] HOW: Ensure 100% pass rate.
- [x] SAFETY: Ensure the hook server gracefully stops.
- [x] Task: Conductor - User Manual Verification 'Phase 3: Final Validation' (Protocol in workflow.md) [f42bee3]

View File

@@ -84,6 +84,11 @@ class ApiHookClient:
"""Retrieves current MMA status (track, tickets, tier, etc.)"""
return self._make_request('GET', '/api/gui/mma_status')
def get_gui_state(self) -> dict | None:
"""Retrieves the current GUI state via /api/gui/state."""
resp = self._make_request("GET", "/api/gui/state")
return resp if resp else None
def push_event(self, event_type: str, payload: dict[str, Any]) -> dict[str, Any] | None:
"""Pushes an event to the GUI's AsyncEventQueue via the /api/gui endpoint."""
return self.post_gui({

View File

@@ -165,6 +165,27 @@ class HookHandler(BaseHTTPRequestHandler):
else:
self.send_response(504)
self.end_headers()
elif self.path == '/api/gui/state':
event = threading.Event()
result = {}
def get_state():
try:
gettable = _get_app_attr(app, "_gettable_fields", {})
for key, attr in gettable.items():
result[key] = _get_app_attr(app, attr, None)
finally: event.set()
lock = _get_app_attr(app, "_pending_gui_tasks_lock")
tasks = _get_app_attr(app, "_pending_gui_tasks")
if lock and tasks is not None:
with lock: tasks.append({"action": "custom_callback", "callback": get_state})
if event.wait(timeout=10):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps(result).encode("utf-8"))
else:
self.send_response(504)
self.end_headers()
else:
self.send_response(404)
self.end_headers()

View File

@@ -707,6 +707,13 @@ class AppController:
'manual_approve': 'ui_manual_approve'
}
self._gettable_fields = dict(self._settable_fields)
self._gettable_fields.update({
'ui_focus_agent': 'ui_focus_agent',
'active_discussion': 'active_discussion',
'_track_discussion_active': '_track_discussion_active'
})
self.hook_server = api_hooks.HookServer(app if app else self)
self.hook_server.start()
@@ -1004,6 +1011,15 @@ class AppController:
"""Returns the health status of the API."""
return {"status": "ok"}
@api.get("/api/gui/state", dependencies=[Depends(get_api_key)])
def get_gui_state() -> dict[str, Any]:
"""Returns the current GUI state for specific fields."""
gettable = getattr(self, "_gettable_fields", {})
state = {}
for key, attr in gettable.items():
state[key] = getattr(self, attr, None)
return state
@api.get("/status", dependencies=[Depends(get_api_key)])
def status() -> dict[str, Any]:
"""Returns the current status of the application."""

View File

@@ -4,6 +4,7 @@ import asyncio
import time
from gui_2 import App
from events import UserRequestEvent
from api_hook_client import ApiHookClient
@pytest.mark.timeout(10)
@pytest.mark.asyncio
@@ -38,7 +39,9 @@ async def test_user_request_integration_flow(mock_app: App) -> None:
mock_send.assert_called_once_with(
"Context", "Hello AI", ".", [], "History",
pre_tool_callback=ANY,
qa_callback=ANY
qa_callback=ANY,
stream=ANY,
stream_callback=ANY
)
# 4. Wait for the response to propagate to _pending_gui_tasks and update UI
# We call _process_pending_gui_tasks manually to simulate a GUI frame update.
@@ -85,3 +88,22 @@ async def test_user_request_error_handling(mock_app: App) -> None:
break
await asyncio.sleep(0.1)
assert success, f"Error state was not reflected in UI. status: {app.ai_status}, response: {app.ai_response}"
def test_api_gui_state_live(live_gui) -> None:
client = ApiHookClient()
client.set_value('current_provider', 'anthropic')
client.set_value('current_model', 'claude-3-haiku-20240307')
start_time = time.time()
success = False
while time.time() - start_time < 10:
state = client.get_gui_state()
if state and state.get('current_provider') == 'anthropic' and state.get('current_model') == 'claude-3-haiku-20240307':
success = True
break
time.sleep(0.5)
assert success, f"GUI state did not update. Got: {client.get_gui_state()}"
final_state = client.get_gui_state()
assert final_state['current_provider'] == 'anthropic'
assert final_state['current_model'] == 'claude-3-haiku-20240307'