This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
MODULE_PATH = PROJECT_ROOT / "jwebot_release_update.py"
|
||||
SPEC = importlib.util.spec_from_file_location("jwebot_release_update", MODULE_PATH)
|
||||
MODULE = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader is not None
|
||||
sys.modules[SPEC.name] = MODULE
|
||||
SPEC.loader.exec_module(MODULE)
|
||||
|
||||
|
||||
class ReleaseUpdateTests(unittest.TestCase):
|
||||
release = "beta 1.0.4.1016"
|
||||
|
||||
def setUp(self):
|
||||
self.temporary = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temporary.name)
|
||||
self.database = self.root / "edge.sqlite3"
|
||||
self.backups = self.root / "backups"
|
||||
self.version = self.root / "VERSION"
|
||||
self.changelog = self.root / "CHANGELOG.md"
|
||||
self.delivery_helper = self.root / "jwebot_edge_delivery.py"
|
||||
self.version.write_text(self.release + "\n", encoding="utf-8")
|
||||
self.changelog.write_text(
|
||||
(
|
||||
"# Changelog\n\n"
|
||||
f"## {self.release} - 2026-10-16\n\n"
|
||||
"- 更新摩拉账户并修复重复回复问题。\n"
|
||||
"- Internal implementation detail.\n"
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
self.delivery_helper.write_text("# test helper\n", encoding="utf-8")
|
||||
connection = sqlite3.connect(self.database)
|
||||
connection.executescript(
|
||||
"""
|
||||
CREATE TABLE metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE player_accounts (
|
||||
group_fingerprint TEXT NOT NULL,
|
||||
person_key TEXT NOT NULL,
|
||||
state_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
PRIMARY KEY (group_fingerprint, person_key)
|
||||
);
|
||||
"""
|
||||
)
|
||||
for person_key, balance in (("person-a", 1000), ("person-b", 50)):
|
||||
connection.execute(
|
||||
"INSERT INTO player_accounts VALUES (?, ?, ?, ?)",
|
||||
(
|
||||
"group-a",
|
||||
person_key,
|
||||
json.dumps({"balance": balance}),
|
||||
"2026-10-16T00:00:00Z",
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
connection.close()
|
||||
|
||||
def tearDown(self):
|
||||
self.temporary.cleanup()
|
||||
|
||||
def account_states(self):
|
||||
connection = sqlite3.connect(self.database)
|
||||
try:
|
||||
rows = connection.execute(
|
||||
"SELECT state_json FROM player_accounts ORDER BY person_key"
|
||||
).fetchall()
|
||||
finally:
|
||||
connection.close()
|
||||
return [json.loads(row[0]) for row in rows]
|
||||
|
||||
def announcement_marker(self):
|
||||
key = MODULE.announcement_key(self.release, "group-a")
|
||||
connection = sqlite3.connect(self.database)
|
||||
try:
|
||||
row = connection.execute(
|
||||
"SELECT value FROM metadata WHERE key = ?",
|
||||
(key,),
|
||||
).fetchone()
|
||||
finally:
|
||||
connection.close()
|
||||
return json.loads(row[0]) if row else None
|
||||
|
||||
def test_changelog_first_bullet_is_the_announcement_summary(self):
|
||||
self.changelog.write_text(
|
||||
(
|
||||
"# Changelog\n\n"
|
||||
f"## {self.release} - 2026-10-16\n\n"
|
||||
"- 第一行更新摘要,\n"
|
||||
" 同一条摘要可以换行。\n"
|
||||
"- 第二条不进入群公告。\n"
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
summary = MODULE.read_changelog_summary(self.changelog, self.release)
|
||||
self.assertEqual(summary, "第一行更新摘要, 同一条摘要可以换行。")
|
||||
announcement = MODULE.build_announcement(self.release, summary)
|
||||
self.assertIn(f"{self.release} 更新现已完成", announcement)
|
||||
self.assertIn("更新补偿:摩拉 x200", announcement)
|
||||
self.assertNotIn("第二条", announcement)
|
||||
|
||||
def test_release_update_grants_once_and_announces_once(self):
|
||||
delivered = []
|
||||
|
||||
def deliverer(path, group, announcement, event_id):
|
||||
delivered.append((path, group, announcement, event_id))
|
||||
|
||||
applied_at = datetime(2026, 10, 16, 8, 0, tzinfo=timezone.utc)
|
||||
first = MODULE.run_release_update(
|
||||
release_file=self.version,
|
||||
changelog=self.changelog,
|
||||
database_path=self.database,
|
||||
backup_dir=self.backups,
|
||||
delivery_helper=self.delivery_helper,
|
||||
deliverer=deliverer,
|
||||
applied_at=applied_at,
|
||||
)
|
||||
second = MODULE.run_release_update(
|
||||
release_file=self.version,
|
||||
changelog=self.changelog,
|
||||
database_path=self.database,
|
||||
backup_dir=self.backups,
|
||||
delivery_helper=self.delivery_helper,
|
||||
deliverer=deliverer,
|
||||
applied_at=applied_at,
|
||||
)
|
||||
|
||||
self.assertEqual(first["compensation"], "applied")
|
||||
self.assertEqual(second["compensation"], "already_applied")
|
||||
self.assertEqual(first["accounts"], 2)
|
||||
self.assertEqual(len(delivered), 1)
|
||||
self.assertEqual(delivered[0][1], "group-a")
|
||||
self.assertIn("更新摩拉账户并修复重复回复问题", delivered[0][2])
|
||||
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250])
|
||||
for state in self.account_states():
|
||||
matches = [
|
||||
event
|
||||
for event in state["coin_events"]
|
||||
if event["delta"] == 200
|
||||
and event["reason"] == f"{self.release} 版本更新补偿"
|
||||
]
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(matches[0]["created_at"], applied_at.timestamp())
|
||||
self.assertEqual(self.announcement_marker()["state"], "delivered")
|
||||
self.assertEqual(len(list(self.backups.glob("*.sqlite3"))), 1)
|
||||
|
||||
def test_uncertain_announcement_never_duplicates_compensation(self):
|
||||
def uncertain(*_args):
|
||||
raise MODULE.AnnouncementUncertainError("uncertain")
|
||||
|
||||
with self.assertRaises(MODULE.AnnouncementUncertainError):
|
||||
MODULE.run_release_update(
|
||||
release_file=self.version,
|
||||
changelog=self.changelog,
|
||||
database_path=self.database,
|
||||
backup_dir=self.backups,
|
||||
delivery_helper=self.delivery_helper,
|
||||
deliverer=uncertain,
|
||||
)
|
||||
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250])
|
||||
self.assertEqual(self.announcement_marker()["state"], "uncertain")
|
||||
|
||||
called = []
|
||||
|
||||
def delivered(*args):
|
||||
called.append(args)
|
||||
|
||||
with self.assertRaises(MODULE.AnnouncementUncertainError):
|
||||
MODULE.run_release_update(
|
||||
release_file=self.version,
|
||||
changelog=self.changelog,
|
||||
database_path=self.database,
|
||||
backup_dir=self.backups,
|
||||
delivery_helper=self.delivery_helper,
|
||||
deliverer=delivered,
|
||||
)
|
||||
self.assertFalse(called)
|
||||
|
||||
result = MODULE.run_release_update(
|
||||
release_file=self.version,
|
||||
changelog=self.changelog,
|
||||
database_path=self.database,
|
||||
backup_dir=self.backups,
|
||||
delivery_helper=self.delivery_helper,
|
||||
deliverer=delivered,
|
||||
retry_uncertain=True,
|
||||
)
|
||||
self.assertEqual(result["compensation"], "already_applied")
|
||||
self.assertEqual(len(called), 1)
|
||||
self.assertEqual([state["balance"] for state in self.account_states()], [1200, 250])
|
||||
self.assertEqual(self.announcement_marker()["state"], "delivered")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user