feat(steering): mesaje mid-tur + /stop pe turul în zbor
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
This commit is contained in:
@@ -87,6 +87,54 @@ class TestStatus:
|
||||
assert "WA Bridge: ONLINE" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# eco status — steering line (T4/A3)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSteeringStatusLine:
|
||||
def _mock_service(self, active="inactive"):
|
||||
return {"ActiveState": active, "MainPID": "0", "ActiveEnterTimestamp": ""}
|
||||
|
||||
def test_steering_off_no_procs(self, iso, capsys):
|
||||
"""pgrep's own no-match case: exit code 1, stdout '0' — a
|
||||
legitimate zero, not a counting failure."""
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.get.return_value = False
|
||||
mock_pgrep = MagicMock(returncode=1, stdout="0\n")
|
||||
with patch("cli._get_service_status", return_value=self._mock_service()), \
|
||||
patch("src.config.Config", return_value=mock_cfg), \
|
||||
patch("subprocess.run", return_value=mock_pgrep):
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "steering: off · 0 procese vii" in out
|
||||
|
||||
def test_steering_counts_live_procs(self, iso, capsys):
|
||||
"""A real match: pgrep -fc finds 2 live steering processes."""
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.get.return_value = True
|
||||
mock_pgrep = MagicMock(returncode=0, stdout="2\n")
|
||||
with patch("cli._get_service_status", return_value=self._mock_service()), \
|
||||
patch("src.config.Config", return_value=mock_cfg), \
|
||||
patch("subprocess.run", return_value=mock_pgrep):
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "steering: on · 2 procese vii" in out
|
||||
|
||||
def test_survives_pgrep_failure(self, iso, capsys):
|
||||
"""D1/D3 guard: a failed process-table scan must not break the
|
||||
rest of `eco status`, and must not silently read as a wrong zero."""
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.get.return_value = False
|
||||
with patch("cli._get_service_status", return_value=self._mock_service()), \
|
||||
patch("src.config.Config", return_value=mock_cfg), \
|
||||
patch("subprocess.run", side_effect=OSError("pgrep not found")):
|
||||
cli.cmd_status(_args())
|
||||
out = capsys.readouterr().out
|
||||
assert "steering: off · necunoscut procese vii" in out
|
||||
assert "Sessions:" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cmd_doctor
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -194,6 +242,22 @@ class TestDoctor:
|
||||
out, code = self._run_doctor(iso, capsys, setup_full=True)
|
||||
assert "Claude CLI functional" in out
|
||||
|
||||
def test_steering_off_by_default(self, iso, capsys):
|
||||
iso["config_file"].write_text('{"bot":{}}')
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.get.return_value = False
|
||||
with patch("src.config.Config", return_value=mock_cfg):
|
||||
out, code = self._run_doctor(iso, capsys, setup_full=True)
|
||||
assert "[PASS] Steering (optional, currently off)" in out
|
||||
|
||||
def test_steering_checks_stream_json_when_enabled(self, iso, capsys):
|
||||
iso["config_file"].write_text('{"bot":{}}')
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.get.return_value = True
|
||||
with patch("src.config.Config", return_value=mock_cfg):
|
||||
out, code = self._run_doctor(iso, capsys, setup_full=True)
|
||||
assert "Claude CLI supports --input-format stream-json" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cmd_restart
|
||||
|
||||
Reference in New Issue
Block a user