"""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(), []) == {}