import json import sqlite3 import subprocess import sys import tempfile import unittest from contextlib import closing from datetime import datetime, timezone from pathlib import Path from server.database.backend.SqliteGameplayBackend import SqliteGameplayBackend from server.database.compact.records import GameBundle, SnakeIdentity, SnakeState, TurnRecord REPO_ROOT = Path(__file__).resolve().parents[1] CUSTOMIZATIONS = {"color": "#663399", "head": "ferret", "tail": "swirl"} def _bundle(game_id:str, winner_you:bool, cleaned:bool=False) -> GameBundle: turns = [ TurnRecord(turn, datetime(2026, 1, 1, 0, 0, turn, tzinfo=timezone.utc), "up", {"score": turn}, [] if turn == 1 else None, [] if turn == 1 else None) for turn in range(1, 6) ] states = [ SnakeState(snake_id, turn, 90, 10, [{"x": x, "y": turn}]) for turn in range(1, 6) for snake_id, x in (("me", 1), ("a", 3), ("b", 5)) ] return GameBundle( game={ "game_id": game_id, "started_at": "2026-01-01T00:00:00+00:00", "ended_at": "2026-01-01T00:01:00+00:00", "width": 11, "height": 11, "source": "league", "map_name": "standard", "ruleset_name": "standard", "ruleset_version": "v1", "your_snake_id": "me", "your_snake_name": "PrismBattleSnake", "your_snake_type": "PrismBattleSnake_GPT_5_6_Sol", "your_snake_version": "1.0.0", "game_type": "standard", "winner_name": "PrismBattleSnake", "winner_you": winner_you, "final_turn": 5, "status": "finished", "has_replay": True, "quality_status": "retained", "quality_score": 90 if cleaned else None, "quality_tier": "high" if cleaned else None, "quality_reasons": ["already_scored"] if cleaned else None, }, snakes=[ SnakeIdentity("me", "PrismBattleSnake", True, CUSTOMIZATIONS), SnakeIdentity("a", "A", False, {}), SnakeIdentity("b", "B", False, {}), ], turns=turns, states=states, ) class TestMergeGameplayDatabases(unittest.TestCase): def _create(self, path:Path, bundle:GameBundle) -> None: SqliteGameplayBackend(str(path)).import_game_bundle_sync(bundle) def _merge(self, root:Path, *extra:str): return subprocess.run([ sys.executable, "scripts/merge_gameplay_databases.py", "--base", str(root / "base.sqlite3"), "--delta", str(root / "delta.sqlite3"), "--destination", str(root / "merged.sqlite3"), *extra, ], cwd=REPO_ROOT, text=True, capture_output=True) def test_merges_base_and_delta_with_fresh_ids(self): with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) self._create(root / "base.sqlite3", _bundle("base-game", True, cleaned=True)) self._create(root / "delta.sqlite3", _bundle("delta-game", False)) result = self._merge(root, "--minimum-quality", "medium") self.assertEqual(result.returncode, 0, result.stderr) merged = SqliteGameplayBackend(str(root / "merged.sqlite3")) with closing(sqlite3.connect(root / "merged.sqlite3")) as connection: games = connection.execute( "SELECT game_id, gid, winner_you, has_replay, quality_tier FROM games ORDER BY game_id" ).fetchall() self.assertEqual(connection.execute("SELECT COUNT(*) FROM game_replays").fetchone()[0], 2) self.assertEqual(connection.execute("PRAGMA foreign_key_check").fetchall(), []) self.assertEqual([(g[0], g[2], g[3]) for g in games], [("base-game", 1, 1), ("delta-game", 0, 1)]) self.assertEqual(games[0][4], "high") self.assertEqual(len({g[1] for g in games}), 2) for game_id in ("base-game", "delta-game"): bundle = merged.read_game_bundle_sync(game_id) self.assertEqual(len(bundle.turns), 5) self.assertEqual(len(bundle.states), 15) me = next(snake for snake in bundle.snakes if snake.snake_id == "me") self.assertEqual(me.customizations, CUSTOMIZATIONS) def test_conflicting_duplicate_aborts_without_destination(self): with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) self._create(root / "base.sqlite3", _bundle("same-game", True, cleaned=True)) self._create(root / "delta.sqlite3", _bundle("same-game", False)) result = self._merge(root) self.assertNotEqual(result.returncode, 0) self.assertIn("Conflicting duplicate game_id", result.stderr) self.assertFalse((root / "merged.sqlite3").exists()) if __name__ == "__main__": unittest.main()