""" Pure-logic tests for BOT/tally.py (no Discord, no I/O). Usage: source ../SHARED/.venv/bin/activate && python BOT/tests/test_tally_logic.py """ import sys import os from pathlib import Path # Set a temporary storage path before importing BOT (which imports utils) os.environ.setdefault("STORAGE_VOL_PATH", "/tmp/tally_test_storage") Path(os.environ["STORAGE_VOL_PATH"]).mkdir(parents=True, exist_ok=True) sys.path.insert(0, str(Path(__file__).resolve().parents[2])) # repo root for `BOT` package from BOT.tally import Tally, team_index_for, evaluate, format_status, strip_tag, team_identities def _teams(): return [ {"squadron_short": "-DSPL-", "players": [{"nick": "Alpha"}, {"nick": "Bravo"}]}, {"squadron_short": "ENEMY", "players": [{"nick": "Echo"}]}, ] def test_strip_tag(): assert strip_tag("-DSPL-") == "DSPL" assert strip_tag("=ABC=") == "ABC" assert strip_tag(None) == "" def test_team_index_player_mode(): t = Tally(guild_id=1, channel_id=2, mode="player", target="bravo", display_target="Bravo") assert team_index_for(t, _teams()) == 0 t2 = Tally(guild_id=1, channel_id=2, mode="player", target="nobody", display_target="nobody") assert team_index_for(t2, _teams()) is None def test_team_index_squadron_mode(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="ENEMY", display_target="ENEMY") assert team_index_for(t, _teams()) == 1 def test_evaluate_win_increments_and_records_opponent(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") kind = evaluate(t, _teams(), winner_short="DSPL", is_draw=False, session_id="s1") assert kind == "win" assert (t.wins, t.losses) == (1, 0) assert t.last_result_kind == "win" assert t.last_opponent == "ENEMY" def test_evaluate_loss(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") kind = evaluate(t, _teams(), winner_short="ENEMY", is_draw=False, session_id="s1") assert kind == "loss" assert (t.wins, t.losses) == (0, 1) assert t.last_result_kind == "loss" def test_evaluate_draw_counts_as_loss_but_kind_draw(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") kind = evaluate(t, _teams(), winner_short=None, is_draw=True, session_id="s1") assert kind == "draw" assert (t.wins, t.losses) == (0, 1) assert t.last_result_kind == "draw" def test_evaluate_dedup_same_session(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") assert evaluate(t, _teams(), "DSPL", False, "s1") == "win" assert evaluate(t, _teams(), "DSPL", False, "s1") is None assert (t.wins, t.losses) == (1, 0) def test_evaluate_not_involved_returns_none(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="OTHER", display_target="OTHER") assert evaluate(t, _teams(), "DSPL", False, "s1") is None def test_format_status_fresh(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") assert format_status(t, "en") == "0W-0L" def test_format_status_after_win(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") evaluate(t, _teams(), "DSPL", False, "s1") assert format_status(t, "en") == "1W-0L: Win against ENEMY" def test_format_status_after_draw(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") evaluate(t, _teams(), None, True, "s1") assert format_status(t, "en") == "0W-1L: Draw against ENEMY" def test_persistence_roundtrip(tmp_path_env=None): import tempfile, os, importlib import BOT.tally as tal with tempfile.TemporaryDirectory() as d: tal.TALLY_PATH = Path(d) / "TALLY.json" tal._REGISTRY.clear() tal.claim(10, 20, "player", "bravo", "Bravo", user_id=99) # reload into a clean registry from disk tal._REGISTRY.clear() tal.load_from_disk() got = tal.get(10, 20) assert got is not None assert got.mode == "player" and got.target == "bravo" and got.claimed_by == 99 def _teams_resolved_mismatch(): # squadron_short was resolved to "DSPLALPHA" but the winning_team_squadron # stripped value is the bare "DSPL". return [ {"squadron_short": "DSPLALPHA", "squadron": "DSPL", "squadron_tagged": "-DSPL-", "players": [{"nick": "Alpha"}]}, {"squadron_short": "ENEMY", "squadron": "ENEMY", "squadron_tagged": "=ENEMY=", "players": [{"nick": "Echo"}]}, ] def test_evaluate_win_when_winner_matches_bare_squadron_not_resolved_short(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") kind = evaluate(t, _teams_resolved_mismatch(), winner_short="DSPL", is_draw=False, session_id="s1") assert kind == "win" # must NOT be scored as a loss assert (t.wins, t.losses) == (1, 0) def test_squadron_mode_matches_bare_name_despite_resolved_short(): t = Tally(guild_id=1, channel_id=2, mode="squadron", target="DSPL", display_target="DSPL") from BOT.tally import team_index_for assert team_index_for(t, _teams_resolved_mismatch()) == 0 def test_apply_manual_result_win_and_loss(): import tempfile import BOT.tally as tal with tempfile.TemporaryDirectory() as d: tal.TALLY_PATH = Path(d) / "TALLY.json" tal._REGISTRY.clear() tal.claim(1, 2, "player", "bravo", "Bravo", user_id=7) win = tal.apply_manual_result(1, 2, "win") assert win is not None assert (win.wins, win.losses) == (1, 0) assert win.last_result_kind == "win" and win.last_opponent == "DEV" loss = tal.apply_manual_result(1, 2, "loss") assert loss is not None assert (loss.wins, loss.losses) == (1, 1) assert loss.last_result_kind == "loss" and loss.last_opponent == "DEV" # No active tally in a different VC → None assert tal.apply_manual_result(1, 999, "win") is None def _run(): fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")] for fn in fns: fn() print(f"ok {fn.__name__}") print(f"\nALL {len(fns)} TESTS PASSED") if __name__ == "__main__": _run()