Files
ROMFASTSQL/proxmox/lxc171-claude-agent/discord-bridge/tests/test_commands.py
Claude Agent 5f34320e3e 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
2026-08-30 12:23:33 +00:00

348 lines
12 KiB
Python

"""Comenzile slash: /new (+ fork), /cd, /model, /status, /stop, /cleanup, /help."""
from __future__ import annotations
import pytest
import bot
try:
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, FakeInteraction,
FakeMessage, FakeUser, allowed, bridge)
__all__ = ["allowed", "bridge"]
async def slash(bridge, name, ch=None, **opts):
"""Ruleaza o comanda slash pe un canal, ca in Discord."""
ch = ch or FakeChannel()
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
# ------------------------------------------------- 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", {}) == []
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 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 slash(bridge, "help")
assert label == "cmd:help"
for c in ("/new", "/cd", "/model", "/status", "/stop", "/cleanup"):
assert c in ch.all_text
assert "!" not in ch.all_text
# ---------------------------------------------------------------------- /new
async def test_new_sterge_sesiunea(bridge, store):
ch = FakeChannel()
await prompt(bridge, "salut", ch) # creeaza sesiunea
assert store.thread(str(ch.id))["sid"] == "sid-fake-0001"
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
async def test_new_fork_pastreaza_contextul(bridge, store):
ch = FakeChannel()
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
async def test_cd_oriunde_in_workspace(bridge, store, tmp_path):
target = tmp_path / "proiect-nou"
target.mkdir()
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):
_, 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 slash(bridge, "cd", cale="")
assert "Foloseste" in ch.all_text
async def test_cd_se_aplica_procesului_urmator(bridge, tmp_path):
ch = FakeChannel()
await prompt(bridge, "salut", ch)
target = tmp_path / "alt-proiect"
target.mkdir()
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
async def test_model_implicit_e_sonnet(bridge, store):
_, 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 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 slash(bridge, "status", ch=ch2)
assert "model : sonnet" in ch2.all_text
async def test_modelul_ales_ajunge_in_subsol(bridge):
ch = FakeChannel()
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 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 slash(bridge, "model", model="")
assert "Model curent" in ch.all_text
# ------------------------------------------------------------------- /status
async def test_status_arata_campurile_cerute(bridge):
ch = FakeChannel()
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"):
assert camp in text
assert "sid-fake-0001" in text
assert "$0.0123" in text
async def test_status_arata_stderr(bridge, monkeypatch):
ch = FakeChannel()
monkeypatch.setenv("FAKE_CLAUDE_SCENARIO", "crash")
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 slash(bridge, "status", ch=ch)
assert "boom" in ch.sent[-1].content
async def test_status_pe_fir_fara_proces(bridge):
_, ch, _ = await slash(bridge, "status")
assert "proces : oprit" in ch.all_text
assert "(noua)" in ch.all_text
# --------------------------------------------------------------------- /stop
async def test_stop_opreste_procesul(bridge, store):
ch = FakeChannel()
await prompt(bridge, "salut", ch)
proc = bridge.runner.procs[str(ch.id)]
assert proc.alive is True # procesul ramane viu intre tururi
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 slash(bridge, "stop")
assert "Nu ruleaza nimic" in ch.all_text
async def test_stop_curata_inflight(bridge, store):
ch = FakeChannel()
store.set_inflight(str(ch.id), "t1", USER, "m1")
await slash(bridge, "stop", ch=ch)
assert store.is_inflight(str(ch.id)) is False
# ------------------------------------------------------------------ /cleanup
async def test_cleanup_deleaga_in_lane_c(bridge, monkeypatch):
calls = {}
class FakeCleanup:
@staticmethod
def find_orphans(state):
calls["state"] = state
return [{"pid": 1234, "cmdline": "claude -p", "age_s": 10, "rss_mb": 406.0}]
@staticmethod
def kill_orphans(orphans, dry_run=True):
calls["dry_run"] = dry_run
return [{"pid": 1234, "action": "dry-run", "detail": "s-ar trimite SIGTERM"}]
@staticmethod
def format_report(orphans, results=None):
calls["results"] = results
return f"{len(orphans)} orfani"
monkeypatch.setattr(bot, "cleanup", FakeCleanup)
_, 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 slash(bridge, "cleanup", force=True)
assert calls["dry_run"] is False and calls["results"] is not None
async def test_cleanup_care_crapa_nu_doboara_botul(bridge, monkeypatch):
class Boom:
@staticmethod
def find_orphans(state):
raise RuntimeError("proc ilizibil")
monkeypatch.setattr(bot, "cleanup", Boom)
_, 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."""
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(), []) == {}