ce289db999
Extract _populate_auto_slices_file_read_result helper from the file read try/except in App._populate_auto_slices. Legacy wrapper drains errors to app._last_request_errors per FR-BC-4 event-handler pattern. [pre-audit] L1293 INTERNAL_BROAD_CATCH [post-audit] V count: 13 -> 12 (L1293 removed)
881 lines
36 KiB
Python
881 lines
36 KiB
Python
"""
|
|
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
|