Merge branch 'codex/task-121-adminlog-storage-tests'
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
---
|
||||
id: TASK-121
|
||||
title: Add RCON AdminLog storage read tests
|
||||
status: pending
|
||||
status: done
|
||||
type: backend
|
||||
team: Backend Senior
|
||||
supporting_teams:
|
||||
@@ -72,3 +72,17 @@ AdminLog storage and manual ingestion exist, and real validation confirmed canon
|
||||
- Stage only intended files.
|
||||
- Commit the completed implementation.
|
||||
- Push the branch to origin.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Added deterministic offline storage tests in `backend/tests/test_rcon_admin_log_storage.py`.
|
||||
- Covered AdminLog table initialization, first insert behavior, duplicate reporting, canonical-message dedupe when relative prefixes change, and event-count grouping.
|
||||
- Tests use pytest `tmp_path` temporary SQLite paths and do not use real RCON or the runtime database.
|
||||
|
||||
## Validation Result
|
||||
|
||||
- `python -m compileall backend/app` passed.
|
||||
- `python -m pytest backend/tests/test_rcon_admin_log_parser.py backend/tests/test_rcon_admin_log_storage.py` could not run locally because `pytest` is not installed.
|
||||
- `docker compose exec backend python -m pytest backend/tests/test_rcon_admin_log_parser.py backend/tests/test_rcon_admin_log_storage.py` also could not run because `pytest` is not installed in the backend image.
|
||||
- Direct Python invocation of the new storage test functions passed with `PYTHONPATH=backend`.
|
||||
- `git diff --name-only` matched the expected task scope after accounting for the new untracked test file.
|
||||
178
backend/tests/test_rcon_admin_log_storage.py
Normal file
178
backend/tests/test_rcon_admin_log_storage.py
Normal file
@@ -0,0 +1,178 @@
|
||||
import gc
|
||||
import sqlite3
|
||||
|
||||
from app.rcon_admin_log_storage import (
|
||||
initialize_rcon_admin_log_storage,
|
||||
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 {
|
||||
"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_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()
|
||||
Reference in New Issue
Block a user