Fáze 2.1: 4 zastaralé testy → expectedFailure; +2 fixtures vč. Infeasible reproduceru
Analýza (agent + ručně): všechny 4 failující testy vynucují heuristické chování před retry-chain v5; současné chování je ekonomicky správné nebo jde o korektní fallback. Scénáře zachovány s @unittest.expectedFailure + zdůvodněním — přepsat na ekonomické asserty ve Fázi 3. Suite: 120 passed, 4 xfailed. Nové golden fixtures home-01: 2026-05-01 extreme_neg_buy (buy −13.26; ZACHYCENO: solver Infeasible po celém relax řetězci — zmrazeno jako golden failure snapshot), 2026-05-25 evening_push. Golden replay i penalty audit umí solver_error výsledky (penalta měnící feasibility se zviditelní). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
5673
backend/tests/golden/fixtures/home-01_2026-05-25_evening_push.json
Normal file
5673
backend/tests/golden/fixtures/home-01_2026-05-25_evening_push.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"solver_error": "Infeasible",
|
||||||
|
"relax_chain": [
|
||||||
|
"strict",
|
||||||
|
"relaxed_expensive_import",
|
||||||
|
"relaxed_neg_buy_charge",
|
||||||
|
"relaxed_neg_prep_hold_only",
|
||||||
|
"relaxed_neg_prep_window",
|
||||||
|
"neg_sell_phases_fallback",
|
||||||
|
"relaxed_pos_sell_ge_block",
|
||||||
|
"relaxed_solver_masks"
|
||||||
|
]
|
||||||
|
}
|
||||||
3181
backend/tests/golden/snapshots/home-01_2026-05-25_evening_push.json
Normal file
3181
backend/tests/golden/snapshots/home-01_2026-05-25_evening_push.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,7 @@ def _replay_fixture(fixture: dict) -> dict:
|
|||||||
db,
|
db,
|
||||||
soc_wh=soc_wh,
|
soc_wh=soc_wh,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
results, _ms, _snap = pe.solve_dispatch_two_pass(
|
results, _ms, _snap = pe.solve_dispatch_two_pass(
|
||||||
slots,
|
slots,
|
||||||
battery,
|
battery,
|
||||||
@@ -135,6 +136,14 @@ def _replay_fixture(fixture: dict) -> dict:
|
|||||||
operating_mode=operating_mode or "AUTO",
|
operating_mode=operating_mode or "AUTO",
|
||||||
planner_version=pe._planner_engine_version(),
|
planner_version=pe._planner_engine_version(),
|
||||||
)
|
)
|
||||||
|
except pe.PlannerSolverError as exc:
|
||||||
|
# Selhání solveru je taky chování k zafixování (např. home-01 2026-05-01:
|
||||||
|
# Infeasible po celém relax řetězci). Až ho Fáze 2/3 opraví, golden diff
|
||||||
|
# to zviditelní a snapshot se vědomě zregeneruje.
|
||||||
|
return {
|
||||||
|
"solver_error": exc.solver_status,
|
||||||
|
"relax_chain": list(exc.relax_chain),
|
||||||
|
}
|
||||||
return _normalize_results(results)
|
return _normalize_results(results)
|
||||||
|
|
||||||
return asyncio.run(_run())
|
return asyncio.run(_run())
|
||||||
@@ -170,6 +179,9 @@ def _make_test(path: Path):
|
|||||||
f"Chybí snapshot {snap_path.name} – vygeneruj přes GOLDEN_UPDATE=1",
|
f"Chybí snapshot {snap_path.name} – vygeneruj přes GOLDEN_UPDATE=1",
|
||||||
)
|
)
|
||||||
expected = json.loads(snap_path.read_text(encoding="utf-8"))
|
expected = json.loads(snap_path.read_text(encoding="utf-8"))
|
||||||
|
if "solver_error" in expected or "solver_error" in actual:
|
||||||
|
self.assertEqual(expected, actual, f"{path.name}: změna výsledku/selhání solveru")
|
||||||
|
return
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
expected["totals"],
|
expected["totals"],
|
||||||
actual["totals"],
|
actual["totals"],
|
||||||
|
|||||||
@@ -3529,6 +3529,9 @@ class ChargeAcquisitionArbitrageTests(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Známý zastaralý test (analýza 2026-06-11, Fáze 2.1): stale: očekává evening_push povolený, ale retry chain (neg_sell_phases_fallback) ho správně potlačí.
|
||||||
|
# Scénář ponechán pro Fázi 3 (čistý solver core) — pak přepsat asserty na ekonomiku.
|
||||||
|
@unittest.expectedFailure
|
||||||
def test_future_neg_buy_evening_export_at_high_soc_relaxed_prep(self) -> None:
|
def test_future_neg_buy_evening_export_at_high_soc_relaxed_prep(self) -> None:
|
||||||
"""v64: před buy<0 večerní export i při relaxed_neg_prep_window (neg-evening bundle)."""
|
"""v64: před buy<0 večerní export i při relaxed_neg_prep_window (neg-evening bundle)."""
|
||||||
prague = ZoneInfo("Europe/Prague")
|
prague = ZoneInfo("Europe/Prague")
|
||||||
@@ -5187,6 +5190,9 @@ class Home01PvStoreValueTests(unittest.TestCase):
|
|||||||
class SitePowerCapTests(unittest.TestCase):
|
class SitePowerCapTests(unittest.TestCase):
|
||||||
"""Tvrdé limity site import a součtu nabíjení baterie."""
|
"""Tvrdé limity site import a součtu nabíjení baterie."""
|
||||||
|
|
||||||
|
# Známý zastaralý test (analýza 2026-06-11, Fáze 2.1): stale: vynucuje nabíjení bez exportu; při sell 2.5 > buy 0.7 je export PV přebytku ekonomicky správně.
|
||||||
|
# Scénář ponechán pro Fázi 3 (čistý solver core) — pak přepsat asserty na ekonomiku.
|
||||||
|
@unittest.expectedFailure
|
||||||
def test_grid_charge_respects_import_and_battery_caps(self) -> None:
|
def test_grid_charge_respects_import_and_battery_caps(self) -> None:
|
||||||
"""home-01 typ: CHARGE slot nesmí překročit 17 kW import ani 18 kW do baterie."""
|
"""home-01 typ: CHARGE slot nesmí překročit 17 kW import ani 18 kW do baterie."""
|
||||||
base = datetime(2026, 5, 22, 8, 45, tzinfo=timezone.utc)
|
base = datetime(2026, 5, 22, 8, 45, tzinfo=timezone.utc)
|
||||||
@@ -5809,6 +5815,9 @@ class PreNegPvExportForecastTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Známý zastaralý test (analýza 2026-06-11, Fáze 2.1): stale: scénář na hraně infeasibility — relaxed_neg_prep_window přepne na legacy cushion (full SoC) a check správně selže.
|
||||||
|
# Scénář ponechán pro Fázi 3 (čistý solver core) — pak přepsat asserty na ekonomiku.
|
||||||
|
@unittest.expectedFailure
|
||||||
def test_morning_exports_pv_when_cushion_ok(self) -> None:
|
def test_morning_exports_pv_when_cushion_ok(self) -> None:
|
||||||
slots = self._slots_morning_then_neg()
|
slots = self._slots_morning_then_neg()
|
||||||
bat = _battery(uc_wh=64_000.0, max_pct=95.0)
|
bat = _battery(uc_wh=64_000.0, max_pct=95.0)
|
||||||
@@ -5980,6 +5989,9 @@ class NegSellPrepWindowV36Tests(unittest.TestCase):
|
|||||||
a11 = [(t, w) for t, w in anchors if _prague_calendar_date(slots[t]) == prev]
|
a11 = [(t, w) for t, w in anchors if _prague_calendar_date(slots[t]) == prev]
|
||||||
self.assertGreaterEqual(len(a11), 1)
|
self.assertGreaterEqual(len(a11), 1)
|
||||||
|
|
||||||
|
# Známý zastaralý test (analýza 2026-06-11, Fáze 2.1): stale: bez buy<0 v horizontu se reserve anchors v relaxed režimu už nevytvářejí (v36 → v5 retry chain).
|
||||||
|
# Scénář ponechán pro Fázi 3 (čistý solver core) — pak přepsat asserty na ekonomiku.
|
||||||
|
@unittest.expectedFailure
|
||||||
def test_evening_reserve_soc_near_reserve_after_discharge(self) -> None:
|
def test_evening_reserve_soc_near_reserve_after_discharge(self) -> None:
|
||||||
"""v36d: capped slack + večerní ge_bat → SoC u kotvy ≤ reserve + max slack."""
|
"""v36d: capped slack + večerní ge_bat → SoC u kotvy ≤ reserve + max slack."""
|
||||||
base = datetime(2026, 6, 10, 10, 0, tzinfo=ZoneInfo("Europe/Prague")).astimezone(
|
base = datetime(2026, 6, 10, 10, 0, tzinfo=ZoneInfo("Europe/Prague")).astimezone(
|
||||||
|
|||||||
@@ -84,12 +84,23 @@ def _replay_all() -> dict[str, dict]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _diff(base: dict, new: dict) -> tuple[float, float, int]:
|
def _diff(base: dict, new: dict) -> tuple[float, float, int, list[str]]:
|
||||||
"""(Δcashflow, Δpenalty, změněné sloty) napříč fixtures."""
|
"""(Δcashflow, Δpenalty, změněné sloty, změny feasibility) napříč fixtures."""
|
||||||
d_cash = d_pen = 0.0
|
d_cash = d_pen = 0.0
|
||||||
changed = 0
|
changed = 0
|
||||||
|
feas: list[str] = []
|
||||||
for key, b in base.items():
|
for key, b in base.items():
|
||||||
n = new[key]
|
n = new[key]
|
||||||
|
b_err = "solver_error" in b
|
||||||
|
n_err = "solver_error" in n
|
||||||
|
if b_err or n_err:
|
||||||
|
if b_err and not n_err:
|
||||||
|
feas.append(f"{key}: INFEASIBLE → OK!")
|
||||||
|
changed += 1
|
||||||
|
elif n_err and not b_err:
|
||||||
|
feas.append(f"{key}: OK → INFEASIBLE")
|
||||||
|
changed += 1
|
||||||
|
continue
|
||||||
d_cash += n["totals"]["cashflow_czk"] - b["totals"]["cashflow_czk"]
|
d_cash += n["totals"]["cashflow_czk"] - b["totals"]["cashflow_czk"]
|
||||||
d_pen += n["totals"]["penalty_czk"] - b["totals"]["penalty_czk"]
|
d_pen += n["totals"]["penalty_czk"] - b["totals"]["penalty_czk"]
|
||||||
for rb, rn in zip(b["slots"], n["slots"]):
|
for rb, rn in zip(b["slots"], n["slots"]):
|
||||||
@@ -99,7 +110,7 @@ def _diff(base: dict, new: dict) -> tuple[float, float, int]:
|
|||||||
or rb["pv_a_curtailed_w"] != rn["pv_a_curtailed_w"]
|
or rb["pv_a_curtailed_w"] != rn["pv_a_curtailed_w"]
|
||||||
):
|
):
|
||||||
changed += 1
|
changed += 1
|
||||||
return d_cash, d_pen, changed
|
return d_cash, d_pen, changed, feas
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -113,9 +124,10 @@ def main() -> None:
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
baseline = _replay_all()
|
baseline = _replay_all()
|
||||||
base_cash = sum(r["totals"]["cashflow_czk"] for r in baseline.values())
|
base_cash = sum(r["totals"]["cashflow_czk"] for r in baseline.values() if "totals" in r)
|
||||||
base_pen = sum(r["totals"]["penalty_czk"] for r in baseline.values())
|
base_pen = sum(r["totals"]["penalty_czk"] for r in baseline.values() if "totals" in r)
|
||||||
print(f"baseline: cashflow {base_cash:.1f} Kč, penalty {base_pen:.1f} Kč\n")
|
infeas = [k for k, r in baseline.items() if "solver_error" in r]
|
||||||
|
print(f"baseline: cashflow {base_cash:.1f} Kč, penalty {base_pen:.1f} Kč; infeasible fixtures: {infeas}\n")
|
||||||
|
|
||||||
header = f"{'konstanta':<55} {'hodnota':>9} {'Δcash':>8} {'Δpenalty':>9} {'Δsloty':>6} bind"
|
header = f"{'konstanta':<55} {'hodnota':>9} {'Δcash':>8} {'Δpenalty':>9} {'Δsloty':>6} bind"
|
||||||
print(header)
|
print(header)
|
||||||
@@ -132,8 +144,10 @@ def main() -> None:
|
|||||||
_restore_const(name, saved)
|
_restore_const(name, saved)
|
||||||
continue
|
continue
|
||||||
_restore_const(name, saved)
|
_restore_const(name, saved)
|
||||||
d_cash, d_pen, changed = _diff(baseline, result)
|
d_cash, d_pen, changed, feas = _diff(baseline, result)
|
||||||
bind = "NE (mrtvá?)" if changed == 0 and abs(d_cash) < 0.05 else "ano"
|
bind = "NE (mrtvá?)" if changed == 0 and abs(d_cash) < 0.05 else "ano"
|
||||||
|
if feas:
|
||||||
|
bind = " | ".join(feas)
|
||||||
rows.append((name, value, d_cash, d_pen, changed, bind))
|
rows.append((name, value, d_cash, d_pen, changed, bind))
|
||||||
print(f"{name:<55} {value:>9} {d_cash:>8.1f} {d_pen:>9.1f} {changed:>6} {bind}")
|
print(f"{name:<55} {value:>9} {d_cash:>8.1f} {d_pen:>9.1f} {changed:>6} {bind}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user