MAINT-21: tests read and patch the Controller through tools/main_files.py

Prepares the split of app/main.py's Controller into files by job, with no app
change. tools/main_files.py gives a test the text and the AST of main.py plus
the files split from it, and `live`, which sets a global in every file whose
functions read it (and raises when none does). Tests that read main.py's
source, and tests and replay tools that patch a main global (datetime,
compute, resolve, _mono, WRITE_BUDGET_S, ...), go through it, so they pass on
this layout and on the split one. test_split_patches.py fails when a test or
tool patches an app.main attribute that main.py no longer reads, or that
another file also reads. tools/maint21_same_ast.py proves every function,
method and module-level assignment of main.py at the base is found unchanged
in its new file.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-09 00:27:07 +01:00
co-authored by Claude
parent 56fc41fffa
commit 0d49027daa
50 changed files with 707 additions and 179 deletions
+13 -6
View File
@@ -135,6 +135,9 @@ from app.arbiter import (
)
from app.control import Tuning, compute, peak_at_risk
from app.freeze import FreezeWatchdogConfig, looks_frozen, rail_w
import os # noqa: E402
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "tools")) # noqa: E402
import main_files # noqa: E402
from app import main as app_main
from app.inverter import Inverter, build_roster
from app.main import Controller
@@ -1295,7 +1298,7 @@ print(" the wiring's own mutations - a default install, maintenance charging")
# `peak_at_risk()` answers False forever. That is the whole point - it is the
# configuration on which the old suspension could not fire.
MAINT_CHARGING = {"phase": "charge"}
_mainsrc = (HERE / "app" / "main.py").read_text(encoding="utf-8")
_mainsrc = main_files.source()
def maint_cycle(tag, want_floor_w=LIVE_MAX_W * 0.3, **kw):
@@ -2709,7 +2712,7 @@ CLAIM_SOURCES = {"device", "supervised", "failsafe", "safety", "peak",
# silent gap in the scan.
CLAIM_BUILDERS = {"peak_claim": "peak", "arb_claim": "arbitrage"}
_main_tree = ast.parse((HERE / "app" / "main.py").read_text(encoding="utf-8"))
_main_tree = main_files.tree()
_cycle_def = first((
f
for cls in ast.walk(_main_tree)
@@ -2762,7 +2765,7 @@ def cycle_claims(run):
call site and nowhere else.
"""
seen = []
real = app_main.resolve
real = main_files.live.resolve
def _spy(claims, **kw):
# ⚠️ `**kw` PASSES THE ROSTER THROUGH UNREAD, AND THAT IS THE WHOLE
@@ -2776,11 +2779,11 @@ def cycle_claims(run):
seen.append(list(claims))
return real(claims, **kw)
app_main.resolve = _spy
main_files.live.resolve = _spy
try:
out = run()
finally:
app_main.resolve = real
main_files.live.resolve = real
return out, (seen[-1] if seen else [])
@@ -4977,7 +4980,11 @@ check("⚠️ AND THE SECOND CONSUMER OF THE SAME REDUCTION. `arbitrage.decide()
# written, at every roster length, in every configuration, and with no fixture
# required.
_APP_SRC = {p.stem: p.read_text(encoding="utf-8")
for p in sorted((HERE / "app").glob("*.py"))}
for p in sorted((HERE / "app").glob("*.py"))
if p.name == "main.py" or p.name not in main_files.files()}
# MAINT-21: main.py and the files split from it, the mixins read as Controller.
_APP_SRC["main"] = re.sub(r"^class \w+Mixin:", "class Controller:",
main_files.source(), flags=re.M)
_ROSTER_OPTION = "inverters" # the ONE seed. Everything else is closure.
_WRAPPERS = {"list", "tuple", "sorted", "enumerate", "zip", "reversed",
"filter", "iter", "set", "dict"}