Merge branch 'codex/task-121-adminlog-storage-tests'
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: TASK-121
|
id: TASK-121
|
||||||
title: Add RCON AdminLog storage read tests
|
title: Add RCON AdminLog storage read tests
|
||||||
status: pending
|
status: done
|
||||||
type: backend
|
type: backend
|
||||||
team: Backend Senior
|
team: Backend Senior
|
||||||
supporting_teams:
|
supporting_teams:
|
||||||
@@ -72,3 +72,17 @@ AdminLog storage and manual ingestion exist, and real validation confirmed canon
|
|||||||
- Stage only intended files.
|
- Stage only intended files.
|
||||||
- Commit the completed implementation.
|
- Commit the completed implementation.
|
||||||
- Push the branch to origin.
|
- 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