d96e54f2df
Two Phase 12 invariant tests in tests/test_gui_2_result.py verify
UNCLEAR count for src/gui_2.py is 0 after the lazy-loading sentinel
fallback heuristic:
- test_phase_12_invariant_unclear_count_zero: scans audit --json
output, asserts 0 UNCLEAR findings in gui_2.py (the 2 lazy-loading
sites in _LazyModule._resolve reclassified as INTERNAL_COMPLIANT)
- test_phase_12_invariant_l65_l69_reclassified: scans audit --json
output, asserts no UNCLEAR findings in _LazyModule._resolve
method context
State.toml updates:
- phase_12 status: completed, checkpointsha: f996aa10
- phase_12_complete: true
- unclear_count_zero: true
- t12_0/t12_1/t12_2 marked completed with their commit SHAs
Pre-Phase 12: gui_2.py had 2 UNCLEAR sites (L65 + L69 in
_LazyModule._resolve). Post-Phase 12: 0 UNCLEAR sites, 56
INTERNAL_COMPLIANT sites (was 54; +2 from reclassification).
Phase 12 result_migration_gui_2_20260619.
2571 lines
107 KiB
Python
2571 lines
107 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."
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Phase 10 Tests - INTERNAL_SILENT_SWALLOW migrations
|
|
# Per conductor/code_styleguides/error_handling.md lines 462-540:
|
|
# "Logging is NOT a drain point." The 13 sites in this phase have logging-only
|
|
# except bodies (sys.stderr.write, print, traceback.print_exc, pass). They
|
|
# MUST be migrated to full Result[T] propagation. NOT narrowing + logging.
|
|
# NOT pass-after-logging. NOT "intentional silent recovery".
|
|
# =============================================================================
|
|
|
|
|
|
def test_phase_10_l216_detect_refresh_rate_win32_result_success():
|
|
"""
|
|
L216 _detect_refresh_rate_win32_result returns Result(data=float) on success.
|
|
|
|
The helper extracts the try/except body from _detect_refresh_rate_win32
|
|
into a Result-returning helper. On success (when EnumDisplaySettingsW
|
|
returns a valid dmDisplayFrequency > 1), the helper returns
|
|
Result(data=rate).
|
|
"""
|
|
from unittest.mock import patch
|
|
import src.gui_2 as gui2_mod
|
|
def fake_eds(_devname, _mode, byref_dm):
|
|
real_dm = byref_dm._obj
|
|
real_dm.dmDisplayFrequency = 144
|
|
return 1
|
|
with patch("ctypes.windll.user32.EnumDisplaySettingsW", side_effect=fake_eds):
|
|
result = gui2_mod._detect_refresh_rate_win32_result()
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data == 144.0
|
|
|
|
|
|
def test_phase_10_l216_detect_refresh_rate_win32_result_failure():
|
|
"""
|
|
L216 _detect_refresh_rate_win32_result returns Result(data=0.0, errors=[ErrorInfo]) on failure.
|
|
|
|
When the ctypes windll call raises (e.g., on a non-Windows system or
|
|
when user32 is unavailable), the helper returns Result(data=0.0)
|
|
with ErrorInfo describing the failure. The original function returned
|
|
0.0 on error (preserved as the safe fallback in the legacy wrapper).
|
|
"""
|
|
from unittest.mock import patch
|
|
import src.gui_2 as gui2_mod
|
|
with patch("ctypes.windll.user32.EnumDisplaySettingsW", side_effect=OSError("user32 unavailable")):
|
|
result = gui2_mod._detect_refresh_rate_win32_result()
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data == 0.0
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._detect_refresh_rate_win32_result"
|
|
assert "user32 unavailable" in err.message
|
|
|
|
|
|
def test_phase_10_l264_resolve_font_path_result_relative_under_assets():
|
|
"""
|
|
L264 _resolve_font_path_result returns Result(data=relative_path) when input is absolute but inside assets_dir.
|
|
|
|
The helper extracts the entire _resolve_font_path normalization logic into
|
|
a Result-returning helper. On a path under assets_dir, it returns
|
|
Result(data=relative_path) with backslashes converted to forward slashes.
|
|
"""
|
|
from pathlib import Path
|
|
import src.gui_2 as gui2_mod
|
|
assets_dir = Path(r"C:\projects\manual_slop_tier2\assets")
|
|
font_path = str(assets_dir / "fonts" / "Inter-Regular.ttf")
|
|
result = gui2_mod._resolve_font_path_result(font_path, assets_dir)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data == "fonts/Inter-Regular.ttf"
|
|
|
|
|
|
def test_phase_10_l264_resolve_font_path_result_is_relative_to_raises():
|
|
"""
|
|
L264 _resolve_font_path_result returns Result(data=fallback, errors=[ErrorInfo])
|
|
when Path.is_relative_to() raises ValueError on Windows path comparison.
|
|
|
|
On Python <3.9, AttributeError is raised because is_relative_to doesn't exist.
|
|
On Python >=3.9 with cross-drive paths, ValueError is raised. Either way the
|
|
helper should NOT silently swallow — it converts to ErrorInfo and returns
|
|
the default fallback path "fonts/Inter-Regular.ttf" so the legacy wrapper
|
|
can return a valid path.
|
|
"""
|
|
from pathlib import Path
|
|
import src.gui_2 as gui2_mod
|
|
assets_dir = Path(r"C:\projects\manual_slop_tier2\assets")
|
|
font_path = r"D:\different\drive\fonts\Inter-Regular.ttf"
|
|
result = gui2_mod._resolve_font_path_result(font_path, assets_dir)
|
|
assert result.ok, f"Expected ok=True (graceful degradation), got errors: {result.errors}"
|
|
assert result.data == "fonts/Inter-Regular.ttf"
|
|
if result.errors:
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._resolve_font_path_result"
|
|
|
|
|
|
def test_phase_10_l612_post_init_callback_result_success():
|
|
"""
|
|
L612 _post_init_callback_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the warmup-complete callback registration from
|
|
App._post_init into a Result-returning helper. On success, it registers
|
|
the lambda callback via self.controller.on_warmup_complete() and returns
|
|
Result(data=None).
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.controller = MagicMock()
|
|
result = gui2_mod._post_init_callback_result(app)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
app.controller.on_warmup_complete.assert_called_once()
|
|
|
|
|
|
def test_phase_10_l612_post_init_callback_result_failure():
|
|
"""
|
|
L612 _post_init_callback_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When self.controller.on_warmup_complete() raises (e.g., controller not
|
|
ready or invalid callback), the helper converts to ErrorInfo and returns
|
|
Result(data=None, errors=[ErrorInfo]). The legacy _post_init wrapper
|
|
drains to self._startup_timeline_errors.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.controller = MagicMock()
|
|
app.controller.on_warmup_complete.side_effect = RuntimeError("controller not ready")
|
|
result = gui2_mod._post_init_callback_result(app)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._post_init_callback_result"
|
|
assert "controller not ready" in err.message
|
|
|
|
|
|
def test_phase_10_l728_run_immapp_result_success():
|
|
"""
|
|
L728 _run_immapp_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the immapp.run() call from App.run into a Result-returning
|
|
helper. On success, returns Result(data=None). The legacy run method
|
|
proceeds to self.shutdown() and session_logger.close_session().
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.runner_params = MagicMock()
|
|
with patch("src.gui_2.immapp") as mock_immapp:
|
|
mock_immapp.AddOnsParams.return_value = MagicMock()
|
|
result = gui2_mod._run_immapp_result(app)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
mock_immapp.run.assert_called_once()
|
|
|
|
|
|
def test_phase_10_l728_run_immapp_result_failure():
|
|
"""
|
|
L728 _run_immapp_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When immapp.run() raises RuntimeError (IM_ASSERT, native bundle crash, etc.),
|
|
the helper converts to ErrorInfo. The legacy run method sets the
|
|
controller._gui_degraded_reason and _last_imgui_assert drain attributes,
|
|
appends to _startup_timeline_errors, and returns.
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.runner_params = MagicMock()
|
|
app.controller = MagicMock()
|
|
with patch("src.gui_2.immapp") as mock_immapp:
|
|
mock_immapp.AddOnsParams.return_value = MagicMock()
|
|
mock_immapp.run.side_effect = RuntimeError("IM_ASSERT: invalid scope")
|
|
result = gui2_mod._run_immapp_result(app)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._run_immapp_result"
|
|
assert "IM_ASSERT" in err.message
|
|
|
|
|
|
def test_phase_10_l1052_shutdown_save_ini_result_success():
|
|
"""
|
|
L1052 _shutdown_save_ini_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the imgui.save_ini_settings_to_disk() try/except from
|
|
App.shutdown into a Result-returning helper. On success, returns
|
|
Result(data=None). The legacy shutdown method proceeds to
|
|
self.controller.shutdown().
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.runner_params = MagicMock()
|
|
app.runner_params.ini_filename = "manualslop_layout.ini"
|
|
with patch("src.gui_2.imgui.save_ini_settings_to_disk", return_value=None):
|
|
result = gui2_mod._shutdown_save_ini_result(app)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
|
|
|
|
def test_phase_10_l1052_shutdown_save_ini_result_failure():
|
|
"""
|
|
L1052 _shutdown_save_ini_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When imgui.save_ini_settings_to_disk() raises (e.g., disk full, path
|
|
not writable), the helper converts to ErrorInfo. The legacy shutdown
|
|
method drains to self._startup_timeline_errors and proceeds to
|
|
self.controller.shutdown() (the original behavior preserved).
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.runner_params = MagicMock()
|
|
app.runner_params.ini_filename = "manualslop_layout.ini"
|
|
with patch("src.gui_2.imgui.save_ini_settings_to_disk", side_effect=OSError("disk full")):
|
|
result = gui2_mod._shutdown_save_ini_result(app)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._shutdown_save_ini_result"
|
|
assert "disk full" in err.message
|
|
|
|
|
|
def test_phase_10_l1152_gui_func_entry_log_result_success():
|
|
"""
|
|
L1152 _gui_func_entry_log_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the startup-timing sys.stderr.write from
|
|
App._gui_func into a Result-returning helper. On success, returns
|
|
Result(data=None). The legacy _gui_func method proceeds to the rest
|
|
of the render loop.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.controller = MagicMock()
|
|
app.controller._init_start_ts = 1000.0
|
|
result = gui2_mod._gui_func_entry_log_result(app)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
|
|
|
|
def test_phase_10_l1152_gui_func_entry_log_result_failure():
|
|
"""
|
|
L1152 _gui_func_entry_log_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When the sys.stderr.write or sys.stderr.flush raises (e.g., broken pipe),
|
|
the helper converts to ErrorInfo. The legacy _gui_func method drains to
|
|
self._last_request_errors and proceeds with the rest of the render loop
|
|
(preserving the original behavior of continuing past the failure).
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.controller = MagicMock()
|
|
app.controller._init_start_ts = 1000.0
|
|
with patch("src.gui_2.sys.stderr.write", side_effect=OSError("broken pipe")):
|
|
result = gui2_mod._gui_func_entry_log_result(app)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._gui_func_entry_log_result"
|
|
assert "broken pipe" in err.message
|
|
|
|
|
|
def test_phase_10_l1466_close_vscode_diff_terminate_result_success():
|
|
"""
|
|
L1466 _close_vscode_diff_terminate_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the self._vscode_diff_process.terminate() try/except
|
|
from App._close_vscode_diff into a Result-returning helper. On success,
|
|
returns Result(data=None). The legacy wrapper sets
|
|
self._vscode_diff_process = None (the original behavior preserved).
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app._vscode_diff_process = MagicMock()
|
|
result = gui2_mod._close_vscode_diff_terminate_result(app)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
app._vscode_diff_process.terminate.assert_called_once()
|
|
|
|
|
|
def test_phase_10_l1466_close_vscode_diff_terminate_result_failure():
|
|
"""
|
|
L1466 _close_vscode_diff_terminate_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When the process.terminate() call raises (e.g., process already exited,
|
|
invalid handle), the helper converts to ErrorInfo. The legacy wrapper
|
|
drains to self._last_request_errors and proceeds to set
|
|
self._vscode_diff_process = None (preserving the original behavior).
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app._vscode_diff_process = MagicMock()
|
|
app._vscode_diff_process.terminate.side_effect = OSError("process already exited")
|
|
result = gui2_mod._close_vscode_diff_terminate_result(app)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._close_vscode_diff_terminate_result"
|
|
assert "process already exited" in err.message
|
|
|
|
|
|
def test_phase_10_l1647_focus_response_window_result_success():
|
|
"""
|
|
L1647 _focus_response_window_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the imgui.set_window_focus("Response") try/except from
|
|
render_main_interface into a Result-returning helper. On success, returns
|
|
Result(data=None).
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
with patch("src.gui_2.imgui.set_window_focus", return_value=None):
|
|
result = gui2_mod._focus_response_window_result()
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
|
|
|
|
def test_phase_10_l1647_focus_response_window_result_failure():
|
|
"""
|
|
L1647 _focus_response_window_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When imgui.set_window_focus("Response") raises (e.g., native bundle
|
|
error, IM_ASSERT), the helper converts to ErrorInfo. The caller
|
|
(render_main_interface) drains to self._last_request_errors.
|
|
"""
|
|
from unittest.mock import patch
|
|
import src.gui_2 as gui2_mod
|
|
with patch("src.gui_2.imgui.set_window_focus", side_effect=RuntimeError("IM_ASSERT: window scope")):
|
|
result = gui2_mod._focus_response_window_result()
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._focus_response_window_result"
|
|
assert "IM_ASSERT" in err.message
|
|
|
|
|
|
def test_phase_10_l1693_autosave_flush_result_success():
|
|
"""
|
|
L1693 _autosave_flush_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the auto-save flush_to_project + flush_to_config +
|
|
save_config try/except from render_main_interface into a Result-returning
|
|
helper. On success, returns Result(data=None). The legacy wrapper
|
|
continues with the GUI loop.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
result = gui2_mod._autosave_flush_result(app)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
app._flush_to_project.assert_called_once()
|
|
app._flush_to_config.assert_called_once()
|
|
app.save_config.assert_called_once()
|
|
|
|
|
|
def test_phase_10_l1693_autosave_flush_result_failure():
|
|
"""
|
|
L1693 _autosave_flush_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When any of _flush_to_project/_flush_to_config/save_config raises
|
|
(disk full, JSON parse error), the helper converts to ErrorInfo. The
|
|
caller (render_main_interface) drains to self._last_request_errors and
|
|
continues with the GUI loop (preserving the original "don't disrupt the
|
|
GUI loop" intent via the data plane rather than silent swallow).
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app._flush_to_project.side_effect = OSError("disk full")
|
|
result = gui2_mod._autosave_flush_result(app)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._autosave_flush_result"
|
|
assert "disk full" in err.message
|
|
|
|
|
|
def test_phase_10_l4911_on_warmup_complete_callback_result_success():
|
|
"""
|
|
L4911 _on_warmup_complete_callback_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the warmup-completion try/except from
|
|
_on_warmup_complete_callback into a Result-returning helper. On success,
|
|
returns Result(data=None). The legacy callback (which runs on a
|
|
background _io_pool thread) sets app._warmup_completion_ts and appends
|
|
to app._warmup_toast_messages under the lock.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.controller = MagicMock()
|
|
status = {"pending": [], "completed": ["ai_client", "mcp_client"], "failed": []}
|
|
result = gui2_mod._on_warmup_complete_callback_result(app, status)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
assert app._warmup_toast_messages, "Expected warmup toast message to be appended"
|
|
|
|
|
|
def test_phase_10_l4911_on_warmup_complete_callback_result_failure():
|
|
"""
|
|
L4911 _on_warmup_complete_callback_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When the warmup-completion body raises (e.g., status dict corruption,
|
|
lock acquisition failure), the helper converts to ErrorInfo. The legacy
|
|
wrapper drains to app._worker_errors with the controller lock acquired
|
|
on append (thread-safety critical per sub-track 4 spec).
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.controller = MagicMock()
|
|
app.controller._worker_errors_lock = MagicMock()
|
|
# Force status.get to raise to trigger the except
|
|
bad_status = MagicMock()
|
|
bad_status.get.side_effect = RuntimeError("status dict corrupted")
|
|
result = gui2_mod._on_warmup_complete_callback_result(app, bad_status)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._on_warmup_complete_callback_result"
|
|
assert "status dict corrupted" in err.message
|
|
|
|
|
|
def test_phase_10_l6908_tier_stream_scroll_sync_result_success():
|
|
"""
|
|
L6908 _tier_stream_scroll_sync_result returns Result(data=None) on success.
|
|
|
|
The helper extracts the imgui.set_scroll_here_y + _tier_stream_last_len
|
|
update try/except from render_tier_stream_panel into a Result-returning
|
|
helper. On success, returns Result(data=None). The caller continues
|
|
the render loop normally.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
len_map = MagicMock()
|
|
len_map.get.return_value = 0
|
|
app._tier_stream_last_len = len_map
|
|
mock_imgui = MagicMock()
|
|
result = gui2_mod._tier_stream_scroll_sync_result(app, "stream_key", "content here", mock_imgui)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is None
|
|
|
|
|
|
def test_phase_10_l6908_tier_stream_scroll_sync_result_failure():
|
|
"""
|
|
L6908 _tier_stream_scroll_sync_result returns Result(data=None, errors=[ErrorInfo]) on failure.
|
|
|
|
When the comparison or imgui.set_scroll_here_y raises (TypeError on
|
|
bad content, AttributeError on missing key), the helper converts to
|
|
ErrorInfo. The caller drains to app._last_request_errors.
|
|
"""
|
|
from unittest.mock import MagicMock
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
bad_app = MagicMock()
|
|
bad_app._tier_stream_last_len.get.side_effect = AttributeError("missing key")
|
|
mock_imgui = MagicMock()
|
|
result = gui2_mod._tier_stream_scroll_sync_result(bad_app, "stream_key", "content", mock_imgui)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is None
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._tier_stream_scroll_sync_result"
|
|
assert "missing key" in err.message
|
|
|
|
|
|
def test_phase_10_l7271_dag_cycle_check_result_no_cycle():
|
|
"""
|
|
L7271 _dag_cycle_check_result returns Result(data=False) when no cycle is found.
|
|
|
|
The helper extracts the TrackDAG() cycle check try/except from
|
|
render_task_dag_panel into a Result-returning helper. On a valid DAG
|
|
(no cycle), returns Result(data=False). The caller continues without
|
|
opening the "Cycle Detected!" popup.
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.active_tickets = [{"id": "T-001", "depends_on": []}]
|
|
mock_dag = MagicMock()
|
|
mock_dag.has_cycle.return_value = False
|
|
with patch("src.dag_engine.TrackDAG", return_value=mock_dag):
|
|
result = gui2_mod._dag_cycle_check_result(app)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is False
|
|
|
|
|
|
def test_phase_10_l7271_dag_cycle_check_result_cycle_detected():
|
|
"""
|
|
L7271 _dag_cycle_check_result returns Result(data=True) when a cycle IS found.
|
|
|
|
The helper detects cycles via TrackDAG.has_cycle(). On a cyclic DAG,
|
|
returns Result(data=True). The caller opens the "Cycle Detected!" popup.
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.active_tickets = [
|
|
{"id": "T-001", "depends_on": ["T-002"]},
|
|
{"id": "T-002", "depends_on": ["T-001"]},
|
|
]
|
|
mock_dag = MagicMock()
|
|
mock_dag.has_cycle.return_value = True
|
|
with patch("src.dag_engine.TrackDAG", return_value=mock_dag):
|
|
result = gui2_mod._dag_cycle_check_result(app)
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data is True
|
|
|
|
|
|
def test_phase_10_l7271_dag_cycle_check_result_failure():
|
|
"""
|
|
L7271 _dag_cycle_check_result returns Result(data=False, errors=[ErrorInfo]) on failure.
|
|
|
|
When TrackDAG() construction or has_cycle() raises (bad ticket dict,
|
|
dag engine error), the helper converts to ErrorInfo. The caller
|
|
drains to app._last_request_errors.
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
import src.gui_2 as gui2_mod
|
|
app = MagicMock()
|
|
app.active_tickets = []
|
|
with patch("src.dag_engine.TrackDAG", side_effect=RuntimeError("dag engine failure")):
|
|
result = gui2_mod._dag_cycle_check_result(app)
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data is False
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._dag_cycle_check_result"
|
|
assert "dag engine failure" in err.message
|
|
|
|
|
|
def test_phase_10_l7315_ticket_id_max_int_result_success():
|
|
"""
|
|
L7315 _ticket_id_max_int_result returns Result(data=int) on success.
|
|
|
|
The helper extracts the int(tid[2:]) parsing from the ticket-ID loop
|
|
in render_task_dag_panel into a Result-returning helper. On success
|
|
(valid T-XXX id), returns Result(data=int). The legacy loop continues
|
|
with the maximum value.
|
|
"""
|
|
import src.gui_2 as gui2_mod
|
|
result = gui2_mod._ticket_id_max_int_result("T-042")
|
|
assert result.ok, f"Expected ok=True on success, got errors: {result.errors}"
|
|
assert result.data == 42
|
|
|
|
|
|
def test_phase_10_l7315_ticket_id_max_int_result_failure():
|
|
"""
|
|
L7315 _ticket_id_max_int_result returns Result(data=0, errors=[ErrorInfo]) on failure.
|
|
|
|
When tid[2:] is not parseable as int (e.g., "T-abc", "T-"), the helper
|
|
converts to ErrorInfo. The legacy loop skips this ticket and continues
|
|
with the current max. The caller drains to app._last_request_errors.
|
|
"""
|
|
import src.gui_2 as gui2_mod
|
|
result = gui2_mod._ticket_id_max_int_result("T-abc")
|
|
assert not result.ok, f"Expected ok=False on failure, got data: {result.data}"
|
|
assert result.data == 0
|
|
assert result.errors, "Expected at least one error on failure"
|
|
err = result.errors[0]
|
|
assert err.source == "gui_2._ticket_id_max_int_result"
|
|
assert "invalid literal" in err.message or "T-abc" in err.message
|
|
|
|
|
|
# =============================================================================
|
|
# Phase 10 Invariant Tests (result_migration_gui_2_20260619)
|
|
# Lock the per-phase progress: 13 INTERNAL_SILENT_SWALLOW sites migrated to
|
|
# Result[T] with full propagation (NO narrowing+logging, NO pass-after-log).
|
|
# logging NOT a drain per the user's principle 2026-06-17.
|
|
# =============================================================================
|
|
|
|
|
|
def test_phase_10_invariant_silent_swallow_count_zero():
|
|
"""
|
|
Phase 10 invariant: the audit's INTERNAL_SILENT_SWALLOW count for src/gui_2.py
|
|
is now 0. All 13 sites in the inventory have been migrated to Result[T]
|
|
propagation. The new helpers are classified as INTERNAL_COMPLIANT (or
|
|
BOUNDARY_CONVERSION for the dispatcher wrappers), not as SILENT_SWALLOW.
|
|
|
|
Per the user's principle 2026-06-17 (logging NOT a drain), the
|
|
migration replaced each site with a full Result[T] pattern:
|
|
- Helper: returns Result(data=X, errors=[ErrorInfo]) on exception
|
|
- Wrapper: calls helper, drains errors to the appropriate data plane
|
|
(NOT silent swallow)
|
|
- Tests: 2 tests per site verify success + failure paths
|
|
|
|
Pre-Phase 10 baseline: 13. Post-Phase 10 baseline: 0. This test pins
|
|
the count to 0.
|
|
"""
|
|
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]
|
|
silent_swallows = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_SILENT_SWALLOW"]
|
|
assert len(silent_swallows) == 0, (
|
|
f"Phase 10 invariant: expected 0 INTERNAL_SILENT_SWALLOW sites in src/gui_2.py "
|
|
f"(post-Phase 10 baseline; all 13 sites migrated to Result[T]); "
|
|
f"found {len(silent_swallows)}. Lines: {[f.get('line') for f in silent_swallows]}"
|
|
)
|
|
|
|
|
|
def test_phase_10_invariant_all_13_sites_have_tests():
|
|
"""
|
|
Phase 10 invariant: all 13 INTERNAL_SILENT_SWALLOW migration sites have
|
|
success and failure tests in this test file. Verifies the test coverage
|
|
for Phase 10 is complete.
|
|
|
|
The 13 sites (from PHASE1_SITE_INVENTORY.md):
|
|
- L216 _detect_refresh_rate_win32
|
|
- L241 _resolve_font_path
|
|
- L567 _post_init
|
|
- L683 run
|
|
- L974 shutdown
|
|
- L1074 _gui_func
|
|
- L1348 _close_vscode_diff
|
|
- L1504 render_main_interface (focus_response)
|
|
- L1530 render_main_interface (autosave)
|
|
- L4742 _on_warmup_complete_callback
|
|
- L6694 render_tier_stream_panel
|
|
- L7029 render_task_dag_panel (cycle_check)
|
|
- L7045 render_task_dag_panel (ticket_id_parse)
|
|
"""
|
|
import re
|
|
text = Path(__file__).read_text(encoding="utf-8")
|
|
sites = [
|
|
("L216", "detect_refresh_rate_win32"),
|
|
("L264", "resolve_font_path"),
|
|
("L612", "post_init_callback"),
|
|
("L728", "run_immapp"),
|
|
("L1052", "shutdown_save_ini"),
|
|
("L1152", "gui_func_entry_log"),
|
|
("L1466", "close_vscode_diff_terminate"),
|
|
("L1647", "focus_response_window"),
|
|
("L1693", "autosave_flush"),
|
|
("L4911", "on_warmup_complete_callback"),
|
|
("L6908", "tier_stream_scroll_sync"),
|
|
("L7271", "dag_cycle_check"),
|
|
("L7315", "ticket_id_max_int"),
|
|
]
|
|
for line, site in sites:
|
|
# Allow either _result_success/_result_failure OR descriptive suffixes
|
|
pattern = rf"def test_phase_10_{line.lower()}_{site}_result_(\w+)\("
|
|
matches = re.findall(pattern, text)
|
|
assert len(matches) >= 2, (
|
|
f"Phase 10 invariant: missing tests for {line} {site}. "
|
|
f"Found {len(matches)} tests matching {pattern}. "
|
|
f"Need at least 2 (one success-like, one failure-like)."
|
|
)
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
# Phase 11 Invariant Tests (result_migration_gui_2_20260619)
|
|
# Lock the INTERNAL_RETHROW count: 2 sites reclassified as INTERNAL_PROGRAMMER_RAISE
|
|
# via the dunder-method bare-raise heuristic in audit_exception_handling.py.
|
|
# The 2 sites are bare `raise AttributeError(name)` in the App class's
|
|
# __getattr__ dunder method (L778 and L781 in src/gui_2.py at the time
|
|
# of Phase 11 closure; line numbers may shift slightly with future edits
|
|
# but the audit's category-based invariant is stable).
|
|
# =============================================================================
|
|
|
|
|
|
def test_phase_11_invariant_rethrow_count_zero():
|
|
"""
|
|
Phase 11 invariant: the audit's INTERNAL_RETHROW count for src/gui_2.py
|
|
is now 0. The 2 sites in the App class's __getattr__ method have been
|
|
reclassified as INTERNAL_PROGRAMMER_RAISE via the new dunder-method
|
|
bare-raise heuristic in scripts/audit_exception_handling.py:_classify_raise.
|
|
|
|
Per the styleguide (error_handling.md lines 625-690, Re-Raise Patterns),
|
|
bare raises of AttributeError/NameError in __getattr__/__getattribute__/
|
|
__setattr__/__delattr__ are the canonical dunder-method programmer-error
|
|
pattern, NOT suspicious rethrows. The new heuristic recognizes this and
|
|
reclassifies the sites as INTERNAL_PROGRAMMER_RAISE.
|
|
|
|
Pre-Phase 11 baseline: 2. Post-Phase 11 baseline: 0.
|
|
"""
|
|
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]
|
|
rethrows = [f for f in gui2.get("findings", []) if f.get("category") == "INTERNAL_RETHROW"]
|
|
assert len(rethrows) == 0, (
|
|
f"Phase 11 invariant: expected 0 INTERNAL_RETHROW sites in src/gui_2.py "
|
|
f"(post-Phase 11 baseline; the 2 dunder-method sites reclassified as "
|
|
f"INTERNAL_PROGRAMMER_RAISE); found {len(rethrows)}. "
|
|
f"Lines: {[f.get('line') for f in rethrows]}"
|
|
)
|
|
|
|
|
|
def test_phase_11_invariant_l757_l760_reclassified():
|
|
"""
|
|
Phase 11 invariant: the 2 dunder-method raise sites at L778 and L781
|
|
(formerly documented as L757/L760; line numbers shifted slightly with
|
|
subsequent edits) in src/gui_2.py are no longer in INTERNAL_RETHROW.
|
|
|
|
The heuristic reclassifies any `raise AttributeError` / `raise NameError`
|
|
in a dunder method (__getattr__/__getattribute__/__setattr__/__delattr__)
|
|
as INTERNAL_PROGRAMMER_RAISE. The audit must NOT report these sites as
|
|
INTERNAL_RETHROW after Phase 11.
|
|
|
|
Note: the line numbers may shift with future edits to src/gui_2.py.
|
|
This test scans ALL gui_2.py findings and asserts that no finding in
|
|
the __getattr__ / __setattr__ method context is INTERNAL_RETHROW.
|
|
"""
|
|
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]
|
|
rethrows = [
|
|
f for f in gui2.get("findings", [])
|
|
if f.get("category") == "INTERNAL_RETHROW"
|
|
and f.get("context") in ("__getattr__", "__getattribute__", "__setattr__", "__delattr__")
|
|
]
|
|
assert len(rethrows) == 0, (
|
|
f"Phase 11 invariant: expected 0 INTERNAL_RETHROW findings in "
|
|
f"gui_2.py dunder-method contexts (__getattr__/__getattribute__/"
|
|
f"__setattr__/__delattr__); found {len(rethrows)}. "
|
|
f"Sites: {[(f.get('line'), f.get('context'), f.get('snippet')) for f in rethrows]}"
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# Phase 12 Invariant Tests (result_migration_gui_2_20260619)
|
|
# Lock the UNCLEAR count: 2 sites in _LazyModule._resolve (L65, L69) reclassified
|
|
# as INTERNAL_COMPLIANT via the lazy-loading sentinel fallback heuristic in
|
|
# scripts/audit_exception_handling.py:_try_compliant_pattern (heuristic B).
|
|
# The 2 sites are the lazy-loading sentinel fallback pattern in src/gui_2.py
|
|
# where a proxy class defers a heavy import (numpy, tkinter, etc.) and falls
|
|
# back to a documented sentinel class instance (_FiledialogStub) with an
|
|
# `available: bool = False` flag when the import or attribute access fails.
|
|
# Per the styleguide (error_handling.md:625-690, Re-Raise Patterns) and the
|
|
# lazy-loading pattern guidance, this is the canonical graceful-degradation
|
|
# pattern — NOT silent sliming.
|
|
# =============================================================================
|
|
|
|
|
|
def test_phase_12_invariant_unclear_count_zero():
|
|
"""
|
|
Phase 12 invariant: the audit's UNCLEAR count for src/gui_2.py is now 0.
|
|
The 2 sites in _LazyModule._resolve (L65, L69) have been reclassified as
|
|
INTERNAL_COMPLIANT via the new lazy-loading sentinel fallback heuristic
|
|
in scripts/audit_exception_handling.py:_try_compliant_pattern (heuristic B).
|
|
|
|
The heuristic recognizes the canonical pattern:
|
|
- Enclosing function is in LAZY_LOADER_METHOD_NAMES
|
|
({_resolve, _load, _get, _try_load}) — the standard naming for
|
|
proxy classes that defer a heavy import until first use.
|
|
- The except body does NOT re-raise.
|
|
- The except set is in {AttributeError, ImportError, ModuleNotFoundError}.
|
|
- The except body assigns to a self.<attr> (directly or via nested try).
|
|
|
|
Pre-Phase 12 baseline: 2. Post-Phase 12 baseline: 0.
|
|
"""
|
|
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]
|
|
unclear = [f for f in gui2.get("findings", []) if f.get("category") == "UNCLEAR"]
|
|
assert len(unclear) == 0, (
|
|
f"Phase 12 invariant: expected 0 UNCLEAR sites in src/gui_2.py "
|
|
f"(post-Phase 12 baseline; the 2 lazy-loading sentinel fallback sites "
|
|
f"in _LazyModule._resolve reclassified as INTERNAL_COMPLIANT); "
|
|
f"found {len(unclear)}. "
|
|
f"Lines: {[f.get('line') for f in unclear]}"
|
|
)
|
|
|
|
|
|
def test_phase_12_invariant_l65_l69_reclassified():
|
|
"""
|
|
Phase 12 invariant: the 2 lazy-loading sentinel fallback sites at L65 and
|
|
L69 in src/gui_2.py (the _LazyModule._resolve method) are no longer UNCLEAR.
|
|
|
|
The heuristic reclassifies except sites in methods named
|
|
_resolve/_load/_get/_try_load that fall back to a documented sentinel
|
|
class instance as INTERNAL_COMPLIANT. The audit must NOT report the
|
|
_LazyModule._resolve sites (L65, L69) as UNCLEAR after Phase 12.
|
|
|
|
Note: the line numbers may shift with future edits to src/gui_2.py.
|
|
This test scans ALL gui_2.py findings and asserts that no finding in
|
|
the _LazyModule._resolve method context is UNCLEAR.
|
|
"""
|
|
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]
|
|
unclear_in_resolve = [
|
|
f for f in gui2.get("findings", [])
|
|
if f.get("category") == "UNCLEAR"
|
|
and f.get("context") == "_resolve"
|
|
]
|
|
assert len(unclear_in_resolve) == 0, (
|
|
f"Phase 12 invariant: expected 0 UNCLEAR findings in "
|
|
f"_LazyModule._resolve method context; found {len(unclear_in_resolve)}. "
|
|
f"Sites: {[(f.get('line'), f.get('context'), f.get('snippet')) for f in unclear_in_resolve]}"
|
|
)
|