"""
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 "| <line_number> | <CATEGORY> | <phase_number> | ...". 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: <name>" 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: <name>" 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="<error message>", 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).
 Uses <= to remain robust against later-phase migrations (Phases 7-8 will
 drop the count to 0; this test continues to pass since 0 <= 3).
 """
 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 upper bound to 3 before Phases 7, 8, 9 each migrate sites.
 assert len(broad_catches) <= 3, (
  f"Phase 6 invariant: expected <= 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."
 )


# =============================================================================
# Phase 10 Tests - INTERNAL_SILENT_SWALLOW migrations
# Per conductor/code_styleguides/error_handling.md lines 462-540:
# "Logging is NOT a drain point." The 13 sites in this phase have logging-only
# except bodies (sys.stderr.write, print, traceback.print_exc, pass). They
# MUST be migrated to full Result[T] propagation. NOT narrowing + logging.
# NOT pass-after-logging. NOT "intentional silent recovery".
# =============================================================================


def test_phase_10_l216_detect_refresh_rate_win32_result_success():
 """
 L216 _detect_refresh_rate_win32_result returns Result(data=float) on success.

 The helper extracts the try/except body from _detect_refresh_rate_win32
 into a Result-returning helper. On success (when EnumDisplaySettingsW
 returns a valid dmDisplayFrequency > 1), the helper returns
 Result(data=rate).
 """
 from unittest.mock import patch
 import src.gui_2 as gui2_mod
 def fake_eds(_devname, _mode, byref_dm):
  real_dm = byref_dm._obj
  real_dm.dmDisplayFrequency = 144
  return 1
 with patch("ctypes.windll.user32.EnumDisplaySettingsW", side_effect=fake_eds):
  result = gui2_mod._detect_refresh_rate_win32_result()
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data == 144.0


def test_phase_10_l216_detect_refresh_rate_win32_result_failure():
 """
 L216 _detect_refresh_rate_win32_result returns Result(data=0.0, errors=[ErrorInfo]) on failure.

 When the ctypes windll call raises (e.g., on a non-Windows system or
 when user32 is unavailable), the helper returns Result(data=0.0)
 with ErrorInfo describing the failure. The original function returned
 0.0 on error (preserved as the safe fallback in the legacy wrapper).
 """
 from unittest.mock import patch
 import src.gui_2 as gui2_mod
 with patch("ctypes.windll.user32.EnumDisplaySettingsW", side_effect=OSError("user32 unavailable")):
  result = gui2_mod._detect_refresh_rate_win32_result()
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data == 0.0
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._detect_refresh_rate_win32_result"
 assert "user32 unavailable" in err.message


def test_phase_10_l264_resolve_font_path_result_relative_under_assets():
 """
 L264 _resolve_font_path_result returns Result(data=relative_path) when input is absolute but inside assets_dir.

 The helper extracts the entire _resolve_font_path normalization logic into
 a Result-returning helper. On a path under assets_dir, it returns
 Result(data=relative_path) with backslashes converted to forward slashes.
 """
 from pathlib import Path
 import src.gui_2 as gui2_mod
 assets_dir = Path(r"C:\projects\manual_slop_tier2\assets")
 font_path = str(assets_dir / "fonts" / "Inter-Regular.ttf")
 result = gui2_mod._resolve_font_path_result(font_path, assets_dir)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data == "fonts/Inter-Regular.ttf"


def test_phase_10_l264_resolve_font_path_result_is_relative_to_raises():
 """
 L264 _resolve_font_path_result returns Result(data=fallback, errors=[ErrorInfo])
 when Path.is_relative_to() raises ValueError on Windows path comparison.

 On Python <3.9, AttributeError is raised because is_relative_to doesn't exist.
 On Python >=3.9 with cross-drive paths, ValueError is raised. Either way the
 helper should NOT silently swallow — it converts to ErrorInfo and returns
 the default fallback path "fonts/Inter-Regular.ttf" so the legacy wrapper
 can return a valid path.
 """
 from pathlib import Path
 import src.gui_2 as gui2_mod
 assets_dir = Path(r"C:\projects\manual_slop_tier2\assets")
 font_path = r"D:\different\drive\fonts\Inter-Regular.ttf"
 result = gui2_mod._resolve_font_path_result(font_path, assets_dir)
 assert result.ok, f"Expected ok=True (graceful degradation), got errors: {result.errors}"
 assert result.data == "fonts/Inter-Regular.ttf"
 if result.errors:
  err = result.errors[0]
  assert err.source == "gui_2._resolve_font_path_result"


def test_phase_10_l612_post_init_callback_result_success():
 """
 L612 _post_init_callback_result returns Result(data=None) on success.

 The helper extracts the warmup-complete callback registration from
 App._post_init into a Result-returning helper. On success, it registers
 the lambda callback via self.controller.on_warmup_complete() and returns
 Result(data=None).
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.controller = MagicMock()
 result = gui2_mod._post_init_callback_result(app)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None
 app.controller.on_warmup_complete.assert_called_once()


def test_phase_10_l612_post_init_callback_result_failure():
 """
 L612 _post_init_callback_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When self.controller.on_warmup_complete() raises (e.g., controller not
 ready or invalid callback), the helper converts to ErrorInfo and returns
 Result(data=None, errors=[ErrorInfo]). The legacy _post_init wrapper
 drains to self._startup_timeline_errors.
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.controller = MagicMock()
 app.controller.on_warmup_complete.side_effect = RuntimeError("controller not ready")
 result = gui2_mod._post_init_callback_result(app)
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._post_init_callback_result"
 assert "controller not ready" in err.message


def test_phase_10_l728_run_immapp_result_success():
 """
 L728 _run_immapp_result returns Result(data=None) on success.

 The helper extracts the immapp.run() call from App.run into a Result-returning
 helper. On success, returns Result(data=None). The legacy run method
 proceeds to self.shutdown() and session_logger.close_session().
 """
 from unittest.mock import MagicMock, patch
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.runner_params = MagicMock()
 with patch("src.gui_2.immapp") as mock_immapp:
  mock_immapp.AddOnsParams.return_value = MagicMock()
  result = gui2_mod._run_immapp_result(app)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None
 mock_immapp.run.assert_called_once()


def test_phase_10_l728_run_immapp_result_failure():
 """
 L728 _run_immapp_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When immapp.run() raises RuntimeError (IM_ASSERT, native bundle crash, etc.),
 the helper converts to ErrorInfo. The legacy run method sets the
 controller._gui_degraded_reason and _last_imgui_assert drain attributes,
 appends to _startup_timeline_errors, and returns.
 """
 from unittest.mock import MagicMock, patch
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.runner_params = MagicMock()
 app.controller = MagicMock()
 with patch("src.gui_2.immapp") as mock_immapp:
  mock_immapp.AddOnsParams.return_value = MagicMock()
  mock_immapp.run.side_effect = RuntimeError("IM_ASSERT: invalid scope")
  result = gui2_mod._run_immapp_result(app)
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._run_immapp_result"
 assert "IM_ASSERT" in err.message


def test_phase_10_l1052_shutdown_save_ini_result_success():
 """
 L1052 _shutdown_save_ini_result returns Result(data=None) on success.

 The helper extracts the imgui.save_ini_settings_to_disk() try/except from
 App.shutdown into a Result-returning helper. On success, returns
 Result(data=None). The legacy shutdown method proceeds to
 self.controller.shutdown().
 """
 from unittest.mock import MagicMock, patch
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.runner_params = MagicMock()
 app.runner_params.ini_filename = "manualslop_layout.ini"
 with patch("src.gui_2.imgui.save_ini_settings_to_disk", return_value=None):
  result = gui2_mod._shutdown_save_ini_result(app)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None


def test_phase_10_l1052_shutdown_save_ini_result_failure():
 """
 L1052 _shutdown_save_ini_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When imgui.save_ini_settings_to_disk() raises (e.g., disk full, path
 not writable), the helper converts to ErrorInfo. The legacy shutdown
 method drains to self._startup_timeline_errors and proceeds to
 self.controller.shutdown() (the original behavior preserved).
 """
 from unittest.mock import MagicMock, patch
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.runner_params = MagicMock()
 app.runner_params.ini_filename = "manualslop_layout.ini"
 with patch("src.gui_2.imgui.save_ini_settings_to_disk", side_effect=OSError("disk full")):
  result = gui2_mod._shutdown_save_ini_result(app)
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._shutdown_save_ini_result"
 assert "disk full" in err.message


def test_phase_10_l1152_gui_func_entry_log_result_success():
 """
 L1152 _gui_func_entry_log_result returns Result(data=None) on success.

 The helper extracts the startup-timing sys.stderr.write from
 App._gui_func into a Result-returning helper. On success, returns
 Result(data=None). The legacy _gui_func method proceeds to the rest
 of the render loop.
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.controller = MagicMock()
 app.controller._init_start_ts = 1000.0
 result = gui2_mod._gui_func_entry_log_result(app)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None


def test_phase_10_l1152_gui_func_entry_log_result_failure():
 """
 L1152 _gui_func_entry_log_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When the sys.stderr.write or sys.stderr.flush raises (e.g., broken pipe),
 the helper converts to ErrorInfo. The legacy _gui_func method drains to
 self._last_request_errors and proceeds with the rest of the render loop
 (preserving the original behavior of continuing past the failure).
 """
 from unittest.mock import MagicMock, patch
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.controller = MagicMock()
 app.controller._init_start_ts = 1000.0
 with patch("src.gui_2.sys.stderr.write", side_effect=OSError("broken pipe")):
  result = gui2_mod._gui_func_entry_log_result(app)
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._gui_func_entry_log_result"
 assert "broken pipe" in err.message


def test_phase_10_l1466_close_vscode_diff_terminate_result_success():
 """
 L1466 _close_vscode_diff_terminate_result returns Result(data=None) on success.

 The helper extracts the self._vscode_diff_process.terminate() try/except
 from App._close_vscode_diff into a Result-returning helper. On success,
 returns Result(data=None). The legacy wrapper sets
 self._vscode_diff_process = None (the original behavior preserved).
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app._vscode_diff_process = MagicMock()
 result = gui2_mod._close_vscode_diff_terminate_result(app)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None
 app._vscode_diff_process.terminate.assert_called_once()


def test_phase_10_l1466_close_vscode_diff_terminate_result_failure():
 """
 L1466 _close_vscode_diff_terminate_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When the process.terminate() call raises (e.g., process already exited,
 invalid handle), the helper converts to ErrorInfo. The legacy wrapper
 drains to self._last_request_errors and proceeds to set
 self._vscode_diff_process = None (preserving the original behavior).
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app._vscode_diff_process = MagicMock()
 app._vscode_diff_process.terminate.side_effect = OSError("process already exited")
 result = gui2_mod._close_vscode_diff_terminate_result(app)
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._close_vscode_diff_terminate_result"
 assert "process already exited" in err.message


def test_phase_10_l1647_focus_response_window_result_success():
 """
 L1647 _focus_response_window_result returns Result(data=None) on success.

 The helper extracts the imgui.set_window_focus("Response") try/except from
 render_main_interface into a Result-returning helper. On success, returns
 Result(data=None).
 """
 from unittest.mock import MagicMock, patch
 import src.gui_2 as gui2_mod
 with patch("src.gui_2.imgui.set_window_focus", return_value=None):
  result = gui2_mod._focus_response_window_result()
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None


def test_phase_10_l1647_focus_response_window_result_failure():
 """
 L1647 _focus_response_window_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When imgui.set_window_focus("Response") raises (e.g., native bundle
 error, IM_ASSERT), the helper converts to ErrorInfo. The caller
 (render_main_interface) drains to self._last_request_errors.
 """
 from unittest.mock import patch
 import src.gui_2 as gui2_mod
 with patch("src.gui_2.imgui.set_window_focus", side_effect=RuntimeError("IM_ASSERT: window scope")):
  result = gui2_mod._focus_response_window_result()
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._focus_response_window_result"
 assert "IM_ASSERT" in err.message


def test_phase_10_l1693_autosave_flush_result_success():
 """
 L1693 _autosave_flush_result returns Result(data=None) on success.

 The helper extracts the auto-save flush_to_project + flush_to_config +
 save_config try/except from render_main_interface into a Result-returning
 helper. On success, returns Result(data=None). The legacy wrapper
 continues with the GUI loop.
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 result = gui2_mod._autosave_flush_result(app)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None
 app._flush_to_project.assert_called_once()
 app._flush_to_config.assert_called_once()
 app.save_config.assert_called_once()


def test_phase_10_l1693_autosave_flush_result_failure():
 """
 L1693 _autosave_flush_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When any of _flush_to_project/_flush_to_config/save_config raises
 (disk full, JSON parse error), the helper converts to ErrorInfo. The
 caller (render_main_interface) drains to self._last_request_errors and
 continues with the GUI loop (preserving the original "don't disrupt the
 GUI loop" intent via the data plane rather than silent swallow).
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app._flush_to_project.side_effect = OSError("disk full")
 result = gui2_mod._autosave_flush_result(app)
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._autosave_flush_result"
 assert "disk full" in err.message


def test_phase_10_l4911_on_warmup_complete_callback_result_success():
 """
 L4911 _on_warmup_complete_callback_result returns Result(data=None) on success.

 The helper extracts the warmup-completion try/except from
 _on_warmup_complete_callback into a Result-returning helper. On success,
 returns Result(data=None). The legacy callback (which runs on a
 background _io_pool thread) sets app._warmup_completion_ts and appends
 to app._warmup_toast_messages under the lock.
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.controller = MagicMock()
 status = {"pending": [], "completed": ["ai_client", "mcp_client"], "failed": []}
 result = gui2_mod._on_warmup_complete_callback_result(app, status)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None
 assert app._warmup_toast_messages, "Expected warmup toast message to be appended"


def test_phase_10_l4911_on_warmup_complete_callback_result_failure():
 """
 L4911 _on_warmup_complete_callback_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When the warmup-completion body raises (e.g., status dict corruption,
 lock acquisition failure), the helper converts to ErrorInfo. The legacy
 wrapper drains to app._worker_errors with the controller lock acquired
 on append (thread-safety critical per sub-track 4 spec).
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 app.controller = MagicMock()
 app.controller._worker_errors_lock = MagicMock()
 # Force status.get to raise to trigger the except
 bad_status = MagicMock()
 bad_status.get.side_effect = RuntimeError("status dict corrupted")
 result = gui2_mod._on_warmup_complete_callback_result(app, bad_status)
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._on_warmup_complete_callback_result"
 assert "status dict corrupted" in err.message


def test_phase_10_l6908_tier_stream_scroll_sync_result_success():
 """
 L6908 _tier_stream_scroll_sync_result returns Result(data=None) on success.

 The helper extracts the imgui.set_scroll_here_y + _tier_stream_last_len
 update try/except from render_tier_stream_panel into a Result-returning
 helper. On success, returns Result(data=None). The caller continues
 the render loop normally.
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 len_map = MagicMock()
 len_map.get.return_value = 0
 app._tier_stream_last_len = len_map
 mock_imgui = MagicMock()
 result = gui2_mod._tier_stream_scroll_sync_result(app, "stream_key", "content here", mock_imgui)
 assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
 assert result.data is None


def test_phase_10_l6908_tier_stream_scroll_sync_result_failure():
 """
 L6908 _tier_stream_scroll_sync_result returns Result(data=None, errors=[ErrorInfo]) on failure.

 When the comparison or imgui.set_scroll_here_y raises (TypeError on
 bad content, AttributeError on missing key), the helper converts to
 ErrorInfo. The caller drains to app._last_request_errors.
 """
 from unittest.mock import MagicMock
 import src.gui_2 as gui2_mod
 app = MagicMock()
 bad_app = MagicMock()
 bad_app._tier_stream_last_len.get.side_effect = AttributeError("missing key")
 mock_imgui = MagicMock()
 result = gui2_mod._tier_stream_scroll_sync_result(bad_app, "stream_key", "content", mock_imgui)
 assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
 assert result.data is None
 assert result.errors, "Expected at least one error on failure"
 err = result.errors[0]
 assert err.source == "gui_2._tier_stream_scroll_sync_result"
 assert "missing key" in err.message



