153 lines
5.7 KiB
Python
153 lines
5.7 KiB
Python
|
|
"""Tests for `wave59_handlers.py` — Python parity for the Rust-only
|
|||
|
|
wol / banter / conversation / ddg_answer packs.
|
|||
|
|
|
|||
|
|
Focus is on the testable pure logic — MAC normalisation, magic-packet
|
|||
|
|
construction, trigger stripping, banter state machine. Network calls
|
|||
|
|
(DDG, LLM) are skipped here; they're integration concerns covered by
|
|||
|
|
the Rust pack tests.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import importlib
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
|
|||
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|||
|
|
|
|||
|
|
import wave59_handlers
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _rewire(isolated_state, *modules):
|
|||
|
|
importlib.reload(wave59_handlers)
|
|||
|
|
out = []
|
|||
|
|
for name in modules:
|
|||
|
|
mod = isolated_state(name, ['_PATH', '_PROFILES_DIR'])
|
|||
|
|
setattr(wave59_handlers, name, mod)
|
|||
|
|
out.append(mod)
|
|||
|
|
return out
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _capture():
|
|||
|
|
captured = []
|
|||
|
|
wave59_handlers.set_speak(captured.append)
|
|||
|
|
return captured
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── WOL ─────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_normalise_mac_accepts_common_formats():
|
|||
|
|
n = wave59_handlers._normalise_mac
|
|||
|
|
assert n("AA:BB:CC:DD:EE:FF") == "AABBCCDDEEFF"
|
|||
|
|
assert n("aa-bb-cc-dd-ee-ff") == "AABBCCDDEEFF"
|
|||
|
|
assert n("AABB.CCDD.EEFF") == "AABBCCDDEEFF"
|
|||
|
|
assert n("AABBCCDDEEFF") == "AABBCCDDEEFF"
|
|||
|
|
assert n("garbage") is None
|
|||
|
|
assert n("") is None
|
|||
|
|
assert n("AABBCCDD") is None # too short
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_build_magic_packet_shape():
|
|||
|
|
p = wave59_handlers._build_magic_packet("AABBCCDDEEFF")
|
|||
|
|
# 6 × 0xFF + 16 × MAC = 102 bytes
|
|||
|
|
assert len(p) == 102
|
|||
|
|
assert p[:6] == b"\xff" * 6
|
|||
|
|
# First repeat of MAC starts at byte 6
|
|||
|
|
assert p[6:12] == bytes.fromhex("AABBCCDDEEFF")
|
|||
|
|
# Last repeat at offset 96
|
|||
|
|
assert p[96:102] == bytes.fromhex("AABBCCDDEEFF")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_wol_speaks_help_when_mac_not_set(isolated_state, monkeypatch):
|
|||
|
|
(mem,) = _rewire(isolated_state, 'memory_store')
|
|||
|
|
monkeypatch.delenv("JARVIS_WOL_TARGET", raising=False)
|
|||
|
|
spoken = _capture()
|
|||
|
|
wave59_handlers.do_wol()
|
|||
|
|
assert "MAC" in spoken[0]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_wol_speaks_malformed_when_mac_invalid(isolated_state, monkeypatch):
|
|||
|
|
(mem,) = _rewire(isolated_state, 'memory_store')
|
|||
|
|
monkeypatch.delenv("JARVIS_WOL_TARGET", raising=False)
|
|||
|
|
mem.remember("wol_server", "totally-not-a-mac")
|
|||
|
|
spoken = _capture()
|
|||
|
|
wave59_handlers.do_wol()
|
|||
|
|
assert "подозрительно" in spoken[0]
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── banter ──────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_banter_pause_then_fire_says_quiet(isolated_state):
|
|||
|
|
importlib.reload(wave59_handlers)
|
|||
|
|
spoken = _capture()
|
|||
|
|
wave59_handlers.do_banter_pause()
|
|||
|
|
wave59_handlers.do_banter_fire()
|
|||
|
|
# Two utterances captured: "Молчу..." then a "без комментариев" reply
|
|||
|
|
assert any("Молчу" in s for s in spoken)
|
|||
|
|
assert any("без комментариев" in s.lower() for s in spoken)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_banter_resume_clears_pause(isolated_state):
|
|||
|
|
importlib.reload(wave59_handlers)
|
|||
|
|
spoken = _capture()
|
|||
|
|
wave59_handlers.do_banter_pause()
|
|||
|
|
wave59_handlers.do_banter_resume()
|
|||
|
|
wave59_handlers.do_banter_fire()
|
|||
|
|
# After resume, fire should return one of the known pool lines.
|
|||
|
|
assert any(s in wave59_handlers._BANTER_LINES for s in spoken)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── conversation ────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_conversation_repeat_with_no_history(isolated_state):
|
|||
|
|
importlib.reload(wave59_handlers)
|
|||
|
|
wave59_handlers.set_history_ref(None)
|
|||
|
|
spoken = _capture()
|
|||
|
|
wave59_handlers.do_conversation_repeat()
|
|||
|
|
assert "ещё ничего не говорил" in spoken[0]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_conversation_repeat_speaks_last_assistant(isolated_state):
|
|||
|
|
importlib.reload(wave59_handlers)
|
|||
|
|
history = [
|
|||
|
|
{"role": "system", "content": "you are jarvis"},
|
|||
|
|
{"role": "user", "content": "hi"},
|
|||
|
|
{"role": "assistant", "content": "Слушаю, сэр."},
|
|||
|
|
{"role": "user", "content": "what time"},
|
|||
|
|
{"role": "assistant", "content": "Восемь утра, сэр."},
|
|||
|
|
]
|
|||
|
|
wave59_handlers.set_history_ref(history)
|
|||
|
|
spoken = _capture()
|
|||
|
|
wave59_handlers.do_conversation_repeat()
|
|||
|
|
assert spoken[0] == "Восемь утра, сэр."
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_conversation_summary_empty_history(isolated_state):
|
|||
|
|
importlib.reload(wave59_handlers)
|
|||
|
|
wave59_handlers.set_history_ref([{"role": "system", "content": "x"}])
|
|||
|
|
spoken = _capture()
|
|||
|
|
wave59_handlers.do_conversation_summary()
|
|||
|
|
assert "пока ещё ни о чём" in spoken[0]
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── DDG trigger stripping ───────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_strip_ddg_trigger_known_prefixes():
|
|||
|
|
s = wave59_handlers._strip_ddg_trigger
|
|||
|
|
assert s("что такое квантовая запутанность") == "квантовая запутанность"
|
|||
|
|
assert s("кто такой никола тесла") == "никола тесла"
|
|||
|
|
assert s("what is the speed of light") == "the speed of light"
|
|||
|
|
assert s("tell me about jupiter") == "jupiter"
|
|||
|
|
# No trigger → phrase passes through unchanged
|
|||
|
|
assert s("просто текст без триггера") == "просто текст без триггера"
|
|||
|
|
assert s("") == ""
|
|||
|
|
|
|||
|
|
|
|||
|
|
def test_ddg_answer_with_empty_query_says_so(isolated_state):
|
|||
|
|
importlib.reload(wave59_handlers)
|
|||
|
|
spoken = _capture()
|
|||
|
|
wave59_handlers.do_ddg_answer(voice="что такое")
|
|||
|
|
assert "Что искать" in spoken[0]
|