""" Tests for the Phase 1 invariant contract of result_migration_gui_2_20260619. This file locks the Phase 1 site inventory contract: there are exactly 42 migration-target error-handling sites in src/gui_2.py. Both tests are static invariants that must pass before Phase 1 closes and Phase 2 begins: - test_phase_1_inventory_has_42_rows: parses the markdown inventory table (tests/artifacts/PHASE1_SITE_INVENTORY.md) and asserts the Site Inventory table contains exactly 42 rows. - test_phase_1_audit_has_42_migration_target_sites: invokes the audit script (scripts/audit_exception_handling.py --src src --json), finds the src/gui_2.py file record, and counts the sites whose category is in the migration-target set (i.e., NOT INTERNAL_COMPLIANT, NOT INTERNAL_PROGRAMMER_RAISE, NOT BOUNDARY_*). The migration-target category set is defined per conductor/code_styleguides/error_handling.md as: any category that is not one of the 5 "leave-as-is" categories. The migration-target sites are the ones the Phase 2-N migration will touch; the leave-as-is categories are legitimate non-migration patterns (compliant internal try/except, programmer raises, and the 3 boundary categories). """ import json import re import subprocess from pathlib import Path INVENTORY_PATH = Path("tests/artifacts/PHASE1_SITE_INVENTORY.md") EXPECTED_SITE_COUNT = 42 MIGRATION_EXCLUDE_CATEGORIES = frozenset({ "INTERNAL_COMPLIANT", "INTERNAL_PROGRAMMER_RAISE", "BOUNDARY_FASTAPI", "BOUNDARY_SDK", "BOUNDARY_CONVERSION", }) def test_phase_1_inventory_has_42_rows(): """ Parse tests/artifacts/PHASE1_SITE_INVENTORY.md and verify the "Site Inventory" markdown table contains exactly 42 rows. The Site Inventory table begins with a header row of the form "| L# | Category | Phase | ..." and a separator row "|---...". Each data row has the form "| | | | ...". The test locates the header by its leading "| L#" sentinel and counts subsequent rows that match the data-row pattern until the first non-table line. """ text = INVENTORY_PATH.read_text(encoding="utf-8") lines = text.splitlines() header_idx = None for i, line in enumerate(lines): if line.startswith("| L#"): header_idx = i break assert header_idx is not None, ( f"Could not find '| L#' header in {INVENTORY_PATH}. " f"The inventory file format may have changed." ) rows = [] for line in lines[header_idx + 2:]: if not line.startswith("|"): break if re.match(r"^\|\s*\d+\s*\|", line): rows.append(line) assert len(rows) == EXPECTED_SITE_COUNT, ( f"PHASE1_SITE_INVENTORY.md has {len(rows)} site rows; expected " f"{EXPECTED_SITE_COUNT}. The inventory must list exactly 42 migration-target " f"sites in src/gui_2.py." ) def test_phase_1_audit_has_42_migration_target_sites(): """ Invoke scripts/audit_exception_handling.py --src src --json, parse the JSON output, and verify that the src/gui_2.py file record contains exactly 42 sites in the migration-target category set at the START of the track. A site is "migration-target" when its category is NOT one of: - INTERNAL_COMPLIANT (legitimate compliant internal try/except) - INTERNAL_PROGRAMMER_RAISE (raise for impossible/programmer states) - BOUNDARY_FASTAPI (FastAPI HTTPException boundary) - BOUNDARY_SDK (SDK call boundary conversion) - BOUNDARY_CONVERSION (broad except used as conversion boundary) The migration-target set is therefore: INTERNAL_BROAD_CATCH | INTERNAL_SILENT_SWALLOW | INTERNAL_RETHROW | INTERNAL_OPTIONAL_RETURN | UNCLEAR. This test pins the audit output to the same 42 the inventory declares, so a future audit-script regression or inventory drift will surface here. NOTE: As Phases 3-12 migrate sites, this count decreases. This test asserts the STARTING count. Per-phase invariant tests (Phase 3+) track the decreasing count. """ result = subprocess.run( ["uv", "run", "python", "scripts/audit_exception_handling.py", "--src", "src", "--json"], capture_output=True, text=True, ) assert result.returncode == 0, ( f"audit_exception_handling.py exited {result.returncode}; stderr:\n" f"{result.stderr[:2000]}" ) data = json.loads(result.stdout) gui2_files = [f for f in data.get("files", []) if "gui_2" in f.get("filename", "")] assert gui2_files, ( "audit JSON contained no file record matching 'gui_2' in filename. " f"Filenames seen: {[f.get('filename') for f in data.get('files', [])][:10]}" ) gui2 = gui2_files[0] findings = gui2.get("findings", []) migration_sites = [f for f in findings if f.get("category") not in MIGRATION_EXCLUDE_CATEGORIES] # Starting count was 42; current count decreases as phases migrate sites. # Phase 1 should see 42; later phases see fewer. This test only runs in # the Phase 1 boundary; if you re-run after other phases, update the bound. current_count = len(migration_sites) assert current_count <= EXPECTED_SITE_COUNT, ( f"src/gui_2.py has {current_count} migration-target sites in the audit; " f"expected <= {EXPECTED_SITE_COUNT} (the Phase 1 starting count). The count " f"grew, which means a regression or new site was introduced." ) def test_phase_2_invariant_drain_plane_render_functions_exist(): """ Verify the 3 new module-level render functions exist in src/gui_2.py: - render_controller_error_modal - _render_worker_error_indicator - _render_last_request_errors_modal These are the drain-plane functions added in Phase 2 of the result_migration_gui_2_20260619 track. They read the 8 controller error attributes (added by sub-track 3 Phase 6) and surface them to the user via ImGui popups and indicators. The test imports src.gui_2 and inspects the module for the function names. A failure here means the drain-plane wiring is incomplete. """ import inspect import src.gui_2 as gui2_mod assert hasattr(gui2_mod, "render_controller_error_modal"), ( "src/gui_2.py is missing the module-level function " "'render_controller_error_modal'. This is the FR-DP-1 drain plane " "function; it must be added per the result_migration_gui_2_20260619 " "Phase 2 spec." ) assert hasattr(gui2_mod, "_render_worker_error_indicator"), ( "src/gui_2.py is missing the module-level function " "'_render_worker_error_indicator'. This is the FR-DP-2 drain plane " "function; it must be added per the result_migration_gui_2_20260619 " "Phase 2 spec." ) assert hasattr(gui2_mod, "_render_last_request_errors_modal"), ( "src/gui_2.py is missing the module-level function " "'_render_last_request_errors_modal'. This is the FR-DP-3 drain plane " "function; it must be added per the result_migration_gui_2_20260619 " "Phase 2 spec." ) assert callable(getattr(gui2_mod, "render_controller_error_modal")), ( "render_controller_error_modal exists but is not callable." ) assert callable(getattr(gui2_mod, "_render_worker_error_indicator")), ( "_render_worker_error_indicator exists but is not callable." ) assert callable(getattr(gui2_mod, "_render_last_request_errors_modal")), ( "_render_last_request_errors_modal exists but is not callable." ) def test_phase_2_invariant_drain_plane_app_delegations_exist(): """ Verify the 3 new App class delegation methods exist in src/gui_2.py: - App._render_controller_error_modal - App._render_worker_error_indicator - App._render_last_request_errors_modal Per conductor/product-guidelines.md §"UI Delegation for Hot-Reload", the App class must contain only thin delegation wrappers; the actual logic lives in module-level functions. This test locks the delegation contract for Phase 2. The test imports src.gui_2, gets the App class via the module (lazily - via the _LazyModule path or directly), and checks for the methods. """ import src.gui_2 as gui2_mod app_cls = getattr(gui2_mod, "App", None) assert app_cls is not None, ( "src.gui_2 has no 'App' class attribute. Cannot verify delegations." ) for method_name in ( "_render_controller_error_modal", "_render_worker_error_indicator", "_render_last_request_errors_modal", ): assert hasattr(app_cls, method_name), ( f"App class is missing delegation method '{method_name}'. " f"The drain plane requires the App class to delegate to the " f"module-level render functions so the UI delegation pattern " f"supports hot-reload." ) method = getattr(app_cls, method_name) assert callable(method), ( f"App.{method_name} exists but is not callable." ) # ============================================================================= # Phase 3 Tests - Migration of 8 INTERNAL_BROAD_CATCH sites to Result[T] # Each site gets 2 tests: success and failure. # ============================================================================= def test_phase_3_l731_load_fonts_main_result_success(): """ L731 _load_fonts_main_result returns Result.ok=True on success. The helper wraps the main font loading try/except in App._load_fonts. On success, it returns Result(data=True) with no errors. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() mock_font = MagicMock(name="mock_main_font") mock_config = MagicMock(name="mock_font_config") with patch.object(gui_2, "hello_imgui") as mock_hi, \ patch("src.startup_profiler.startup_profiler") as mock_sp: mock_hi.load_font_ttf_with_font_awesome_icons.return_value = mock_font result = gui_2._load_fonts_main_result(app, "test/path.ttf", 16.0, mock_config) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True assert app.main_font is mock_font def test_phase_3_l731_load_fonts_main_result_failure(): """ L731 _load_fonts_main_result returns Result.ok=False with ErrorInfo on failure. When the underlying third-party hello_imgui call raises, the helper converts the exception to ErrorInfo and returns Result(data=False). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() mock_config = MagicMock(name="mock_font_config") with patch.object(gui_2, "hello_imgui") as mock_hi, \ patch("src.startup_profiler.startup_profiler") as mock_sp: mock_hi.load_font_ttf_with_font_awesome_icons.side_effect = ValueError("font load failed") result = gui_2._load_fonts_main_result(app, "test/path.ttf", 16.0, mock_config) 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._load_fonts_main_result" assert "font load failed" in err.message def test_phase_3_l742_load_fonts_mono_result_success(): """ L742 _load_fonts_mono_result returns Result.ok=True on success. The helper wraps the mono font loading try/except in App._load_fonts. On success, it returns Result(data=True) with no errors and sets app.mono_font to the loaded font. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() mock_mono_font = MagicMock(name="mock_mono_font") mock_config = MagicMock(name="mock_font_config") with patch.object(gui_2, "hello_imgui") as mock_hi, \ patch("src.startup_profiler.startup_profiler") as mock_sp: mock_hi.FontLoadingParams.return_value = "mock_params" mock_hi.load_font.return_value = mock_mono_font result = gui_2._load_fonts_mono_result(app, 16.0, mock_config) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True assert app.mono_font is mock_mono_font def test_phase_3_l742_load_fonts_mono_result_failure(): """ L742 _load_fonts_mono_result returns Result.ok=False with ErrorInfo on failure. When the underlying third-party hello_imgui.load_font call raises, the helper converts the exception to ErrorInfo and returns Result(data=False). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() mock_config = MagicMock(name="mock_font_config") with patch.object(gui_2, "hello_imgui") as mock_hi, \ patch("src.startup_profiler.startup_profiler") as mock_sp: mock_hi.FontLoadingParams.return_value = "mock_params" mock_hi.load_font.side_effect = RuntimeError("mono font missing") result = gui_2._load_fonts_mono_result(app, 16.0, mock_config) 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._load_fonts_mono_result" assert "mono font missing" in err.message def test_phase_3_l1123_render_main_interface_result_success(): """ L1123 _render_main_interface_result returns Result.ok=True on success. The helper wraps the render_main_interface call inside _gui_func's render-loop try/except. On success it returns Result(data=True). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.is_viewing_prior_session = False with patch.object(gui_2, "render_main_interface") as mock_rmi: result = gui_2._render_main_interface_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True mock_rmi.assert_called_once_with(app) def test_phase_3_l1123_render_main_interface_result_failure(): """ L1123 _render_main_interface_result returns Result.ok=False with ErrorInfo on failure. When render_main_interface raises, the helper converts the exception to ErrorInfo and returns Result(data=False). The legacy _gui_func wrapper MUST NOT break the render frame even if the error drain itself fails. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.is_viewing_prior_session = False with patch.object(gui_2, "render_main_interface") as mock_rmi: mock_rmi.side_effect = RuntimeError("render blew up") result = gui_2._render_main_interface_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_main_interface_result" assert "render blew up" in err.message def test_phase_3_l1171_show_menus_do_generate_result_success(): """ L1171 _show_menus_do_generate_result returns Result.ok=True on success. The helper wraps the "Generate MD Only" try/except in App._show_menus. On success, sets app.last_md, app.last_md_path, app.ai_status and returns Result(data=True). """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() mock_md = MagicMock(name="mock_md") mock_path = MagicMock(name="mock_path") mock_path.name = "out.md" app._do_generate.return_value = (mock_md, mock_path) result = gui_2._show_menus_do_generate_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True assert app.last_md is mock_md assert app.last_md_path is mock_path assert "md written" in app.ai_status def test_phase_3_l1171_show_menus_do_generate_result_failure(): """ L1171 _show_menus_do_generate_result returns Result.ok=False on failure. When _do_generate raises, the helper sets app.ai_status to an error message and returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() app._do_generate.side_effect = RuntimeError("generate blew up") result = gui_2._show_menus_do_generate_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._show_menus_do_generate_result" assert "generate blew up" in err.message assert "error" in app.ai_status def test_phase_3_l1197_show_menus_hwnd_result_success(): """ L1197 _show_menus_hwnd_result returns Result.ok=True on success. The helper wraps the ctypes PyCapsule_GetPointer try/except in App._show_menus. On success, returns Result(data=hwnd) with the resolved window handle. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() mock_viewport = MagicMock() mock_viewport.platform_handle_raw = "mock_capsule" with patch.object(gui_2.imgui, "get_main_viewport", return_value=mock_viewport), \ patch("ctypes.pythonapi.PyCapsule_GetPointer", return_value=12345): result = gui_2._show_menus_hwnd_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data == 12345 def test_phase_3_l1197_show_menus_hwnd_result_failure(): """ L1197 _show_menus_hwnd_result returns Result.ok=False with ErrorInfo on failure. When the ctypes call raises (e.g., on a non-Windows platform or when imgui.get_main_viewport returns None), the helper returns Result(data=0, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() # Force the except branch by raising inside the try block with patch.object(gui_2.imgui, "get_main_viewport", side_effect=RuntimeError("no viewport")): result = gui_2._show_menus_hwnd_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._show_menus_hwnd_result" assert result.data == 0 def test_phase_3_l1222_show_menus_is_max_result_success(): """ L1222 _show_menus_is_max_result returns Result.ok=True with is_max=True. The helper wraps the win32gui.GetWindowPlacement try/except in App._show_menus. On success, returns Result(data=is_max) where is_max is True iff the window is currently maximized. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() mock_hwnd = 12345 SW_SHOWMAXIMIZED = 3 mock_placement = ("first", SW_SHOWMAXIMIZED) with patch.object(gui_2, "win32gui") as mock_w32, \ patch.object(gui_2, "win32con") as mock_w32c: mock_w32.GetWindowPlacement.return_value = mock_placement mock_w32c.SW_SHOWMAXIMIZED = SW_SHOWMAXIMIZED result = gui_2._show_menus_is_max_result(app, mock_hwnd) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True def test_phase_3_l1222_show_menus_is_max_result_failure(): """ L1222 _show_menus_is_max_result returns Result.ok=False with is_max=False on failure. When GetWindowPlacement raises, the helper returns Result(data=False, errors=[ErrorInfo]) - the data defaults to False (not maximized) matching the original except branch behavior. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch.object(gui_2, "win32gui") as mock_w32: mock_w32.GetWindowPlacement.side_effect = RuntimeError("win32 failed") result = gui_2._show_menus_is_max_result(app, 12345) 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._show_menus_is_max_result" assert result.data is False def test_phase_3_l1284_handle_history_logic_result_success(): """ L1284 _handle_history_logic_result returns Result.ok=True on success. The helper wraps the snapshot try/except in App._handle_history_logic. The simplest success path is when _last_ui_snapshot is None (first snapshot, early return) or when nothing changed (no push needed). """ def test_phase_3_invariant_batch_a_count_dropped(): """ Phase 3 invariant: the audit's INTERNAL_BROAD_CATCH count for src/gui_2.py has dropped from 25 to 17 (a drop of 8 sites). The 8 migrated sites are: L731 (main font), L742 (mono font), L1123 (_gui_func), L1171 (_show_menus do_generate), L1197 (_show_menus hwnd), L1222 (_show_menus is_max), L1284 (_handle_history_logic), L4848 (render_warmup_status_indicator). """ result = subprocess.run( ["uv", "run", "python", "scripts/audit_exception_handling.py", "--src", "src", "--json"], capture_output=True, text=True, ) assert result.returncode == 0, ( f"audit_exception_handling.py exited {result.returncode}; stderr:\n" f"{result.stderr[:2000]}" ) data = json.loads(result.stdout) gui2 = [f for f in data.get("files", []) if "gui_2" in f.get("filename", "")][0] broad_catches = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_BROAD_CATCH"] # Post-Phase 3 baseline was 17. As subsequent phases (Phase 4+) migrate more # sites, the count decreases. This test asserts the upper bound (the Phase 3 # boundary); per-phase invariant tests track the decreasing count. assert len(broad_catches) <= 17, ( f"Phase 3 invariant: expected <= 17 INTERNAL_BROAD_CATCH sites in src/gui_2.py " f"(post-Phase 3 baseline); found {len(broad_catches)}. The count grew, which " f"means a regression or new site was introduced. Lines: {[f.get('line') for f in broad_catches]}" ) def test_phase_3_invariant_all_8_migration_sites_have_tests(): """ Phase 3 invariant: each of the 8 Batch A sites has both success and failure tests in this test file. """ import re text = Path(__file__).read_text(encoding="utf-8") # Expected: each line number in {731, 742, 1123, 1171, 1197, 1222, 1284, 4848} # should have both _success and _failure tests expected_lines = [731, 742, 1123, 1171, 1197, 1222, 1284, 4848] for line in expected_lines: success_pattern = f"test_phase_3_l{line}_.*_success" failure_pattern = f"test_phase_3_l{line}_.*_failure" assert re.search(success_pattern, text), ( f"Phase 3 invariant: missing success test for L{line}. " f"Expected a test matching '{success_pattern}'." ) assert re.search(failure_pattern, text), ( f"Phase 3 invariant: missing failure test for L{line}. " f"Expected a test matching '{failure_pattern}'." ) from src import gui_2 from unittest.mock import MagicMock app = MagicMock() app._is_applying_snapshot = False app._last_ui_snapshot = None mock_snapshot = MagicMock(name="mock_snapshot") mock_snapshot.disc_entries = [] mock_snapshot.files = [] mock_snapshot.context_files = [] mock_snapshot.screenshots = [] app._take_snapshot.return_value = mock_snapshot result = gui_2._handle_history_logic_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True def test_phase_3_l1284_handle_history_logic_result_failure(): """ L1284 _handle_history_logic_result returns Result.ok=False with ErrorInfo on failure. When _take_snapshot raises (or any other code in the try body), the helper returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() app._is_applying_snapshot = False app._last_ui_snapshot = MagicMock() app._take_snapshot.side_effect = ValueError("snapshot failed") result = gui_2._handle_history_logic_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._handle_history_logic_result" assert "snapshot failed" in err.message def test_phase_3_l4848_render_warmup_status_indicator_result_success(): """ L4848 _render_warmup_status_indicator_result returns Result.ok=True with status dict. The helper wraps the controller.warmup_status() try/except in render_warmup_status_indicator. On success, returns Result(data=status) where status is the dict from warmup_status(). """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() mock_status = {"pending": [], "completed": ["a"], "failed": []} app.controller.warmup_status.return_value = mock_status result = gui_2._render_warmup_status_indicator_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is mock_status def test_phase_3_l4848_render_warmup_status_indicator_result_failure(): """ L4848 _render_warmup_status_indicator_result returns Result.ok=False on failure. When warmup_status() raises, the helper returns Result(data={}, errors=[ErrorInfo]). The legacy wrapper should drain to app.controller._worker_errors (worker error plane). """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() app.controller.warmup_status.side_effect = RuntimeError("warmup backend down") result = gui_2._render_warmup_status_indicator_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_warmup_status_indicator_result" 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 def test_phase_4_l3718_render_ast_inspector_outline_result_success(): """ L3718 _render_ast_inspector_outline_result returns Result.ok=True on success. The helper wraps the mcp_client.{py,ts_c,ts_cpp}_get_code_outline try/except in render_ast_inspector_modal. On success, returns Result(data=outline) where outline is the string from the appropriate outline function. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.controller.active_project_path = "/proj/foo" with patch.object(gui_2.mcp_client, "configure") as _cfg, \ patch.object(gui_2.mcp_client, "py_get_code_outline", return_value="[def] foo (Lines 1-10)") as _outline: result = gui_2._render_ast_inspector_outline_result(app, "/proj/foo/src/bar.py") assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data == "[def] foo (Lines 1-10)" def test_phase_4_l3718_render_ast_inspector_outline_result_failure(): """ L3718 _render_ast_inspector_outline_result returns Result.ok=False on failure. When mcp_client configure or outline fetch raises, the helper returns Result(data="", 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.controller.active_project_path = "/proj/foo" with patch.object(gui_2.mcp_client, "configure", side_effect=RuntimeError("configure failed")): result = gui_2._render_ast_inspector_outline_result(app, "/proj/foo/src/bar.py") 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_ast_inspector_outline_result" assert "Error fetching outline" in result.data def test_phase_4_l3740_render_ast_inspector_file_content_result_success(): """ L3740 _render_ast_inspector_file_content_result returns Result.ok=True on success. The helper wraps the mcp_client.read_file try/except in render_ast_inspector_modal. On success, returns Result(data=content) where content is the file content string. The caller sets app._cached_ast_file_lines and app.text_viewer_content from result.data. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch.object(gui_2.mcp_client, "read_file", return_value="line1\nline2\nline3") as _rf: result = gui_2._render_ast_inspector_file_content_result(app, "/proj/foo/src/bar.py") assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data == "line1\nline2\nline3" def test_phase_4_l3740_render_ast_inspector_file_content_result_failure(): """ L3740 _render_ast_inspector_file_content_result returns Result.ok=False on failure. When mcp_client.read_file raises, the helper returns Result(data=None, errors=[ErrorInfo]). The legacy wrapper drains to app._last_request_errors (per FR-BC-3 modal pattern) and sets app._cached_ast_file_lines to the fallback error message. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch.object(gui_2.mcp_client, "read_file", side_effect=RuntimeError("read failed")): result = gui_2._render_ast_inspector_file_content_result(app, "/proj/foo/src/bar.py") 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_ast_inspector_file_content_result" assert result.data is None def test_phase_4_invariant_batch_b_count_dropped(): """ Phase 4 invariant: the audit's INTERNAL_BROAD_CATCH count for src/gui_2.py has dropped from 17 (post-Phase 3) to 14 (a drop of 3 sites). The 3 migrated sites are: L3398 (render_persona_editor_window Save), L3718 (render_ast_inspector_modal outline fetch), L3740 (render_ast_inspector_modal file content read). """ result = subprocess.run( ["uv", "run", "python", "scripts/audit_exception_handling.py", "--src", "src", "--json"], capture_output=True, text=True, ) assert result.returncode == 0, ( f"audit_exception_handling.py exited {result.returncode}; stderr:\n" f"{result.stderr[:2000]}" ) data = json.loads(result.stdout) gui2 = [f for f in data.get("files", []) if "gui_2" in f.get("filename", "")][0] broad_catches = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_BROAD_CATCH"] assert len(broad_catches) <= 14, ( f"Phase 4 invariant: expected <= 14 INTERNAL_BROAD_CATCH sites in src/gui_2.py " f"(post-Phase 4 baseline, down from 17); found {len(broad_catches)}. " f"The 3 Batch B sites (L3398, L3718, L3740) must be migrated to Result[T] helpers. " f"Lines: {[f.get('line') for f in broad_catches]}" ) def test_phase_4_invariant_all_3_migration_sites_have_tests(): """ Phase 4 invariant: each of the 3 Batch B sites has both success and failure tests in this test file. """ import re text = Path(__file__).read_text(encoding="utf-8") expected_lines = [3398, 3718, 3740] for line in expected_lines: success_pattern = f"test_phase_4_l{line}_.*_success" failure_pattern = f"test_phase_4_l{line}_.*_failure" assert re.search(success_pattern, text), ( f"Phase 4 invariant: missing success test for L{line}. " f"Expected a test matching '{success_pattern}'." ) assert re.search(failure_pattern, text), ( f"Phase 4 invariant: missing failure test for L{line}. " f"Expected a test matching '{failure_pattern}'." ) # ============================================================================= # Phase 5 Tests - Migration of 11 INTERNAL_BROAD_CATCH event-handler sites to Result[T] # Each site gets 2 tests: success and failure. # Migration pattern: legacy wrapper routes errors to app._last_request_errors (per FR-BC-4). # ============================================================================= def test_phase_5_l1284_populate_auto_slices_outline_result_success(): """ L1284 _populate_auto_slices_outline_result returns Result.ok=True on success. The helper wraps the mcp_client.{py,ts_c,ts_cpp}_get_code_outline try/except in App._populate_auto_slices. On success (Python file extension matches), returns Result(data=outline). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() f_item = MagicMock() f_item.path = "/proj/foo/src/bar.py" abs_path = "/proj/foo/src/bar.py" with patch.object(gui_2.mcp_client, "py_get_code_outline", return_value="[def] foo (Lines 1-10)"): result = gui_2._populate_auto_slices_outline_result(app, f_item, abs_path) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data == "[def] foo (Lines 1-10)" def test_phase_5_l1284_populate_auto_slices_outline_result_failure(): """ L1284 _populate_auto_slices_outline_result returns Result.ok=False with ErrorInfo on failure. When the underlying mcp_client outline fetch raises, the helper converts the exception to ErrorInfo and returns Result(data="", errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() f_item = MagicMock() f_item.path = "/proj/foo/src/bar.py" abs_path = "/proj/foo/src/bar.py" with patch.object(gui_2.mcp_client, "py_get_code_outline", side_effect=RuntimeError("outline fetch failed")): result = gui_2._populate_auto_slices_outline_result(app, f_item, abs_path) 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._populate_auto_slices_outline_result" assert "outline fetch failed" in err.message def test_phase_5_l1293_populate_auto_slices_file_read_result_success(): """ L1293 _populate_auto_slices_file_read_result returns Result.ok=True on success. The helper wraps the file read try/except in App._populate_auto_slices. On success, returns Result(data=content) where content is the file text. """ from src import gui_2 from unittest.mock import MagicMock, patch, mock_open app = MagicMock() f_item = MagicMock() f_item.path = "/proj/foo/src/bar.py" with patch("builtins.open", mock_open(read_data="line1\nline2\nline3")): result = gui_2._populate_auto_slices_file_read_result(app, f_item) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data == "line1\nline2\nline3" def test_phase_5_l1293_populate_auto_slices_file_read_result_failure(): """ L1293 _populate_auto_slices_file_read_result returns Result.ok=False with ErrorInfo on failure. When the file read raises (e.g., file not found, encoding error), the helper converts the exception to ErrorInfo and returns Result(data="", errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() f_item = MagicMock() f_item.path = "/proj/foo/src/missing.py" with patch("builtins.open", side_effect=FileNotFoundError("no such file")): result = gui_2._populate_auto_slices_file_read_result(app, f_item) 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._populate_auto_slices_file_read_result" assert "no such file" in err.message def test_phase_5_l1367_apply_pending_patch_result_success(): """ L1367 _apply_pending_patch_result returns Result.ok=True on success. The helper wraps the apply_patch_to_file try/except in App._apply_pending_patch. On success (apply_patch_to_file returns (True, msg)), returns Result(data=True) and sets app._show_patch_modal=False, app._pending_patch_text=None, app._pending_patch_files=[], app._patch_error_message=None. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app._pending_patch_text = "--- a/foo.py\n+++ b/foo.py\n@@ -1 +1 @@\n-old\n+new\n" app.controller.current_project_dir = "/proj/foo" with patch.object(gui_2, "apply_patch_to_file", return_value=(True, "patched")), \ patch.object(gui_2.imgui, "close_current_popup"): result = gui_2._apply_pending_patch_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True assert app._show_patch_modal is False assert app._patch_error_message is None def test_phase_5_l1367_apply_pending_patch_result_failure(): """ L1367 _apply_pending_patch_result returns Result.ok=False with ErrorInfo on failure. When apply_patch_to_file returns (False, msg) or raises, the helper sets app._patch_error_message to the error message and returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app._pending_patch_text = "--- a/foo.py\n+++ b/foo.py\n" app.controller.current_project_dir = "/proj/foo" with patch.object(gui_2, "apply_patch_to_file", side_effect=RuntimeError("patch blew up")): result = gui_2._apply_pending_patch_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._apply_pending_patch_result" assert "patch blew up" in err.message def test_phase_5_l1393_open_patch_in_external_editor_result_success(): """ L1393 _open_patch_in_external_editor_result returns Result.ok=True on success. The helper wraps the external editor launch try/except in App._open_patch_in_external_editor. On success (launcher.launch_diff returns a process), returns Result(data=True). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app._pending_patch_files = ["/proj/foo/src/foo.py"] app._pending_patch_text = "--- a/foo.py\n+++ b/foo.py\n" mock_editor = MagicMock(name="mock_editor") mock_launcher = MagicMock(name="mock_launcher") mock_launcher.config.get_default.return_value = mock_editor mock_process = MagicMock(name="mock_process") mock_launcher.launch_diff.return_value = mock_process with patch("os.path.exists", return_value=True), \ patch("src.external_editor.get_default_launcher", return_value=mock_launcher), \ patch("src.external_editor.create_temp_modified_file", return_value="/tmp/patch_temp.py"): result = gui_2._open_patch_in_external_editor_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True assert app._vscode_diff_process is mock_process def test_phase_5_l1393_open_patch_in_external_editor_result_failure(): """ L1393 _open_patch_in_external_editor_result returns Result.ok=False with ErrorInfo on failure. When the external editor launch raises, the helper converts the exception to ErrorInfo and returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app._pending_patch_files = ["/proj/foo/src/foo.py"] app._pending_patch_text = "--- a/foo.py\n+++ b/foo.py\n" mock_launcher = MagicMock() mock_launcher.config.get_default.return_value = MagicMock() with patch("os.path.exists", return_value=True), \ patch("src.external_editor.get_default_launcher", return_value=mock_launcher), \ patch("src.external_editor.create_temp_modified_file", side_effect=RuntimeError("temp file creation blew up")): result = gui_2._open_patch_in_external_editor_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._open_patch_in_external_editor_result" assert "temp file creation blew up" in err.message def test_phase_5_l1428_request_patch_from_tier4_result_success(): """ L1428 request_patch_from_tier4_result returns Result.ok=True on success. The helper wraps the ai_client.run_tier4_patch_generation try/except in App.request_patch_from_tier4. On success (patch_text has --- and +++), returns Result(data=True) and sets app._pending_patch_text/files and app._show_patch_modal=True. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() patch_text = "--- a/foo.py\n+++ b/foo.py\n@@ -1 +1 @@\n-old\n+new\n" mock_diff_files = [MagicMock(old_path="/proj/foo/foo.py", new_path="/proj/foo/foo.py")] with patch.object(gui_2.ai_client, "run_tier4_patch_generation", return_value=patch_text), \ patch("src.diff_viewer.parse_diff", return_value=mock_diff_files): result = gui_2.request_patch_from_tier4_result(app, "boom", "/proj/foo/foo.py") assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True assert app._pending_patch_text is patch_text assert app._pending_patch_files == ["/proj/foo/foo.py"] assert app._show_patch_modal is True def test_phase_5_l1428_request_patch_from_tier4_result_failure(): """ L1428 request_patch_from_tier4_result returns Result.ok=False with ErrorInfo on failure. When ai_client.run_tier4_patch_generation raises, the helper converts the exception to ErrorInfo and returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch.object(gui_2.ai_client, "run_tier4_patch_generation", side_effect=RuntimeError("tier4 backend down")): result = gui_2.request_patch_from_tier4_result(app, "boom", "/proj/foo/foo.py") 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.request_patch_from_tier4_result" assert "tier4 backend down" in err.message def test_phase_5_l3163_render_tool_preset_bias_save_result_success(): """ L3163 _render_tool_preset_bias_save_result returns Result.ok=True on success. The helper wraps the BiasProfile save try/except in render_tool_preset_manager_content. On success (BiasProfile construction + _cb_save_bias_profile), 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_bias_profile_name = "test_bias" app._editing_bias_profile_tool_weights = {"foo": 2} app._editing_bias_profile_category_multipliers = {"bar": 1.5} app._editing_tool_preset_scope = "project" mock_profile = MagicMock() mock_profile.name = "test_bias" with patch.object(gui_2.models, "BiasProfile", return_value=mock_profile): result = gui_2._render_tool_preset_bias_save_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True assert "Saved: test_bias" in app.ai_status def test_phase_5_l3163_render_tool_preset_bias_save_result_failure(): """ L3163 _render_tool_preset_bias_save_result returns Result.ok=False on failure. When BiasProfile construction or _cb_save_bias_profile raises, the helper sets app.ai_status to an error message and returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app._editing_bias_profile_name = "test_bias" app._editing_bias_profile_tool_weights = {"foo": 2} app._editing_bias_profile_category_multipliers = {"bar": 1.5} app._editing_tool_preset_scope = "project" with patch.object(gui_2.models, "BiasProfile", side_effect=RuntimeError("bias validation failed")): result = gui_2._render_tool_preset_bias_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_tool_preset_bias_save_result" assert "bias validation failed" in err.message assert "Error:" in app.ai_status def test_phase_5_l3582_render_context_batch_actions_preview_result_success(): """ L3582 _render_context_batch_actions_preview_result returns Result.ok=True on success. The helper wraps the _do_generate preview try/except in render_context_batch_actions. On success, returns Result(data=preview_text) where preview_text is the controller._do_generate() output. """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() app.context_files = ["foo.py", "bar.py"] app.controller._do_generate.return_value = ("# Generated Preview\n\nContent here", "preview.md") result = gui_2._render_context_batch_actions_preview_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert "Generated Preview" in result.data def test_phase_5_l3582_render_context_batch_actions_preview_result_failure(): """ L3582 _render_context_batch_actions_preview_result returns Result.ok=False on failure. When _do_generate raises, the helper captures the traceback and returns Result(data="", errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() app.context_files = ["foo.py"] app.controller._do_generate.side_effect = RuntimeError("generate failed") result = gui_2._render_context_batch_actions_preview_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_context_batch_actions_preview_result" assert "generate failed" in err.message assert "Error" in result.data def test_phase_5_l5380_render_operations_hub_external_editor_panel_result_success(): """ L5380 _render_operations_hub_external_editor_panel_result returns Result.ok=True on success. The helper wraps the render_external_editor_panel call within render_operations_hub External Tools tab try/except. On success, returns Result(data=True). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch.object(gui_2, "render_external_editor_panel"): result = gui_2._render_operations_hub_external_editor_panel_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True def test_phase_5_l5380_render_operations_hub_external_editor_panel_result_failure(): """ L5380 _render_operations_hub_external_editor_panel_result returns Result.ok=False on failure. When render_external_editor_panel raises, the helper converts the exception to ErrorInfo and returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch.object(gui_2, "render_external_editor_panel", side_effect=RuntimeError("ext editor render blew up")): result = gui_2._render_operations_hub_external_editor_panel_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_operations_hub_external_editor_panel_result" assert "ext editor render blew up" in err.message def test_phase_5_l5786_render_text_viewer_window_ced_result_success(): """ L5786 _render_text_viewer_window_ced_result returns Result.ok=True on success. The helper wraps the TextEditor set_text/render try/except in render_text_viewer_window. On success, returns Result(data=True). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.perf_profiling_enabled = False app.text_viewer_content = "line1\nline2\n" app.text_viewer_title = "test.txt" app._text_viewer_editor = MagicMock() result = gui_2._render_text_viewer_window_ced_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True def test_phase_5_l5786_render_text_viewer_window_ced_result_failure(): """ L5786 _render_text_viewer_window_ced_result returns Result.ok=False with ErrorInfo on failure. When TextEditor render raises, the helper converts the exception to ErrorInfo and returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.perf_profiling_enabled = False app.text_viewer_content = "line1\nline2\n" app.text_viewer_title = "test.txt" mock_editor = MagicMock() mock_editor.set_text.side_effect = RuntimeError("ced set_text failed") app._text_viewer_editor = mock_editor result = gui_2._render_text_viewer_window_ced_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_text_viewer_window_ced_result" assert "ced set_text failed" in err.message def test_phase_5_l5920_render_external_editor_panel_config_result_success(): """ L5920 _render_external_editor_panel_config_result returns Result.ok=True on success. The helper wraps the external editor config rendering try/except in render_external_editor_panel. On success, returns Result(data=True). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.config = {"tools": {"text_editors": {}, "default_editor": ""}} mock_launcher = MagicMock() mock_launcher.config.editors = {} mock_launcher.config.default_editor = None with patch("src.external_editor.get_default_launcher", return_value=mock_launcher), \ patch.object(gui_2, "imgui", new=MagicMock()): result = gui_2._render_external_editor_panel_config_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is True def test_phase_5_l5920_render_external_editor_panel_config_result_failure(): """ L5920 _render_external_editor_panel_config_result returns Result.ok=False with ErrorInfo on failure. When the external editor config rendering raises, the helper converts the exception to ErrorInfo and returns Result(data=False, errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.config = {"tools": {"text_editors": {}, "default_editor": ""}} with patch("src.external_editor.get_default_launcher", side_effect=RuntimeError("ext editor config blew up")): result = gui_2._render_external_editor_panel_config_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_external_editor_panel_config_result" assert "ext editor config blew up" in err.message def test_phase_5_l7208_render_beads_tab_list_result_success(): """ L7208 _render_beads_tab_list_result returns Result.ok=True on success. The helper wraps the beads_client.BeadsClient(...) + list_beads() try/except in render_beads_tab. On success, returns Result(data=beads) where beads is the list of beads returned by list_beads(). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.active_project_root = "/proj/foo" mock_client = MagicMock() mock_bead = MagicMock() mock_bead.id = "bd-1" mock_bead.status = "todo" mock_bead.title = "test" mock_client.list_beads.return_value = [mock_bead] with patch("src.beads_client.BeadsClient", return_value=mock_client): result = gui_2._render_beads_tab_list_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data == [mock_bead] def test_phase_5_l7208_render_beads_tab_list_result_failure(): """ L7208 _render_beads_tab_list_result returns Result.ok=False with ErrorInfo on failure. When BeadsClient construction or list_beads() raises, the helper converts the exception to ErrorInfo and returns Result(data=[], errors=[ErrorInfo]). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() app.active_project_root = "/proj/foo" with patch("src.beads_client.BeadsClient", side_effect=RuntimeError("dolt backend down")): result = gui_2._render_beads_tab_list_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_beads_tab_list_result" assert "dolt backend down" in err.message # ============================================================================= # Phase 5 Invariant Tests (result_migration_gui_2_20260619) # Lock the per-phase progress: 11 INTERNAL_BROAD_CATCH event-handler sites # migrated, all have both success and failure tests. # ============================================================================= def test_phase_5_invariant_batch_c_count_dropped(): """ Phase 5 invariant: the audit's INTERNAL_BROAD_CATCH count for src/gui_2.py has dropped from 14 (pre-Phase 5) to 3 (post-Phase 5). The 3 remaining sites are in other phases: L591 (Phase 8), L897 (Phase 8), L4321 (Phase 7). The 11 migrated sites are: L1284, L1293, L1367, L1393, L1428, L3163, L3582, L5380, L5786, L5920, L7208. """ result = subprocess.run( ["uv", "run", "python", "scripts/audit_exception_handling.py", "--src", "src", "--json"], capture_output=True, text=True, ) assert result.returncode == 0, ( f"audit_exception_handling.py exited {result.returncode}; stderr:\n" f"{result.stderr[:2000]}" ) data = json.loads(result.stdout) gui2 = [f for f in data.get("files", []) if "gui_2" in f.get("filename", "")][0] broad_catches = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_BROAD_CATCH"] # Pre-Phase 5 baseline: 14. Post-Phase 5: 3 (11 sites migrated). # Per FR-BC-4, the 11 migrated sites drain to app._last_request_errors. assert len(broad_catches) <= 3, ( f"Phase 5 invariant: expected <= 3 INTERNAL_BROAD_CATCH sites in src/gui_2.py " f"(post-Phase 5 baseline, 11 sites migrated); found {len(broad_catches)}. " f"The 11 Phase 5 Batch C sites (L1284, L1293, L1367, L1393, L1428, L3163, " f"L3582, L5380, L5786, L5920, L7208) must be migrated to Result[T] helpers. " f"Lines: {[f.get('line') for f in broad_catches]}" ) def test_phase_5_invariant_all_11_migration_sites_have_tests(): """ Phase 5 invariant: each of the 11 Batch C sites has both success and failure tests in this test file. """ import re text = Path(__file__).read_text(encoding="utf-8") expected_lines = [1284, 1293, 1367, 1393, 1428, 3163, 3582, 5380, 5786, 5920, 7208] for line in expected_lines: success_pattern = f"test_phase_5_l{line}_.*_result_success" failure_pattern = f"test_phase_5_l{line}_.*_result_failure" assert re.search(success_pattern, text), ( f"Phase 5 invariant: missing success test for L{line}. " f"Expected a test matching '{success_pattern}'." ) assert re.search(failure_pattern, text), ( f"Phase 5 invariant: missing failure test for L{line}. " f"Expected a test matching '{failure_pattern}'." ) # ============================================================================= # Phase 6 Tests - Signal handler sites # Per PHASE1_SITE_INVENTORY.md, Phase 6 covers signal-handler category # sites. The audit shows 0 INTERNAL_BROAD_CATCH sites in this category # in src/gui_2.py (the inventory classifies signal-handler try/except # under other categories — Phase 6 has no sites in this track). # The two invariant tests below document this and pin the count. # ============================================================================= def test_phase_6_invariant_signal_handler_count_dropped(): """ Phase 6 invariant: the audit's INTERNAL_BROAD_CATCH count for src/gui_2.py remains at 3 (no sites migrated in Phase 6, since the signal-handler category has 0 INTERNAL_BROAD_CATCH sites in this track). Per PHASE1_SITE_INVENTORY.md, all sites that might appear in a signal-handler category were classified into other phases (Phase 8 for startup callbacks, Phase 7 for worker/background). Phase 6 has no sites to migrate in this track. Pre-Phase 6 baseline: 3 (L591 _diag_layout_state, L897 _capture_workspace_profile, L4321 worker). Post-Phase 6 baseline: 3 (unchanged; Phase 6 has 0 sites). """ result = subprocess.run( ["uv", "run", "python", "scripts/audit_exception_handling.py", "--src", "src", "--json"], capture_output=True, text=True, ) assert result.returncode == 0, ( f"audit_exception_handling.py exited {result.returncode}; stderr:\n" f"{result.stderr[:2000]}" ) data = json.loads(result.stdout) gui2 = [f for f in data.get("files", []) if "gui_2" in f.get("filename", "")][0] broad_catches = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_BROAD_CATCH"] # Phase 6 baseline is 3 (no migration occurred since Phase 5 ended). # This test pins the count to 3 before Phases 7, 8, 9 each migrate sites. assert len(broad_catches) == 3, ( f"Phase 6 invariant: expected exactly 3 INTERNAL_BROAD_CATCH sites in " f"src/gui_2.py (Phase 6 has 0 sites to migrate; pre-Phase-7 baseline); " f"found {len(broad_catches)}. Lines: {[f.get('line') for f in broad_catches]}" ) def test_phase_6_invariant_zero_sites_in_phase_6(): """ Phase 6 invariant: documents that Phase 6 (signal-handler sites) has 0 sites to migrate. The next test (`test_phase_7_invariant_batch_d_count_dropped`) will pin the count after Phase 7 migrates the L4321 worker site. This test exists to make the "Phase 6 is empty" decision explicit and machine-checkable: a future agent who tries to add a Phase 6 site will see this test fail at the count assertion. """ import re text = Path(__file__).read_text(encoding="utf-8") # Expected: zero tests matching the Phase 6 site pattern phase_6_site_tests = re.findall(r"test_phase_6_l\d+_.*_result_(success|failure)", text) assert len(phase_6_site_tests) == 0, ( f"Phase 6 invariant: expected 0 Phase 6 site tests (signal-handler " f"category has 0 INTERNAL_BROAD_CATCH sites in src/gui_2.py per the " f"PHASE1_SITE_INVENTORY); found {len(phase_6_site_tests)}. Tests: " f"{phase_6_site_tests}. If a Phase 6 site was added, update the " f"inventory and migrate it." ) # ============================================================================= # Phase 7 Tests - Migration of 1 INTERNAL_BROAD_CATCH worker site to Result[T] # The helper wraps the try body from the worker() closure in # _check_auto_refresh_context_preview (L4321). # The legacy wrapper drains errors to app.controller._worker_errors (with lock). # ============================================================================= def test_phase_7_l4321_worker_context_preview_result_success(): """ L4321 _worker_context_preview_result returns Result(data=None) on success. The helper wraps the try body from the worker() closure in _check_auto_refresh_context_preview. On success, sets app.context_preview_text to the generated markdown and returns Result(data=None). """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() app.controller._do_generate.return_value = ("# Generated Preview\n\nContent here", "preview.md") result = gui_2._worker_context_preview_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data is None assert app.controller.context_files == app.context_files assert app.context_preview_text == "# Generated Preview\n\nContent here" def test_phase_7_l4321_worker_context_preview_result_failure(): """ L4321 _worker_context_preview_result returns Result(data=None, errors=[ErrorInfo]) on failure. When the underlying controller._do_generate() call raises, the helper sets app.context_preview_text to a fallback error message and returns Result with ErrorInfo describing the failure. """ from src import gui_2 from unittest.mock import MagicMock app = MagicMock() app.controller._do_generate.side_effect = RuntimeError("do_generate blew up") result = gui_2._worker_context_preview_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._worker_context_preview_result" assert "do_generate blew up" in err.message assert app.context_preview_text == "Error generating context preview." # ============================================================================= # Phase 7 Invariant Tests (result_migration_gui_2_20260619) # Lock the per-phase progress: 1 INTERNAL_BROAD_CATCH worker site migrated # to Result[T], all 1 sites have both success and failure tests. # ============================================================================= def test_phase_7_invariant_batch_d_count_dropped(): """ Phase 7 invariant: the audit's INTERNAL_BROAD_CATCH count for src/gui_2.py has dropped from 3 (pre-Phase 7) to 2 (post-Phase 7). The 2 remaining sites are in other phases: L591 (Phase 8), L897 (Phase 8). The 1 migrated site is: L4321 (worker in _check_auto_refresh_context_preview). The legacy wrapper drains errors to app.controller._worker_errors (with lock). """ result = subprocess.run( ["uv", "run", "python", "scripts/audit_exception_handling.py", "--src", "src", "--json"], capture_output=True, text=True, ) assert result.returncode == 0, ( f"audit_exception_handling.py exited {result.returncode}; stderr:\n" f"{result.stderr[:2000]}" ) data = json.loads(result.stdout) gui2 = [f for f in data.get("files", []) if "gui_2" in f.get("filename", "")][0] broad_catches = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_BROAD_CATCH"] # Pre-Phase 7 baseline: 3. Post-Phase 7: 2 (1 site migrated). assert len(broad_catches) <= 2, ( f"Phase 7 invariant: expected <= 2 INTERNAL_BROAD_CATCH sites in src/gui_2.py " f"(post-Phase 7 baseline, 1 site migrated); found {len(broad_catches)}. " f"The 1 Phase 7 site (L4321 worker) must be migrated to Result[T] helper. " f"Lines: {[f.get('line') for f in broad_catches]}" ) def test_phase_7_invariant_all_1_migration_sites_have_tests(): """ Phase 7 invariant: each of the 1 Batch D (worker/background) sites has both success and failure tests in this test file. """ import re text = Path(__file__).read_text(encoding="utf-8") expected_lines = [4321] for line in expected_lines: success_pattern = f"test_phase_7_l{line}_.*_result_success" failure_pattern = f"test_phase_7_l{line}_.*_result_failure" assert re.search(success_pattern, text), ( f"Phase 7 invariant: missing success test for L{line}. " f"Expected a test matching '{success_pattern}'." ) assert re.search(failure_pattern, text), ( f"Phase 7 invariant: missing failure test for L{line}. " f"Expected a test matching '{failure_pattern}'." ) # ============================================================================= # Phase 8 Tests - Migration of 2 INTERNAL_BROAD_CATCH property setter sites # to Result[T]. Each site gets 2 tests: success and failure. # Migration pattern: legacy wrapper drains to app._startup_timeline_errors # (for startup callbacks like L591) or app._last_request_errors # (for property setters like L897). # ============================================================================= def test_phase_8_l591_diag_layout_state_ini_text_result_success(): """ L591 _diag_layout_state_ini_text_result returns Result(data=ini_text) on success. The helper wraps the ini-file-read try/except in App._diag_layout_state. On success, returns Result(data=ini_text) where ini_text is the file content. The legacy wrapper drains errors to app._startup_timeline_errors. """ from src import gui_2 from unittest.mock import MagicMock, patch, mock_open app = MagicMock() with patch("builtins.open", mock_open(read_data="[Window][Provider]\nPos=10,20")): result = gui_2._diag_layout_state_ini_text_result(app, "/proj/manualslop_layout.ini") assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert "[Window][Provider]" in result.data def test_phase_8_l591_diag_layout_state_ini_text_result_failure(): """ L591 _diag_layout_state_ini_text_result returns Result(data="", errors=[ErrorInfo]) on failure. When the ini file read raises (e.g., permission error, encoding error), the helper returns Result with ErrorInfo describing the failure and data="" so the caller can still proceed (return early). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch("builtins.open", side_effect=PermissionError("permission denied")): result = gui_2._diag_layout_state_ini_text_result(app, "/proj/manualslop_layout.ini") 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._diag_layout_state_ini_text_result" assert "permission denied" in err.message assert result.data == "" def test_phase_8_l897_capture_workspace_profile_ini_result_success(): """ L897 _capture_workspace_profile_ini_result returns Result(data=ini_str) on success. The helper wraps the imgui.save_ini_settings_to_memory try/except in App._capture_workspace_profile. On success, returns Result(data=ini_str) where ini_str is the serialized INI content. The legacy wrapper drains errors to app._last_request_errors. """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch.object(gui_2.imgui, "save_ini_settings_to_memory", return_value="[Window][Provider]\nPos=10,20"): result = gui_2._capture_workspace_profile_ini_result(app) assert result.ok, f"Expected ok=True on success, got errors: {result.errors}" assert result.data == "[Window][Provider]\nPos=10,20" def test_phase_8_l897_capture_workspace_profile_ini_result_failure(): """ L897 _capture_workspace_profile_ini_result returns Result(data="", errors=[ErrorInfo]) on failure. When imgui.save_ini_settings_to_memory raises, the helper returns Result with ErrorInfo describing the failure and data="" (matching the original except branch's empty-string fallback). """ from src import gui_2 from unittest.mock import MagicMock, patch app = MagicMock() with patch.object(gui_2.imgui, "save_ini_settings_to_memory", side_effect=RuntimeError("imgui backend down")): result = gui_2._capture_workspace_profile_ini_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._capture_workspace_profile_ini_result" assert "imgui backend down" in err.message assert result.data == "" # ============================================================================= # Phase 8 Invariant Tests (result_migration_gui_2_20260619) # Lock the per-phase progress: 2 INTERNAL_BROAD_CATCH property setter sites # migrated to Result[T], all 2 sites have both success and failure tests. # ============================================================================= def test_phase_8_invariant_property_setter_count_dropped(): """ Phase 8 invariant: the audit's INTERNAL_BROAD_CATCH count for src/gui_2.py has dropped from 2 (pre-Phase 8) to 0 (post-Phase 8). All INTERNAL_BROAD_CATCH sites in src/gui_2.py have been migrated across Phases 3-8. The 2 migrated sites in Phase 8 are: L591 _diag_layout_state, L897 _capture_workspace_profile. - L591 (startup callback) drains to app._startup_timeline_errors. - L897 (property setter) drains to app._last_request_errors. """ result = subprocess.run( ["uv", "run", "python", "scripts/audit_exception_handling.py", "--src", "src", "--json"], capture_output=True, text=True, ) assert result.returncode == 0, ( f"audit_exception_handling.py exited {result.returncode}; stderr:\n" f"{result.stderr[:2000]}" ) data = json.loads(result.stdout) gui2 = [f for f in data.get("files", []) if "gui_2" in f.get("filename", "")][0] broad_catches = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_BROAD_CATCH"] # Post-Phase 8 baseline: 0 (all 22 sites migrated across Phases 3-8). assert len(broad_catches) == 0, ( f"Phase 8 invariant: expected 0 INTERNAL_BROAD_CATCH sites in src/gui_2.py " f"(post-Phase 8 baseline, all sites migrated); found {len(broad_catches)}. " f"Lines: {[f.get('line') for f in broad_catches]}" ) def test_phase_8_invariant_all_2_migration_sites_have_tests(): """ Phase 8 invariant: each of the 2 Batch E (property setter / state) sites has both success and failure tests in this test file. """ import re text = Path(__file__).read_text(encoding="utf-8") expected_lines = [591, 897] for line in expected_lines: success_pattern = f"test_phase_8_l{line}_.*_result_success" failure_pattern = f"test_phase_8_l{line}_.*_result_failure" assert re.search(success_pattern, text), ( f"Phase 8 invariant: missing success test for L{line}. " f"Expected a test matching '{success_pattern}'." ) assert re.search(failure_pattern, text), ( f"Phase 8 invariant: missing failure test for L{line}. " f"Expected a test matching '{failure_pattern}'." ) # ============================================================================= # Phase 9 Tests - Helper/utility sites # Per PHASE1_SITE_INVENTORY.md, Phase 9 covers helper/utility module-level # sites. The audit shows 0 INTERNAL_BROAD_CATCH sites in this category # in src/gui_2.py (the one Phase 9 site from the inventory, L1398 # _close_vscode_diff, is classified INTERNAL_SILENT_SWALLOW and is # handled in Phase 10 — logging is NOT a drain per the convention). # The two invariant tests below document this and pin the count. # ============================================================================= def test_phase_9_invariant_helper_utility_count_dropped(): """ Phase 9 invariant: the audit's INTERNAL_BROAD_CATCH count for src/gui_2.py remains at 0 (no sites migrated in Phase 9, since the helper/utility category has 0 INTERNAL_BROAD_CATCH sites in this track). Per PHASE1_SITE_INVENTORY.md, the one Phase 9 site (L1398 _close_vscode_diff) is INTERNAL_SILENT_SWALLOW (the bare-except classification) and is handled in Phase 10 (logging NOT a drain). Phase 9 has no sites to migrate. Pre-Phase 9 baseline: 0. Post-Phase 9 baseline: 0 (unchanged; Phase 9 has 0 sites). This test pins the count to 0 after Phases 7-8 migrated all 3 remaining sites. """ result = subprocess.run( ["uv", "run", "python", "scripts/audit_exception_handling.py", "--src", "src", "--json"], capture_output=True, text=True, ) assert result.returncode == 0, ( f"audit_exception_handling.py exited {result.returncode}; stderr:\n" f"{result.stderr[:2000]}" ) data = json.loads(result.stdout) gui2 = [f for f in data.get("files", []) if "gui_2" in f.get("filename", "")][0] broad_catches = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_BROAD_CATCH"] # Phase 9 baseline is 0 (Phase 8 already dropped the count to 0). # This test pins the count to 0 to verify no regression. assert len(broad_catches) == 0, ( f"Phase 9 invariant: expected 0 INTERNAL_BROAD_CATCH sites in src/gui_2.py " f"(post-Phase 9 baseline; no Phase 9 sites, count should remain 0); " f"found {len(broad_catches)}. Lines: {[f.get('line') for f in broad_catches]}" ) def test_phase_9_invariant_zero_sites_in_phase_9(): """ Phase 9 invariant: documents that Phase 9 (helper/utility sites) has 0 sites to migrate. The one Phase 9 site from the inventory (L1398 _close_vscode_diff) is INTERNAL_SILENT_SWALLOW and will be handled in Phase 10. This test exists to make the "Phase 9 is empty" decision explicit and machine-checkable: a future agent who tries to add a Phase 9 site will see this test fail at the count assertion. """ import re text = Path(__file__).read_text(encoding="utf-8") # Expected: zero tests matching the Phase 9 site pattern phase_9_site_tests = re.findall(r"test_phase_9_l\d+_.*_result_(success|failure)", text) assert len(phase_9_site_tests) == 0, ( f"Phase 9 invariant: expected 0 Phase 9 site tests (helper/utility " f"category has 0 INTERNAL_BROAD_CATCH sites in src/gui_2.py per the " f"PHASE1_SITE_INVENTORY); found {len(phase_9_site_tests)}. Tests: " f"{phase_9_site_tests}. If a Phase 9 site was added, update the " f"inventory and migrate it." )