diff --git a/src/gui_2.py b/src/gui_2.py index faa32b35..4b41194e 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -3392,10 +3392,10 @@ def render_persona_editor_window(app: App, is_embedded: bool = False) -> None: imgui.separator() if imgui.button("Save##pers", imgui.ImVec2(100, 0)): if app._editing_persona_name.strip(): - try: - import copy; persona = models.Persona(name=app._editing_persona_name.strip(), system_prompt=app._editing_persona_system_prompt, tool_preset=app._editing_persona_tool_preset_id or None, bias_profile=app._editing_persona_bias_profile_id or None, context_preset=app._editing_persona_context_preset_id or None, aggregation_strategy=app._editing_persona_aggregation_strategy or None, preferred_models=copy.deepcopy(app._editing_persona_preferred_models_list)) - app.controller._cb_save_persona(persona, getattr(app, '_editing_persona_scope', 'project')); app.ai_status = f"Saved: {persona.name}" - except Exception as e: app.ai_status = f"Error: {e}" + result = _render_persona_editor_save_result(app) + if not result.ok: + if not hasattr(app, '_last_request_errors'): app._last_request_errors = [] + app._last_request_errors.append(("render_persona_editor.save", result.errors[0])) else: app.ai_status = "Name required" imgui.same_line(); if imgui.button("Delete##pers", imgui.ImVec2(100, 0)): @@ -7633,4 +7633,46 @@ def _handle_history_logic_result(app: "App") -> Result[bool]: #endregion: Phase 3 Render-Loop Result Helpers +#region: Phase 4 Modal/Dialog Result Helpers (result_migration_gui_2_20260619) + +def _render_persona_editor_save_result(app: "App") -> Result[bool]: + """Drain-aware variant of L3398 render_persona_editor_window Save button try/except. + + Extracts the models.Persona(...) construction + app.controller._cb_save_persona + try/except from the Save button handler in render_persona_editor_window into a + Result-returning helper. On success, sets app.ai_status to "Saved: " + and returns Result(data=True). On failure (any exception in Persona + construction or _cb_save_persona), sets app.ai_status to an error message + and returns Result(data=False, errors=[ErrorInfo]). + + The legacy wrapper drains errors to app._last_request_errors (per FR-BC-3 + modal pattern: data plane attribute; the modal itself is the UI surface). + + [C: src/gui_2.py:render_persona_editor_window (L3398 legacy wrapper)] + """ + try: + import copy + persona = models.Persona( + name=app._editing_persona_name.strip(), + system_prompt=app._editing_persona_system_prompt, + tool_preset=app._editing_persona_tool_preset_id or None, + bias_profile=app._editing_persona_bias_profile_id or None, + context_preset=app._editing_persona_context_preset_id or None, + aggregation_strategy=app._editing_persona_aggregation_strategy or None, + preferred_models=copy.deepcopy(app._editing_persona_preferred_models_list), + ) + app.controller._cb_save_persona(persona, getattr(app, '_editing_persona_scope', 'project')) + app.ai_status = f"Saved: {persona.name}" + return Result(data=True) + except Exception as e: + app.ai_status = f"Error: {e}" + return Result(data=False, errors=[ErrorInfo( + kind=ErrorKind.INTERNAL, + message=f"Persona save failed: {e}", + source="gui_2._render_persona_editor_save_result", + original=e, + )]) + +#endregion: Phase 4 Modal/Dialog Result Helpers + #endregion: MMA diff --git a/tests/test_gui_2_result.py b/tests/test_gui_2_result.py index bdb793db..eaa459d5 100644 --- a/tests/test_gui_2_result.py +++ b/tests/test_gui_2_result.py @@ -604,4 +604,57 @@ def test_phase_3_l4848_render_warmup_status_indicator_result_failure(): assert result.errors, "Expected at least one error on failure" err = result.errors[0] assert err.source == "gui_2._render_warmup_status_indicator_result" - assert "warmup backend down" in err.message \ No newline at end of file +def test_phase_4_l3398_render_persona_editor_save_result_success(): + """ + L3398 _render_persona_editor_save_result returns Result.ok=True on success. + + The helper wraps the Save button try/except in render_persona_editor_window + (Persona creation: models.Persona(...) + _cb_save_persona). On success, + sets app.ai_status to "Saved: " and returns Result(data=True). + """ + from src import gui_2 + from unittest.mock import MagicMock, patch + app = MagicMock() + app._editing_persona_name = "test_persona" + app._editing_persona_system_prompt = "you are a helper" + app._editing_persona_tool_preset_id = "" + app._editing_persona_bias_profile_id = "" + app._editing_persona_context_preset_id = "" + app._editing_persona_aggregation_strategy = "" + app._editing_persona_preferred_models_list = [] + mock_persona = MagicMock(name="mock_persona") + mock_persona.name = "test_persona" + with patch.object(gui_2.models, "Persona", return_value=mock_persona): + result = gui_2._render_persona_editor_save_result(app) + assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" + assert result.data is True + assert "Saved: test_persona" in app.ai_status + + +def test_phase_4_l3398_render_persona_editor_save_result_failure(): + """ + L3398 _render_persona_editor_save_result returns Result.ok=False on failure. + + When Persona construction or _cb_save_persona raises, the helper sets + app.ai_status to an error message and returns Result(data=False, + errors=[ErrorInfo]). The legacy wrapper should drain to + app._last_request_errors (per FR-BC-3 modal pattern). + """ + from src import gui_2 + from unittest.mock import MagicMock, patch + app = MagicMock() + app._editing_persona_name = "test_persona" + app._editing_persona_system_prompt = "you are a helper" + app._editing_persona_tool_preset_id = "" + app._editing_persona_bias_profile_id = "" + app._editing_persona_context_preset_id = "" + app._editing_persona_aggregation_strategy = "" + app._editing_persona_preferred_models_list = [] + with patch.object(gui_2.models, "Persona", side_effect=RuntimeError("validation failed")): + result = gui_2._render_persona_editor_save_result(app) + assert not result.ok, f"Expected ok=False on failure, got data: {result.data}" + assert result.errors, "Expected at least one error on failure" + err = result.errors[0] + assert err.source == "gui_2._render_persona_editor_save_result" + assert "validation failed" in err.message + assert "Error:" in app.ai_status