"""Comenzile slash: /new (+ fork), /cd, /model, /status, /stop, /cleanup, /help.""" from __future__ import annotations import asyncio 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(), []) == {} # --------------------------------------------------------------------- /infra import infra_actions # noqa: E402 def _st(**over) -> dict: base = { "quorum": {"quorate": True, "votes": 3, "expected": 3}, "guests": {"running": 2, "total": 3}, "ha": [], "replication": {"ok": 0, "total": 0, "last": 0, "failed": []}, "ups": None, "backup": None, "ssl": None, "vm109": None, "dr_failover_active": None, "maint": 0, "jobs": [], "errors": {}, "nodes": [{"name": "pvemini", "ip": "10.0.20.201", "online": True, "cpu": 0.0, "mem": 0, "maxmem": 0, "uptime": 0, "guests": 2}], } base.update(over) return base def test_slash_args_infra(): assert bot.slash_args("infra", {"actiune": "oprire", "simulare": None}) == ["oprire"] assert bot.slash_args("infra", {"actiune": "oprire", "simulare": False}) == ["oprire", "real"] assert bot.slash_args("infra", {"actiune": "oprire", "simulare": True}) == ["oprire", "simulare"] async def test_infra_actiune_necunoscuta_fara_subprocess(bridge, monkeypatch): import subprocess monkeypatch.setattr(subprocess, "run", lambda *a, **k: pytest.fail("nu trebuia sa ruleze nimic")) _, ch, _ = await slash(bridge, "infra", actiune="nu-exista", simulare=None) assert "necunoscuta" in ch.all_text async def test_infra_fara_actiune(bridge): _, ch, _ = await slash(bridge, "infra", actiune="", simulare=None) assert "necunoscuta" in ch.all_text or "Foloseste" in ch.all_text async def test_infra_stare(bridge, monkeypatch): st = _st(ups={"status": "OL", "charge": 100.0, "runtime_s": 1200}) monkeypatch.setattr(infra_actions, "status", lambda fresh=False: st) _, ch, _ = await slash(bridge, "infra", actiune="stare", simulare=None) text = ch.all_text assert "cvorum" in text and "3/3" in text and "guest-uri" in text and "pvemini" in text assert "UPS" in text and "OL" in text async def test_infra_sarcini_fara_joburi(bridge, monkeypatch): monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st(jobs=[])) _, ch, _ = await slash(bridge, "infra", actiune="sarcini", simulare=None) assert "Niciun job activ" in ch.all_text async def test_infra_sarcini_cu_job_activ(bridge, monkeypatch): jobs = [{"id": "oprire", "host": "pvemini", "active": True, "log": "/var/log/x.log"}] monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st(jobs=jobs)) monkeypatch.setattr(infra_actions, "log_tail", lambda aid, n=60: {"file": "/x", "lines": ["linia 1"]}) _, ch, _ = await slash(bridge, "infra", actiune="sarcini", simulare=None) assert "oprire" in ch.all_text and "linia 1" in ch.all_text async def test_infra_precondition_esuata(bridge, monkeypatch): monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st()) monkeypatch.setattr(infra_actions, "check", lambda aid, st, dry: (False, "motiv de test")) apelat = [] monkeypatch.setattr(infra_actions, "run", lambda *a, **k: apelat.append(a) or {"ok": True, "code": 200}) _, ch, _ = await slash(bridge, "infra", actiune="wol", simulare=None) assert "⛔" in ch.all_text and "motiv de test" in ch.all_text assert apelat == [] async def test_infra_fara_confirmare_ruleaza_direct(bridge, monkeypatch): """`wol` nu cere confirmare: ruleaza imediat daca precondiția trece.""" monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st()) monkeypatch.setattr(infra_actions, "check", lambda aid, st, dry: (True, "")) apelat = [] def _run(aid, dry): apelat.append((aid, dry)) return {"ok": True, "code": 200, "host": "oracle-prod-admin"} monkeypatch.setattr(infra_actions, "run", _run) _, ch, _ = await slash(bridge, "infra", actiune="wol", simulare=None) assert apelat == [("wol", False)] # "wol" nu are simulare -> dry mereu False assert "pornit" in ch.all_text async def test_infra_cu_confirmare_arata_intrebarea_si_nu_ruleaza(bridge, monkeypatch): monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st()) monkeypatch.setattr(infra_actions, "check", lambda aid, st, dry: (True, "")) apelat = [] monkeypatch.setattr(infra_actions, "run", lambda *a, **k: apelat.append(a) or {"ok": True, "code": 200}) _, ch, _ = await slash(bridge, "infra", actiune="oprire", simulare=False) # "real" -> dry=False assert "confirmi" in ch.all_text.lower() assert apelat == [] # butonul nu a fost apasat, comanda nu a rulat nimic async def test_infra_actiune_de_simulare_are_dry_true(bridge, monkeypatch): vazute = [] monkeypatch.setattr(infra_actions, "status", lambda fresh=False: _st()) def _check(aid, st, dry): vazute.append(dry) return (True, "") monkeypatch.setattr(infra_actions, "check", _check) monkeypatch.setattr(infra_actions, "run", lambda *a, **k: {"ok": True, "code": 200}) await slash(bridge, "infra", actiune="oprire", simulare=None) # implicit: simulare await slash(bridge, "infra", actiune="oprire", simulare=False) # explicit: real assert vazute == [True, False] def test_infra_view_e_none_fara_discordpy(bridge): if bot.discord is None: assert bridge.infra_view("oprire", True) is None assert bridge.infra_boot_view() is None async def test_infra_boot_check_anunta_mentenanta_apoi_se_opreste(bridge, monkeypatch): ch = FakeChannel() monkeypatch.setenv("DISCORD_CHANNEL_IDS", str(ch.id)) bridge.get_channel = lambda cid: ch if str(cid) == str(ch.id) else None st = _st(maint=1) monkeypatch.setattr(infra_actions, "status", lambda fresh=False: st) monkeypatch.setattr(infra_actions, "maintenance_pending", lambda s: True) await bridge.infra_boot_check() assert "guest-uri de pornit" in ch.all_text async def test_infra_boot_check_iese_daca_nu_e_in_mentenanta(bridge, monkeypatch): st = _st(maint=0) monkeypatch.setattr(infra_actions, "status", lambda fresh=False: st) apelat = [] monkeypatch.setattr(infra_actions, "maintenance_pending", lambda s: apelat.append(s) or False) await bridge.infra_boot_check() assert apelat == [] # a iesit la prima citire (maint == 0), fara sa mai verifice async def test_infra_boot_check_nu_doboara_botul_la_eroare(bridge, monkeypatch): """`status()` care arunca nu are voie sa opreasca bucla sau botul.""" def _boom(fresh=False): raise RuntimeError("retea picata") async def _fast_sleep(_): return None monkeypatch.setattr(infra_actions, "status", _boom) monkeypatch.setattr(asyncio, "sleep", _fast_sleep) await bridge.infra_boot_check() # 20 incercari esuate, nici o exceptie in afara