feat: add IRCv3 cap negotiation, channel management, state persistence
Implement CAP LS 302 flow with configurable ircv3_caps list, replacing the minimal SASL-only registration. Parse IRCv3 message tags (@key=value) with proper value unescaping. Add channel management plugin (kick, ban, unban, topic, mode) and bot API methods. Add SQLite key-value StateStore for plugin state persistence with !state inspection command. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from derp.bot import _AMBIGUOUS, Bot
|
||||
from derp.irc import Message
|
||||
from derp.plugin import PluginRegistry, command, event
|
||||
from derp.state import StateStore
|
||||
|
||||
|
||||
class TestDecorators:
|
||||
@@ -445,11 +446,11 @@ class TestIsAdmin:
|
||||
def _msg(prefix: str) -> Message:
|
||||
"""Create a minimal Message with a given prefix."""
|
||||
return Message(raw="", prefix=prefix, nick=prefix.split("!")[0],
|
||||
command="PRIVMSG", params=["#test", "!test"])
|
||||
command="PRIVMSG", params=["#test", "!test"], tags={})
|
||||
|
||||
def test_no_prefix_not_admin(self):
|
||||
bot = self._make_bot()
|
||||
msg = Message(raw="", prefix=None, nick=None, command="PRIVMSG", params=[])
|
||||
msg = Message(raw="", prefix=None, nick=None, command="PRIVMSG", params=[], tags={})
|
||||
assert bot._is_admin(msg) is False
|
||||
|
||||
def test_oper_is_admin(self):
|
||||
@@ -476,3 +477,71 @@ class TestIsAdmin:
|
||||
bot = self._make_bot()
|
||||
msg = self._msg("nobody!~user@host")
|
||||
assert bot._is_admin(msg) is False
|
||||
|
||||
|
||||
class TestStateStore:
|
||||
"""Test the SQLite key-value state store."""
|
||||
|
||||
def test_get_missing_returns_default(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
assert store.get("plug", "key") is None
|
||||
assert store.get("plug", "key", "fallback") == "fallback"
|
||||
|
||||
def test_set_and_get(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
store.set("plug", "color", "blue")
|
||||
assert store.get("plug", "color") == "blue"
|
||||
|
||||
def test_set_overwrite(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
store.set("plug", "val", "one")
|
||||
store.set("plug", "val", "two")
|
||||
assert store.get("plug", "val") == "two"
|
||||
|
||||
def test_namespace_isolation(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
store.set("alpha", "key", "a")
|
||||
store.set("beta", "key", "b")
|
||||
assert store.get("alpha", "key") == "a"
|
||||
assert store.get("beta", "key") == "b"
|
||||
|
||||
def test_delete_existing(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
store.set("plug", "key", "val")
|
||||
assert store.delete("plug", "key") is True
|
||||
assert store.get("plug", "key") is None
|
||||
|
||||
def test_delete_missing(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
assert store.delete("plug", "ghost") is False
|
||||
|
||||
def test_keys(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
store.set("plug", "b", "2")
|
||||
store.set("plug", "a", "1")
|
||||
store.set("other", "c", "3")
|
||||
assert store.keys("plug") == ["a", "b"]
|
||||
assert store.keys("other") == ["c"]
|
||||
assert store.keys("empty") == []
|
||||
|
||||
def test_clear(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
store.set("plug", "a", "1")
|
||||
store.set("plug", "b", "2")
|
||||
store.set("other", "c", "3")
|
||||
count = store.clear("plug")
|
||||
assert count == 2
|
||||
assert store.keys("plug") == []
|
||||
assert store.keys("other") == ["c"]
|
||||
|
||||
def test_clear_empty(self, tmp_path: Path):
|
||||
store = StateStore(tmp_path / "state.db")
|
||||
assert store.clear("empty") == 0
|
||||
|
||||
def test_close_and_reopen(self, tmp_path: Path):
|
||||
db_path = tmp_path / "state.db"
|
||||
store = StateStore(db_path)
|
||||
store.set("plug", "persist", "yes")
|
||||
store.close()
|
||||
store2 = StateStore(db_path)
|
||||
assert store2.get("plug", "persist") == "yes"
|
||||
|
||||
Reference in New Issue
Block a user