from pathlib import Path from hockey_data.service import HockeyDataService ROOT = Path(__file__).resolve().parents[1] APP_JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8") APP = (ROOT / "app.py").read_text(encoding="utf-8") def _penalty(side: str, remaining: int, *, started: bool) -> dict: return { "side": side, "infraction": {"id": "minor"}, "preset": "2m", "durationMs": 120000, "remainingMs": remaining, "running": False, "startedOnce": started, "finished": False, } def _strength(*penalties: dict) -> dict: settings = { "strength_regulation_skaters": 5, "strength_min_skaters": 3, "strength_state_labels": { "regulation": { "5x4": {"ru": "БОЛ"}, "4x4": {"ru": "4x4"}, "4x3": {"ru": "4x3"}, } }, } return HockeyDataService._strength_payload( settings, stage="regular", current_period="2", timer_state={"penalty_board": {"penalties": list(penalties)}}, language="ru", ) def test_prepared_penalty_does_not_change_strength_until_explicit_start(): prepared = _strength(_penalty("home", 120000, started=False)) assert prepared["strength_label"] == "5×5" assert prepared["advantage_side"] == "" started = _strength(_penalty("home", 120000, started=True)) assert started["strength_label"] == "4×5" assert started["advantage_side"] == "away" assert started["state_label"] == "БОЛ" def test_complex_transition_strength_sequence_matches_requested_flow(): # Initial HOME penalty: AWAY/right has the advantage. p1 = _penalty("home", 80000, started=True) value = _strength(p1) assert value["strength_label"] == "4×5" assert value["advantage_side"] == "away" # Coincidental pair is added while the first penalty is still running. p2 = _penalty("home", 120000, started=True) p3 = _penalty("away", 120000, started=True) value = _strength(p1, p2, p3) assert value["strength_label"] == "3×4" assert value["advantage_side"] == "away" assert value["state_label"] == "4x3" # The first penalty expires: the pair remains, so strength is equal 4x4. value = _strength( _penalty("home", 40000, started=True), _penalty("away", 40000, started=True), ) assert value["strength_label"] == "4×4" assert value["advantage_side"] == "" assert value["state_label"] == "4x4" # AWAY/right takes another penalty: HOME/left now has a 4x3 advantage. value = _strength( _penalty("home", 30000, started=True), _penalty("away", 30000, started=True), _penalty("away", 120000, started=True), ) assert value["strength_label"] == "4×3" assert value["advantage_side"] == "home" assert value["state_label"] == "4x3" # Coincidental pair expires, leaving ordinary HOME power play. value = _strength(_penalty("away", 90000, started=True)) assert value["strength_label"] == "5×4" assert value["advantage_side"] == "home" assert value["state_label"] == "БОЛ" def test_runtime_holds_single_plate_through_temporary_equal_strength(): start = APP_JS.index("function penaltyDisplayEntriesByTargetSide") end = APP_JS.index("function hockeyPenaltySideMappingDetail", start) block = APP_JS[start:end] assert "canHoldEqualStrength" in block assert "state.hockeyPenaltyAdvantageCycle.hadAdvantage" in block assert "lastAdvantageSide" in block assert "const canHoldEqualStrength = Boolean(state.hockeyPenaltyAdvantageCycle.hadAdvantage)" in block assert 'home: rememberedSide === "home" && transitionEntry ? [transitionEntry] : []' in block assert 'away: rememberedSide === "away" && transitionEntry ? [transitionEntry] : []' in block assert 'home: advantageSide === "home" && transitionEntry ? [transitionEntry] : []' in block assert 'away: advantageSide === "away" && transitionEntry ? [transitionEntry] : []' in block assert "holdingEqualStrength" in block # The countdown source remains the globally soonest active penalty. assert "const transitionEntry = all[0] || null;" in block def test_pure_equal_strength_without_previous_advantage_still_has_no_plate(): start = APP_JS.index("function penaltyDisplayEntriesByTargetSide") end = APP_JS.index("function hockeyPenaltySideMappingDetail", start) block = APP_JS[start:end] assert "if (!canHoldEqualStrength)" in block assert "return { home: [], away: [], routedToAdvantage: false" in block def test_advantage_cycle_is_saved_and_restored_for_reload_during_4x4(): snapshot = APP_JS.split("function hockeyGameTimerSnapshot", 1)[1].split("function hockeyApplySavedTimers", 1)[0] assert "advantage_cycle" in snapshot assert "had_advantage" in snapshot assert "last_advantage_side" in snapshot restore = APP_JS.split("function hockeyApplySavedTimers", 1)[1].split("async function hockeyPersistGameTimers", 1)[0] assert "restoredPenaltyAdvantageCycle" in restore assert "savedCycle.last_advantage_side" in restore normalized = HockeyDataService._normalise_game_timer_state({ "penalty_board": { "penalties": [], "history": [], "advantage_cycle": {"had_advantage": True, "last_advantage_side": "away"}, } }) assert normalized["penalty_board"]["advantage_cycle"] == { "had_advantage": True, "last_advantage_side": "away", } def test_full_strength_waits_for_last_started_penalty_not_prepared_draft(): control = APP_JS.split("function controlHockeyPenalty", 1)[1].split("function formatHockeyPenaltyTime", 1)[0] assert "hockeyPenaltyHasStarted(item)" in control ticker = APP_JS.split("function updateHockeyPenaltyBoards", 1)[1].split("function registerHockeyBoardNode", 1)[0] assert "remainingTotal = board.penalties.filter((item) => !item.finished && hockeyEventReady(item) && hockeyPenaltyHasStarted(item)).length" in ticker def test_build96_runtime_version(): assert 'BUILD_VERSION = "2026.08.20.17"' in APP