feat: add multi-server support
Connect to multiple IRC servers concurrently from a single config file. Plugins are loaded once and shared; per-server state is isolated via separate SQLite databases and per-bot runtime state (bot._pstate). - Add build_server_configs() for [servers.*] config layout - Bot.__init__ gains name parameter, _pstate dict for plugin isolation - cli.py runs multiple bots via asyncio.gather - 9 stateful plugins migrated from module-level dicts to _ps(bot) pattern - Backward compatible: legacy [server] config works unchanged Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,8 +19,6 @@ sys.modules[_spec.name] = _mod
|
||||
_spec.loader.exec_module(_mod)
|
||||
|
||||
from plugins.remind import ( # noqa: E402
|
||||
_by_user,
|
||||
_calendar,
|
||||
_cleanup,
|
||||
_delete_saved,
|
||||
_format_duration,
|
||||
@@ -30,9 +28,9 @@ from plugins.remind import ( # noqa: E402
|
||||
_parse_date,
|
||||
_parse_duration,
|
||||
_parse_time,
|
||||
_ps,
|
||||
_remind_once,
|
||||
_remind_repeat,
|
||||
_reminders,
|
||||
_restore,
|
||||
_save,
|
||||
_schedule_at,
|
||||
@@ -74,6 +72,7 @@ class _FakeBot:
|
||||
self.replied: list[str] = []
|
||||
self.config: dict = {"bot": {"timezone": tz}}
|
||||
self.state = _FakeState()
|
||||
self._pstate: dict = {}
|
||||
|
||||
async def send(self, target: str, text: str) -> None:
|
||||
self.sent.append((target, text))
|
||||
@@ -98,15 +97,18 @@ def _pm(text: str, nick: str = "alice") -> Message:
|
||||
)
|
||||
|
||||
|
||||
def _clear() -> None:
|
||||
"""Reset global module state between tests."""
|
||||
for entry in _reminders.values():
|
||||
def _clear(bot=None) -> None:
|
||||
"""Reset per-bot plugin state between tests."""
|
||||
if bot is None:
|
||||
return
|
||||
ps = _ps(bot)
|
||||
for entry in ps["reminders"].values():
|
||||
task = entry[0]
|
||||
if task is not None and not task.done():
|
||||
task.cancel()
|
||||
_reminders.clear()
|
||||
_by_user.clear()
|
||||
_calendar.clear()
|
||||
ps["reminders"].clear()
|
||||
ps["by_user"].clear()
|
||||
ps["calendar"].clear()
|
||||
|
||||
|
||||
async def _run_cmd(bot, msg):
|
||||
@@ -120,7 +122,7 @@ async def _run_cmd_and_cleanup(bot, msg):
|
||||
"""Run cmd_remind, yield, then cancel all spawned tasks."""
|
||||
await cmd_remind(bot, msg)
|
||||
await asyncio.sleep(0)
|
||||
for entry in list(_reminders.values()):
|
||||
for entry in list(_ps(bot)["reminders"].values()):
|
||||
if entry[0] is not None and not entry[0].done():
|
||||
entry[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
@@ -229,47 +231,51 @@ class TestMakeId:
|
||||
|
||||
class TestCleanup:
|
||||
def test_removes_from_both_structures(self):
|
||||
_clear()
|
||||
_reminders["abc123"] = (None, "#ch", "alice", "label", "12:00", False)
|
||||
_by_user[("#ch", "alice")] = ["abc123"]
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
ps["reminders"]["abc123"] = (None, "#ch", "alice", "label", "12:00", False)
|
||||
ps["by_user"][("#ch", "alice")] = ["abc123"]
|
||||
|
||||
_cleanup("abc123", "#ch", "alice")
|
||||
_cleanup(bot, "abc123", "#ch", "alice")
|
||||
|
||||
assert "abc123" not in _reminders
|
||||
assert ("#ch", "alice") not in _by_user
|
||||
assert "abc123" not in ps["reminders"]
|
||||
assert ("#ch", "alice") not in ps["by_user"]
|
||||
|
||||
def test_removes_single_entry_from_multi(self):
|
||||
_clear()
|
||||
_reminders["aaa"] = (None, "#ch", "alice", "", "12:00", False)
|
||||
_reminders["bbb"] = (None, "#ch", "alice", "", "12:00", False)
|
||||
_by_user[("#ch", "alice")] = ["aaa", "bbb"]
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
ps["reminders"]["aaa"] = (None, "#ch", "alice", "", "12:00", False)
|
||||
ps["reminders"]["bbb"] = (None, "#ch", "alice", "", "12:00", False)
|
||||
ps["by_user"][("#ch", "alice")] = ["aaa", "bbb"]
|
||||
|
||||
_cleanup("aaa", "#ch", "alice")
|
||||
_cleanup(bot, "aaa", "#ch", "alice")
|
||||
|
||||
assert "aaa" not in _reminders
|
||||
assert _by_user[("#ch", "alice")] == ["bbb"]
|
||||
assert "aaa" not in ps["reminders"]
|
||||
assert ps["by_user"][("#ch", "alice")] == ["bbb"]
|
||||
|
||||
def test_missing_rid_no_error(self):
|
||||
_clear()
|
||||
_cleanup("nonexistent", "#ch", "alice")
|
||||
bot = _FakeBot()
|
||||
_cleanup(bot, "nonexistent", "#ch", "alice")
|
||||
|
||||
def test_missing_user_key_no_error(self):
|
||||
_clear()
|
||||
_reminders["abc"] = (None, "#ch", "alice", "", "12:00", False)
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
ps["reminders"]["abc"] = (None, "#ch", "alice", "", "12:00", False)
|
||||
|
||||
_cleanup("abc", "#ch", "bob") # different nick, user key absent
|
||||
_cleanup(bot, "abc", "#ch", "bob") # different nick, user key absent
|
||||
|
||||
assert "abc" not in _reminders
|
||||
assert "abc" not in ps["reminders"]
|
||||
|
||||
def test_clears_calendar_set(self):
|
||||
_clear()
|
||||
_reminders["cal01"] = (None, "#ch", "alice", "", "12:00", False)
|
||||
_by_user[("#ch", "alice")] = ["cal01"]
|
||||
_calendar.add("cal01")
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
ps["reminders"]["cal01"] = (None, "#ch", "alice", "", "12:00", False)
|
||||
ps["by_user"][("#ch", "alice")] = ["cal01"]
|
||||
ps["calendar"].add("cal01")
|
||||
|
||||
_cleanup("cal01", "#ch", "alice")
|
||||
_cleanup(bot, "cal01", "#ch", "alice")
|
||||
|
||||
assert "cal01" not in _calendar
|
||||
assert "cal01" not in ps["calendar"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -278,16 +284,16 @@ class TestCleanup:
|
||||
|
||||
class TestRemindOnce:
|
||||
def test_fires_metadata_and_label(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
|
||||
async def inner():
|
||||
rid = "once01"
|
||||
task = asyncio.create_task(
|
||||
_remind_once(bot, rid, "#ch", "alice", "check oven", 0, "12:00:00 UTC"),
|
||||
)
|
||||
_reminders[rid] = (task, "#ch", "alice", "check oven", "12:00:00 UTC", False)
|
||||
_by_user[("#ch", "alice")] = [rid]
|
||||
ps["reminders"][rid] = (task, "#ch", "alice", "check oven", "12:00:00 UTC", False)
|
||||
ps["by_user"][("#ch", "alice")] = [rid]
|
||||
await task
|
||||
|
||||
asyncio.run(inner())
|
||||
@@ -295,42 +301,42 @@ class TestRemindOnce:
|
||||
assert "alice: reminder #once01" in bot.sent[0][1]
|
||||
assert "12:00:00 UTC" in bot.sent[0][1]
|
||||
assert bot.sent[1] == ("#ch", "check oven")
|
||||
assert "once01" not in _reminders
|
||||
assert "once01" not in _ps(bot)["reminders"]
|
||||
|
||||
def test_empty_label_sends_one_line(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
|
||||
async def inner():
|
||||
rid = "once02"
|
||||
task = asyncio.create_task(
|
||||
_remind_once(bot, rid, "#ch", "bob", "", 0, "12:00:00 UTC"),
|
||||
)
|
||||
_reminders[rid] = (task, "#ch", "bob", "", "12:00:00 UTC", False)
|
||||
_by_user[("#ch", "bob")] = [rid]
|
||||
ps["reminders"][rid] = (task, "#ch", "bob", "", "12:00:00 UTC", False)
|
||||
ps["by_user"][("#ch", "bob")] = [rid]
|
||||
await task
|
||||
|
||||
asyncio.run(inner())
|
||||
assert len(bot.sent) == 1
|
||||
|
||||
def test_cancellation_cleans_up(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
|
||||
async def inner():
|
||||
rid = "once03"
|
||||
task = asyncio.create_task(
|
||||
_remind_once(bot, rid, "#ch", "alice", "text", 9999, "12:00:00 UTC"),
|
||||
)
|
||||
_reminders[rid] = (task, "#ch", "alice", "text", "12:00:00 UTC", False)
|
||||
_by_user[("#ch", "alice")] = [rid]
|
||||
ps["reminders"][rid] = (task, "#ch", "alice", "text", "12:00:00 UTC", False)
|
||||
ps["by_user"][("#ch", "alice")] = [rid]
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
|
||||
asyncio.run(inner())
|
||||
assert len(bot.sent) == 0
|
||||
assert "once03" not in _reminders
|
||||
assert "once03" not in _ps(bot)["reminders"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -339,16 +345,16 @@ class TestRemindOnce:
|
||||
|
||||
class TestRemindRepeat:
|
||||
def test_fires_at_least_once(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
|
||||
async def inner():
|
||||
rid = "rpt01"
|
||||
task = asyncio.create_task(
|
||||
_remind_repeat(bot, rid, "#ch", "alice", "hydrate", 0, "12:00:00 UTC"),
|
||||
)
|
||||
_reminders[rid] = (task, "#ch", "alice", "hydrate", "12:00:00 UTC", True)
|
||||
_by_user[("#ch", "alice")] = [rid]
|
||||
ps["reminders"][rid] = (task, "#ch", "alice", "hydrate", "12:00:00 UTC", True)
|
||||
ps["by_user"][("#ch", "alice")] = [rid]
|
||||
for _ in range(5):
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
@@ -357,26 +363,26 @@ class TestRemindRepeat:
|
||||
asyncio.run(inner())
|
||||
assert len(bot.sent) >= 2 # at least one fire (metadata + label)
|
||||
assert any("rpt01" in t for _, t in bot.sent)
|
||||
assert "rpt01" not in _reminders
|
||||
assert "rpt01" not in _ps(bot)["reminders"]
|
||||
|
||||
def test_cancellation_cleans_up(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
ps = _ps(bot)
|
||||
|
||||
async def inner():
|
||||
rid = "rpt02"
|
||||
task = asyncio.create_task(
|
||||
_remind_repeat(bot, rid, "#ch", "bob", "stretch", 9999, "12:00:00 UTC"),
|
||||
)
|
||||
_reminders[rid] = (task, "#ch", "bob", "stretch", "12:00:00 UTC", True)
|
||||
_by_user[("#ch", "bob")] = [rid]
|
||||
ps["reminders"][rid] = (task, "#ch", "bob", "stretch", "12:00:00 UTC", True)
|
||||
ps["by_user"][("#ch", "bob")] = [rid]
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
|
||||
asyncio.run(inner())
|
||||
assert len(bot.sent) == 0
|
||||
assert "rpt02" not in _reminders
|
||||
assert "rpt02" not in _ps(bot)["reminders"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -385,25 +391,21 @@ class TestRemindRepeat:
|
||||
|
||||
class TestCmdRemindUsage:
|
||||
def test_no_args_shows_usage(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_remind(bot, _msg("!remind")))
|
||||
assert "Usage:" in bot.replied[0]
|
||||
|
||||
def test_invalid_duration(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_remind(bot, _msg("!remind xyz some text")))
|
||||
assert "Invalid duration" in bot.replied[0]
|
||||
|
||||
def test_every_no_args(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_remind(bot, _msg("!remind every")))
|
||||
assert "Invalid duration" in bot.replied[0]
|
||||
|
||||
def test_every_invalid_duration(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_remind(bot, _msg("!remind every abc")))
|
||||
assert "Invalid duration" in bot.replied[0]
|
||||
@@ -415,7 +417,6 @@ class TestCmdRemindUsage:
|
||||
|
||||
class TestCmdRemindOneshot:
|
||||
def test_creates_with_duration(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -427,7 +428,6 @@ class TestCmdRemindOneshot:
|
||||
assert "#" in bot.replied[0]
|
||||
|
||||
def test_no_label(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -437,27 +437,26 @@ class TestCmdRemindOneshot:
|
||||
assert "set (5m)" in bot.replied[0]
|
||||
|
||||
def test_stores_in_tracking(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
await _run_cmd(bot, _msg("!remind 9999s task"))
|
||||
assert len(_reminders) == 1
|
||||
entry = next(iter(_reminders.values()))
|
||||
ps = _ps(bot)
|
||||
assert len(ps["reminders"]) == 1
|
||||
entry = next(iter(ps["reminders"].values()))
|
||||
assert entry[1] == "#test" # target
|
||||
assert entry[2] == "alice" # nick
|
||||
assert entry[3] == "task" # label
|
||||
assert entry[5] is False # not repeating
|
||||
assert ("#test", "alice") in _by_user
|
||||
assert ("#test", "alice") in ps["by_user"]
|
||||
# cleanup
|
||||
for e in _reminders.values():
|
||||
for e in ps["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_days_duration(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -473,7 +472,6 @@ class TestCmdRemindOneshot:
|
||||
|
||||
class TestCmdRemindRepeat:
|
||||
def test_creates_repeating(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -484,21 +482,20 @@ class TestCmdRemindRepeat:
|
||||
assert "every 1h" in bot.replied[0]
|
||||
|
||||
def test_repeating_stores_flag(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
await _run_cmd(bot, _msg("!remind every 30m stretch"))
|
||||
entry = next(iter(_reminders.values()))
|
||||
ps = _ps(bot)
|
||||
entry = next(iter(ps["reminders"].values()))
|
||||
assert entry[5] is True # repeating flag
|
||||
for e in _reminders.values():
|
||||
for e in ps["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_repeating_no_label(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -514,20 +511,18 @@ class TestCmdRemindRepeat:
|
||||
|
||||
class TestCmdRemindList:
|
||||
def test_empty_list(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_remind(bot, _msg("!remind list")))
|
||||
assert "No active reminders" in bot.replied[0]
|
||||
|
||||
def test_shows_active(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
await _run_cmd(bot, _msg("!remind 9999s task"))
|
||||
bot.replied.clear()
|
||||
await cmd_remind(bot, _msg("!remind list"))
|
||||
for e in _reminders.values():
|
||||
for e in _ps(bot)["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
@@ -536,14 +531,13 @@ class TestCmdRemindList:
|
||||
assert "#" in bot.replied[0]
|
||||
|
||||
def test_shows_repeat_tag(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
await _run_cmd(bot, _msg("!remind every 9999s task"))
|
||||
bot.replied.clear()
|
||||
await cmd_remind(bot, _msg("!remind list"))
|
||||
for e in _reminders.values():
|
||||
for e in _ps(bot)["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
@@ -561,7 +555,6 @@ class TestCmdRemindCancel:
|
||||
return reply.split("#")[1].split(" ")[0]
|
||||
|
||||
def test_cancel_valid(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -575,7 +568,6 @@ class TestCmdRemindCancel:
|
||||
assert "Cancelled" in bot.replied[0]
|
||||
|
||||
def test_cancel_with_hash_prefix(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -589,7 +581,6 @@ class TestCmdRemindCancel:
|
||||
assert "Cancelled" in bot.replied[0]
|
||||
|
||||
def test_cancel_wrong_user(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -597,7 +588,7 @@ class TestCmdRemindCancel:
|
||||
rid = self._extract_rid(bot.replied[0])
|
||||
bot.replied.clear()
|
||||
await cmd_remind(bot, _msg(f"!remind cancel {rid}", nick="eve"))
|
||||
for e in _reminders.values():
|
||||
for e in _ps(bot)["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
@@ -605,13 +596,11 @@ class TestCmdRemindCancel:
|
||||
assert "No active reminder" in bot.replied[0]
|
||||
|
||||
def test_cancel_nonexistent(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_remind(bot, _msg("!remind cancel ffffff")))
|
||||
assert "No active reminder" in bot.replied[0]
|
||||
|
||||
def test_cancel_no_id(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
asyncio.run(cmd_remind(bot, _msg("!remind cancel")))
|
||||
assert "Usage:" in bot.replied[0]
|
||||
@@ -623,28 +612,28 @@ class TestCmdRemindCancel:
|
||||
|
||||
class TestCmdRemindTarget:
|
||||
def test_channel_target(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
await _run_cmd(bot, _msg("!remind 9999s task", target="#ops"))
|
||||
entry = next(iter(_reminders.values()))
|
||||
ps = _ps(bot)
|
||||
entry = next(iter(ps["reminders"].values()))
|
||||
assert entry[1] == "#ops"
|
||||
for e in _reminders.values():
|
||||
for e in ps["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_pm_uses_nick(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
await _run_cmd(bot, _pm("!remind 9999s task"))
|
||||
entry = next(iter(_reminders.values()))
|
||||
ps = _ps(bot)
|
||||
entry = next(iter(ps["reminders"].values()))
|
||||
assert entry[1] == "alice" # nick, not "botname"
|
||||
for e in _reminders.values():
|
||||
for e in ps["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
@@ -788,7 +777,6 @@ class TestCmdRemindAt:
|
||||
return reply.split("#")[1].split(" ")[0]
|
||||
|
||||
def test_valid_future_date(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
future = (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
|
||||
@@ -801,7 +789,6 @@ class TestCmdRemindAt:
|
||||
assert "deploy release" not in bot.replied[0] # label not in confirmation
|
||||
|
||||
def test_past_date_rejected(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -811,7 +798,6 @@ class TestCmdRemindAt:
|
||||
assert "past" in bot.replied[0].lower()
|
||||
|
||||
def test_default_time_noon(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
future = (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
|
||||
@@ -822,7 +808,6 @@ class TestCmdRemindAt:
|
||||
assert "12:00" in bot.replied[0]
|
||||
|
||||
def test_with_explicit_time(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
future = (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
|
||||
@@ -833,7 +818,6 @@ class TestCmdRemindAt:
|
||||
assert "14:30" in bot.replied[0]
|
||||
|
||||
def test_stores_in_state(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
future = (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
|
||||
@@ -846,15 +830,15 @@ class TestCmdRemindAt:
|
||||
assert data["type"] == "at"
|
||||
assert data["nick"] == "alice"
|
||||
assert data["label"] == "persist me"
|
||||
assert rid in _calendar
|
||||
for e in _reminders.values():
|
||||
ps = _ps(bot)
|
||||
assert rid in ps["calendar"]
|
||||
for e in ps["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_invalid_date_format(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -864,7 +848,6 @@ class TestCmdRemindAt:
|
||||
assert "Invalid date" in bot.replied[0]
|
||||
|
||||
def test_no_args_shows_usage(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -883,7 +866,6 @@ class TestCmdRemindYearly:
|
||||
return reply.split("#")[1].split(" ")[0]
|
||||
|
||||
def test_valid_creation(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -894,7 +876,6 @@ class TestCmdRemindYearly:
|
||||
assert "yearly 06-15" in bot.replied[0]
|
||||
|
||||
def test_invalid_date(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -904,7 +885,6 @@ class TestCmdRemindYearly:
|
||||
assert "Invalid date" in bot.replied[0]
|
||||
|
||||
def test_invalid_day(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -914,7 +894,6 @@ class TestCmdRemindYearly:
|
||||
assert "Invalid date" in bot.replied[0]
|
||||
|
||||
def test_stores_in_state(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -926,15 +905,15 @@ class TestCmdRemindYearly:
|
||||
assert data["type"] == "yearly"
|
||||
assert data["month_day"] == "02-14"
|
||||
assert data["nick"] == "alice"
|
||||
assert rid in _calendar
|
||||
for e in _reminders.values():
|
||||
ps = _ps(bot)
|
||||
assert rid in ps["calendar"]
|
||||
for e in ps["reminders"].values():
|
||||
e[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_with_explicit_time(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -944,7 +923,6 @@ class TestCmdRemindYearly:
|
||||
assert "yearly 12-25" in bot.replied[0]
|
||||
|
||||
def test_no_args_shows_usage(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -954,7 +932,6 @@ class TestCmdRemindYearly:
|
||||
assert "Usage:" in bot.replied[0]
|
||||
|
||||
def test_leap_day_allowed(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -997,7 +974,6 @@ class TestCalendarPersistence:
|
||||
assert bot.state.get("remind", "abc123") is None
|
||||
|
||||
def test_cancel_deletes_from_state(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
future = (datetime.now(timezone.utc) + timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
|
||||
@@ -1013,7 +989,6 @@ class TestCalendarPersistence:
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_at_fire_deletes_from_state(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
|
||||
async def inner():
|
||||
@@ -1026,12 +1001,13 @@ class TestCalendarPersistence:
|
||||
"created": "12:00:00 UTC",
|
||||
}
|
||||
_save(bot, rid, data)
|
||||
_calendar.add(rid)
|
||||
ps = _ps(bot)
|
||||
ps["calendar"].add(rid)
|
||||
task = asyncio.create_task(
|
||||
_schedule_at(bot, rid, "#ch", "alice", "fire now", fire_dt, "12:00:00 UTC"),
|
||||
)
|
||||
_reminders[rid] = (task, "#ch", "alice", "fire now", "12:00:00 UTC", False)
|
||||
_by_user[("#ch", "alice")] = [rid]
|
||||
ps["reminders"][rid] = (task, "#ch", "alice", "fire now", "12:00:00 UTC", False)
|
||||
ps["by_user"][("#ch", "alice")] = [rid]
|
||||
await task
|
||||
assert bot.state.get("remind", rid) is None
|
||||
|
||||
@@ -1044,7 +1020,6 @@ class TestCalendarPersistence:
|
||||
|
||||
class TestRestore:
|
||||
def test_restores_at_from_state(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
fire_dt = datetime.now(timezone.utc) + timedelta(hours=1)
|
||||
data = {
|
||||
@@ -1057,9 +1032,10 @@ class TestRestore:
|
||||
|
||||
async def inner():
|
||||
_restore(bot)
|
||||
assert "rest01" in _reminders
|
||||
assert "rest01" in _calendar
|
||||
entry = _reminders["rest01"]
|
||||
ps = _ps(bot)
|
||||
assert "rest01" in ps["reminders"]
|
||||
assert "rest01" in ps["calendar"]
|
||||
entry = ps["reminders"]["rest01"]
|
||||
assert not entry[0].done()
|
||||
entry[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
@@ -1067,7 +1043,6 @@ class TestRestore:
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_restores_yearly_from_state(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
fire_dt = datetime.now(timezone.utc) + timedelta(days=180)
|
||||
data = {
|
||||
@@ -1080,9 +1055,10 @@ class TestRestore:
|
||||
|
||||
async def inner():
|
||||
_restore(bot)
|
||||
assert "rest02" in _reminders
|
||||
assert "rest02" in _calendar
|
||||
entry = _reminders["rest02"]
|
||||
ps = _ps(bot)
|
||||
assert "rest02" in ps["reminders"]
|
||||
assert "rest02" in ps["calendar"]
|
||||
entry = ps["reminders"]["rest02"]
|
||||
assert not entry[0].done()
|
||||
entry[0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
@@ -1090,7 +1066,6 @@ class TestRestore:
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_skips_active_rids(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
fire_dt = datetime.now(timezone.utc) + timedelta(hours=1)
|
||||
data = {
|
||||
@@ -1103,18 +1078,18 @@ class TestRestore:
|
||||
|
||||
async def inner():
|
||||
# Pre-populate with an active task
|
||||
ps = _ps(bot)
|
||||
dummy = asyncio.create_task(asyncio.sleep(9999))
|
||||
_reminders["skip01"] = (dummy, "#ch", "alice", "active", "12:00:00 UTC", False)
|
||||
ps["reminders"]["skip01"] = (dummy, "#ch", "alice", "active", "12:00:00 UTC", False)
|
||||
_restore(bot)
|
||||
# Should still be the dummy task, not replaced
|
||||
assert _reminders["skip01"][0] is dummy
|
||||
assert ps["reminders"]["skip01"][0] is dummy
|
||||
dummy.cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_past_at_cleaned_up(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
past_dt = datetime.now(timezone.utc) - timedelta(hours=1)
|
||||
data = {
|
||||
@@ -1128,13 +1103,12 @@ class TestRestore:
|
||||
async def inner():
|
||||
_restore(bot)
|
||||
# Past at-reminder should be deleted from state, not scheduled
|
||||
assert "past01" not in _reminders
|
||||
assert "past01" not in _ps(bot)["reminders"]
|
||||
assert bot.state.get("remind", "past01") is None
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
def test_past_yearly_recalculated(self):
|
||||
_clear()
|
||||
bot = _FakeBot()
|
||||
past_dt = datetime.now(timezone.utc) - timedelta(days=30)
|
||||
data = {
|
||||
@@ -1147,13 +1121,14 @@ class TestRestore:
|
||||
|
||||
async def inner():
|
||||
_restore(bot)
|
||||
assert "yearly01" in _reminders
|
||||
ps = _ps(bot)
|
||||
assert "yearly01" in ps["reminders"]
|
||||
# fire_iso should have been updated to a future date
|
||||
raw = bot.state.get("remind", "yearly01")
|
||||
updated = json.loads(raw)
|
||||
new_fire = datetime.fromisoformat(updated["fire_iso"])
|
||||
assert new_fire > datetime.now(timezone.utc)
|
||||
_reminders["yearly01"][0].cancel()
|
||||
ps["reminders"]["yearly01"][0].cancel()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
asyncio.run(inner())
|
||||
|
||||
Reference in New Issue
Block a user