Implementeaza planul claude-master-plan-discord-bridge-20260830 (15 taskuri, 3 lane-uri paralele) — un bot subtire discord.py peste CLI-ul `claude`, cu proces persistent per fir alimentat pe stdin cu --input-format stream-json. Nucleu: runner (proces persistent + reaper 20min + respawn --resume), stream (parser tolerant), session_store (scriere atomica, lock per fir, detectare PID reuse, recovery), limits (max 4 procese, timeout tur, rate per user, plafon cost pe zi), render (un loop de editare per canal, interval adaptiv). Adaptor: allowlist guild/canal/user fail-closed cu respingerea webhook-urilor, comenzi !new/!cd/!model/!status/!stop/!cleanup, cost si model in subsolul fiecarui raspuns. Mesajul sosit in timpul unui tur devine steering, nu tur nou. Securitate: hook PreToolUse fail-closed care cere confirmare in Discord pentru operatiuni ireversibile, wrapper `infra` cu lista explicita de hosturi. Deny rules raman strat cosmetic, nu bariera (verificat: /usr/bin/ssh trece pe langa). Ops: alerte email pe conventia repo-ului, !cleanup pentru orfani, unit systemd user cu KillMode=control-group si limite de memorie, install.sh idempotent. Verificat: 275 teste fara retea/Discord/API (10.8s), identic cu si fara discord.py instalat; e2e pe CLI real confirma steering-ul mid-tur (mesaj la 6s intr-un tool call de 25s schimba raspunsul final). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B29CApsP1JkSdjYaGaHpE7
123 lines
4.3 KiB
Python
123 lines
4.3 KiB
Python
"""T6: parser tolerant."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
import stream
|
|
|
|
|
|
def P(**kw):
|
|
return stream.StreamParser(**kw)
|
|
|
|
|
|
def test_system_init_aduce_session_id():
|
|
p = P()
|
|
(ev,) = p.feed_line(json.dumps({"type": "system", "subtype": "init",
|
|
"session_id": "s1", "model": "sonnet", "cwd": "/workspace",
|
|
"tools": ["Bash"]}))
|
|
assert isinstance(ev, stream.SystemInit)
|
|
assert ev.session_id == "s1" and ev.model == "sonnet" and ev.tools == ("Bash",)
|
|
|
|
|
|
def test_assistant_text_si_tool_use():
|
|
p = P()
|
|
evs = p.feed_line(json.dumps({"type": "assistant", "session_id": "s1", "message": {"content": [
|
|
{"type": "text", "text": "salut"},
|
|
{"type": "tool_use", "id": "tu1", "name": "Bash", "input": {"command": "ls"}}]}}))
|
|
assert [type(e) for e in evs] == [stream.AssistantText, stream.ToolUse]
|
|
assert evs[0].text == "salut"
|
|
assert evs[1].name == "Bash" and evs[1].input["command"] == "ls"
|
|
|
|
|
|
def test_tool_result_din_user():
|
|
p = P()
|
|
(ev,) = p.feed_line(json.dumps({"type": "user", "message": {"content": [
|
|
{"type": "tool_result", "tool_use_id": "tu1", "content": [{"type": "text", "text": "out"}]}]}}))
|
|
assert isinstance(ev, stream.ToolResult) and ev.text == "out" and ev.is_error is False
|
|
|
|
|
|
def test_result_aduce_cost_si_durata():
|
|
p = P()
|
|
(ev,) = p.feed_line(json.dumps({"type": "result", "subtype": "success", "is_error": False,
|
|
"total_cost_usd": 0.1547, "duration_ms": 34500,
|
|
"num_turns": 3, "result": "gata"}))
|
|
assert (ev.total_cost_usd, ev.duration_ms, ev.num_turns, ev.text) == (0.1547, 34500, 3, "gata")
|
|
assert ev.is_error is False and p.saw_result
|
|
|
|
|
|
def test_result_cu_campuri_stricate_nu_crapa():
|
|
p = P()
|
|
(ev,) = p.feed_line(json.dumps({"type": "result", "total_cost_usd": "nu-i numar",
|
|
"duration_ms": None, "num_turns": "x"}))
|
|
assert ev.total_cost_usd == 0.0 and ev.duration_ms == 0 and ev.num_turns == 0
|
|
|
|
|
|
def test_result_de_eroare_marcat():
|
|
p = P()
|
|
(ev,) = p.feed_line(json.dumps({"type": "result", "subtype": "error_during_execution"}))
|
|
assert ev.is_error is True
|
|
|
|
|
|
def test_tip_necunoscut_logat_o_singura_data():
|
|
calls = []
|
|
p = P(version_fn=lambda: (calls.append(1), "2.1.251")[1])
|
|
line = json.dumps({"type": "rate_limit_event", "detail": "x"})
|
|
assert p.feed_line(line) == []
|
|
assert p.feed_line(line) == []
|
|
assert p.feed_line(json.dumps({"type": "alt_tip_nou"})) == []
|
|
assert p.unknown_types == {"rate_limit_event", "alt_tip_nou"}
|
|
assert len(calls) == 2 # o data per tip, nu per linie
|
|
|
|
|
|
def test_linie_non_json_ignorata(caplog):
|
|
p = P()
|
|
with caplog.at_level("WARNING"):
|
|
assert p.feed_line("Error: something went wrong") == []
|
|
assert p.feed_line("[1, 2, 3]") == []
|
|
assert p.bad_lines == 2
|
|
assert any("non-JSON" in r.message or "nu e obiect" in r.message for r in caplog.records)
|
|
|
|
|
|
def test_linii_goale_ignorate():
|
|
p = P()
|
|
assert p.feed_line("") == [] and p.feed_line(" \n") == []
|
|
|
|
|
|
async def _aiter(lines):
|
|
for x in lines:
|
|
yield x
|
|
|
|
|
|
async def test_eof_inainte_de_result_da_eroare_explicita():
|
|
p = P()
|
|
got = []
|
|
with pytest.raises(stream.StreamEOFError):
|
|
async for ev in p.aiter_events(_aiter([
|
|
json.dumps({"type": "system", "subtype": "init", "session_id": "s1"}),
|
|
json.dumps({"type": "assistant", "message": {"content": [{"type": "text", "text": "hm"}]}}),
|
|
])):
|
|
got.append(ev)
|
|
assert len(got) == 2
|
|
|
|
|
|
async def test_aiter_se_opreste_la_result():
|
|
p = P()
|
|
evs = [e async for e in p.aiter_events(_aiter([
|
|
json.dumps({"type": "system", "subtype": "init", "session_id": "s1"}),
|
|
json.dumps({"type": "result", "total_cost_usd": 1.0}),
|
|
json.dumps({"type": "assistant", "message": {"content": [{"type": "text", "text": "dupa"}]}}),
|
|
]))]
|
|
assert [type(e) for e in evs] == [stream.SystemInit, stream.Result]
|
|
|
|
|
|
async def test_aiter_accepta_bytes_si_gunoi():
|
|
p = P()
|
|
evs = [e async for e in p.aiter_events(_aiter([
|
|
b"gunoi binar\n",
|
|
b'{"type":"result","total_cost_usd":0.5}\n',
|
|
]))]
|
|
assert len(evs) == 1 and evs[0].total_cost_usd == 0.5
|