Un al doilea mesaj trimis cât Claude încă lucra aștepta până se termina turul 1 — corecția „stai, nu în master" ajungea după ce greșeala era gata. Verificat în producție înainte de commit: mesajul 2 stătea 25s blocat în lock, apoi pornea ca tur separat. Acum canalele de chat pot ține un proces `claude` viu per canal, cu stdin deschis, și al doilea mesaj intră în ACELAȘI tur. - `src/claude_runner.py` — ClaudeProcess (steering, respawn cu --resume, drenare stderr, respawn la comutarea OpenRouter) + RunnerRegistry (max_live, reaper pe inactivitate, stop_all la shutdown) - `src/stream_json.py` — parser stream-json partajat cu `_run_claude`; pur, nu aruncă niciodată pe is_error (PlanningSession retrimite pe error_max_turns și depinde de asta) - `src/sentinels.py` — un singur loc pentru __AUDIO__/__STEERED__, în loc de 4 verificări copiate; repară și bug-ul preexistent prin care WhatsApp posta literal `__AUDIO__:/cale` - dispecer în `send_message`: lock.acquire(blocking=False) — eșecul de a lua lock-ul ESTE „rulează un tur", ceea ce elimină flagul inflight din decizie și cursa TOCTOU odată cu el - `/stop` oprește turul, nu sesiunea — active.json rămâne valid - rate limit prin proces persistent vine ca result.is_error, nu ca exit code; convertit înapoi în același RuntimeError, altfel fallback-ul local nu s-ar mai declanșa niciodată, în tăcere Steering-ul nu face niciodată cross-adapter (un mesaj text nu intră într-un tur voice: împart același channel_id). Mesajele steered dintr-un tur care pică sunt re-livrate, nu pierdute. Testat live cu CLI-ul real: corecție la secunda 10 dintr-un tur de 24s, un singur result, num_turns=2. Notă: mesajele steered sunt împachetate în [EXTERNAL CONTENT], deci o corecție formulată ca override agresiv poate fi refuzată ca prompt injection — pentru oprire folosește /stop. Suită: 1199 passed, 12 failed (toate pre-existente pe HEAD curat). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SiJGsZVSEGjRHZEJiXaxCC
59 lines
1.9 KiB
Python
59 lines
1.9 KiB
Python
"""Tests for /stop — Etapa 0 of the steering plan (one-shot architecture).
|
|
|
|
`stop_turn(channel_id)` kills the in-flight Claude CLI subprocess registered
|
|
for a channel, without touching sessions/active.json (the session survives,
|
|
only the turn dies).
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.claude_session import _live_procs, stop_turn
|
|
from src.router import route_message
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear_live_procs():
|
|
"""Fresh registry per test — fake processes must not leak between tests."""
|
|
_live_procs.clear()
|
|
yield
|
|
_live_procs.clear()
|
|
|
|
|
|
class TestStopTurn:
|
|
def test_idle_channel_returns_false(self):
|
|
assert stop_turn("ch-1") is False
|
|
|
|
def test_terminates_registered_process_and_cleans_up_registry(self):
|
|
fake_proc = MagicMock()
|
|
fake_proc.poll.return_value = None # still running
|
|
_live_procs["ch-1"] = fake_proc
|
|
|
|
assert stop_turn("ch-1") is True
|
|
fake_proc.terminate.assert_called_once()
|
|
fake_proc.wait.assert_called_once()
|
|
assert "ch-1" not in _live_procs
|
|
|
|
def test_already_exited_process_returns_false(self):
|
|
fake_proc = MagicMock()
|
|
fake_proc.poll.return_value = 0 # exited before /stop ran
|
|
_live_procs["ch-1"] = fake_proc
|
|
assert stop_turn("ch-1") is False
|
|
|
|
|
|
class TestStopCommand:
|
|
def test_stop_nothing_running(self):
|
|
response, is_cmd = route_message("ch-1", "user-1", "/stop")
|
|
assert response == "Nu rulează nimic pe canalul ăsta."
|
|
assert is_cmd is True
|
|
|
|
def test_stop_kills_in_flight_turn(self):
|
|
fake_proc = MagicMock()
|
|
fake_proc.poll.return_value = None
|
|
_live_procs["ch-1"] = fake_proc
|
|
response, is_cmd = route_message("ch-1", "user-1", "/stop")
|
|
assert response == "⏹ Oprit."
|
|
assert is_cmd is True
|
|
fake_proc.terminate.assert_called_once()
|