From de2cf726eaf9b45e5de522dd2b6d1e467faf74c6 Mon Sep 17 00:00:00 2001 From: devRaGonSa Date: Thu, 21 May 2026 21:29:56 +0200 Subject: [PATCH] fix: handle postgres killfeed cursor parameter --- backend/app/rcon_admin_log_storage.py | 83 ++++++++++++------ backend/tests/test_rcon_admin_log_storage.py | 90 ++++++++++++++++++++ 2 files changed, 146 insertions(+), 27 deletions(-) diff --git a/backend/app/rcon_admin_log_storage.py b/backend/app/rcon_admin_log_storage.py index a2fa049..b32dca2 100644 --- a/backend/app/rcon_admin_log_storage.py +++ b/backend/app/rcon_admin_log_storage.py @@ -372,36 +372,65 @@ def list_current_match_kill_feed( else None ) if open_start_time is None: - rows = connection.execute( - """ - SELECT id, target_key, external_server_id, event_timestamp, server_time, - parsed_payload_json - FROM rcon_admin_log_events - WHERE (target_key = ? OR external_server_id = ?) - AND event_type = 'kill' - AND (? IS NULL OR id > ?) - ORDER BY server_time DESC, id DESC - LIMIT ? - """, - (server_key, server_key, since_row_id, since_row_id, limit), - ).fetchall() + if since_row_id is None: + rows = connection.execute( + """ + SELECT id, target_key, external_server_id, event_timestamp, server_time, + parsed_payload_json + FROM rcon_admin_log_events + WHERE (target_key = ? OR external_server_id = ?) + AND event_type = 'kill' + ORDER BY server_time DESC, id DESC + LIMIT ? + """, + (server_key, server_key, limit), + ).fetchall() + else: + rows = connection.execute( + """ + SELECT id, target_key, external_server_id, event_timestamp, server_time, + parsed_payload_json + FROM rcon_admin_log_events + WHERE (target_key = ? OR external_server_id = ?) + AND event_type = 'kill' + AND id > ? + ORDER BY server_time DESC, id DESC + LIMIT ? + """, + (server_key, server_key, since_row_id, limit), + ).fetchall() scope = "recent-admin-log-window" confidence = "partial" else: - rows = connection.execute( - """ - SELECT id, target_key, external_server_id, event_timestamp, server_time, - parsed_payload_json - FROM rcon_admin_log_events - WHERE (target_key = ? OR external_server_id = ?) - AND event_type = 'kill' - AND server_time >= ? - AND (? IS NULL OR id > ?) - ORDER BY server_time DESC, id DESC - LIMIT ? - """, - (server_key, server_key, open_start_time, since_row_id, since_row_id, limit), - ).fetchall() + if since_row_id is None: + rows = connection.execute( + """ + SELECT id, target_key, external_server_id, event_timestamp, server_time, + parsed_payload_json + FROM rcon_admin_log_events + WHERE (target_key = ? OR external_server_id = ?) + AND event_type = 'kill' + AND server_time >= ? + ORDER BY server_time DESC, id DESC + LIMIT ? + """, + (server_key, server_key, open_start_time, limit), + ).fetchall() + else: + rows = connection.execute( + """ + SELECT id, target_key, external_server_id, event_timestamp, server_time, + parsed_payload_json + FROM rcon_admin_log_events + WHERE (target_key = ? OR external_server_id = ?) + AND event_type = 'kill' + AND server_time >= ? + AND id > ? + ORDER BY server_time DESC, id DESC + LIMIT ? + """, + (server_key, server_key, open_start_time, since_row_id, limit), + ).fetchall() scope = "open-admin-log-match-window" confidence = "admin-log-boundary" diff --git a/backend/tests/test_rcon_admin_log_storage.py b/backend/tests/test_rcon_admin_log_storage.py index 406958f..7e62b4e 100644 --- a/backend/tests/test_rcon_admin_log_storage.py +++ b/backend/tests/test_rcon_admin_log_storage.py @@ -2,6 +2,7 @@ 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, @@ -405,3 +406,92 @@ def test_current_match_kill_feed_filters_rows_before_incremental_cursor(tmp_path 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()