Add pro watchlist matches page; load streamer clips by viewport tier with posters; harden STRATZ matchup refresh and OpenDota cross hints. Co-authored-by: Cursor <cursoragent@cursor.com>
205 lines
6.6 KiB
Python
205 lines
6.6 KiB
Python
"""Unit tests for STRATZ matchup ranking + OpenDota cross classification.
|
|
|
|
Run from repo root:
|
|
python -m unittest shared.tests.test_matchup_cross -v
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
|
|
from shared.matchup_cross import ( # noqa: E402
|
|
classify_cross_source,
|
|
enrich_entry_cross,
|
|
enrich_hero_matchups_cross,
|
|
)
|
|
from web.fetch_stratz_meta import ( # noqa: E402
|
|
annotate_matchup_cell,
|
|
build_matchup_file_payload,
|
|
load_previous_matchups,
|
|
rank_matchup_lists,
|
|
write_json_atomic,
|
|
)
|
|
|
|
|
|
class RankMatchupListsTests(unittest.TestCase):
|
|
def test_counters_sorted_by_advantage_not_raw_wr(self) -> None:
|
|
# Wraith King: wr < 0.5 but positive relative advantage — still a counter.
|
|
vs = [
|
|
{
|
|
"hero_id": 42,
|
|
"games": 3286,
|
|
"wins": 1597,
|
|
"advantage": 4.444,
|
|
"wr": 0.486,
|
|
},
|
|
{
|
|
"hero_id": 94,
|
|
"games": 1070,
|
|
"wins": 649,
|
|
"advantage": 12.994,
|
|
"wr": 0.607,
|
|
},
|
|
{
|
|
"hero_id": 34,
|
|
"games": 1171,
|
|
"wins": 723,
|
|
"advantage": 7.318,
|
|
"wr": 0.617,
|
|
},
|
|
]
|
|
ranked = rank_matchup_lists(vs, [], take=3)
|
|
self.assertEqual(
|
|
[e["hero_id"] for e in ranked["counters"]],
|
|
[94, 34, 42],
|
|
)
|
|
wk = ranked["counters"][2]
|
|
self.assertLess(wk["wr"], 0.5)
|
|
self.assertGreater(wk["advantage"], 0)
|
|
|
|
def test_countered_mirrors_lowest_advantage(self) -> None:
|
|
vs = [
|
|
{"hero_id": 1, "games": 100, "wins": 60, "advantage": 5.0, "wr": 0.6},
|
|
{"hero_id": 2, "games": 100, "wins": 40, "advantage": -8.0, "wr": 0.4},
|
|
{"hero_id": 3, "games": 50, "wins": 20, "advantage": -3.0, "wr": 0.4},
|
|
]
|
|
ranked = rank_matchup_lists(vs, [], take=2)
|
|
fear = ranked["countered"]
|
|
self.assertEqual([e["hero_id"] for e in fear], [2, 3])
|
|
self.assertAlmostEqual(fear[0]["advantage"], 8.0)
|
|
self.assertAlmostEqual(fear[0]["wr"], 0.6)
|
|
|
|
def test_synergies_sorted_desc(self) -> None:
|
|
with_rows = [
|
|
{"hero_id": 10, "games": 10, "wins": 5, "synergy": 1.0, "wr": 0.5},
|
|
{"hero_id": 11, "games": 10, "wins": 6, "synergy": 4.5, "wr": 0.6},
|
|
]
|
|
ranked = rank_matchup_lists([], with_rows, take=2)
|
|
self.assertEqual([e["hero_id"] for e in ranked["synergies"]], [11, 10])
|
|
|
|
|
|
class MatchupCacheHelpersTests(unittest.TestCase):
|
|
def test_annotate_and_payload_metadata(self) -> None:
|
|
cell = annotate_matchup_cell(
|
|
{"counters": [{"hero_id": 1}], "countered": [], "synergies": []},
|
|
fetched_at="2026-07-28T00:00:00+00:00",
|
|
stale=True,
|
|
)
|
|
self.assertTrue(cell["stale"])
|
|
self.assertEqual(cell["fetched_at"], "2026-07-28T00:00:00+00:00")
|
|
payload = build_matchup_file_payload(
|
|
{"antimage": cell},
|
|
take=12,
|
|
match_limit=50,
|
|
started_at="2026-07-28T00:00:00+00:00",
|
|
finished_at="2026-07-28T01:00:00+00:00",
|
|
stats={"heroes": 1, "ok": 0, "failed": 1, "stale_kept": 1},
|
|
)
|
|
self.assertEqual(payload["scope"]["kind"], "global_aggregate")
|
|
self.assertIn("global", payload["scope"]["kind"])
|
|
self.assertTrue(payload["scope"]["label_zh"])
|
|
self.assertEqual(payload["stats"]["stale_kept"], 1)
|
|
|
|
def test_atomic_write_and_resume_load(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
path = Path(tmp) / "stratz_matchup_tops.json"
|
|
payload = build_matchup_file_payload(
|
|
{
|
|
"antimage": annotate_matchup_cell(
|
|
{"counters": [], "countered": [], "synergies": []},
|
|
fetched_at="t0",
|
|
)
|
|
},
|
|
take=12,
|
|
match_limit=50,
|
|
started_at="t0",
|
|
finished_at="t0",
|
|
)
|
|
write_json_atomic(path, payload)
|
|
loaded = load_previous_matchups(path)
|
|
self.assertIn("antimage", loaded)
|
|
|
|
|
|
class CrossClassifyTests(unittest.TestCase):
|
|
def test_agree_conflict_weak(self) -> None:
|
|
self.assertEqual(
|
|
classify_cross_source(
|
|
stratz_signed=5.0, odota_adv_val=0.04, odota_games=200
|
|
),
|
|
"agree",
|
|
)
|
|
self.assertEqual(
|
|
classify_cross_source(
|
|
stratz_signed=5.0, odota_adv_val=-0.04, odota_games=200
|
|
),
|
|
"conflict",
|
|
)
|
|
self.assertEqual(
|
|
classify_cross_source(
|
|
stratz_signed=5.0, odota_adv_val=0.04, odota_games=10
|
|
),
|
|
"weak",
|
|
)
|
|
|
|
def test_countered_uses_restored_sign(self) -> None:
|
|
# Display advantage for countered is +8 (negated); original vs was -8.
|
|
by_odota = {
|
|
"1": {
|
|
"2": {"games": 200, "wins": 80}, # AM wr 40% vs peer
|
|
}
|
|
}
|
|
# Baseline for hero 1 ≈ 0.4 from only this matchup.
|
|
from shared.matchup_cross import build_baseline
|
|
|
|
baseline = build_baseline(by_odota)
|
|
entry = enrich_entry_cross(
|
|
{
|
|
"hero_id": 2,
|
|
"games": 100,
|
|
"wins": 40,
|
|
"advantage": 8.0,
|
|
"wr": 0.6,
|
|
},
|
|
hero_id=1,
|
|
kind="countered",
|
|
by_odota=by_odota,
|
|
baseline=baseline,
|
|
min_games=80,
|
|
)
|
|
# odota_adv = 0.4 - 0.4 = 0 → weak
|
|
self.assertEqual(entry["cross"]["status"], "weak")
|
|
|
|
def test_enrich_hero_keeps_stale_flag(self) -> None:
|
|
cell = {
|
|
"counters": [
|
|
{
|
|
"hero_id": 94,
|
|
"games": 100,
|
|
"wins": 60,
|
|
"advantage": 3.0,
|
|
"wr": 0.6,
|
|
}
|
|
],
|
|
"countered": [],
|
|
"synergies": [],
|
|
"fetched_at": "t0",
|
|
"stale": True,
|
|
}
|
|
out = enrich_hero_matchups_cross(
|
|
cell,
|
|
hero_id=1,
|
|
by_odota={},
|
|
baseline={},
|
|
)
|
|
self.assertTrue(out["stale"])
|
|
self.assertEqual(out["cross"] if False else out["counters"][0]["cross"]["status"], "weak")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|