a6c89dc754
The Phase 6 invariant test was originally written to assert ==3 (the pre-Phase-7 baseline). After Phases 7-8 migrated the 3 remaining sites, the count dropped to 0, which broke the strict equality assertion. Changed to <=3 (matching the Phase 5 invariant test pattern) so the test passes at every point in the migration timeline. Documented the robustness rationale in the test docstring.
1725 lines
72 KiB
Python
1725 lines
72 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
|
|
|
|
|
|
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."
|
|
)
|
|
|