Skip to content

Commit daa77fd

Browse files
yilibinbinAaronhaohaoclaude
authored
test(batch10-stage0): window-mixin composition guardrails (no production change) (#81)
Stage 0 of the ExtrapolationWindow decomposition plan: pure characterization tests that make any future mixin split provably behavior-preserving. Encodes the failure modes the two-model plan review (Codex + Antigravity/Gemini) identified: 1. MRO frozen — a base re-order (or a shim forgetting its sub-mixins) silently changes method precedence. 2. Qt bases precede all mixins — documents the severance ordering. 3. No mixin overrides a Qt event handler — QMainWindow's C++ wrapper doesn't cooperatively super(), so such an override is silently unreachable (Gemini). 4. No unexpected duplicate method names across sibling mixins — splitting a monolith reverses left-to-right shadowing precedence; only the known intentional shim override (_on_fit_finished) is allowlisted (Codex+Gemini). 5. No __init__ in mixins — construction stays driven by the window (Codex). 6. Internals imported directly by tests (_statistics_raw_table_preserving_cells) stay importable — a split must re-export moved internals (Codex). Each behavioral guard is RED-proven (injecting a Qt-event override / duplicate name / mixin __init__ makes its test fail). 70 neighborhood tests pass; ruff clean. No production code touched. Co-authored-by: fanghao <fanghaoaa@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent cdc9604 commit daa77fd

1 file changed

Lines changed: 153 additions & 0 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Batch-10 Stage 0 guardrails: structural invariants that make any future
2+
``ExtrapolationWindow`` mixin split provably behavior-preserving.
3+
4+
These are pure characterization tests (no production change). Two external
5+
adversarial reviews (Codex + Antigravity/Gemini) of the decomposition plan
6+
identified the failure modes below; this file pins them so a split PR that would
7+
trip one fails loudly instead of silently changing behavior.
8+
9+
See docs/BATCH10_WINDOW_DECOMPOSITION_PLAN.md.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import os
15+
from collections import defaultdict
16+
17+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
18+
19+
import pytest
20+
21+
pytest.importorskip("PySide6")
22+
23+
from PySide6.QtWidgets import QMainWindow
24+
25+
from app_desktop.window import ExtrapolationWindow
26+
27+
28+
def _window_mixins() -> list[type]:
29+
"""The Window*Mixin classes composed into ExtrapolationWindow, in MRO order."""
30+
return [
31+
cls
32+
for cls in ExtrapolationWindow.__mro__
33+
if cls.__name__.startswith("Window") and cls.__name__.endswith("Mixin")
34+
]
35+
36+
37+
# --- Invariant 1: MRO snapshot (provider order is load-bearing) -----------------
38+
# The Qt C++ bases MUST precede every mixin; a split must not re-order this.
39+
_EXPECTED_MRO = [
40+
"ExtrapolationWindow",
41+
"QMainWindow",
42+
"QWidget",
43+
"QObject",
44+
"QPaintDevice",
45+
"Object",
46+
"WindowLatexPdfMixin",
47+
"WindowI18nMixin",
48+
"WindowImagesMixin",
49+
"WindowStatisticsMixin",
50+
"WindowDataMixin",
51+
"WindowFittingMixin",
52+
"WindowFittingResidualsMixin",
53+
"WindowFittingModelsMixin",
54+
"WindowFittingFormattersMixin",
55+
"WindowExtrapolationMixin",
56+
"object",
57+
]
58+
59+
60+
def test_extrapolation_window_mro_is_frozen():
61+
"""A split that re-orders bases (or forgets a shim's sub-mixins) changes
62+
method-resolution precedence — freeze the full MRO. Update this list
63+
*deliberately* when intentionally adding/splitting a mixin."""
64+
actual = [c.__name__ for c in ExtrapolationWindow.__mro__]
65+
assert actual == _EXPECTED_MRO, (
66+
"ExtrapolationWindow MRO changed. If this is an intentional mixin "
67+
"split/reorder, update _EXPECTED_MRO; otherwise the change silently "
68+
"alters method precedence.\n"
69+
f"expected: {_EXPECTED_MRO}\nactual: {actual}"
70+
)
71+
72+
73+
# --- Invariant 2: Qt bases sit before the mixins (severance hazard) --------------
74+
def test_qt_bases_precede_all_mixins_in_mro():
75+
"""QMainWindow (a C++ wrapper that does NOT cooperatively call super())
76+
precedes every mixin, so a Qt event override placed in a mixin would be
77+
silently unreachable. This test documents that ordering so the next guard
78+
(no Qt-event overrides in mixins) is meaningful."""
79+
names = [c.__name__ for c in ExtrapolationWindow.__mro__]
80+
qmain = names.index("QMainWindow")
81+
first_mixin = min(
82+
(i for i, n in enumerate(names) if n.startswith("Window") and n.endswith("Mixin")),
83+
default=len(names),
84+
)
85+
assert qmain < first_mixin, "QMainWindow must precede the mixins in the MRO"
86+
87+
88+
# --- Invariant 3: no mixin overrides a Qt event handler -------------------------
89+
_QT_EVENT_HANDLERS = frozenset(n for n in dir(QMainWindow) if n.endswith("Event"))
90+
91+
92+
def test_no_mixin_overrides_a_qt_event_handler():
93+
"""A mixin that defines e.g. closeEvent/resizeEvent/showEvent would be
94+
silently ignored (Qt C++ base severs the cooperative-super chain). Forbid it
95+
so a split can't accidentally move Qt event handling into a mixin (Gemini)."""
96+
offenders = []
97+
for cls in _window_mixins():
98+
for name in cls.__dict__:
99+
if name in _QT_EVENT_HANDLERS:
100+
offenders.append(f"{cls.__name__}.{name}")
101+
assert offenders == [], (
102+
"Window mixins must not override Qt event handlers (unreachable through "
103+
f"the Qt C++ base). Offenders: {offenders}"
104+
)
105+
106+
107+
# --- Invariant 4: no unexpected duplicate method names across sibling mixins ------
108+
# Splitting a monolith bottom->top and composing Shim(A_top, B, C_bottom) reverses
109+
# shadowing precedence (A shadows C under left-to-right MRO). Any duplicate method
110+
# name across mixins is therefore a precedence hazard — allow only the KNOWN,
111+
# intentional shim override, fail on anything new (Codex + Gemini).
112+
_ALLOWED_DUP_METHODS = {
113+
# The fitting shim intentionally overrides this: it calls super() then adds
114+
# fallback-history UI (window_fitting_mixin.py). Both the shim and the
115+
# residuals sub-mixin define it, by design.
116+
"_on_fit_finished": {"WindowFittingMixin", "WindowFittingResidualsMixin"},
117+
}
118+
119+
120+
def test_no_unexpected_duplicate_method_names_across_mixins():
121+
owners: dict[str, set[str]] = defaultdict(set)
122+
for cls in _window_mixins():
123+
for name, val in cls.__dict__.items():
124+
if callable(val) and not name.startswith("__"):
125+
owners[name].add(cls.__name__)
126+
dups = {n: cs for n, cs in owners.items() if len(cs) > 1}
127+
unexpected = {n: cs for n, cs in dups.items() if _ALLOWED_DUP_METHODS.get(n) != cs}
128+
assert unexpected == {}, (
129+
"Unexpected method-name collision across sibling window mixins — under "
130+
"left-to-right MRO the earlier mixin shadows the later one, which a "
131+
"split can silently reverse. Resolve the collision or, if intentional, "
132+
f"add it to _ALLOWED_DUP_METHODS. Collisions: {unexpected}"
133+
)
134+
135+
136+
# --- Invariant 5: split mixins define no __init__ (construction order) ----------
137+
def test_window_mixins_do_not_define_init():
138+
"""Mixins must not define __init__ — ExtrapolationWindow.__init__ drives
139+
construction, and a mixin __init__ would either be skipped or fight the Qt
140+
base's construction order. A split must keep pulling all setup from the
141+
window's own __init__ (Codex/Gemini Stage-0 check)."""
142+
offenders = [cls.__name__ for cls in _window_mixins() if "__init__" in cls.__dict__]
143+
assert offenders == [], f"Window mixins must not define __init__: {offenders}"
144+
145+
146+
# --- Invariant 6: internal helpers imported by tests stay importable -------------
147+
# Splits must re-export moved internals from their original module, or these
148+
# direct imports (in the test suite) break. Enumerated so a split is forced to
149+
# preserve them (Codex).
150+
def test_directly_imported_mixin_internals_remain_importable():
151+
from app_desktop.window_statistics_mixin import ( # noqa: F401
152+
_statistics_raw_table_preserving_cells,
153+
)

0 commit comments

Comments
 (0)