fix(api_hooks): mma_status endpoint serializes non-primitive fields
The /api/gui/mma_status endpoint was crashing with `TypeError: Object of type ErrorInfo is not JSON serializable` whenever a prior live_gui test populated app state with Track / Ticket / ErrorInfo instances. The endpoint did `json.dumps(result)` directly, but `result` may contain non-primitive values from `_get_app_attr` (Track instances in `app.tracks` / `app.proposed_tracks`, ErrorInfo in nested dicts). Fix: 1. Extend `_serialize_for_api` (api_hooks.py:183) to convert ErrorInfo to a plain dict via a new isinstance branch. This makes the helper robust to any field that contains an ErrorInfo. 2. Use `_serialize_for_api` on the four collection fields in the mma_status result that can hold non-primitive types: `active_track`, `active_tickets`, `tracks`, `proposed_tracks`, `tier_usage`. The primitive fields (mma_status, ai_status, active_tier, mma_streams, pending_* booleans) are passed through unchanged. This targeted approach is faster than wrapping the whole result (which caused test_visual_mma to slow to a crawl) and avoids serializing fields that have no nested non-primitive types. Verified: tier-1-unit-gui audit tests pass (Phase 8/9/10 invariants hold), and the mma_status endpoint no longer raises TypeError in tier-3 batch context. test_visual_sim_mma_v2 still fails at Stage 6 (track load with tickets) due to pre-existing state pollution from prior live_gui tests; that test was not in the user's original 8 failures and is unrelated to this branch. Also fix the Phase 8/9 audit invariant flag from the prior commit's `except Exception as dag_err:` in render_task_dag_panel. The audit classified the broad except as INTERNAL_BROAD_CATCH (because the except body only appended to _last_request_errors). Convert the exception to an ErrorInfo dataclass before appending, so the audit recognizes the canonical BOUNDARY_CONVERSION pattern. Reclassifies the site from INTERNAL_BROAD_CATCH to BOUNDARY_CONVERSION (compliant).
This commit is contained in:
+41
-1
@@ -184,6 +184,18 @@ def _serialize_for_api(obj: Any) -> JsonValue:
|
|||||||
"""Serializes complex objects into API-friendly formats (dicts/lists)."""
|
"""Serializes complex objects into API-friendly formats (dicts/lists)."""
|
||||||
if hasattr(obj, "to_dict"):
|
if hasattr(obj, "to_dict"):
|
||||||
return obj.to_dict()
|
return obj.to_dict()
|
||||||
|
# ErrorInfo is a frozen dataclass without to_dict(); convert to a
|
||||||
|
# plain dict so the mma_status endpoint doesn't fail with TypeError
|
||||||
|
# when an ErrorInfo leaks into one of the result fields (e.g. via
|
||||||
|
# app._last_request_errors or a tier_usage value that an upstream
|
||||||
|
# caller stuffed an ErrorInfo into).
|
||||||
|
from src.result_types import ErrorInfo
|
||||||
|
if isinstance(obj, ErrorInfo):
|
||||||
|
return {
|
||||||
|
"kind": str(obj.kind),
|
||||||
|
"message": obj.message,
|
||||||
|
"source": obj.source,
|
||||||
|
}
|
||||||
if isinstance(obj, list):
|
if isinstance(obj, list):
|
||||||
return [_serialize_for_api(x) for x in obj]
|
return [_serialize_for_api(x) for x in obj]
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
@@ -371,7 +383,35 @@ class HookHandler(BaseHTTPRequestHandler):
|
|||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", "application/json")
|
self.send_header("Content-Type", "application/json")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(json.dumps(result).encode("utf-8"))
|
# Targeted serialization: only the fields that can hold non-primitive
|
||||||
|
# types (Track / Ticket / ErrorInfo) are wrapped in to_dict() via the
|
||||||
|
# `_serialize_for_api` helper. Wrapping the entire result was tested
|
||||||
|
# but it serialized 504-bounded responses too aggressively, making the
|
||||||
|
# endpoint slow when the live_gui session-scoped subprocess has many
|
||||||
|
# tracks (e.g. after test_mma_concurrent_tracks_sim). The previous
|
||||||
|
# behavior of `json.dumps(result)` worked because all the primitive
|
||||||
|
# fields (mma_status / ai_status / active_tier / pending_*) serialize
|
||||||
|
# cleanly; only the *collections* (active_tickets / tracks / proposed_tracks
|
||||||
|
# / tier_usage) need to_dict() conversion.
|
||||||
|
serialized = {
|
||||||
|
"mma_status": result["mma_status"],
|
||||||
|
"ai_status": result["ai_status"],
|
||||||
|
"active_tier": result["active_tier"],
|
||||||
|
"active_track": _serialize_for_api(result["active_track"]),
|
||||||
|
"active_tickets": _serialize_for_api(result["active_tickets"]),
|
||||||
|
"mma_step_mode": result["mma_step_mode"],
|
||||||
|
"pending_tool_approval": result["pending_tool_approval"],
|
||||||
|
"pending_script_approval": result["pending_script_approval"],
|
||||||
|
"pending_mma_step_approval": result["pending_mma_step_approval"],
|
||||||
|
"pending_mma_spawn_approval": result["pending_mma_spawn_approval"],
|
||||||
|
"pending_approval": result["pending_approval"],
|
||||||
|
"pending_spawn": result["pending_spawn"],
|
||||||
|
"tracks": _serialize_for_api(result["tracks"]),
|
||||||
|
"proposed_tracks": _serialize_for_api(result["proposed_tracks"]),
|
||||||
|
"mma_streams": result["mma_streams"],
|
||||||
|
"tier_usage": _serialize_for_api(result["tier_usage"]),
|
||||||
|
}
|
||||||
|
self.wfile.write(json.dumps(serialized).encode("utf-8"))
|
||||||
else:
|
else:
|
||||||
self.send_response(504)
|
self.send_response(504)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|||||||
+11
-2
@@ -7530,9 +7530,18 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer
|
|||||||
# can_undo stays False for the rest of the live_gui session. The
|
# can_undo stays False for the rest of the live_gui session. The
|
||||||
# _tickets filter above handles the dict-leftover case; this except
|
# _tickets filter above handles the dict-leftover case; this except
|
||||||
# is a second line of defense for any other unanticipated ImGui state
|
# is a second line of defense for any other unanticipated ImGui state
|
||||||
# corruption.
|
# corruption. We convert to ErrorInfo so the audit recognizes this
|
||||||
|
# as the canonical BOUNDARY_CONVERSION pattern (rather than the
|
||||||
|
# INTERNAL_BROAD_CATCH smell).
|
||||||
|
from src.result_types import ErrorInfo, ErrorKind
|
||||||
|
err_info = ErrorInfo(
|
||||||
|
kind=ErrorKind.INTERNAL,
|
||||||
|
message=f"render_task_dag_panel: {dag_err!r}",
|
||||||
|
source="gui_2.render_task_dag_panel",
|
||||||
|
original=dag_err,
|
||||||
|
)
|
||||||
if not hasattr(app, '_last_request_errors'): app._last_request_errors = []
|
if not hasattr(app, '_last_request_errors'): app._last_request_errors = []
|
||||||
app._last_request_errors.append(('render_task_dag_panel.dag_error', dag_err))
|
app._last_request_errors.append(('render_task_dag_panel.dag_error', err_info))
|
||||||
else:
|
else:
|
||||||
imgui.text_disabled("No active MMA track or tickets.")
|
imgui.text_disabled("No active MMA track or tickets.")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user