This commit is contained in:
devRaGonSa
2026-06-05 16:57:25 +02:00
commit 0da8338ba8
310 changed files with 45849 additions and 0 deletions

View File

@@ -0,0 +1,524 @@
from http import HTTPStatus
from datetime import datetime, timezone
from unittest.mock import patch
from app.payloads import build_current_match_payload
from app.rcon_admin_log_storage import list_current_match_player_stats, persist_rcon_admin_log_entries
from app.rcon_client import RconServerTarget
from app.routes import resolve_get_payload
TARGET = RconServerTarget(
name="Comunidad Hispana #01",
host="127.0.0.1",
port=7779,
password="test-password",
source_name="test-rcon",
external_server_id="comunidad-hispana-01",
)
def test_current_match_payload_projects_rich_live_rcon_session_fields():
data = _build_with_rcon_sample(
{
"normalized": {
"server_name": "Comunidad Hispana #01",
"status": "online",
"current_map": "carentan_warfare",
"game_mode": "Warfare",
"allied_score": 2,
"axis_score": 2,
"allied_players": 0,
"axis_players": 0,
"players": 0,
"max_players": 100,
"match_time_seconds": 5400,
"remaining_match_time_seconds": 0,
},
"raw_session": {"mapId": "carentan_warfare", "mapName": "CARENTAN"},
}
)
assert data["map"] == "Carentan"
assert data["map_id"] == "carentan_warfare"
assert data["map_pretty_name"] == "Carentan"
assert data["game_mode"] == "Warfare"
assert data["allied_score"] == 2
assert data["axis_score"] == 2
assert data["players"] == 0
assert data["player_count_quality"] == "rcon-session-unverified"
assert data["player_count_source"] == "rcon-session"
assert data["score_source"] == "rcon-session"
assert data["map_source"] == "rcon-session"
assert data["public_scoreboard_url"] == "https://scoreboard.comunidadhll.es"
assert "/games" not in data["public_scoreboard_url"]
def test_current_match_payload_preserves_missing_values_as_null():
data = _build_with_rcon_sample(
{
"normalized": {
"server_name": "Comunidad Hispana #01",
"status": "online",
"current_map": None,
"game_mode": None,
"players": None,
"max_players": None,
},
"raw_session": {},
}
)
assert data["map"] is None
assert data["map_id"] is None
assert data["game_mode"] is None
assert data["allied_score"] is None
assert data["axis_score"] is None
assert data["players"] is None
assert data["player_count_quality"] is None
assert data["player_count_source"] is None
assert data["score_source"] is None
assert data["map_source"] is None
def test_current_match_payload_keeps_explicit_zero_score():
data = _build_with_rcon_sample(
{
"normalized": {
"server_name": "Comunidad Hispana #01",
"status": "online",
"current_map": "stmariedumont_warfare",
"allied_score": 0,
"axis_score": 0,
},
"raw_session": {
"mapId": "stmariedumont_warfare",
"mapName": "ST MARIE DU MONT",
},
}
)
assert data["map"] == "St. Marie Du Mont"
assert data["allied_score"] == 0
assert data["axis_score"] == 0
assert data["score_source"] == "rcon-session"
def test_current_match_payload_fallback_resolves_legacy_rcon_external_id_for_01():
data = _build_with_snapshot_fallback(
"comunidad-hispana-01",
{
"external_server_id": "rcon:152.114.195.174:7779",
"server_name": "#01 [ESP] Comunidad Hispana",
"status": "online",
"current_map": "St. Marie Du Mont",
"players": 0,
"max_players": 100,
"captured_at": "2026-03-24T14:08:41.008487Z",
},
)
assert data["found"] is True
assert data["map"] == "St. Marie Du Mont"
assert data["map_pretty_name"] == "St. Marie Du Mont"
assert data["status"] == "online"
assert data["players"] == 0
assert data["max_players"] == 100
assert data["captured_at"] == "2026-03-24T14:08:41.008487Z"
assert data["updated_at"] == "2026-03-24T14:08:41.008487Z"
assert data["public_scoreboard_url"] == "https://scoreboard.comunidadhll.es"
def test_current_match_payload_fallback_resolves_legacy_rcon_source_ref_for_02():
data = _build_with_snapshot_fallback(
"comunidad-hispana-02",
{
"external_server_id": "snapshot-server-02",
"source_ref": "rcon://152.114.195.150:7879",
"status": "online",
"current_map": "Elsenborn Ridge",
"captured_at": "2026-03-24T14:08:41.008487Z",
},
)
assert data["found"] is True
assert data["server_slug"] == "comunidad-hispana-02"
assert data["map"] == "Elsenborn Ridge"
assert data["map_pretty_name"] == "Elsenborn Ridge"
assert data["public_scoreboard_url"] == "https://scoreboard.comunidadhll.es:5443"
def test_current_match_payload_fallback_resolves_community_server_names():
number_first = _build_with_snapshot_fallback(
"comunidad-hispana-01",
{
"external_server_id": "snapshot-server-01",
"server_name": "#01 [ESP] Comunidad Hispana - Spa Onl",
"current_map": "Mortain",
},
)
community_first = _build_with_snapshot_fallback(
"comunidad-hispana-02",
{
"external_server_id": "snapshot-server-02",
"name": "Comunidad Hispana #02",
"current_map": "Carentan",
},
)
assert number_first["found"] is True
assert number_first["map"] == "Mortain"
assert community_first["found"] is True
assert community_first["map"] == "Carentan"
def test_current_match_payload_fallback_does_not_match_unknown_snapshot():
data = _build_with_snapshot_fallback(
"comunidad-hispana-01",
{
"external_server_id": "rcon:203.0.113.10:9000",
"source_ref": "rcon://203.0.113.10:9000",
"server_name": "#03 Comunidad Hispana",
"current_map": "Unknown Match",
},
)
assert data["found"] is False
assert data["map"] is None
assert data["status"] == "unavailable"
def test_current_match_route_rejects_unsupported_server():
status, payload = resolve_get_payload("/api/current-match?server=not-trusted")
assert status == HTTPStatus.NOT_FOUND
assert payload["status"] == "error"
def test_current_match_player_route_rejects_unsupported_server():
status, payload = resolve_get_payload("/api/current-match/players?server=not-trusted")
assert status == HTTPStatus.NOT_FOUND
assert payload["status"] == "error"
def test_current_match_player_stats_aggregate_safe_admin_log_rows(tmp_path):
db_path = tmp_path / "admin-log.sqlite3"
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: Bravo(Axis/steam-bravo) -> "
"Alpha(Allies/steam-alpha) with MP40"
),
},
{
"timestamp": "2026-05-21T10:02:00Z",
"message": (
"[3:00 min (140)] KILL: Alpha(Allies/steam-alpha) -> "
"Charlie(Allies/steam-charlie) with M1 GARAND"
),
},
{
"timestamp": "2026-05-21T10:03:00Z",
"message": (
"[4:00 min (160)] KILL: Alpha(Allies/steam-alpha) -> "
"Bravo(Axis/steam-bravo) with M1 GARAND"
),
},
],
db_path=db_path,
)
stats = list_current_match_player_stats(
server_key="comunidad-hispana-01",
db_path=db_path,
)
assert stats["scope"] == "open-admin-log-match-window"
assert stats["confidence"] == "admin-log-boundary"
assert stats["source"] == "rcon-admin-log-current-match-summary"
assert [item["player_name"] for item in stats["items"]] == ["Alpha", "Bravo", "Charlie"]
assert stats["items"][0] == {
"player_name": "Alpha",
"player_id": "steam-alpha",
"team": "Allies",
"kills": 1,
"deaths": 1,
"teamkills": 1,
"deaths_by_teamkill": 0,
"is_connected": None,
"connected": None,
"last_seen_at": "2026-05-21T10:03:00Z",
"favorite_weapon": "M1 GARAND",
"source": "kill",
"confidence": "admin-log-boundary",
}
assert "raw_message" not in stats["items"][0]
def test_current_match_player_stats_include_connected_players_without_kills(tmp_path):
db_path = tmp_path / "admin-log.sqlite3"
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": "[2:00 min (120)] CONNECTED Quiet Player (steam-quiet)",
},
],
db_path=db_path,
)
stats = list_current_match_player_stats(
server_key="comunidad-hispana-01",
db_path=db_path,
)
assert stats["scope"] == "open-admin-log-match-window"
assert stats["items"] == [
{
"player_name": "Quiet Player",
"player_id": "steam-quiet",
"team": None,
"kills": 0,
"deaths": 0,
"teamkills": 0,
"deaths_by_teamkill": 0,
"favorite_weapon": None,
"last_seen_at": "2026-05-21T10:01:00Z",
"is_connected": True,
"connected": True,
"source": "connected",
"confidence": "admin-log-boundary",
}
]
def test_current_match_player_stats_keep_disconnected_participants_visible(tmp_path):
db_path = tmp_path / "admin-log.sqlite3"
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": "[2:00 min (120)] CONNECTED Brief Player (steam-brief)",
},
{
"timestamp": "2026-05-21T10:05:00Z",
"message": "[6:00 min (180)] DISCONNECTED Brief Player (steam-brief)",
},
],
db_path=db_path,
)
stats = list_current_match_player_stats(
server_key="comunidad-hispana-01",
db_path=db_path,
)
assert stats["items"] == [
{
"player_name": "Brief Player",
"player_id": "steam-brief",
"team": None,
"kills": 0,
"deaths": 0,
"teamkills": 0,
"deaths_by_teamkill": 0,
"favorite_weapon": None,
"last_seen_at": "2026-05-21T10:05:00Z",
"is_connected": False,
"connected": False,
"source": "connected,disconnected",
"confidence": "admin-log-boundary",
}
]
def test_current_match_player_stats_include_victim_only_players(tmp_path):
db_path = tmp_path / "admin-log.sqlite3"
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: Killer One(Axis/steam-killer) -> "
"Victim Only(Allies/steam-victim) with MP40"
),
},
],
db_path=db_path,
)
stats = list_current_match_player_stats(
server_key="comunidad-hispana-01",
db_path=db_path,
)
by_name = {item["player_name"]: item for item in stats["items"]}
assert by_name["Victim Only"]["kills"] == 0
assert by_name["Victim Only"]["deaths"] == 1
assert by_name["Victim Only"]["favorite_weapon"] is None
def test_current_match_player_stats_exclude_players_before_open_match_start(tmp_path):
db_path = tmp_path / "admin-log.sqlite3"
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-21T09:55:00Z",
"message": "[0:30 min (90)] CONNECTED Old Match Player (steam-old)",
},
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": "[2:00 min (120)] CONNECTED New Match Player (steam-new)",
},
],
db_path=db_path,
)
stats = list_current_match_player_stats(
server_key="comunidad-hispana-01",
db_path=db_path,
)
assert [item["player_name"] for item in stats["items"]] == ["New Match Player"]
def test_current_match_player_stats_sort_connected_before_disconnected_with_same_stats(tmp_path):
db_path = tmp_path / "admin-log.sqlite3"
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": "[2:00 min (120)] CONNECTED Connected Alpha (steam-connected)",
},
{
"timestamp": "2026-05-21T10:02:00Z",
"message": "[3:00 min (140)] CONNECTED Disconnected Bravo (steam-disconnected)",
},
{
"timestamp": "2026-05-21T10:03:00Z",
"message": "[4:00 min (160)] DISCONNECTED Disconnected Bravo (steam-disconnected)",
},
],
db_path=db_path,
)
stats = list_current_match_player_stats(
server_key="comunidad-hispana-01",
db_path=db_path,
)
assert [item["player_name"] for item in stats["items"]] == [
"Connected Alpha",
"Disconnected Bravo",
]
def test_current_match_player_stats_filter_stale_recent_events(tmp_path):
db_path = tmp_path / "admin-log.sqlite3"
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-21T09:30:00Z",
"message": (
"[1:00 min (1779355800)] KILL: Old Killer(Allies/steam-old) -> "
"Old Victim(Axis/steam-victim-old) with M1 GARAND"
),
}
],
db_path=db_path,
)
stats = list_current_match_player_stats(
server_key="comunidad-hispana-01",
db_path=db_path,
now=datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc),
)
assert stats["scope"] == "no-current-match-events"
assert stats["confidence"] == "stale-filtered"
assert stats["items"] == []
def _build_with_rcon_sample(sample: dict[str, object]) -> dict[str, object]:
with (
patch("app.payloads.load_rcon_targets", return_value=(TARGET,)),
patch("app.payloads.query_live_server_sample", return_value=sample),
):
payload = build_current_match_payload(server_slug="comunidad-hispana-01")
return payload["data"]
def _build_with_snapshot_fallback(
server_slug: str,
item: dict[str, object],
) -> dict[str, object]:
with (
patch("app.payloads._query_current_match_rcon_sample", return_value=None),
patch(
"app.payloads.build_servers_payload",
return_value={
"status": "ok",
"data": {
"last_snapshot_at": "2026-03-24T14:08:41.008487Z",
"items": [item],
},
},
),
):
payload = build_current_match_payload(server_slug=server_slug)
return payload["data"]

View File

@@ -0,0 +1,448 @@
from __future__ import annotations
import io
import json
import sqlite3
import tempfile
import unittest
from contextlib import closing, redirect_stdout
from datetime import datetime, timedelta, timezone
from pathlib import Path
from app.database_maintenance import run_database_maintenance_cleanup
from app.rcon_admin_log_materialization import MATCH_RESULT_SOURCE, initialize_rcon_materialized_storage
from app.rcon_admin_log_storage import initialize_rcon_admin_log_storage
from app.storage import initialize_storage
class DatabaseMaintenanceTests(unittest.TestCase):
def test_dry_run_does_not_delete(self) -> None:
with _temp_db() as db_path:
_insert_server_snapshot(db_path, snapshot_id=1, captured_at="2026-05-01T00:00:00Z")
payload = run_database_maintenance_cleanup(
db_path=db_path,
now="2026-06-20T12:00:00Z",
)
self.assertEqual(payload["status"], "ok")
self.assertEqual(payload["mode"], "dry-run")
with closing(sqlite3.connect(db_path)) as connection:
self.assertEqual(
connection.execute("SELECT COUNT(*) FROM server_snapshots").fetchone()[0],
1,
)
def test_apply_deletes_old_server_snapshots(self) -> None:
with _temp_db() as db_path:
_insert_server_snapshot(db_path, snapshot_id=1, captured_at="2026-05-01T00:00:00Z")
_insert_server_snapshot(db_path, snapshot_id=2, captured_at="2026-06-18T00:00:00Z")
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-20T12:00:00Z",
recent_matches_keep=1,
)
with closing(sqlite3.connect(db_path)) as connection:
ids = [row[0] for row in connection.execute("SELECT id FROM server_snapshots ORDER BY id")]
self.assertEqual(ids, [2])
def test_apply_deletes_old_noncritical_admin_log_events(self) -> None:
with _temp_db() as db_path:
_insert_admin_log_event(
db_path,
event_id=1,
event_type="chat",
event_timestamp="2026-04-01T00:00:00Z",
server_time=100,
)
_insert_admin_log_event(
db_path,
event_id=2,
event_type="chat",
event_timestamp="2026-06-15T00:00:00Z",
server_time=200,
)
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-20T12:00:00Z",
)
with closing(sqlite3.connect(db_path)) as connection:
remaining = [
tuple(row)
for row in connection.execute(
"SELECT id, event_type FROM rcon_admin_log_events ORDER BY id"
)
]
self.assertEqual(remaining, [(2, "chat")])
def test_apply_preserves_critical_events_within_retention(self) -> None:
with _temp_db() as db_path:
_insert_admin_log_event(
db_path,
event_id=1,
event_type="kill",
event_timestamp="2026-06-10T00:00:00Z",
server_time=100,
)
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-20T12:00:00Z",
)
with closing(sqlite3.connect(db_path)) as connection:
count = connection.execute(
"SELECT COUNT(*) FROM rcon_admin_log_events WHERE event_type = 'kill'"
).fetchone()[0]
self.assertEqual(count, 1)
def test_apply_preserves_latest_100_materialized_matches(self) -> None:
with _temp_db() as db_path:
for index in range(101):
ended_at = (
datetime(2026, 1, 1, 12, tzinfo=timezone.utc) + timedelta(days=index)
).isoformat().replace("+00:00", "Z")
_insert_materialized_match(
db_path,
match_id=index + 1,
match_key=f"match-{index + 1}",
ended_at=ended_at,
server_time_start=(index + 1) * 10,
server_time_end=(index + 1) * 10 + 5,
)
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-20T12:00:00Z",
)
with closing(sqlite3.connect(db_path)) as connection:
remaining = connection.execute(
"SELECT COUNT(*) FROM rcon_materialized_matches"
).fetchone()[0]
oldest = connection.execute(
"SELECT COUNT(*) FROM rcon_materialized_matches WHERE match_key = 'match-1'"
).fetchone()[0]
self.assertEqual(remaining, 100)
self.assertEqual(oldest, 0)
def test_apply_preserves_current_month_matches(self) -> None:
with _temp_db() as db_path:
_insert_materialized_match(
db_path,
match_id=1,
match_key="old",
ended_at="2026-01-10T12:00:00Z",
server_time_start=10,
server_time_end=20,
)
_insert_materialized_match(
db_path,
match_id=2,
match_key="current-month",
ended_at="2026-06-03T12:00:00Z",
server_time_start=30,
server_time_end=40,
)
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-20T12:00:00Z",
recent_matches_keep=1,
)
with closing(sqlite3.connect(db_path)) as connection:
keys = [row[0] for row in connection.execute("SELECT match_key FROM rcon_materialized_matches")]
self.assertEqual(keys, ["current-month"])
def test_apply_preserves_previous_month_when_now_day_is_early(self) -> None:
with _temp_db() as db_path:
_insert_materialized_match(
db_path,
match_id=1,
match_key="previous-month",
ended_at="2026-05-15T12:00:00Z",
server_time_start=10,
server_time_end=20,
)
_insert_materialized_match(
db_path,
match_id=2,
match_key="older",
ended_at="2026-04-15T12:00:00Z",
server_time_start=30,
server_time_end=40,
)
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-05T12:00:00Z",
recent_matches_keep=1,
)
with closing(sqlite3.connect(db_path)) as connection:
keys = [row[0] for row in connection.execute("SELECT match_key FROM rcon_materialized_matches")]
self.assertEqual(keys, ["previous-month"])
def test_apply_preserves_current_week(self) -> None:
with _temp_db() as db_path:
_insert_materialized_match(
db_path,
match_id=1,
match_key="current-week",
ended_at="2026-06-10T12:00:00Z",
server_time_start=10,
server_time_end=20,
)
_insert_materialized_match(
db_path,
match_id=2,
match_key="older",
ended_at="2026-05-01T12:00:00Z",
server_time_start=30,
server_time_end=40,
)
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-10T13:00:00Z",
recent_matches_keep=1,
)
with closing(sqlite3.connect(db_path)) as connection:
keys = [row[0] for row in connection.execute("SELECT match_key FROM rcon_materialized_matches")]
self.assertEqual(keys, ["current-week"])
def test_apply_preserves_previous_week_when_fallback_may_need_it(self) -> None:
with _temp_db() as db_path:
_insert_materialized_match(
db_path,
match_id=1,
match_key="previous-week",
ended_at="2026-06-03T12:00:00Z",
server_time_start=10,
server_time_end=20,
)
_insert_materialized_match(
db_path,
match_id=2,
match_key="current-week-sample",
ended_at="2026-06-09T12:00:00Z",
server_time_start=30,
server_time_end=40,
)
_insert_materialized_match(
db_path,
match_id=3,
match_key="older",
ended_at="2026-05-01T12:00:00Z",
server_time_start=50,
server_time_end=60,
)
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-10T13:00:00Z",
recent_matches_keep=1,
)
with closing(sqlite3.connect(db_path)) as connection:
keys = {
row[0]
for row in connection.execute("SELECT match_key FROM rcon_materialized_matches")
}
self.assertEqual(keys, {"previous-week", "current-week-sample"})
def test_apply_deletes_old_non_protected_match_and_child_stats(self) -> None:
with _temp_db() as db_path:
_insert_materialized_match(
db_path,
match_id=1,
match_key="delete-me",
ended_at="2026-01-10T12:00:00Z",
server_time_start=10,
server_time_end=20,
)
_insert_materialized_match(
db_path,
match_id=2,
match_key="keep-me",
ended_at="2026-06-18T12:00:00Z",
server_time_start=30,
server_time_end=40,
)
_insert_player_stat(db_path, match_key="delete-me", player_id="player-1")
_insert_player_stat(db_path, match_key="keep-me", player_id="player-2")
run_database_maintenance_cleanup(
apply=True,
db_path=db_path,
now="2026-06-20T12:00:00Z",
recent_matches_keep=1,
)
with closing(sqlite3.connect(db_path)) as connection:
deleted_match_count = connection.execute(
"SELECT COUNT(*) FROM rcon_materialized_matches WHERE match_key = 'delete-me'"
).fetchone()[0]
deleted_stat_count = connection.execute(
"SELECT COUNT(*) FROM rcon_match_player_stats WHERE match_key = 'delete-me'"
).fetchone()[0]
kept_stat_count = connection.execute(
"SELECT COUNT(*) FROM rcon_match_player_stats WHERE match_key = 'keep-me'"
).fetchone()[0]
self.assertEqual(deleted_match_count, 0)
self.assertEqual(deleted_stat_count, 0)
self.assertEqual(kept_stat_count, 1)
def test_missing_optional_tables_are_logged_and_do_not_crash(self) -> None:
with _temp_db(create_schema=False) as db_path:
stream = io.StringIO()
with redirect_stdout(stream):
payload = run_database_maintenance_cleanup(
db_path=db_path,
now="2026-06-20T12:00:00Z",
)
self.assertEqual(payload["status"], "ok")
self.assertIn("database-maintenance-table-skipped", stream.getvalue())
def _temp_db(*, create_schema: bool = True):
class _TempDbContext:
def __enter__(self) -> Path:
self._tmpdir = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.db_path = Path(self._tmpdir.name) / "maintenance.sqlite3"
if create_schema:
initialize_storage(db_path=self.db_path)
initialize_rcon_admin_log_storage(db_path=self.db_path)
initialize_rcon_materialized_storage(db_path=self.db_path)
return self.db_path
def __exit__(self, exc_type, exc, tb) -> None:
self._tmpdir.cleanup()
return _TempDbContext()
def _insert_server_snapshot(db_path: Path, *, snapshot_id: int, captured_at: str) -> None:
with closing(sqlite3.connect(db_path)) as connection:
connection.execute(
"""
INSERT OR IGNORE INTO game_sources (
id, slug, display_name, provider_kind, is_active, created_at, updated_at
) VALUES (1, 'current-hll', 'Current Hell Let Loose', 'development', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
"""
)
connection.execute(
"""
INSERT OR IGNORE INTO servers (
id, game_source_id, external_server_id, server_name, region, first_seen_at, last_seen_at
) VALUES (1, 1, 'server-1', 'Server 1', 'ES', ?, ?)
""",
(captured_at, captured_at),
)
connection.execute(
"""
INSERT INTO server_snapshots (
id, server_id, captured_at, status, players, max_players, current_map, source_name
) VALUES (?, 1, ?, 'online', 10, 100, 'hurtgen', 'test')
""",
(snapshot_id, captured_at),
)
connection.commit()
def _insert_admin_log_event(
db_path: Path,
*,
event_id: int,
event_type: str,
event_timestamp: str,
server_time: int,
) -> None:
with closing(sqlite3.connect(db_path)) as connection:
connection.execute(
"""
INSERT INTO rcon_admin_log_events (
id, target_key, external_server_id, event_timestamp, server_time,
relative_time, event_type, raw_message, canonical_message,
parsed_payload_json, raw_entry_json
) VALUES (?, 'comunidad-hispana-01', 'comunidad-hispana-01', ?, ?, '', ?, '', '', '{}', '{}')
""",
(event_id, event_timestamp, server_time, event_type),
)
connection.commit()
def _insert_materialized_match(
db_path: Path,
*,
match_id: int,
match_key: str,
ended_at: str,
server_time_start: int,
server_time_end: int,
) -> None:
started_at = _shift_iso(ended_at, hours=-1)
with closing(sqlite3.connect(db_path)) as connection:
connection.execute(
"""
INSERT INTO rcon_materialized_matches (
id, target_key, external_server_id, match_key, map_name, map_pretty_name,
game_mode, started_server_time, ended_server_time, started_at, ended_at,
allied_score, axis_score, winner, confidence_mode, source_basis
) VALUES (?, 'comunidad-hispana-01', 'comunidad-hispana-01', ?, 'hurtgen', 'Hurtgen Forest',
'warfare', ?, ?, ?, ?, 5, 3, 'allied', 'exact', ?)
""",
(
match_id,
match_key,
server_time_start,
server_time_end,
started_at,
ended_at,
MATCH_RESULT_SOURCE,
),
)
connection.commit()
def _insert_player_stat(db_path: Path, *, match_key: str, player_id: str) -> None:
with closing(sqlite3.connect(db_path)) as connection:
connection.execute(
"""
INSERT INTO rcon_match_player_stats (
target_key, match_key, player_id, player_name, team,
kills, deaths, teamkills, deaths_by_teamkill,
weapons_json, death_by_weapons_json, most_killed_json, death_by_json
) VALUES (
'comunidad-hispana-01', ?, ?, ?, 'Allies',
1, 1, 0, 0, '{}', '{}', '{}', '{}'
)
""",
(match_key, player_id, player_id),
)
connection.commit()
def _shift_iso(value: str, *, hours: int) -> str:
point = datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(timezone.utc)
shifted = point + timedelta(hours=hours)
return shifted.isoformat().replace("+00:00", "Z")
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,153 @@
from __future__ import annotations
import io
import os
import unittest
from contextlib import nullcontext, redirect_stdout
from datetime import datetime, timezone
from unittest.mock import patch
import app.historical_runner as historical_runner
from app.historical_runner import _maybe_run_database_maintenance, _run_refresh_with_retries
class HistoricalRunnerMaintenanceTests(unittest.TestCase):
def setUp(self) -> None:
historical_runner._LAST_DATABASE_MAINTENANCE_RUN_AT = None
def tearDown(self) -> None:
historical_runner._LAST_DATABASE_MAINTENANCE_RUN_AT = None
def test_scheduler_disabled_does_not_call_cleanup(self) -> None:
with (
patch.dict(os.environ, {"HLL_DB_MAINTENANCE_ENABLED": "false"}, clear=False),
patch("app.historical_runner.run_database_maintenance_cleanup") as cleanup,
):
result = _maybe_run_database_maintenance(
now=datetime(2026, 6, 20, 12, tzinfo=timezone.utc)
)
cleanup.assert_not_called()
self.assertEqual(result["status"], "skipped")
self.assertEqual(result["reason"], "disabled")
def test_scheduler_enabled_but_not_due_does_not_call_cleanup(self) -> None:
with (
patch.dict(
os.environ,
{
"HLL_DB_MAINTENANCE_ENABLED": "true",
"HLL_DB_MAINTENANCE_INTERVAL_SECONDS": "43200",
},
clear=False,
),
patch(
"app.historical_runner.run_database_maintenance_cleanup",
return_value={"status": "ok"},
) as cleanup,
):
first = _maybe_run_database_maintenance(
now=datetime(2026, 6, 20, 0, tzinfo=timezone.utc)
)
second = _maybe_run_database_maintenance(
now=datetime(2026, 6, 20, 1, tzinfo=timezone.utc)
)
self.assertEqual(first["status"], "ok")
self.assertEqual(second["status"], "skipped")
self.assertEqual(second["reason"], "not-due")
cleanup.assert_called_once()
def test_scheduler_enabled_and_due_calls_cleanup(self) -> None:
with (
patch.dict(os.environ, {"HLL_DB_MAINTENANCE_ENABLED": "true"}, clear=False),
patch(
"app.historical_runner.run_database_maintenance_cleanup",
return_value={"status": "ok"},
) as cleanup,
):
result = _maybe_run_database_maintenance(
now=datetime(2026, 6, 20, 12, tzinfo=timezone.utc)
)
cleanup.assert_called_once()
self.assertEqual(result["status"], "ok")
def test_cleanup_exception_is_logged_and_runner_continues(self) -> None:
stream = io.StringIO()
with (
patch.dict(os.environ, {"HLL_DB_MAINTENANCE_ENABLED": "true"}, clear=False),
patch("app.historical_runner.backend_writer_lock", return_value=nullcontext()),
patch(
"app.historical_runner._run_primary_rcon_capture",
return_value={"status": "ok", "targets": []},
),
patch(
"app.historical_runner.run_incremental_refresh",
return_value={"status": "ok"},
),
patch(
"app.historical_runner.generate_historical_snapshots",
return_value={"status": "ok"},
),
patch(
"app.historical_runner.rebuild_elo_mmr_models",
return_value={"status": "ok"},
),
patch(
"app.historical_runner.run_database_maintenance_cleanup",
side_effect=RuntimeError("maintenance failed"),
),
redirect_stdout(stream),
):
result = _run_refresh_with_retries(
max_retries=0,
retry_delay_seconds=0,
server_slug="comunidad-hispana-01",
max_pages=None,
page_size=None,
run_number=1,
)
self.assertEqual(result["status"], "ok")
self.assertEqual(result["database_maintenance_result"]["status"], "error")
self.assertIn("database-maintenance-scheduler-failed", stream.getvalue())
def test_interval_parsing_handles_invalid_values_safely(self) -> None:
with patch.dict(
os.environ,
{
"HLL_DB_MAINTENANCE_ENABLED": "true",
"HLL_DB_MAINTENANCE_INTERVAL_SECONDS": "bad",
},
clear=False,
):
interval_seconds, source = historical_runner._resolve_db_maintenance_interval_seconds()
self.assertEqual(interval_seconds, 43200)
self.assertEqual(source, "default-invalid-env-fallback")
def test_maintenance_state_is_tracked_in_process(self) -> None:
with (
patch.dict(
os.environ,
{
"HLL_DB_MAINTENANCE_ENABLED": "true",
"HLL_DB_MAINTENANCE_INTERVAL_SECONDS": "3600",
},
clear=False,
),
patch(
"app.historical_runner.run_database_maintenance_cleanup",
return_value={"status": "ok"},
),
):
_maybe_run_database_maintenance(now=datetime(2026, 6, 20, 12, tzinfo=timezone.utc))
self.assertEqual(
historical_runner._LAST_DATABASE_MAINTENANCE_RUN_AT,
datetime(2026, 6, 20, 12, tzinfo=timezone.utc),
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,126 @@
"""Regression coverage for historical snapshot runner refreshes."""
from __future__ import annotations
import io
import json
import os
import unittest
from contextlib import nullcontext, redirect_stdout
from datetime import datetime, timezone
from unittest.mock import patch
from app.config import (
get_historical_refresh_interval_seconds,
get_historical_refresh_max_retries,
get_historical_refresh_retry_delay_seconds,
)
from app.historical_runner import _run_refresh_with_retries, run_periodic_historical_refresh
from app.historical_snapshots import _normalize_snapshot_limit
from app.postgres_display_storage import _json_payload_default
from app.rcon_historical_read_model import (
_calculate_coverage_hours,
_calculate_duration_seconds,
)
class HistoricalSnapshotRefreshTests(unittest.TestCase):
def test_runner_numeric_env_values_are_parsed_before_use(self) -> None:
with patch.dict(
os.environ,
{
"HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS": "300",
"HLL_HISTORICAL_REFRESH_MAX_RETRIES": "4",
"HLL_HISTORICAL_REFRESH_RETRY_DELAY_SECONDS": "0.5",
},
clear=False,
):
self.assertEqual(get_historical_refresh_interval_seconds(), 300)
self.assertEqual(get_historical_refresh_max_retries(), 4)
self.assertEqual(get_historical_refresh_retry_delay_seconds(), 0.5)
def test_runner_numeric_env_values_fail_with_clear_names(self) -> None:
with patch.dict(
os.environ,
{"HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS": "hourly"},
clear=False,
):
with self.assertRaisesRegex(
ValueError,
"HLL_HISTORICAL_SNAPSHOT_REFRESH_INTERVAL_SECONDS must be an integer",
):
get_historical_refresh_interval_seconds()
def test_rcon_coverage_accepts_postgres_datetime_values(self) -> None:
start = datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc)
end = datetime(2026, 5, 21, 11, 30, tzinfo=timezone.utc)
self.assertEqual(_calculate_coverage_hours(start, end), 1.5)
self.assertEqual(_calculate_duration_seconds(start, end), 5400)
def test_snapshot_limits_are_numeric_before_snapshot_queries(self) -> None:
self.assertEqual(_normalize_snapshot_limit("recent_matches_limit", "10"), 10)
with self.assertRaisesRegex(ValueError, "recent_matches_limit"):
_normalize_snapshot_limit("recent_matches_limit", "ten")
def test_postgres_snapshot_payload_serializes_datetime_values(self) -> None:
payload = {
"captured_at": datetime(2026, 5, 21, 20, 12, 54, tzinfo=timezone.utc),
}
self.assertEqual(
json.loads(json.dumps(payload, default=_json_payload_default)),
{"captured_at": "2026-05-21T20:12:54Z"},
)
def test_runner_failure_log_includes_exception_type_and_traceback(self) -> None:
stream = io.StringIO()
with (
patch("app.historical_runner.backend_writer_lock", return_value=nullcontext()),
patch(
"app.historical_runner._run_primary_rcon_capture",
side_effect=TypeError("bad timestamp"),
),
redirect_stdout(stream),
):
result = _run_refresh_with_retries(
max_retries=0,
retry_delay_seconds=0,
server_slug=None,
max_pages=None,
page_size=None,
run_number=1,
)
self.assertEqual(result["status"], "error")
self.assertEqual(result["error_type"], "TypeError")
self.assertIn("Traceback", result["traceback"])
self.assertIn('"event": "historical-refresh-attempt-failed"', stream.getvalue())
def test_runner_success_log_serializes_datetime_values(self) -> None:
stream = io.StringIO()
with (
patch(
"app.historical_runner._run_refresh_with_retries",
return_value={
"status": "ok",
"rcon_capture_result": {
"captured_at": datetime(2026, 5, 22, tzinfo=timezone.utc),
},
},
),
redirect_stdout(stream),
):
run_periodic_historical_refresh(
interval_seconds=1,
max_retries=0,
retry_delay_seconds=0,
max_runs=1,
)
self.assertIn('"status": "ok"', stream.getvalue())
self.assertIn('"captured_at": "2026-05-22 00:00:00+00:00"', stream.getvalue())
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,27 @@
"""Regression coverage for API JSON encoding of PostgreSQL value types."""
from __future__ import annotations
import json
import unittest
from datetime import date, datetime, timezone
from app.main import _json_default
class JsonSerializationTests(unittest.TestCase):
def test_json_default_serializes_postgres_datetime_and_date_values(self) -> None:
payload = {
"started_at": datetime(2026, 5, 21, 10, 11, 12, tzinfo=timezone.utc),
"day": date(2026, 5, 21),
}
encoded = json.loads(json.dumps(payload, default=_json_default))
self.assertEqual(
encoded,
{
"started_at": "2026-05-21T10:11:12+00:00",
"day": "2026-05-21",
},
)

View File

@@ -0,0 +1,166 @@
from app.rcon_admin_log_parser import parse_rcon_admin_log_message
from app.rcon_admin_log_parser import parse_rcon_player_profile_snapshot
def test_parse_match_start():
parsed = parse_rcon_admin_log_message(
"[2:09:15 hours (1779178245)] MATCH START UTAH BEACH Warfare"
)
assert parsed.event_type == "match_start"
assert parsed.server_time == 1779178245
assert parsed.map_name == "UTAH BEACH"
assert parsed.game_mode == "Warfare"
def test_parse_match_end():
parsed = parse_rcon_admin_log_message(
"[20:36:53 hours (1779111786)] MATCH ENDED `ST MARIE DU MONT Warfare` ALLIED (5 - 0) AXIS "
)
assert parsed.event_type == "match_end"
assert parsed.map_name == "ST MARIE DU MONT Warfare"
assert parsed.allied_score == 5
assert parsed.axis_score == 0
assert parsed.winner == "allied"
def test_parse_kill():
parsed = parse_rcon_admin_log_message(
"[1:20:19 hours (1779181181)] KILL: AntonioPruna(Allies/76561198000000000) -> "
"[7DV] NEⓇA TACTICAL FEMB✡Y(Axis/76561199000000000) with M1 GARAND"
)
assert parsed.event_type == "kill"
assert parsed.killer_name == "AntonioPruna"
assert parsed.killer_team == "Allies"
assert parsed.killer_id == "76561198000000000"
assert parsed.victim_name == "[7DV] NEⓇA TACTICAL FEMB✡Y"
assert parsed.victim_team == "Axis"
assert parsed.victim_id == "76561199000000000"
assert parsed.weapon == "M1 GARAND"
def test_parse_team_switch():
parsed = parse_rcon_admin_log_message(
"[21:34:19 hours (1779108340)] TEAMSWITCH Ekenef (None > Allies)"
)
assert parsed.event_type == "team_switch"
assert parsed.player_name == "Ekenef"
assert parsed.from_team == "None"
assert parsed.to_team == "Allies"
def test_parse_connected():
parsed = parse_rcon_admin_log_message(
"[21:34:22 hours (1779108337)] CONNECTED Ekenef (76561198109813520)"
)
assert parsed.event_type == "connected"
assert parsed.player_name == "Ekenef"
assert parsed.player_id == "76561198109813520"
def test_parse_disconnected():
parsed = parse_rcon_admin_log_message(
"[21:10:53 hours (1779109746)] DISCONNECTED [BxB] Rab◯l◯k◯ (76561198111111111)"
)
assert parsed.event_type == "disconnected"
assert parsed.player_name == "[BxB] Rab◯l◯k◯"
assert parsed.player_id == "76561198111111111"
def test_parse_chat():
parsed = parse_rcon_admin_log_message(
"[18:38:35 hours (1779118884)] CHAT[Team][BXB Ivanxu(Axis/6215e24a1f05c5815ed9e8bf185f94fd)]: !vip"
)
assert parsed.event_type == "chat"
assert parsed.chat_scope == "Team"
assert parsed.player_name == "BXB Ivanxu"
assert parsed.chat_team == "Axis"
assert parsed.player_id == "6215e24a1f05c5815ed9e8bf185f94fd"
assert parsed.content == "!vip"
def test_parse_kick():
parsed = parse_rcon_admin_log_message(
"[2:09:10 hours (1779178249)] KICK: [[7DV] NEⓇA TACTICAL FEMB✡Y] has been kicked. "
"[Making free spaces for members of the Spanish Discord community.]"
)
assert parsed.event_type == "kick"
assert parsed.player_name == "[7DV] NEⓇA TACTICAL FEMB✡Y"
assert "Making free spaces" in parsed.reason
def test_parse_message_profile():
parsed = parse_rcon_admin_log_message(
"[21:34:19 hours (1779108340)] MESSAGE: player [Ekenef(76561198109813520)], "
"content [─ Ekenef ─\\n▒ Totales ▒\\nbajas : 141 (6 TKs)\\nmuertes : 268 (5 TKs)]"
)
assert parsed.event_type == "message"
assert parsed.player_name == "Ekenef"
assert parsed.player_id == "76561198109813520"
assert "bajas : 141" in parsed.content
def test_parse_player_profile_snapshot_spanish_sections():
parsed = parse_rcon_admin_log_message(
"[21:34:19 hours (1779108340)] MESSAGE: player [Jugador Uno(steam-profile-1)], "
"content [─ Jugador Uno ─\n"
"▒ Totales ▒\n"
"Visto por primera vez : 2026-01-01\n"
"sesiones : 12\n"
"partidas jugadas : 9\n"
"tiempo jugado : 18 h 30 min\n"
"bajas : 141 (6 TKs)\n"
"muertes : 268 (5 TKs)\n"
"K/D : 0,53\n"
"▒ Víctimas ▒\n"
"Rival Dos : 7\n"
"▒ Némesis ▒\n"
"Rival Tres : 4\n"
"▒ Armas favoritas ▒\n"
"M1 GARAND : 31\n"
"▒ Promedios ▒\n"
"bajas por partida : 15,6\n"
"▒ Sanciones ▒\n"
"kicks : 1]"
)
snapshot = parse_rcon_player_profile_snapshot(
parsed,
event_timestamp="2026-05-19T10:00:00Z",
)
assert snapshot is not None
assert snapshot.player_name == "Jugador Uno"
assert snapshot.player_id == "steam-profile-1"
assert snapshot.source_server_time == 1779108340
assert snapshot.sessions == 12
assert snapshot.matches_played == 9
assert snapshot.total_kills == 141
assert snapshot.total_deaths == 268
assert snapshot.teamkills_done == 6
assert snapshot.teamkills_received == 5
assert snapshot.kd_ratio == 0.53
assert snapshot.favorite_weapons == {"M1 GARAND": 31}
assert snapshot.victims == {"Rival Dos": 7}
assert snapshot.nemesis == {"Rival Tres": 4}
assert snapshot.averages == {"bajas por partida": 15.6}
assert snapshot.sanctions == {"kicks": 1.0}
def test_non_profile_message_does_not_parse_as_profile_snapshot():
parsed = parse_rcon_admin_log_message(
"[21:34:19 hours (1779108340)] MESSAGE: player [Jugador Uno(steam-profile-1)], "
"content [Bienvenido al servidor]"
)
assert parse_rcon_player_profile_snapshot(parsed) is None

View File

@@ -0,0 +1,497 @@
import gc
import json
import sqlite3
from datetime import datetime, timezone
from unittest.mock import patch
from app.rcon_admin_log_storage import (
initialize_rcon_admin_log_storage,
list_current_match_kill_feed,
list_rcon_admin_log_event_counts,
persist_rcon_admin_log_entries,
)
TARGET = {
"target_key": "test-rcon-target",
"external_server_id": "test-rcon-target",
}
def test_initialize_rcon_admin_log_storage_creates_event_table(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
resolved_path = initialize_rcon_admin_log_storage(db_path=db_path)
assert resolved_path == db_path
connection = sqlite3.connect(db_path)
try:
table_names = {
row[0]
for row in connection.execute(
"SELECT name FROM sqlite_master WHERE type = 'table'"
).fetchall()
}
columns = {
row[1]
for row in connection.execute("PRAGMA table_info(rcon_admin_log_events)")
}
finally:
connection.close()
gc.collect()
assert "rcon_admin_log_events" in table_names
assert "rcon_player_profile_snapshots" in table_names
assert {
"target_key",
"event_type",
"raw_message",
"canonical_message",
"parsed_payload_json",
"raw_entry_json",
}.issubset(columns)
def test_persist_rcon_admin_log_entries_inserts_then_reports_duplicates(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
entries = [
{
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] CONNECTED Player One (steam-1)",
},
{
"timestamp": "2026-05-19T10:01:00Z",
"message": "[2:00 min (120)] DISCONNECTED Player One (steam-1)",
},
]
first_delta = persist_rcon_admin_log_entries(
target=TARGET,
entries=entries,
db_path=db_path,
)
second_delta = persist_rcon_admin_log_entries(
target=TARGET,
entries=entries,
db_path=db_path,
)
assert first_delta == {
"events_seen": 2,
"events_inserted": 2,
"duplicate_events": 0,
}
assert second_delta == {
"events_seen": 2,
"events_inserted": 0,
"duplicate_events": 2,
}
gc.collect()
def test_profile_message_snapshots_are_materialized_and_deduped(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
entry = {
"timestamp": "2026-05-19T10:00:00Z",
"message": (
"[21:34:19 hours (1779108340)] MESSAGE: player [Jugador Uno(steam-profile-1)], "
"content [─ Jugador Uno ─\n"
"▒ Totales ▒\n"
"sesiones : 12\n"
"partidas jugadas : 9\n"
"bajas : 141 (6 TKs)\n"
"muertes : 268 (5 TKs)\n"
"K/D : 0.53\n"
"▒ Víctimas ▒\n"
"Rival Dos : 7\n"
"▒ Némesis ▒\n"
"Rival Tres : 4\n"
"▒ Armas favoritas ▒\n"
"M1 GARAND : 31\n"
"▒ Promedios ▒\n"
"bajas por partida : 15.6\n"
"▒ Sanciones ▒\n"
"kicks : 1]"
),
}
persist_rcon_admin_log_entries(target=TARGET, entries=[entry], db_path=db_path)
persist_rcon_admin_log_entries(target=TARGET, entries=[entry], db_path=db_path)
connection = sqlite3.connect(db_path)
connection.row_factory = sqlite3.Row
try:
rows = connection.execute("SELECT * FROM rcon_player_profile_snapshots").fetchall()
finally:
connection.close()
gc.collect()
assert len(rows) == 1
row = rows[0]
assert row["target_key"] == "test-rcon-target"
assert row["player_id"] == "steam-profile-1"
assert row["source_server_time"] == 1779108340
assert row["sessions"] == 12
assert row["matches_played"] == 9
assert row["total_kills"] == 141
assert row["total_deaths"] == 268
assert row["teamkills_done"] == 6
assert row["teamkills_received"] == 5
assert row["kd_ratio"] == 0.53
assert json.loads(row["favorite_weapons_json"]) == {"M1 GARAND": 31}
assert json.loads(row["victims_json"]) == {"Rival Dos": 7}
assert json.loads(row["nemesis_json"]) == {"Rival Tres": 4}
assert "bajas : 141" in row["raw_content"]
def test_non_profile_messages_do_not_create_profile_snapshots(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] MESSAGE: player [Player One(steam-1)], content [hello]",
}
],
db_path=db_path,
)
connection = sqlite3.connect(db_path)
try:
count = connection.execute(
"SELECT COUNT(*) FROM rcon_player_profile_snapshots"
).fetchone()[0]
finally:
connection.close()
gc.collect()
assert count == 0
def test_canonical_message_dedupes_changing_relative_prefixes(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
original_entry = {
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] MESSAGE: player [Player One(steam-1)], content [hello]",
}
repeated_read_entry = {
"timestamp": "2026-05-19T10:05:00Z",
"message": "[6:00 min (100)] MESSAGE: player [Player One(steam-1)], content [hello]",
}
first_delta = persist_rcon_admin_log_entries(
target=TARGET,
entries=[original_entry],
db_path=db_path,
)
second_delta = persist_rcon_admin_log_entries(
target=TARGET,
entries=[repeated_read_entry],
db_path=db_path,
)
assert first_delta["events_inserted"] == 1
assert second_delta == {
"events_seen": 1,
"events_inserted": 0,
"duplicate_events": 1,
}
gc.collect()
def test_list_rcon_admin_log_event_counts_groups_by_target_and_event_type(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
other_target = {
"target_key": "other-rcon-target",
"external_server_id": "other-rcon-target",
}
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] CONNECTED Player One (steam-1)",
},
{
"timestamp": "2026-05-19T10:01:00Z",
"message": "[2:00 min (120)] DISCONNECTED Player One (steam-1)",
},
],
db_path=db_path,
)
persist_rcon_admin_log_entries(
target=other_target,
entries=[
{
"timestamp": "2026-05-19T10:02:00Z",
"message": "[3:00 min (140)] CONNECTED Player Two (steam-2)",
}
],
db_path=db_path,
)
counts = {
(row["target_key"], row["event_type"]): row
for row in list_rcon_admin_log_event_counts(db_path=db_path)
}
assert counts == {
("other-rcon-target", "connected"): {
"target_key": "other-rcon-target",
"event_type": "connected",
"event_count": 1,
"first_server_time": 140,
"last_server_time": 140,
},
("test-rcon-target", "connected"): {
"target_key": "test-rcon-target",
"event_type": "connected",
"event_count": 1,
"first_server_time": 100,
"last_server_time": 100,
},
("test-rcon-target", "disconnected"): {
"target_key": "test-rcon-target",
"event_type": "disconnected",
"event_count": 1,
"first_server_time": 120,
"last_server_time": 120,
},
}
gc.collect()
def test_current_match_kill_feed_prefers_open_match_window_and_normalizes_rows(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-19T09:59:00Z",
"message": (
"[0:59 min (90)] KILL: Old Killer(Allies/steam-old) -> "
"Old Victim(Axis/steam-victim-old) with M1 GARAND"
),
},
{
"timestamp": "2026-05-19T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-19T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: Alpha(Allies/steam-alpha) -> "
"Bravo(Allies/steam-bravo) with GRENADE"
),
},
],
db_path=db_path,
)
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
)
assert feed["scope"] == "open-admin-log-match-window"
assert feed["confidence"] == "admin-log-boundary"
assert len(feed["items"]) == 1
assert feed["items"][0] == {
"event_id": "rcon-admin-log:test-rcon-target:3",
"event_timestamp": "2026-05-19T10:01:00Z",
"server_time": 120,
"killer_name": "Alpha",
"killer_team": "Allies",
"victim_name": "Bravo",
"victim_team": "Allies",
"weapon": "GRENADE",
"is_teamkill": True,
}
gc.collect()
def test_current_match_kill_feed_filters_stale_recent_fallback_rows(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T09:30:00Z",
"message": (
"[1:00 min (1779355800)] KILL: Old Killer(Allies/steam-old) -> "
"Old Victim(Axis/steam-victim-old) with M1 GARAND"
),
}
],
db_path=db_path,
)
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
now=datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc),
)
assert feed["scope"] == "no-current-match-events"
assert feed["confidence"] == "stale-filtered"
assert feed["stale_events_filtered"] == 1
assert feed["items"] == []
gc.collect()
def test_current_match_kill_feed_marks_fresh_recent_fallback_rows_partial(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T09:50:00Z",
"message": (
"[1:00 min (1779357000)] KILL: Fresh Killer(Allies/steam-fresh) -> "
"Fresh Victim(Axis/steam-victim-fresh) with M1 GARAND"
),
}
],
db_path=db_path,
)
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
now=datetime(2026, 5, 21, 10, 0, tzinfo=timezone.utc),
)
assert feed["scope"] == "recent-admin-log-window"
assert feed["confidence"] == "partial"
assert feed["stale_events_filtered"] == 0
assert [item["killer_name"] for item in feed["items"]] == ["Fresh Killer"]
gc.collect()
def test_current_match_kill_feed_filters_rows_before_incremental_cursor(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: First Killer(Allies/steam-first) -> "
"First Victim(Axis/steam-first-victim) with M1 GARAND"
),
},
{
"timestamp": "2026-05-21T10:02:00Z",
"message": (
"[3:00 min (140)] KILL: Next Killer(Axis/steam-next) -> "
"Next Victim(Allies/steam-next-victim) with MP40"
),
},
],
db_path=db_path,
)
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
since_event_id="rcon-admin-log:test-rcon-target:2",
)
assert [item["killer_name"] for item in feed["items"]] == ["Next Killer"]
gc.collect()
def test_current_match_kill_feed_without_cursor_omits_nullable_id_predicate(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: Cursor Killer(Allies/steam-cursor) -> "
"Cursor Victim(Axis/steam-cursor-victim) with M1 GARAND"
),
},
],
db_path=db_path,
)
traced_sql = []
connect = sqlite3.connect
def connect_with_trace(*args, **kwargs):
connection = connect(*args, **kwargs)
connection.set_trace_callback(traced_sql.append)
return connection
with patch(
"app.rcon_admin_log_storage.sqlite3.connect",
side_effect=connect_with_trace,
):
feed = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
)
kill_queries = [
sql
for sql in traced_sql
if "FROM rcon_admin_log_events" in sql and "event_type = 'kill'" in sql
]
assert [item["killer_name"] for item in feed["items"]] == ["Cursor Killer"]
assert kill_queries
assert all("IS NULL OR id >" not in sql for sql in kill_queries)
assert all("AND id >" not in sql for sql in kill_queries)
gc.collect()
def test_current_match_kill_feed_invalid_cursor_behaves_like_no_cursor(tmp_path):
db_path = tmp_path / "admin_log.sqlite3"
persist_rcon_admin_log_entries(
target=TARGET,
entries=[
{
"timestamp": "2026-05-21T10:00:00Z",
"message": "[1:00 min (100)] MATCH START Mortain Warfare",
},
{
"timestamp": "2026-05-21T10:01:00Z",
"message": (
"[2:00 min (120)] KILL: First Killer(Allies/steam-first) -> "
"First Victim(Axis/steam-first-victim) with M1 GARAND"
),
},
{
"timestamp": "2026-05-21T10:02:00Z",
"message": (
"[3:00 min (140)] KILL: Next Killer(Axis/steam-next) -> "
"Next Victim(Allies/steam-next-victim) with MP40"
),
},
],
db_path=db_path,
)
without_cursor = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
)
with_invalid_cursor = list_current_match_kill_feed(
server_key="test-rcon-target",
db_path=db_path,
since_event_id="not-an-admin-log-event",
)
assert with_invalid_cursor == without_cursor
gc.collect()

View File

@@ -0,0 +1,171 @@
from __future__ import annotations
import json
import os
import sqlite3
import tempfile
import unittest
from datetime import datetime, timezone
from pathlib import Path
from contextlib import closing
from unittest.mock import patch
from app.rcon_admin_log_materialization import (
MATCH_RESULT_SOURCE,
initialize_rcon_materialized_storage,
)
from app.rcon_historical_backfill import (
count_recent_materialized_closed_matches,
run_rcon_historical_backfill,
select_backfill_targets,
)
from app.rcon_historical_leaderboards import list_rcon_materialized_leaderboard
TARGETS_JSON = json.dumps(
[
{
"name": "Comunidad Hispana #01",
"slug": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
"host": "127.0.0.1",
"port": 7779,
"password": "secret",
},
{
"name": "Comunidad Hispana #02",
"slug": "comunidad-hispana-02",
"external_server_id": "comunidad-hispana-02",
"host": "127.0.0.1",
"port": 7879,
"password": "secret",
},
{
"name": "Comunidad Hispana #03",
"slug": "comunidad-hispana-03",
"external_server_id": "comunidad-hispana-03",
"host": "127.0.0.1",
"port": 7979,
"password": "secret",
},
]
)
class RconHistoricalBackfillTests(unittest.TestCase):
def test_monthly_window_selects_previous_month_on_days_1_to_7(self) -> None:
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
payload = list_rcon_materialized_leaderboard(
server_key="all-servers",
timeframe="monthly",
metric="kills",
db_path=Path(tmpdir) / "historical.sqlite3",
now=datetime(2026, 5, 7, 12, tzinfo=timezone.utc),
)
self.assertEqual(payload["window_kind"], "previous-month")
self.assertEqual(payload["selected_month_start"], "2026-04-01T00:00:00Z")
self.assertEqual(payload["selected_month_end"], "2026-05-01T00:00:00Z")
def test_monthly_window_selects_current_month_on_day_8_plus(self) -> None:
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir:
payload = list_rcon_materialized_leaderboard(
server_key="all-servers",
timeframe="monthly",
metric="kills",
db_path=Path(tmpdir) / "historical.sqlite3",
now=datetime(2026, 5, 8, 12, tzinfo=timezone.utc),
)
self.assertEqual(payload["window_kind"], "current-month")
self.assertEqual(payload["selected_month_start"], "2026-05-01T00:00:00Z")
def test_recent_match_ensure_stops_when_count_is_already_satisfied(self) -> None:
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir, _patched_targets():
db_path = Path(tmpdir) / "historical.sqlite3"
_insert_closed_matches(db_path, 100)
payload = run_rcon_historical_backfill(
servers="comunidad-hispana-01,comunidad-hispana-02",
ensure_recent_matches=100,
dry_run=True,
db_path=db_path,
)
self.assertEqual(payload["recent_materialized_closed_match_count_before"], 100)
self.assertEqual(payload["actual_windows_scanned"], [])
def test_unknown_server_is_rejected(self) -> None:
with _patched_targets():
with self.assertRaises(ValueError):
select_backfill_targets("unknown-server")
def test_comunidad_hispana_03_is_not_included_by_default(self) -> None:
with _patched_targets():
selected = select_backfill_targets(None)
self.assertEqual(
[target.external_server_id for target in selected],
["comunidad-hispana-01", "comunidad-hispana-02"],
)
def test_dry_run_does_not_insert_data(self) -> None:
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir, _patched_targets():
db_path = Path(tmpdir) / "historical.sqlite3"
payload = run_rcon_historical_backfill(
servers="comunidad-hispana-01",
ensure_current_month=True,
dry_run=True,
db_path=db_path,
)
count_after = count_recent_materialized_closed_matches(db_path=db_path)
self.assertEqual(payload["status"], "dry-run")
self.assertEqual(payload["events_inserted"], 0)
self.assertEqual(count_after, 0)
def test_backfill_output_is_json_serializable(self) -> None:
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir, _patched_targets():
payload = run_rcon_historical_backfill(
servers="comunidad-hispana-01",
ensure_current_month=True,
dry_run=True,
db_path=Path(tmpdir) / "historical.sqlite3",
)
json.dumps(payload, ensure_ascii=True)
def _insert_closed_matches(db_path: Path, count: int) -> None:
initialize_rcon_materialized_storage(db_path=db_path)
with closing(sqlite3.connect(db_path)) as connection:
for index in range(count):
connection.execute(
"""
INSERT INTO rcon_materialized_matches (
target_key, external_server_id, match_key, map_name, map_pretty_name,
started_at, ended_at, confidence_mode, source_basis
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
"comunidad-hispana-01",
"comunidad-hispana-01",
f"match-{index}",
"stmariedumont",
"ST MARIE DU MONT",
"2026-05-01T10:00:00Z",
f"2026-05-{(index % 28) + 1:02d}T12:00:00Z",
"exact",
MATCH_RESULT_SOURCE,
),
)
connection.commit()
def _patched_targets():
return patch.dict(os.environ, {"HLL_BACKEND_RCON_TARGETS": TARGETS_JSON})
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,171 @@
from __future__ import annotations
import os
import unittest
from contextlib import contextmanager
from types import SimpleNamespace
from unittest.mock import patch
from app.rcon_historical_worker import (
CAPTURE_MODE_CURRENT_LIVE,
CAPTURE_MODE_HISTORICAL,
build_arg_parser,
main,
run_rcon_historical_capture,
run_rcon_historical_capture_unlocked,
)
TARGET = SimpleNamespace(
external_server_id="comunidad-hispana-01",
name="Comunidad Hispana #01",
host="203.0.113.10",
port=7779,
region="ES",
game_port=7777,
query_port=7778,
source_name="community-hispana-rcon",
)
class RconHistoricalWorkerTests(unittest.TestCase):
def test_current_live_capture_skips_materialization(self) -> None:
with (
patch("app.rcon_historical_worker.initialize_rcon_historical_storage"),
patch("app.rcon_historical_worker._select_targets", return_value=[TARGET]),
patch(
"app.rcon_historical_worker.query_live_server_sample",
return_value={"normalized": {"players": 10}, "raw_session": {"raw": True}},
),
patch(
"app.rcon_historical_worker.persist_rcon_historical_sample",
return_value={"samples_inserted": 1, "duplicate_samples": 0},
),
patch(
"app.rcon_historical_worker._ingest_target_admin_log",
return_value={
"status": "ok",
"errors": [],
"totals": {
"events_seen": 4,
"events_inserted": 2,
"duplicate_events": 2,
"failed_targets": 0,
},
},
),
patch("app.rcon_historical_worker.start_rcon_historical_capture_run", return_value=101),
patch("app.rcon_historical_worker.finalize_rcon_historical_capture_run"),
patch(
"app.rcon_historical_worker.list_rcon_historical_target_statuses",
return_value=[{"target_key": "comunidad-hispana-01"}],
),
patch("app.rcon_historical_worker.materialize_rcon_admin_log") as materialize,
):
payload = run_rcon_historical_capture_unlocked(capture_mode=CAPTURE_MODE_CURRENT_LIVE)
materialize.assert_not_called()
self.assertEqual(payload["capture_mode"], CAPTURE_MODE_CURRENT_LIVE)
self.assertIs(payload["materialization_skipped"], True)
self.assertEqual(payload["admin_log_events_seen"], 4)
self.assertEqual(payload["admin_log_events_inserted"], 2)
self.assertEqual(payload["duplicate_events"], 2)
self.assertEqual(payload["samples_inserted"], 1)
self.assertEqual(payload["materialization_result"]["status"], "skipped")
def test_historical_capture_keeps_materialization(self) -> None:
with (
patch("app.rcon_historical_worker.initialize_rcon_historical_storage"),
patch("app.rcon_historical_worker._select_targets", return_value=[TARGET]),
patch(
"app.rcon_historical_worker.query_live_server_sample",
return_value={"normalized": {"players": 10}, "raw_session": {"raw": True}},
),
patch(
"app.rcon_historical_worker.persist_rcon_historical_sample",
return_value={"samples_inserted": 1, "duplicate_samples": 0},
),
patch(
"app.rcon_historical_worker._ingest_target_admin_log",
return_value={
"status": "ok",
"errors": [],
"totals": {
"events_seen": 1,
"events_inserted": 1,
"duplicate_events": 0,
"failed_targets": 0,
},
},
),
patch("app.rcon_historical_worker.start_rcon_historical_capture_run", return_value=102),
patch("app.rcon_historical_worker.finalize_rcon_historical_capture_run"),
patch(
"app.rcon_historical_worker.list_rcon_historical_target_statuses",
return_value=[{"target_key": "comunidad-hispana-01"}],
),
patch(
"app.rcon_historical_worker.materialize_rcon_admin_log",
return_value={"matches_materialized": 3, "matches_updated": 2},
) as materialize,
):
payload = run_rcon_historical_capture_unlocked(capture_mode=CAPTURE_MODE_HISTORICAL)
materialize.assert_called_once_with()
self.assertEqual(payload["capture_mode"], CAPTURE_MODE_HISTORICAL)
self.assertIs(payload["materialization_skipped"], False)
self.assertEqual(payload["totals"]["materialized_matches_inserted"], 3)
self.assertEqual(payload["totals"]["materialized_matches_updated"], 2)
def test_cli_and_env_can_activate_current_live_mode(self) -> None:
with _temporary_env(
HLL_RCON_CURRENT_MATCH_MODE="true",
HLL_RCON_CURRENT_MATCH_CAPTURE_INTERVAL_SECONDS="5",
):
args = build_arg_parser().parse_args(["loop"])
with patch("app.rcon_historical_worker.run_periodic_rcon_historical_capture") as runner:
exit_code = main(["loop"])
self.assertEqual(args.capture_mode, CAPTURE_MODE_CURRENT_LIVE)
self.assertEqual(exit_code, 0)
runner.assert_called_once()
self.assertEqual(runner.call_args.kwargs["capture_mode"], CAPTURE_MODE_CURRENT_LIVE)
self.assertEqual(runner.call_args.kwargs["interval_seconds"], 5)
explicit = build_arg_parser().parse_args(["capture", "--skip-materialization"])
self.assertIs(explicit.skip_materialization, True)
def test_current_live_capture_uses_short_lock_timeout(self) -> None:
seen: dict[str, object] = {}
@contextmanager
def fake_lock(**kwargs):
seen.update(kwargs)
yield {"holder": kwargs["holder"]}
with (
_temporary_env(HLL_RCON_CURRENT_MATCH_WRITER_LOCK_TIMEOUT_SECONDS="3.5"),
patch("app.rcon_historical_worker.backend_writer_lock", side_effect=fake_lock),
patch(
"app.rcon_historical_worker.run_rcon_historical_capture_unlocked",
return_value={"status": "ok"},
),
):
run_rcon_historical_capture(capture_mode=CAPTURE_MODE_CURRENT_LIVE)
self.assertEqual(seen["timeout_seconds"], 3.5)
@contextmanager
def _temporary_env(**values: str):
previous = {name: os.environ.get(name) for name in values}
try:
for name, value in values.items():
os.environ[name] = value
yield
finally:
for name, value in previous.items():
if value is None:
os.environ.pop(name, None)
else:
os.environ[name] = value

View File

@@ -0,0 +1,400 @@
"""Regression tests for the materialized RCON AdminLog pipeline."""
from __future__ import annotations
import gc
import os
import tempfile
import unittest
from pathlib import Path
from app.historical_storage import upsert_historical_match
from app.payloads import build_recent_historical_matches_payload
from app.rcon_admin_log_materialization import (
get_materialized_rcon_match_detail,
materialize_rcon_admin_log,
summarize_rcon_materialization_status,
)
from app.rcon_admin_log_storage import persist_rcon_admin_log_entries
from app.rcon_historical_read_model import (
get_rcon_historical_match_detail,
list_rcon_historical_recent_activity,
)
from app.scoreboard_origins import resolve_trusted_scoreboard_match_url
class RconMaterializationPipelineTests(unittest.TestCase):
def test_materializes_match_result_and_player_stats_idempotently(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
_persist_admin_log_fixture(db_path)
first = materialize_rcon_admin_log(db_path=db_path)
second = materialize_rcon_admin_log(db_path=db_path)
detail = get_materialized_rcon_match_detail(
server_key="comunidad-hispana-01",
match_key="comunidad-hispana-01:100:500:stmariedumontwarfare",
db_path=db_path,
)
status = summarize_rcon_materialization_status(db_path=db_path)
self.assertEqual(first["matches_materialized"], 1)
self.assertEqual(second["matches_materialized"], 0)
self.assertEqual(second["matches_updated"], 1)
self.assertIsNotNone(detail)
match = detail["match"]
self.assertEqual(match["allied_score"], 5)
self.assertEqual(match["axis_score"], 0)
self.assertEqual(match["winner"], "allied")
players = {row["player_name"]: row for row in detail["players"]}
self.assertEqual(players["Alpha"]["kills"], 1)
self.assertEqual(players["Alpha"]["teamkills"], 1)
self.assertEqual(players["Bravo"]["deaths"], 1)
self.assertEqual(players["Charlie"]["deaths_by_teamkill"], 1)
self.assertEqual(status["materialized_matches"], 1)
self.assertEqual(status["matches_with_player_stats"], 1)
gc.collect()
def test_match_detail_read_model_hides_raw_player_ids(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
_persist_admin_log_fixture(db_path)
materialize_rcon_admin_log(db_path=db_path)
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-01",
match_id="comunidad-hispana-01:100:500:stmariedumontwarfare",
)
finally:
_restore_env("HLL_BACKEND_STORAGE_PATH", previous_storage_path)
self.assertIsNotNone(detail)
self.assertEqual(detail["result_source"], "admin-log-match-ended")
self.assertEqual(detail["result"]["allied_score"], 5)
self.assertEqual(detail["timestamp_confidence"], "absolute")
players = {row["player_name"]: row for row in detail["players"]}
self.assertNotIn("player_id", players["Alpha"])
self.assertIn("kd_ratio", players["Alpha"])
self.assertEqual(players["Alpha"]["steam_id_64"], "76561198000000001")
self.assertEqual(players["Alpha"]["platform"], "steam")
self.assertEqual(
players["Alpha"]["external_profile_links"]["hellor"],
"https://hellor.pro/player/76561198000000001",
)
self.assertEqual(players["Charlie"]["platform"], "unknown")
self.assertNotIn("steam_id_64", players["Charlie"])
self.assertNotIn("external_profile_links", players["Charlie"])
gc.collect()
def test_match_detail_marks_equal_materialized_timestamps_as_server_time_only(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-01T12:00:00Z",
"message": "[1 min (100)] MATCH START ST MARIE DU MONT Warfare",
},
{
"timestamp": "2026-05-01T12:00:00Z",
"message": "[91 min (5500)] MATCH ENDED `ST MARIE DU MONT Warfare` ALLIED (5 - 0) AXIS",
},
],
db_path=db_path,
)
materialize_rcon_admin_log(db_path=db_path)
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-01",
match_id="comunidad-hispana-01:100:5500:stmariedumontwarfare",
)
finally:
_restore_env("HLL_BACKEND_STORAGE_PATH", previous_storage_path)
self.assertIsNotNone(detail)
self.assertIsNone(detail["started_at"])
self.assertIsNone(detail["ended_at"])
self.assertEqual(detail["closed_at"], "2026-05-01T12:00:00Z")
self.assertEqual(detail["timestamp_confidence"], "server-time-only")
self.assertEqual(detail["duration_seconds"], 5400)
gc.collect()
def test_equal_timestamp_materialized_detail_uses_closed_at_window_for_scoreboard_link(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
upsert_historical_match(
server_slug="comunidad-hispana-02",
match_payload={
"id": "1779183861",
"creation_time": "2026-05-01T10:30:00Z",
"start": "2026-05-01T10:30:00Z",
"end": "2026-05-01T12:00:00Z",
"map": {"name": "ST MARIE DU MONT Warfare"},
"result": {"allied": 5, "axis": 0},
"player_stats": [],
},
db_path=db_path,
)
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-02",
"external_server_id": "comunidad-hispana-02",
},
entries=[
{
"timestamp": "2026-05-01T12:00:00Z",
"message": "[1 min (100)] MATCH START ST MARIE DU MONT Warfare",
},
{
"timestamp": "2026-05-01T12:00:00Z",
"message": "[91 min (5500)] MATCH ENDED `ST MARIE DU MONT Warfare` ALLIED (5 - 0) AXIS",
},
],
db_path=db_path,
)
materialize_rcon_admin_log(db_path=db_path)
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-02",
match_id="comunidad-hispana-02:100:5500:stmariedumontwarfare",
)
finally:
_restore_env("HLL_BACKEND_STORAGE_PATH", previous_storage_path)
self.assertIsNotNone(detail)
self.assertIsNone(detail["started_at"])
self.assertIsNone(detail["ended_at"])
self.assertEqual(detail["duration_seconds"], 5400)
self.assertEqual(
detail["match_url"],
"https://scoreboard.comunidadhll.es:5443/games/1779183861",
)
gc.collect()
def test_match_detail_adds_safe_profile_summary_when_snapshot_exists(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
_persist_admin_log_fixture(db_path)
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-01T10:30:00Z",
"message": (
"[31 min (300)] MESSAGE: player [Alpha(76561198000000001)], "
"content [─ Alpha ─\n"
"▒ Totales ▒\n"
"sesiones : 12\n"
"partidas jugadas : 9\n"
"bajas : 141 (6 TKs)\n"
"muertes : 268 (5 TKs)\n"
"K/D : 0.53\n"
"▒ Armas favoritas ▒\n"
"M1 Garand : 31]"
),
}
],
db_path=db_path,
)
materialize_rcon_admin_log(db_path=db_path)
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-01",
match_id="comunidad-hispana-01:100:500:stmariedumontwarfare",
)
finally:
_restore_env("HLL_BACKEND_STORAGE_PATH", previous_storage_path)
self.assertIsNotNone(detail)
players = {row["player_name"]: row for row in detail["players"]}
self.assertIn("profile_summary", players["Alpha"])
self.assertNotIn("profile_summary", players["Bravo"])
profile_summary = players["Alpha"]["profile_summary"]
self.assertEqual(profile_summary["sessions"], 12)
self.assertEqual(profile_summary["matches_played"], 9)
self.assertEqual(profile_summary["totals"]["kills"], 141)
self.assertEqual(profile_summary["favorite_weapons"], {"M1 Garand": 31})
self.assertNotIn("raw_content", profile_summary)
self.assertNotIn("player_id", players["Alpha"])
gc.collect()
def test_recent_matches_prefer_materialized_rcon_over_scoreboard_fallback(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
_persist_admin_log_fixture(db_path)
materialize_rcon_admin_log(db_path=db_path)
_persist_scoreboard_match(db_path)
payload = build_recent_historical_matches_payload(
limit=5,
server_slug="comunidad-hispana-01",
)
recent = list_rcon_historical_recent_activity(
server_key="comunidad-hispana-01",
limit=5,
)
finally:
_restore_env("HLL_BACKEND_STORAGE_PATH", previous_storage_path)
self.assertEqual(payload["data"]["selected_source"], "rcon")
self.assertEqual(payload["data"]["items"][0]["result_source"], "admin-log-match-ended")
self.assertEqual(recent[0]["result_source"], "admin-log-match-ended")
self.assertNotEqual(payload["data"]["selected_source"], "public-scoreboard")
gc.collect()
def test_recent_materialized_detail_id_resolves_through_detail_read_model(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
_persist_admin_log_fixture(db_path)
materialize_rcon_admin_log(db_path=db_path)
recent = list_rcon_historical_recent_activity(
server_key="comunidad-hispana-01",
limit=1,
)[0]
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-01",
match_id=str(recent["internal_detail_match_id"]),
)
finally:
_restore_env("HLL_BACKEND_STORAGE_PATH", previous_storage_path)
self.assertIsNotNone(detail)
self.assertEqual(detail["match_id"], recent["internal_detail_match_id"])
gc.collect()
def test_public_scoreboard_fallback_used_only_without_rcon_activity(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
_persist_scoreboard_match(db_path)
payload = build_recent_historical_matches_payload(
limit=5,
server_slug="comunidad-hispana-01",
)
finally:
_restore_env("HLL_BACKEND_STORAGE_PATH", previous_storage_path)
self.assertTrue(payload["data"]["fallback_used"])
self.assertEqual(payload["data"]["selected_source"], "public-scoreboard")
self.assertEqual(payload["data"]["items"][0]["result_source"], "public-scoreboard-fallback")
gc.collect()
def test_safe_scoreboard_match_url_allowlist_for_active_origins(self) -> None:
self.assertEqual(
resolve_trusted_scoreboard_match_url(
"https://scoreboard.comunidadhll.es/games/1561515",
"comunidad-hispana-01",
),
"https://scoreboard.comunidadhll.es/games/1561515",
)
self.assertEqual(
resolve_trusted_scoreboard_match_url(
"https://scoreboard.comunidadhll.es:5443/games/222",
"comunidad-hispana-02",
),
"https://scoreboard.comunidadhll.es:5443/games/222",
)
self.assertIsNone(
resolve_trusted_scoreboard_match_url(
"https://example.com/games/222",
"comunidad-hispana-02",
)
)
self.assertIsNone(
resolve_trusted_scoreboard_match_url(
"https://scoreboard.comunidadhll.es:5443/admin/222",
"comunidad-hispana-02",
)
)
def _persist_admin_log_fixture(db_path: Path) -> None:
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
},
entries=[
{
"timestamp": "2026-05-01T10:00:00Z",
"message": "[1 min (100)] MATCH START ST MARIE DU MONT Warfare",
},
{
"timestamp": "2026-05-01T10:05:00Z",
"message": "[6 min (150)] CONNECTED Alpha (76561198000000001)",
},
{
"timestamp": "2026-05-01T10:06:00Z",
"message": "[7 min (160)] TEAMSWITCH Alpha (None > Allies)",
},
{
"timestamp": "2026-05-01T10:10:00Z",
"message": (
"[11 min (200)] KILL: Alpha(Allies/76561198000000001) -> "
"Bravo(Axis/76561198000000002) with M1 Garand"
),
},
{
"timestamp": "2026-05-01T10:12:00Z",
"message": (
"[13 min (220)] KILL: Alpha(Allies/76561198000000001) -> "
"Charlie(Allies/nonsteam-local) with M1 Garand"
),
},
{
"timestamp": "2026-05-01T11:20:00Z",
"message": "[81 min (500)] MATCH ENDED `ST MARIE DU MONT Warfare` ALLIED (5 - 0) AXIS",
},
],
db_path=db_path,
)
def _persist_scoreboard_match(db_path: Path) -> None:
upsert_historical_match(
server_slug="comunidad-hispana-01",
match_payload={
"id": "1561515",
"creation_time": "2026-05-01T10:00:00Z",
"start": "2026-05-01T10:00:00Z",
"end": "2026-05-01T11:20:00Z",
"map": {"name": "ST MARIE DU MONT Warfare"},
"result": {"allied": 2, "axis": 3},
"player_stats": [],
},
db_path=db_path,
)
def _restore_env(name: str, previous_value: str | None) -> None:
if previous_value is None:
os.environ.pop(name, None)
else:
os.environ[name] = previous_value
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,436 @@
"""Regression checks for persisted public-scoreboard match links."""
from __future__ import annotations
import gc
import os
import sqlite3
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from app.scoreboard_candidate_backfill import run_backfill
from app.historical_storage import (
get_historical_match_detail,
initialize_historical_storage,
list_recent_historical_matches,
upsert_historical_match,
)
from app.rcon_historical_storage import initialize_rcon_historical_storage
from app.rcon_historical_storage import persist_rcon_historical_sample
from app.rcon_historical_storage import start_rcon_historical_capture_run
from app.rcon_historical_read_model import get_rcon_historical_match_detail
from app.rcon_admin_log_materialization import materialize_rcon_admin_log
from app.rcon_admin_log_storage import persist_rcon_admin_log_entries
from app.rcon_scoreboard_relink import relink_materialized_matches
from app.scoreboard_correlation_diagnostics import inspect_materialized_match_correlation
class PersistedScoreboardMatchLinkTests(unittest.TestCase):
def test_list_backfill_persists_foy_candidate_before_detail_fetch_failure(self) -> None:
stored: dict[tuple[str, str], dict[str, object]] = {}
class FoyListProvider:
def fetch_match_page(self, *, base_url: str, page: int, limit: int) -> dict[str, object]:
return {"maps": [_foy_list_match()]} if page == 1 else {"maps": []}
def fetch_match_details(
self,
*,
base_url: str,
match_ids: list[str],
max_workers: int,
) -> list[dict[str, object]]:
raise RuntimeError("detail endpoint unavailable")
def fake_upsert(*, server_slug: str, candidate: dict[str, object]) -> str:
key = (server_slug, str(candidate["external_match_id"]))
outcome = "updated" if key in stored else "inserted"
stored[key] = dict(candidate)
return outcome
server = {
"slug": "comunidad-hispana-02",
"scoreboard_base_url": "https://scoreboard.comunidadhll.es:5443",
"server_number": 2,
}
with (
patch("app.scoreboard_candidate_backfill.initialize_historical_storage"),
patch(
"app.scoreboard_candidate_backfill.PublicScoreboardHistoricalDataSource",
return_value=FoyListProvider(),
),
patch(
"app.scoreboard_candidate_backfill.upsert_scoreboard_candidate",
side_effect=fake_upsert,
),
):
first = run_backfill(
server=server,
start_at=_backfill_timestamp("2026-05-20T00:00:00Z"),
end_at=_backfill_timestamp("2026-05-21T23:59:59Z"),
max_pages=2,
page_size=100,
detail_workers=1,
)
second = run_backfill(
server=server,
start_at=_backfill_timestamp("2026-05-20T00:00:00Z"),
end_at=_backfill_timestamp("2026-05-21T23:59:59Z"),
max_pages=2,
page_size=100,
detail_workers=1,
)
candidate = stored[("comunidad-hispana-02", "1562115")]
self.assertEqual(
candidate["match_url"],
"https://scoreboard.comunidadhll.es:5443/games/1562115",
)
self.assertEqual(first["list_candidates_inserted"], 1)
self.assertEqual(first["list_candidates_updated"], 0)
self.assertEqual(first["errors"][0]["stage"], "fetch_match_details")
self.assertEqual(second["list_candidates_inserted"], 0)
self.assertEqual(second["list_candidates_updated"], 1)
self.assertEqual(len(stored), 1)
def test_recent_and_detail_payloads_expose_safe_persisted_match_url(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
match_url = "https://scoreboard.comunidadhll.es:5443/games/12345"
_persist_match(db_path, server_slug="comunidad-hispana-02", match_id="12345")
recent_items = list_recent_historical_matches(
server_slug="comunidad-hispana-02",
limit=5,
db_path=db_path,
)
detail = get_historical_match_detail(
server_slug="comunidad-hispana-02",
match_id="12345",
db_path=db_path,
)
self.assertEqual(recent_items[0]["match_url"], match_url)
self.assertIsNotNone(detail)
self.assertEqual(detail["match_url"], match_url)
gc.collect()
def test_untrusted_persisted_match_url_is_not_exposed(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
_persist_match(db_path, server_slug="comunidad-hispana-01", match_id="999")
_set_raw_payload_ref(
db_path,
match_id="999",
raw_payload_ref="https://scoreboard.comunidadhll.es:3443/games/999",
)
recent_items = list_recent_historical_matches(
server_slug="comunidad-hispana-01",
limit=5,
db_path=db_path,
)
detail = get_historical_match_detail(
server_slug="comunidad-hispana-01",
match_id="999",
db_path=db_path,
)
self.assertIsNone(recent_items[0]["match_url"])
self.assertIsNotNone(detail)
self.assertIsNone(detail["match_url"])
gc.collect()
def test_detail_player_links_use_trusted_scoreboard_steam_id(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
_persist_match(
db_path,
server_slug="comunidad-hispana-02",
match_id="steam-player-match",
player_stats=[
{
"player": "Steam Player",
"steaminfo": {"profile": {"steamid": "76561198000000009"}},
"team": {"side": "allies"},
"kills": 4,
"deaths": 2,
}
],
)
detail = get_historical_match_detail(
server_slug="comunidad-hispana-02",
match_id="steam-player-match",
db_path=db_path,
)
self.assertIsNotNone(detail)
player = detail["players"][0]
self.assertEqual(player["steam_id_64"], "76561198000000009")
self.assertEqual(player["platform"], "steam")
self.assertEqual(
player["external_profile_links"]["hll_records"],
"https://hllrecords.com/profiles/76561198000000009",
)
gc.collect()
def test_rcon_match_detail_does_not_fabricate_external_scoreboard_url(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
initialize_rcon_historical_storage(db_path=db_path)
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-01",
match_id="rcon:synthetic-window",
)
finally:
if previous_storage_path is None:
os.environ.pop("HLL_BACKEND_STORAGE_PATH", None)
else:
os.environ["HLL_BACKEND_STORAGE_PATH"] = previous_storage_path
self.assertIsNone(detail)
gc.collect()
def test_rcon_match_detail_exposes_correlated_scoreboard_url_on_strong_evidence(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
_persist_match(
db_path,
server_slug="comunidad-hispana-01",
match_id="1561515",
map_name="St. Mere Eglise",
started_at="2026-04-12T16:20:00Z",
ended_at="2026-04-12T17:45:00Z",
)
session_key = _persist_rcon_window(
db_path,
map_name="St. Mere Eglise",
first_seen_at="2026-04-12T16:28:55.761810Z",
last_seen_at="2026-04-12T16:43:55.761810Z",
players=94,
max_players=98,
)
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-01",
match_id=session_key,
)
finally:
if previous_storage_path is None:
os.environ.pop("HLL_BACKEND_STORAGE_PATH", None)
else:
os.environ["HLL_BACKEND_STORAGE_PATH"] = previous_storage_path
self.assertIsNotNone(detail)
self.assertEqual(
detail["match_url"],
"https://scoreboard.comunidadhll.es/games/1561515",
)
gc.collect()
def test_rcon_match_detail_keeps_low_confidence_correlation_unlinked(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
_persist_match(
db_path,
server_slug="comunidad-hispana-01",
match_id="1561515",
map_name="Carentan",
started_at="2026-04-12T10:00:00Z",
ended_at="2026-04-12T11:30:00Z",
)
session_key = _persist_rcon_window(
db_path,
map_name="St. Mere Eglise",
first_seen_at="2026-04-12T16:28:55.761810Z",
last_seen_at="2026-04-12T16:43:55.761810Z",
players=94,
max_players=98,
)
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-01",
match_id=session_key,
)
finally:
if previous_storage_path is None:
os.environ.pop("HLL_BACKEND_STORAGE_PATH", None)
else:
os.environ["HLL_BACKEND_STORAGE_PATH"] = previous_storage_path
self.assertIsNotNone(detail)
self.assertIsNone(detail["match_url"])
gc.collect()
def test_foy_relink_reports_existing_materialized_match_url(self) -> None:
with tempfile.TemporaryDirectory() as tmpdir:
db_path = Path(tmpdir) / "historical.sqlite3"
previous_storage_path = os.environ.get("HLL_BACKEND_STORAGE_PATH")
os.environ["HLL_BACKEND_STORAGE_PATH"] = str(db_path)
try:
_persist_match(
db_path,
server_slug="comunidad-hispana-02",
match_id="1562115",
map_name="Foy Warfare",
started_at="2026-05-20T20:54:11Z",
ended_at="2026-05-20T22:24:11Z",
)
persist_rcon_admin_log_entries(
target={
"target_key": "comunidad-hispana-02",
"external_server_id": "comunidad-hispana-02",
},
entries=[
{
"timestamp": "2026-05-20T20:54:11Z",
"message": "[1 min (1779310451)] MATCH START Foy Warfare",
},
{
"timestamp": "2026-05-20T22:24:11Z",
"message": "[91 min (1779315851)] MATCH ENDED `Foy Warfare` ALLIED (4 - 1) AXIS",
},
],
db_path=db_path,
)
materialize_rcon_admin_log(db_path=db_path)
report = relink_materialized_matches(
server_key="comunidad-hispana-02",
db_path=db_path,
)
detail = get_rcon_historical_match_detail(
server_key="comunidad-hispana-02",
match_id="comunidad-hispana-02:1779310451:1779315851:foywarfare",
)
diagnostics = inspect_materialized_match_correlation(
server_key="comunidad-hispana-02",
match_key="comunidad-hispana-02:1779310451:1779315851:foywarfare",
db_path=db_path,
)
finally:
if previous_storage_path is None:
os.environ.pop("HLL_BACKEND_STORAGE_PATH", None)
else:
os.environ["HLL_BACKEND_STORAGE_PATH"] = previous_storage_path
self.assertEqual(report["matches_scanned"], 1)
self.assertEqual(report["matches_linked"], 1)
self.assertGreaterEqual(report["candidates_scanned"], 1)
self.assertIsNotNone(detail)
self.assertEqual(
detail["match_url"],
"https://scoreboard.comunidadhll.es:5443/games/1562115",
)
self.assertEqual(diagnostics["final_reason"], "linked")
self.assertEqual(diagnostics["selected_candidate"]["external_match_id"], "1562115")
self.assertEqual(diagnostics["top_candidates"][0]["map"], "Foy Warfare")
gc.collect()
def _persist_match(
db_path: Path,
*,
server_slug: str,
match_id: str,
map_name: str = "carentan",
started_at: str = "2026-05-01T10:00:00Z",
ended_at: str = "2026-05-01T11:20:00Z",
player_stats: list[dict[str, object]] | None = None,
) -> None:
upsert_historical_match(
server_slug=server_slug,
match_payload={
"id": match_id,
"creation_time": started_at,
"start": started_at,
"end": ended_at,
"map": {"name": map_name},
"result": {"allied": 3, "axis": 2},
"player_stats": player_stats or [],
},
db_path=db_path,
)
def _foy_list_match() -> dict[str, object]:
return {
"id": 1562115,
"server_number": 2,
"start": "2026-05-20T20:54:11+00:00",
"end": "2026-05-20T22:24:11+00:00",
"map": {"id": "foywarfare", "pretty_name": "Foy Warfare"},
"result": {"allied": 4, "axis": 1},
}
def _backfill_timestamp(raw_value: str):
from app.scoreboard_candidate_backfill import _parse_timestamp
return _parse_timestamp(raw_value, option_name="test")
def _persist_rcon_window(
db_path: Path,
*,
map_name: str,
first_seen_at: str,
last_seen_at: str,
players: int,
max_players: int,
) -> str:
initialize_rcon_historical_storage(db_path=db_path)
run_id = start_rcon_historical_capture_run(
mode="test",
target_scope="comunidad-hispana-01",
db_path=db_path,
)
target = {
"target_key": "comunidad-hispana-01",
"external_server_id": "comunidad-hispana-01",
"name": "Comunidad Hispana #01",
"host": "127.0.0.1",
"port": 7779,
}
for captured_at in (first_seen_at, last_seen_at):
persist_rcon_historical_sample(
run_id=run_id,
captured_at=captured_at,
target=target,
normalized_payload={
"status": "online",
"players": players,
"max_players": max_players,
"current_map": map_name,
},
raw_payload={},
db_path=db_path,
)
return f"1:{first_seen_at}"
def _set_raw_payload_ref(db_path: Path, *, match_id: str, raw_payload_ref: str) -> None:
with sqlite3.connect(db_path) as connection:
connection.execute(
"""
UPDATE historical_matches
SET raw_payload_ref = ?
WHERE external_match_id = ?
""",
(raw_payload_ref, match_id),
)
if __name__ == "__main__":
unittest.main()