feat(discord-bridge): comenzi slash in loc de prefixul !
Comenzile devin application commands inregistrate pe guild (sync instantaneu, spre deosebire de cel global care dureaza ~1h): /new [fork], /cd <cale>, /model <sonnet|opus> cu Choice, /status, /stop, /cleanup [force], /help. - allowlist-ul se aplica identic la interactiuni (check_ids comun, ca sa nu existe a doua implementare care diverge); refuz efemer, fara executie - fiecare comanda face defer() inainte de lucru — altfel Discord marcheaza interactiunea esuata dupa 3s desi comanda a rulat - sync tolerant: la esec (lipsa scope applications.commands) botul porneste normal si logheaza linkul de reinvitare necesar - mesajele obisnuite raman neschimbate, inclusiv steering-ul mid-tur - linkul de invitatie primeste scope=bot%20applications.commands; referintele la ! din ops/ si documentatie trecute pe / Verificat in productie: 7 comenzi inregistrate pe guild, citite inapoi din API. Suita: 296 passed cu discord.py, 293 passed + 3 skipped fara. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
This commit is contained in:
@@ -12,10 +12,10 @@ import bot
|
||||
|
||||
try: # rulat ca pachet `tests` sau ca module libere
|
||||
from tests.test_bot import (CHANNEL, GUILD, USER, FakeChannel, FakeGuild,
|
||||
FakeMessage, FakeUser, allowed, bridge)
|
||||
FakeInteraction, FakeMessage, FakeUser, allowed, bridge)
|
||||
except ImportError: # pragma: no cover
|
||||
from test_bot import (CHANNEL, GUILD, USER, FakeChannel, FakeGuild,
|
||||
FakeMessage, FakeUser, allowed, bridge)
|
||||
FakeInteraction, FakeMessage, FakeUser, allowed, bridge)
|
||||
|
||||
__all__ = ["allowed", "bridge"] # fixturi reexportate
|
||||
|
||||
@@ -133,3 +133,66 @@ async def test_webhookul_nu_declanseaza_comenzi(bridge):
|
||||
assert await bridge.handle_message(msg) == "rejected"
|
||||
assert ch.sent == []
|
||||
assert bridge.runner.procs == {}
|
||||
|
||||
|
||||
# ------------------------------------------- acelasi filtru la comenzile slash
|
||||
def iok(interaction) -> bool:
|
||||
return bot.check_interaction(interaction, self_id="999").ok
|
||||
|
||||
|
||||
def iwhy(interaction) -> str:
|
||||
return bot.check_interaction(interaction, self_id="999").reason
|
||||
|
||||
|
||||
def test_interactiune_permisa(allowed):
|
||||
assert iok(FakeInteraction())
|
||||
|
||||
|
||||
def test_interactiune_din_fir_permis(allowed):
|
||||
thread = FakeChannel(cid="99999", parent_id=CHANNEL)
|
||||
assert iok(FakeInteraction(channel=thread))
|
||||
|
||||
|
||||
def test_interactiune_din_guild_strain_refuzata(allowed):
|
||||
assert "guild" in iwhy(FakeInteraction(guild_id="777"))
|
||||
|
||||
|
||||
def test_interactiune_in_mesaj_privat_refuzata(allowed):
|
||||
assert "guild" in iwhy(FakeInteraction(guild_id=None))
|
||||
|
||||
|
||||
def test_interactiune_din_canal_strain_refuzata(allowed):
|
||||
assert "canal" in iwhy(FakeInteraction(channel=FakeChannel("777")))
|
||||
|
||||
|
||||
def test_interactiune_de_la_utilizator_strain_refuzata(allowed):
|
||||
assert "utilizator" in iwhy(FakeInteraction(user=FakeUser("777")))
|
||||
|
||||
|
||||
def test_interactiune_de_la_bot_refuzata(allowed):
|
||||
assert iwhy(FakeInteraction(user=FakeUser("40", is_bot=True))) == "bot"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lipsa", ["DISCORD_GUILD_IDS", "DISCORD_CHANNEL_IDS", "DISCORD_USER_IDS"])
|
||||
def test_interactiunea_e_fail_closed(allowed, monkeypatch, lipsa):
|
||||
monkeypatch.setenv(lipsa, "")
|
||||
assert "fail-closed" in iwhy(FakeInteraction())
|
||||
|
||||
|
||||
async def test_comanda_slash_neautorizata_nu_executa_nimic(bridge, caplog):
|
||||
"""Refuz efemer, fara defer, fara efect: `/cleanup force:True` de la un strain."""
|
||||
ch = FakeChannel()
|
||||
it = FakeInteraction(channel=ch, user=FakeUser("777"))
|
||||
with caplog.at_level("WARNING"):
|
||||
assert await bridge.handle_slash(it, "cleanup", force=True) == "rejected"
|
||||
assert ch.sent == [] # nimic vizibil in canal
|
||||
assert it.response.deferred == 0 # nici macar nu amanam
|
||||
assert "Nu esti autorizat" in it.ephemeral
|
||||
assert bridge.rejected and "utilizator" in bridge.rejected[-1]
|
||||
assert "interactiune respinsa" in caplog.text
|
||||
|
||||
|
||||
async def test_comanda_slash_din_alt_canal_refuzata(bridge):
|
||||
it = FakeInteraction(channel=FakeChannel("777"))
|
||||
assert await bridge.handle_slash(it, "status") == "rejected"
|
||||
assert "Nu esti autorizat" in it.ephemeral
|
||||
|
||||
@@ -81,6 +81,51 @@ class FakeMessage:
|
||||
self.reactions.append(emoji)
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
"""`interaction.response`: se poate folosi o singura data, ca la Discord."""
|
||||
|
||||
def __init__(self):
|
||||
self.deferred = 0
|
||||
self.messages: list[FakeSent] = []
|
||||
|
||||
async def defer(self, **kw):
|
||||
self.deferred += 1
|
||||
|
||||
async def send_message(self, content=None, **kw):
|
||||
msg = FakeSent(None, content, **kw)
|
||||
self.messages.append(msg)
|
||||
return msg
|
||||
|
||||
|
||||
class FakeFollowup:
|
||||
def __init__(self, channel):
|
||||
self.channel = channel
|
||||
|
||||
async def send(self, content=None, **kw):
|
||||
return await self.channel.send(content, **kw)
|
||||
|
||||
|
||||
class FakeInteraction:
|
||||
"""Minimul din `discord.Interaction` pe care il atinge puntea."""
|
||||
|
||||
def __init__(self, *, channel=None, user=None, guild_id=GUILD,
|
||||
channel_id=None, iid="i1"):
|
||||
self.channel = channel if channel is not None else FakeChannel()
|
||||
self.channel_id = channel_id if channel_id is not None else self.channel.id
|
||||
self.guild_id = guild_id
|
||||
self.user = user if user is not None else FakeUser()
|
||||
self.id = iid
|
||||
self.response = FakeResponse()
|
||||
self.followup = FakeFollowup(self.channel)
|
||||
|
||||
@property
|
||||
def ephemeral(self) -> str:
|
||||
"""Textul raspunsurilor efemere (refuzuri)."""
|
||||
return "\n".join(
|
||||
(m.content or "") for m in self.response.messages if m.kwargs.get("ephemeral")
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ fixturi
|
||||
@pytest.fixture
|
||||
def allowed(monkeypatch):
|
||||
@@ -225,6 +270,19 @@ async def test_raspuns_lung_devine_atasament(bridge, store, monkeypatch):
|
||||
assert any(m.kwargs.get("file") is not None for m in msg.channel.sent)
|
||||
|
||||
|
||||
# ------------------------------------------------- prefixul vechi nu mai executa
|
||||
async def test_prefixul_vechi_da_doar_un_indiciu(bridge):
|
||||
ch = FakeChannel()
|
||||
assert await bridge.handle_message(FakeMessage("!status", channel=ch)) == "legacy-prefix"
|
||||
assert "/status" in ch.all_text
|
||||
assert bridge.runner.procs == {} # niciun tur pornit, niciun cost
|
||||
|
||||
|
||||
async def test_text_care_incepe_cu_exclamare_dar_nu_e_comanda_merge_la_claude(bridge):
|
||||
ch = FakeChannel()
|
||||
assert await bridge.handle_message(FakeMessage("!!! ce faci?", channel=ch)) == "ok"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ aprobari
|
||||
def test_decizia_cere_allowlist(bridge, allowed):
|
||||
assert "allowlist" in bridge.decide("777", "req-1", "allow")
|
||||
@@ -251,7 +309,7 @@ async def test_botul_porneste_si_fara_modulele_lui_b_si_c(bridge, monkeypatch):
|
||||
monkeypatch.setattr(bot, "cleanup", None)
|
||||
assert bridge.wire_approvals() is False
|
||||
ch = FakeChannel()
|
||||
await bridge.handle_message(FakeMessage("!cleanup", channel=ch))
|
||||
await bridge.handle_slash(FakeInteraction(channel=ch), "cleanup")
|
||||
assert "nu e disponibil" in ch.all_text
|
||||
assert bridge.approval_view("req-1") is None
|
||||
assert await bridge.handle_message(FakeMessage("salut", channel=ch)) == "ok"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Comenzile puntii: !new (+ --fork), !cd, !model, !status, !stop, !cleanup, !help."""
|
||||
"""Comenzile slash: /new (+ fork), /cd, /model, /status, /stop, /cleanup, /help."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,48 +7,77 @@ import pytest
|
||||
import bot
|
||||
|
||||
try:
|
||||
from tests.test_bot import (CHANNEL, USER, FakeChannel, FakeMessage, allowed, bridge)
|
||||
from tests.test_bot import (CHANNEL, USER, FakeChannel, FakeInteraction,
|
||||
FakeMessage, FakeUser, allowed, bridge)
|
||||
except ImportError: # pragma: no cover
|
||||
from test_bot import (CHANNEL, USER, FakeChannel, FakeMessage, allowed, bridge)
|
||||
from test_bot import (CHANNEL, USER, FakeChannel, FakeInteraction,
|
||||
FakeMessage, FakeUser, allowed, bridge)
|
||||
|
||||
__all__ = ["allowed", "bridge"]
|
||||
|
||||
|
||||
async def run(bridge, text, ch=None):
|
||||
async def slash(bridge, name, ch=None, **opts):
|
||||
"""Ruleaza o comanda slash pe un canal, ca in Discord."""
|
||||
ch = ch or FakeChannel()
|
||||
msg = FakeMessage(text, channel=ch)
|
||||
label = await bridge.handle_message(msg)
|
||||
it = FakeInteraction(channel=ch)
|
||||
label = await bridge.handle_slash(it, name, **opts)
|
||||
return label, ch, it
|
||||
|
||||
|
||||
async def prompt(bridge, text, ch=None):
|
||||
"""Un mesaj obisnuit: pleaca la Claude ca prompt."""
|
||||
ch = ch or FakeChannel()
|
||||
label = await bridge.handle_message(FakeMessage(text, channel=ch))
|
||||
return label, ch
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- parser
|
||||
def test_parse_command():
|
||||
cmd = bot.parse_command("!cd /workspace/romfastsql")
|
||||
assert cmd.name == "cd" and cmd.rest == "/workspace/romfastsql"
|
||||
assert bot.parse_command("!MODEL opus").name == "model"
|
||||
assert bot.parse_command("salut") is None
|
||||
assert bot.parse_command("!") is None
|
||||
# ------------------------------------------------- optiuni slash -> argumente
|
||||
def test_slash_args():
|
||||
assert bot.slash_args("new", {"fork": True}) == ["--fork"]
|
||||
assert bot.slash_args("new", {"fork": False}) == []
|
||||
assert bot.slash_args("cleanup", {"force": True}) == ["--force"]
|
||||
assert bot.slash_args("cd", {"cale": " /workspace/romfastsql "}) == ["/workspace/romfastsql"]
|
||||
assert bot.slash_args("model", {"model": "opus"}) == ["opus"]
|
||||
assert bot.slash_args("status", {}) == []
|
||||
|
||||
|
||||
async def test_comanda_necunoscuta(bridge):
|
||||
label, ch = await run(bridge, "!inexistenta")
|
||||
def test_prefixul_vechi_nu_mai_e_parsat():
|
||||
"""`parse_command` a disparut: `!` nu mai executa nimic."""
|
||||
assert not hasattr(bot, "parse_command")
|
||||
assert bot.legacy_prefix_hint("!cd /x").startswith("Comenzile sunt acum slash")
|
||||
assert bot.legacy_prefix_hint("salut") is None
|
||||
assert bot.legacy_prefix_hint("!") is None
|
||||
assert bot.legacy_prefix_hint("!ceva-inexistent") is None
|
||||
|
||||
|
||||
async def test_comanda_necunoscuta_e_refuzata_efemer(bridge):
|
||||
label, ch, it = await slash(bridge, "inexistenta")
|
||||
assert label == "unknown-command"
|
||||
assert "nu exista" in ch.all_text
|
||||
assert ch.sent == []
|
||||
assert "nu exista" in it.ephemeral
|
||||
|
||||
|
||||
async def test_orice_comanda_confirma_in_3_secunde(bridge):
|
||||
"""Fara `defer()`, Discord marcheaza interactiunea ca esuata."""
|
||||
for name in bot.COMMANDS:
|
||||
_, _, it = await slash(bridge, name, cale="/tmp", model="sonnet")
|
||||
assert it.response.deferred == 1, name
|
||||
|
||||
|
||||
async def test_help(bridge):
|
||||
label, ch = await run(bridge, "!help")
|
||||
label, ch, _ = await slash(bridge, "help")
|
||||
assert label == "cmd:help"
|
||||
for c in ("!new", "!cd", "!model", "!status", "!stop", "!cleanup"):
|
||||
for c in ("/new", "/cd", "/model", "/status", "/stop", "/cleanup"):
|
||||
assert c in ch.all_text
|
||||
assert "!" not in ch.all_text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------- !new
|
||||
# ---------------------------------------------------------------------- /new
|
||||
async def test_new_sterge_sesiunea(bridge, store):
|
||||
ch = FakeChannel()
|
||||
await run(bridge, "salut", ch) # creeaza sesiunea
|
||||
await prompt(bridge, "salut", ch) # creeaza sesiunea
|
||||
assert store.thread(str(ch.id))["sid"] == "sid-fake-0001"
|
||||
label, _ = await run(bridge, "!new", ch)
|
||||
label, _, _ = await slash(bridge, "new", ch=ch, fork=False)
|
||||
assert label == "cmd:new"
|
||||
assert store.thread(str(ch.id))["sid"] is None
|
||||
assert bridge.runner.procs[str(ch.id)].alive is False
|
||||
@@ -56,86 +85,87 @@ async def test_new_sterge_sesiunea(bridge, store):
|
||||
|
||||
async def test_new_fork_pastreaza_contextul(bridge, store):
|
||||
ch = FakeChannel()
|
||||
await run(bridge, "salut", ch)
|
||||
await run(bridge, "!new --fork", ch)
|
||||
await prompt(bridge, "salut", ch)
|
||||
await slash(bridge, "new", ch=ch, fork=True)
|
||||
assert store.thread(str(ch.id))["sid"] == "sid-fake-0001"
|
||||
assert "fork" in ch.all_text
|
||||
assert bridge.runner.procs[str(ch.id)].sid == "sid-fake-0001"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------- !cd
|
||||
# ----------------------------------------------------------------------- /cd
|
||||
async def test_cd_oriunde_in_workspace(bridge, store, tmp_path):
|
||||
target = tmp_path / "proiect-nou"
|
||||
target.mkdir()
|
||||
label, ch = await run(bridge, f"!cd {target}")
|
||||
label, ch, _ = await slash(bridge, "cd", cale=str(target))
|
||||
assert label == "cmd:cd"
|
||||
assert store.thread(str(ch.id))["cwd"] == str(target)
|
||||
assert str(target) in ch.all_text
|
||||
|
||||
|
||||
async def test_cd_refuza_ce_nu_e_director(bridge, store):
|
||||
label, ch = await run(bridge, "!cd /nu/exista/nicaieri")
|
||||
_, ch, _ = await slash(bridge, "cd", cale="/nu/exista/nicaieri")
|
||||
assert "nu e un director" in ch.all_text
|
||||
assert store.thread(str(ch.id))["cwd"] != "/nu/exista/nicaieri"
|
||||
|
||||
|
||||
async def test_cd_fara_argument(bridge):
|
||||
_, ch = await run(bridge, "!cd")
|
||||
_, ch, _ = await slash(bridge, "cd", cale="")
|
||||
assert "Foloseste" in ch.all_text
|
||||
|
||||
|
||||
async def test_cd_se_aplica_procesului_urmator(bridge, tmp_path):
|
||||
ch = FakeChannel()
|
||||
await run(bridge, "salut", ch)
|
||||
await prompt(bridge, "salut", ch)
|
||||
target = tmp_path / "alt-proiect"
|
||||
target.mkdir()
|
||||
await run(bridge, f"!cd {target}", ch)
|
||||
await slash(bridge, "cd", ch=ch, cale=str(target))
|
||||
assert bridge.runner.procs[str(ch.id)].cwd == str(target)
|
||||
assert bridge.runner.procs[str(ch.id)].alive is False # se reia cu --resume
|
||||
|
||||
|
||||
# -------------------------------------------------------------------- !model
|
||||
# -------------------------------------------------------------------- /model
|
||||
async def test_model_implicit_e_sonnet(bridge, store):
|
||||
_, ch = await run(bridge, "salut")
|
||||
_, ch = await prompt(bridge, "salut")
|
||||
assert store.thread(str(ch.id))["model"] == "sonnet"
|
||||
assert "sonnet" in ch.all_text
|
||||
|
||||
|
||||
async def test_model_opus_per_fir(bridge, store):
|
||||
ch = FakeChannel()
|
||||
label, _ = await run(bridge, "!model opus", ch)
|
||||
label, _, _ = await slash(bridge, "model", ch=ch, model="opus")
|
||||
assert label == "cmd:model"
|
||||
assert store.thread(str(ch.id))["model"] == "opus"
|
||||
# firul vecin (alt thread, acelasi canal-parinte) ramane pe sonnet
|
||||
ch2 = FakeChannel("201", parent_id=CHANNEL)
|
||||
await run(bridge, "!status", ch2)
|
||||
await slash(bridge, "status", ch=ch2)
|
||||
assert "model : sonnet" in ch2.all_text
|
||||
|
||||
|
||||
async def test_modelul_ales_ajunge_in_subsol(bridge):
|
||||
ch = FakeChannel()
|
||||
await run(bridge, "!model opus", ch)
|
||||
await run(bridge, "salut", ch)
|
||||
await slash(bridge, "model", ch=ch, model="opus")
|
||||
await prompt(bridge, "salut", ch)
|
||||
assert "opus" in ch.sent[-1].content
|
||||
|
||||
|
||||
async def test_model_necunoscut_refuzat(bridge, store):
|
||||
"""Discord filtreaza prin Choice, dar centura ramane si in cod."""
|
||||
ch = FakeChannel()
|
||||
await run(bridge, "!model gpt", ch)
|
||||
await slash(bridge, "model", ch=ch, model="gpt")
|
||||
assert "necunoscut" in ch.all_text
|
||||
assert store.thread(str(ch.id))["model"] == "sonnet"
|
||||
|
||||
|
||||
async def test_model_fara_argument_arata_curentul(bridge):
|
||||
_, ch = await run(bridge, "!model")
|
||||
_, ch, _ = await slash(bridge, "model", model="")
|
||||
assert "Model curent" in ch.all_text
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- !status
|
||||
# ------------------------------------------------------------------- /status
|
||||
async def test_status_arata_campurile_cerute(bridge):
|
||||
ch = FakeChannel()
|
||||
await run(bridge, "salut", ch)
|
||||
_, _ = await run(bridge, "!status", ch)
|
||||
await prompt(bridge, "salut", ch)
|
||||
await slash(bridge, "status", ch=ch)
|
||||
text = ch.sent[-1].content
|
||||
for camp in ("fir", "sesiune", "director", "model", "proces", "tur in zbor",
|
||||
"in coada", "cost fir", "cost azi"):
|
||||
@@ -147,43 +177,43 @@ async def test_status_arata_campurile_cerute(bridge):
|
||||
async def test_status_arata_stderr(bridge, monkeypatch):
|
||||
ch = FakeChannel()
|
||||
monkeypatch.setenv("FAKE_CLAUDE_SCENARIO", "crash")
|
||||
await run(bridge, "salut", ch) # esueaza si lasa stderr in buffer
|
||||
await prompt(bridge, "salut", ch) # esueaza si lasa stderr in buffer
|
||||
proc = bridge.runner.procs[str(ch.id)]
|
||||
proc.stderr_buf.append("fake_claude: boom")
|
||||
_, _ = await run(bridge, "!status", ch)
|
||||
await slash(bridge, "status", ch=ch)
|
||||
assert "boom" in ch.sent[-1].content
|
||||
|
||||
|
||||
async def test_status_pe_fir_fara_proces(bridge):
|
||||
_, ch = await run(bridge, "!status")
|
||||
_, ch, _ = await slash(bridge, "status")
|
||||
assert "proces : oprit" in ch.all_text
|
||||
assert "(noua)" in ch.all_text
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- !stop
|
||||
# --------------------------------------------------------------------- /stop
|
||||
async def test_stop_opreste_procesul(bridge, store):
|
||||
ch = FakeChannel()
|
||||
await run(bridge, "salut", ch)
|
||||
await prompt(bridge, "salut", ch)
|
||||
proc = bridge.runner.procs[str(ch.id)]
|
||||
assert proc.alive is True # procesul ramane viu intre tururi
|
||||
await run(bridge, "!stop", ch)
|
||||
await slash(bridge, "stop", ch=ch)
|
||||
assert "oprit" in ch.all_text
|
||||
assert proc.alive is False
|
||||
|
||||
|
||||
async def test_stop_fara_proces(bridge):
|
||||
_, ch = await run(bridge, "!stop")
|
||||
_, ch, _ = await slash(bridge, "stop")
|
||||
assert "Nu ruleaza nimic" in ch.all_text
|
||||
|
||||
|
||||
async def test_stop_curata_inflight(bridge, store, monkeypatch):
|
||||
async def test_stop_curata_inflight(bridge, store):
|
||||
ch = FakeChannel()
|
||||
store.set_inflight(str(ch.id), "t1", USER, "m1")
|
||||
await run(bridge, "!stop", ch)
|
||||
await slash(bridge, "stop", ch=ch)
|
||||
assert store.is_inflight(str(ch.id)) is False
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ !cleanup
|
||||
# ------------------------------------------------------------------ /cleanup
|
||||
async def test_cleanup_deleaga_in_lane_c(bridge, monkeypatch):
|
||||
calls = {}
|
||||
|
||||
@@ -204,12 +234,12 @@ async def test_cleanup_deleaga_in_lane_c(bridge, monkeypatch):
|
||||
return f"{len(orphans)} orfani"
|
||||
|
||||
monkeypatch.setattr(bot, "cleanup", FakeCleanup)
|
||||
_, ch = await run(bridge, "!cleanup")
|
||||
_, ch, _ = await slash(bridge, "cleanup", force=False)
|
||||
assert "1 orfani" in ch.all_text
|
||||
assert calls["dry_run"] is True and calls["results"] is None
|
||||
assert "threads" in calls["state"]
|
||||
|
||||
_, ch2 = await run(bridge, "!cleanup --force")
|
||||
_, ch2, _ = await slash(bridge, "cleanup", force=True)
|
||||
assert calls["dry_run"] is False and calls["results"] is not None
|
||||
|
||||
|
||||
@@ -220,12 +250,98 @@ async def test_cleanup_care_crapa_nu_doboara_botul(bridge, monkeypatch):
|
||||
raise RuntimeError("proc ilizibil")
|
||||
|
||||
monkeypatch.setattr(bot, "cleanup", Boom)
|
||||
_, ch = await run(bridge, "!cleanup")
|
||||
_, ch, _ = await slash(bridge, "cleanup")
|
||||
assert "a esuat" in ch.all_text
|
||||
|
||||
|
||||
async def test_cleanup_real_ruleaza_in_dry_run(bridge):
|
||||
"""Modulul real al lui Lane C: nu omoara nimic implicit."""
|
||||
cleanup = pytest.importorskip("cleanup")
|
||||
_, ch = await run(bridge, "!cleanup")
|
||||
pytest.importorskip("cleanup")
|
||||
_, ch, _ = await slash(bridge, "cleanup")
|
||||
assert ch.all_text.strip() != ""
|
||||
|
||||
|
||||
# ------------------------------------------------ declararea comenzilor slash
|
||||
def test_toate_comenzile_au_handler(bridge):
|
||||
for name in bot.COMMANDS:
|
||||
assert callable(getattr(bridge, f"cmd_{name}", None)), name
|
||||
|
||||
|
||||
def test_arborele_declara_exact_comenzile_puntii(bridge):
|
||||
"""Cu discord.py instalat: semnaturile chiar ajung in arbore."""
|
||||
pytest.importorskip("discord")
|
||||
import commands_slash
|
||||
|
||||
tree = commands_slash.build_tree(_FakeClient(), bridge)
|
||||
cmds = {c.name: c for c in tree.get_commands()}
|
||||
assert set(cmds) == set(bot.COMMANDS)
|
||||
assert {p.name for p in cmds["new"].parameters} == {"fork"}
|
||||
assert {p.name for p in cmds["cd"].parameters} == {"cale"}
|
||||
assert {p.name for p in cmds["cleanup"].parameters} == {"force"}
|
||||
assert cmds["cd"].parameters[0].required is True
|
||||
assert cmds["new"].parameters[0].required is False
|
||||
choices = [c.value for c in cmds["model"].parameters[0].choices]
|
||||
assert choices == ["sonnet", "opus"]
|
||||
|
||||
|
||||
class _FakeState:
|
||||
_command_tree = None
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
"""`CommandTree` cere doar http + _connection; nu se deschide nicio conexiune."""
|
||||
|
||||
application_id = 42
|
||||
http = None
|
||||
|
||||
def __init__(self):
|
||||
self._connection = _FakeState()
|
||||
|
||||
|
||||
class _FakeTree:
|
||||
"""Arbore fals pentru testarea sync-ului, fara retea."""
|
||||
|
||||
def __init__(self, client=None, fail=None):
|
||||
self.client = client or _FakeClient()
|
||||
self.fail = fail
|
||||
self.synced: list[int] = []
|
||||
self.copied: list[int] = []
|
||||
|
||||
def copy_global_to(self, guild):
|
||||
self.copied.append(guild.id)
|
||||
|
||||
async def sync(self, guild=None):
|
||||
if self.fail is not None:
|
||||
raise self.fail
|
||||
self.synced.append(guild.id)
|
||||
return ["/new", "/cd"]
|
||||
|
||||
|
||||
# ------------------------------------------------- inregistrarea in Discord
|
||||
async def test_sync_se_face_pe_guild(bridge):
|
||||
pytest.importorskip("discord")
|
||||
import commands_slash
|
||||
|
||||
tree = _FakeTree()
|
||||
out = await commands_slash.sync_guilds(tree, ["100", "101"])
|
||||
assert tree.synced == [100, 101] and tree.copied == [100, 101]
|
||||
assert out == {"100": "ok (2 comenzi)", "101": "ok (2 comenzi)"}
|
||||
|
||||
|
||||
async def test_sync_esuat_nu_doboara_botul_si_spune_linkul(bridge, caplog):
|
||||
pytest.importorskip("discord")
|
||||
import commands_slash
|
||||
|
||||
tree = _FakeTree(fail=RuntimeError("403 Forbidden (Missing Access)"))
|
||||
with caplog.at_level("ERROR"):
|
||||
out = await commands_slash.sync_guilds(tree, ["100"])
|
||||
assert out["100"].startswith("esuat")
|
||||
assert "applications.commands" in caplog.text
|
||||
assert "client_id=42" in caplog.text
|
||||
|
||||
|
||||
async def test_sync_fara_arbore_sau_fara_guilduri_nu_arunca():
|
||||
import commands_slash
|
||||
|
||||
assert await commands_slash.sync_guilds(None, ["100"]) == {}
|
||||
assert await commands_slash.sync_guilds(_FakeTree(), []) == {}
|
||||
|
||||
Reference in New Issue
Block a user