""" 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. 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. """ 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] assert len(migration_sites) == EXPECTED_SITE_COUNT, ( f"src/gui_2.py has {len(migration_sites)} migration-target sites in the audit; " f"expected {EXPECTED_SITE_COUNT}. Categories seen: " f"{sorted({f.get('category') for f in migration_sites})}. " f"This must match the 42 sites declared in PHASE1_SITE_INVENTORY.md." ) 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). """ 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" assert "warmup backend down" in err.message