Bolt Console: desktop app with chat list / new chat, dictation, file tools

Add a chat list and server-minted new chats on top of the desk API's
GET/POST /desk/chats:

- server_client.list_chats() / create_chat(), same shape and error
  handling as list_outbox_files().
- Switching is config.set_session(id): a plain reassignment of the
  SESSION_ID every call reads, persisted to session.json. The window
  refuses to switch mid-turn rather than locking.
- gui/chat_list_window.py: a Chats dialog (Ctrl+K) with a QListWidget,
  populated on open, with Refresh and New Chat. Network calls run as
  QRunnables on the global QThreadPool and come back through Signals,
  never from a slot. The header New chat (Ctrl+N) uses the same path.
- Switching clears the local transcript; there is no endpoint for a
  past chat's messages. Bolt's memory is per user, so it carries over.
- Terminal: /chats, /chat <n|id>, and /new (server-minted id, local
  fallback).

This commit also carries the uncommitted work since 54030a7 that turned
this Bolt-Pet fork into Bolt Console: the pet, wake word, TTS and screen
features removed; a Claude Code-style terminal client; the PySide6
window with tool cards, approvals and diffs; mic dictation through the
server's /desk/stt relay; filectl/JSON repairs; and detection of
invented tool results.

Build output (build/, dist/, Release/, *.spec) is now ignored.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 19:50:10 -06:00
co-authored by Claude Opus 5.5
parent 54030a7b31
commit 3977804ae3
144 changed files with 5477 additions and 14736 deletions
-218
View File
@@ -1,218 +0,0 @@
"""Barge-in detection, driven by a fake mic stream and a fake wake model
(no audio hardware, no ONNX runtime)."""
import sys
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet.audio.barge_in import BargeInDetector, WakeWordBargeIn, make_detector
class FakeStream:
"""Yields frames of a given amplitude, mimicking sounddevice's
(data, overflowed) 2-D int16 return shape."""
def __init__(self, amplitudes):
self._amplitudes = list(amplitudes)
def read(self, frames):
amplitude = self._amplitudes.pop(0) if self._amplitudes else 0
data = np.full((frames, 1), amplitude, dtype=np.int16)
return data, False
def test_silence_never_interrupts():
detector = BargeInDetector(FakeStream([0] * 20), threshold=1000, required_frames=3)
assert not any(detector.check() for _ in range(20))
def test_sustained_speech_interrupts_after_the_required_frames():
detector = BargeInDetector(FakeStream([2000] * 5), threshold=1000, required_frames=3)
assert detector.check() is False
assert detector.check() is False
assert detector.check() is True
def test_a_single_thump_does_not_interrupt():
# loud, quiet, loud, quiet ... never three in a row
detector = BargeInDetector(FakeStream([2000, 0, 2000, 0, 2000, 0]), threshold=1000, required_frames=3)
assert not any(detector.check() for _ in range(6))
def test_counter_resets_after_a_quiet_frame():
detector = BargeInDetector(FakeStream([2000, 2000, 0, 2000, 2000, 2000]), threshold=1000, required_frames=3)
results = [detector.check() for _ in range(6)]
assert results == [False, False, False, False, False, True]
def test_reset_clears_progress():
detector = BargeInDetector(FakeStream([2000] * 6), threshold=1000, required_frames=3)
detector.check()
detector.check()
detector.reset()
assert detector.check() is False
assert detector.loud_frames == 1
def test_a_mic_error_mid_playback_is_not_fatal():
class BrokenStream:
def read(self, frames):
raise OSError("device disappeared")
detector = BargeInDetector(BrokenStream(), threshold=1000, required_frames=1)
assert detector.check() is False
# ── wake-word mode ───────────────────────────────────────────────────────────
class FakeModel:
"""Scores frames from a canned list, mimicking openWakeWord's
{class_name: score} return. Records reset() calls."""
def __init__(self, scores):
self._scores = list(scores)
self.resets = 0
def predict(self, frame):
score = self._scores.pop(0) if self._scores else 0.0
return {"thunderbolt": score}
def reset(self):
self.resets += 1
def _wake_detector(scores, threshold=0.5, amplitudes=None):
stream = FakeStream(amplitudes if amplitudes is not None else [500] * len(scores))
return WakeWordBargeIn(stream, model=FakeModel(scores), threshold=threshold), stream
def test_loud_noise_alone_does_not_interrupt_in_wake_mode():
"""The whole point of wake mode: a slammed door is deafening and scores
nothing, so the pet keeps talking."""
detector, _ = _wake_detector([0.01] * 6, amplitudes=[30000] * 6)
assert not any(detector.check() for _ in range(6))
def test_the_wake_word_interrupts():
detector, _ = _wake_detector([0.1, 0.2, 0.9])
assert [detector.check() for _ in range(3)] == [False, False, True]
def test_a_single_frame_is_enough_when_it_clears_the_threshold():
detector, _ = _wake_detector([0.55])
assert detector.check() is True
def test_scores_just_under_the_threshold_do_not_fire():
detector, _ = _wake_detector([0.49, 0.499], threshold=0.5)
assert not any(detector.check() for _ in range(2))
def test_detecting_resets_the_model_so_the_tail_is_not_reused():
model = FakeModel([0.9])
detector = WakeWordBargeIn(FakeStream([500]), model=model, threshold=0.5)
assert detector.check() is True
assert model.resets == 1
def test_reset_clears_the_models_audio_window_not_just_predictions():
"""The regression that made the pet interrupt itself a word into every
reply: openwakeword's reset() clears only the prediction buffer, so the
"thunderbolt" that started the turn was still in the preprocessor's
rolling window when playback began, and the first frame fed to the model
re-fired on it."""
class FakePreprocessor:
def __init__(self):
self.raw_data_buffer = [1, 2, 3]
self.feature_buffer = np.ones((120, 96))
self.melspectrogram_buffer = np.zeros((76, 32))
self.accumulated_samples = 4096
def _get_embeddings(self, audio):
return np.zeros((120, 96))
model = FakeModel([0.9])
model.preprocessor = FakePreprocessor()
WakeWordBargeIn(FakeStream([500]), model=model, threshold=0.5).reset()
assert model.preprocessor.raw_data_buffer == []
assert model.preprocessor.accumulated_samples == 0
assert not model.preprocessor.feature_buffer.any() # blank, not the old audio
assert model.preprocessor.melspectrogram_buffer.all() # restored to ones
def test_the_lazy_wrapper_exposes_its_preprocessor():
"""The pet holds _default_model — a lazy *wrapper* around openwakeword's
Model. If the wrapper stops proxying .preprocessor, hard_reset() finds
nothing to clear and silently degrades to the shallow reset that leaves
the last detection in the audio window. That failure is invisible: no
exception, no log, the pet just interrupts itself again."""
from bolt_pet.audio.wake_word import _OpenWakeWordModel
wrapper = _OpenWakeWordModel()
assert hasattr(wrapper, "preprocessor")
assert wrapper.preprocessor is None # not loaded yet: a no-op, not a load
class FakeInner:
preprocessor = object()
def reset(self):
pass
wrapper._model = FakeInner()
assert wrapper.preprocessor is FakeInner.preprocessor
def test_a_model_without_a_preprocessor_still_resets():
"""Fakes in tests, and any future openwakeword whose internals moved."""
model = FakeModel([0.0])
WakeWordBargeIn(FakeStream([500]), model=model, threshold=0.5).reset()
assert model.resets == 1
def test_a_callable_threshold_is_read_every_frame():
"""The tray tuner's slider has to apply mid-playback, not just mid-idle."""
threshold = {"value": 0.9}
detector = WakeWordBargeIn(
FakeStream([500] * 2), model=FakeModel([0.6, 0.6]),
threshold=lambda: threshold["value"],
)
assert detector.check() is False
threshold["value"] = 0.5
assert detector.check() is True
def test_a_model_that_blows_up_mid_playback_is_not_fatal():
class BrokenModel:
def predict(self, frame):
raise RuntimeError("onnx session died")
def reset(self):
raise RuntimeError("still dead")
detector = WakeWordBargeIn(FakeStream([500]), model=BrokenModel(), threshold=0.5)
assert detector.check() is False
detector.reset() # must not raise either
def test_a_mic_error_is_not_fatal_in_wake_mode():
class BrokenStream:
def read(self, frames):
raise OSError("device disappeared")
detector = WakeWordBargeIn(BrokenStream(), model=FakeModel([0.9]), threshold=0.5)
assert detector.check() is False
def test_make_detector_picks_the_mode():
stream = FakeStream([0])
assert isinstance(make_detector(stream, mode="wake"), WakeWordBargeIn)
assert isinstance(make_detector(stream, mode="energy"), BargeInDetector)
# A typo in .env shouldn't stop the pet from starting.
assert isinstance(make_detector(stream, mode="waek"), BargeInDetector)
+209
View File
@@ -0,0 +1,209 @@
"""The Chats dialog, the header New chat, /chats in the terminal, and the
row helpers. No network: list/create are injected fakes, and they still run
on the real QThreadPool, so the off-UI-thread hand-off is exercised."""
import io
import os
import sys
import threading
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import pytest
from PySide6.QtCore import Qt
from bolt_console import chats, config, console, server_client
from bolt_console.tools import Tools
from bolt_console.ui import Terminal
CHATS = [
{"session_id": "chat-new1", "label": "Fix the build",
"last_message": "try again [Bolt Console: a text terminal on Windows 10; working directory C:/x. To act",
"last_active": "2026-09-25T23:46:31+00:00"},
{"session_id": "pet-console-test", "label": "console DESKTOP",
"last_message": "what is in this folder", "last_active": "2026-09-25T21:26:20+00:00"},
{"session_id": "android-SM-S928U", "label": "android SM S928U",
"last_message": "wifi networks", "last_active": "2026-09-20T22:33:34+00:00"},
]
@pytest.fixture(autouse=True)
def _session(monkeypatch, tmp_path):
monkeypatch.setattr(config, "SESSION_ID", "pet-console-test")
monkeypatch.setattr(config, "STATE_FILE", tmp_path / "session.json")
monkeypatch.setattr(config, "SERVER_URL", "http://127.0.0.1:9")
monkeypatch.setattr(config, "API_KEY", "k")
monkeypatch.delenv("DESK_SESSION_ID", raising=False)
class Calls:
"""Fake list/create that record which thread they ran on."""
def __init__(self, chat_list=CHATS, new_id="chat-fresh", fail=None):
self.chat_list, self.new_id, self.fail, self.threads = chat_list, new_id, fail, []
def list(self):
self.threads.append(threading.current_thread())
if self.fail:
raise server_client.ServerError(self.fail)
return self.chat_list
def create(self):
self.threads.append(threading.current_thread())
if self.fail:
raise server_client.ServerError(self.fail)
return self.new_id
# ── helpers ─────────────────────────────────────────────────────────────────
def test_preview_drops_client_context_notes_even_when_cut_off():
assert chats.clean_preview(CHATS[0]["last_message"]) == "try again"
assert chats.clean_preview("hi [on screen right now: VS Code] there") == "hi there"
assert chats.clean_preview("x" * 200, 20).endswith("…")
def test_relative_time():
now = datetime(2026, 9, 26, 0, 0, tzinfo=timezone.utc)
def ago(**delta):
return (now - timedelta(**delta)).isoformat()
assert chats.relative_time(ago(seconds=10), now) == "just now"
assert chats.relative_time(ago(minutes=12), now) == "12 min ago"
assert chats.relative_time(ago(hours=3), now) == "3 h ago"
assert chats.relative_time(ago(days=1, hours=2), now) == "yesterday"
assert chats.relative_time(ago(days=4), now) == "4 days ago"
assert chats.relative_time("garbage", now) == "garbage"
# ── terminal ────────────────────────────────────────────────────────────────
def terminal_session(calls):
out = io.StringIO()
ui = Terminal(out=out, color=False, input_fn=lambda _p: "", animate=False)
session = console.Session(ui, tools=Tools(ui), list_chats=calls.list,
create_chat=calls.create, list_files=lambda: [])
return session, out
def test_chats_command_lists_and_marks_the_current_one():
session, out = terminal_session(Calls())
session.handle("/chats")
text = out.getvalue()
assert "Fix the build" in text and "try again" in text and "Bolt Console:" not in text
assert "* 2 console DESKTOP" in text # the current chat is starred
def test_chat_command_switches_by_number_and_remembers_it():
session, _ = terminal_session(Calls())
session.handle("/chats")
session.handle("/chat 3")
assert config.SESSION_ID == "android-SM-S928U"
assert config._remembered_session() == "android-SM-S928U"
def test_new_uses_the_server_minted_id_and_falls_back_locally():
session, _ = terminal_session(Calls(new_id="chat-abc123"))
session.handle("/new")
assert config.SESSION_ID == "chat-abc123"
session, out = terminal_session(Calls(fail="HTTP 401"))
session.handle("/new")
assert config.SESSION_ID.startswith("pet-console-") and "made locally" in out.getvalue()
# ── window ──────────────────────────────────────────────────────────────────
def pump(app, predicate, timeout=5.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
app.processEvents()
if predicate():
return True
time.sleep(0.01)
return False
@pytest.fixture
def window():
from bolt_console.gui.window import MainWindow, make_app
app = make_app(["test"])
calls = Calls()
win = MainWindow(list_chats=calls.list, create_chat=calls.create)
win.calls, win.app = calls, app
yield win
win.close()
def test_dialog_lists_chats_off_the_ui_thread(window):
window.open_chats()
dialog = window.chat_dialog
assert pump(window.app, lambda: dialog.list.count() == 3)
assert window.calls.threads and all(t is not threading.main_thread() for t in window.calls.threads)
assert dialog.list.currentItem().data(Qt.UserRole) == "pet-console-test" # current one selected
dialog.close()
def test_picking_a_chat_switches_session_and_clears_the_transcript(window):
from bolt_console.gui.widgets import UserMessage
window._add(UserMessage("old conversation"))
window.open_chats()
dialog = window.chat_dialog
assert pump(window.app, lambda: dialog.list.count() == 3)
dialog.list.setCurrentRow(0)
dialog.open_button.click()
window.app.processEvents()
assert config.SESSION_ID == "chat-new1"
assert not window.empty.isHidden()
assert "Fix the build" in window.hint.text()
def test_new_chat_in_the_dialog_switches_to_the_minted_id(window):
window.open_chats()
dialog = window.chat_dialog
assert pump(window.app, lambda: dialog.list.count() == 3)
dialog.new_button.click()
assert pump(window.app, lambda: config.SESSION_ID == "chat-fresh")
assert not dialog.isVisible()
def test_header_new_chat_mints_off_thread_and_clears(window):
from bolt_console.gui.widgets import UserMessage
window._add(UserMessage("old"))
window.start_new_chat()
assert pump(window.app, lambda: config.SESSION_ID == "chat-fresh")
assert window.calls.threads[0] is not threading.main_thread()
assert not window.empty.isHidden() and window.new_chat.isEnabled()
def test_header_new_chat_failure_keeps_the_current_chat(window):
window.calls.fail = "HTTP 401 unauthorized"
window.start_new_chat()
assert pump(window.app, lambda: "Couldn't start a new chat" in window.hint.text())
assert config.SESSION_ID == "pet-console-test"
assert window.new_chat.isEnabled()
def test_dialog_shows_a_load_failure(window):
window.calls.fail = "HTTP 401"
window.open_chats()
dialog = window.chat_dialog
assert pump(window.app, lambda: "Couldn't load chats" in dialog.status.text())
assert dialog.refresh_button.isEnabled()
dialog.close()
def test_switching_is_refused_while_bolt_is_working(window):
window._set_busy(True)
window.open_chats()
assert "Wait for Bolt" in window.hint.text()
window.switch_chat("chat-other", "Other")
assert config.SESSION_ID == "pet-console-test"
window._set_busy(False)
-62
View File
@@ -1,62 +0,0 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import config
def test_set_env_value_uncomments_existing_line(tmp_path, monkeypatch):
(tmp_path / ".env").write_text("#MIC_DEVICE=\nOTHER=1\n", encoding="utf-8")
monkeypatch.setattr(config, "HERE", tmp_path)
config.set_env_value("MIC_DEVICE", "USB Mic")
lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
assert lines == ["MIC_DEVICE=USB Mic", "OTHER=1"]
assert config.os.environ["MIC_DEVICE"] == "USB Mic"
def test_set_env_value_overwrites_existing_uncommented_line(tmp_path, monkeypatch):
(tmp_path / ".env").write_text("MIC_DEVICE=Old Mic\n", encoding="utf-8")
monkeypatch.setattr(config, "HERE", tmp_path)
config.set_env_value("MIC_DEVICE", "New Mic")
lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
assert lines == ["MIC_DEVICE=New Mic"]
def test_set_env_value_appends_when_key_is_missing(tmp_path, monkeypatch):
(tmp_path / ".env").write_text("OTHER=1\n", encoding="utf-8")
monkeypatch.setattr(config, "HERE", tmp_path)
config.set_env_value("MIC_DEVICE", "New Mic")
lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
assert lines == ["OTHER=1", "MIC_DEVICE=New Mic"]
def test_set_env_value_creates_file_if_missing(tmp_path, monkeypatch):
monkeypatch.setattr(config, "HERE", tmp_path)
config.set_env_value("MIC_DEVICE", "New Mic")
assert (tmp_path / ".env").read_text(encoding="utf-8") == "MIC_DEVICE=New Mic\n"
def test_mic_device_parses_a_numeric_index_as_int():
# sounddevice only takes an int index as-is; a string (even "1") goes
# through substring matching against every device name instead, which is
# not what a saved index means — see the tray's mic picker.
assert config._mic_device("1") == 1
assert isinstance(config._mic_device("1"), int)
def test_mic_device_keeps_a_name_as_a_string():
assert config._mic_device("USB Mic") == "USB Mic"
def test_mic_device_blank_is_none():
assert config._mic_device("") is None
assert config._mic_device(" ") is None
+293
View File
@@ -0,0 +1,293 @@
"""Whole turns over real HTTP: the real server_client, the real Tools and the
real Terminal (writing to a buffer), against a threaded server on a loopback
port that speaks the desk protocol. Only the keyboard is faked."""
import io
import json
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_console import config, console
from bolt_console.console import Session
from bolt_console.tools import Tools
from bolt_console.ui import Terminal
class FakeDesk:
"""Scripted responses, real HTTP. Set `.script` per test."""
def __init__(self):
self.script = {}
self.requests = []
self._server = ThreadingHTTPServer(("127.0.0.1", 0), self._handler())
threading.Thread(target=self._server.serve_forever, daemon=True).start()
@property
def url(self) -> str:
host, port = self._server.server_address[:2]
return f"http://{host}:{port}"
def stop(self) -> None:
self._server.shutdown()
self._server.server_close()
def _handler(self):
desk = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_args):
pass
def do_GET(self):
path = self.path.split("?")[0]
desk.requests.append(("GET", path))
response = desk.script.get(path, {"files": []})
if isinstance(response, bytes):
self.send_response(200)
self.send_header("Content-Length", str(len(response)))
self.end_headers()
self.wfile.write(response)
else:
self._json(response)
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
body = json.loads(self.rfile.read(length) or b"{}")
path = self.path.split("?")[0]
desk.requests.append(("POST", path, body))
response = desk.script.get(path)
if callable(response):
response = response(body)
if path.endswith("converse_stream"):
if response is None:
self.send_response(404)
self.end_headers()
return
self.send_response(200)
self.send_header("Content-Type", "application/x-ndjson")
self.end_headers()
for event in response:
self.wfile.write((json.dumps(event) + "\n").encode())
self.wfile.flush()
else:
self._json(response if response is not None else {"type": "reply", "text": "ok"})
def _json(self, payload):
data = json.dumps(payload).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
return Handler
@pytest.fixture
def desk(monkeypatch, tmp_path):
server = FakeDesk()
monkeypatch.setattr(config, "SERVER_URL", server.url)
monkeypatch.setattr(config, "API_KEY", "k")
monkeypatch.setattr(config, "SESSION_ID", "pet-console-test")
monkeypatch.setattr(config, "DELIVERED_FILES_DIR", tmp_path / "delivered")
monkeypatch.chdir(tmp_path)
yield server
server.stop()
def make_session(answers=(), mode="ask"):
answers = list(answers)
out = io.StringIO()
ui = Terminal(out=out, color=False, input_fn=lambda _p: answers.pop(0), animate=False)
return Session(ui, tools=Tools(ui, mode=mode)), out
def converse_bodies(desk):
return [r[2] for r in desk.requests if r[0] == "POST" and "converse" in r[1]]
def test_a_plain_reply_is_printed_once(desk):
desk.script["/desk/converse"] = {"type": "reply", "text": "Hi there."}
session, out = make_session()
session.handle("hello")
assert out.getvalue().count("Hi there.") == 1
def test_streamed_sentences_print_as_one_block_and_are_not_repeated(desk):
desk.script["/desk/converse_stream"] = [
{"type": "say", "text": "First."},
{"type": "say", "text": "Second."},
{"type": "reply", "text": "First. Second.", "already_spoken": True},
]
session, out = make_session()
session.handle("hello")
text = out.getvalue()
assert text.count("First.") == 1 and text.count("Second.") == 1
assert "* First.\n Second." in text
def test_the_message_carries_the_working_directory(desk, tmp_path):
session, _ = make_session()
session.handle("what's here?")
body = converse_bodies(desk)[-1]
assert body["session_id"] == "pet-console-test"
assert body["text"].startswith("what's here?")
assert str(tmp_path) in body["text"]
def test_an_approved_edit_lands_on_disk_and_the_result_goes_back_up(desk, tmp_path):
target = tmp_path / "app.py"
target.write_text("DEBUG = True\n", encoding="utf-8")
edit = "filectl " + json.dumps(
{"op": "edit", "path": str(target), "old": "DEBUG = True", "new": "DEBUG = False"})
desk.script["/desk/converse"] = {"type": "command", "command": edit, "token": "t1"}
desk.script["/desk/tool_result"] = {"type": "reply", "text": "Turned debug off."}
session, out = make_session(answers=["y"])
session.handle("turn debug off in app.py")
assert target.read_text(encoding="utf-8") == "DEBUG = False\n"
posted = [r[2] for r in desk.requests if r[1] == "/desk/tool_result"]
assert posted[0]["token"] == "t1" and "edited" in posted[0]["output"]
text = out.getvalue()
assert "Edit(" in text and "- DEBUG = True" in text and "+ DEBUG = False" in text
assert "Turned debug off." in text
def test_a_declined_shell_command_reports_back_and_the_turn_still_ends(desk, tmp_path):
desk.script["/desk/converse"] = {"type": "command", "command": "echo hi > made.txt", "token": "t"}
desk.script["/desk/tool_result"] = {"type": "reply", "text": "Okay, I won't."}
session, out = make_session(answers=["n"])
session.handle("make a file")
assert not (tmp_path / "made.txt").exists()
posted = [r[2] for r in desk.requests if r[1] == "/desk/tool_result"]
assert "declined" in posted[0]["output"]
assert "Okay, I won't." in out.getvalue()
def test_delivered_files_are_saved_after_the_turn(desk, tmp_path):
desk.script["/desk/files"] = {"files": [{"id": "f1", "name": "report.txt", "size": 5}]}
desk.script["/desk/files/f1"] = b"hello"
session, out = make_session()
session.handle("send me the report")
assert (tmp_path / "delivered" / "report.txt").read_bytes() == b"hello"
assert "Received report.txt" in out.getvalue()
def test_server_errors_are_shown_not_raised(desk):
desk.script["/desk/converse"] = {"type": "error", "error": "unauthorized"}
session, out = make_session()
session.handle("hi")
assert "Error: unauthorized" in out.getvalue()
def test_slash_commands(desk, tmp_path):
(tmp_path / "sub").mkdir()
session, out = make_session()
assert session.handle("/cd sub") is True
assert Path.cwd() == tmp_path / "sub"
session.handle("/auto")
assert session.tools.mode == "auto"
session.handle("/ask")
assert session.tools.mode == "ask"
assert session.handle("/exit") is False
assert converse_bodies(desk) == [] # none of that went to Bolt
def test_loop_exits_cleanly_on_eof(desk):
out = io.StringIO()
def no_more_input(_prompt):
raise EOFError
ui = Terminal(out=out, color=False, input_fn=no_more_input, animate=False)
assert Session(ui).loop() == 0
assert "Bolt Console" in out.getvalue()
def test_backslash_continues_a_message_onto_the_next_line(desk):
lines = ["first line \\", "second line"]
ui = Terminal(out=io.StringIO(), color=False, input_fn=lambda _p: lines.pop(0), animate=False)
assert ui.read_message() == "first line \nsecond line"
def test_annotate_names_the_directory_and_says_there_is_no_pet():
note = console.annotate("hi", "/work", "Linux 6.1")
assert note.startswith("hi\n\n")
assert "/work" in note and "Linux 6.1" in note and "no pet body" in note
def test_a_reply_that_invents_command_results_is_flagged_and_corrected(desk):
"""Seen live: 'wrote 1200 chars to …' three turns running, no command sent,
no file on disk."""
desk.script["/desk/converse_stream"] = [
{"type": "say", "text": "Let me check that."},
{"type": "say", "text": "→ Command result: [filectl] wrote 1200 chars to C:/x/main.py"},
{"type": "reply", "text": "", "already_spoken": True},
]
session, out = make_session()
session.handle("write it")
assert console.FABRICATION_WARNING in out.getvalue()
desk.script["/desk/converse_stream"] = [{"type": "reply", "text": "Sorry.", "already_spoken": False}]
session.handle("did you write it?")
corrected = converse_bodies(desk)[-1]["text"]
assert corrected.startswith(console.CORRECTION)
assert "did you write it?" in corrected
session.handle("thanks") # the correction goes out once, not forever
assert not converse_bodies(desk)[-1]["text"].startswith(console.CORRECTION)
def test_real_command_output_quoted_in_a_reply_is_not_flagged(desk, tmp_path):
desk.script["/desk/converse"] = {"type": "command", "command": "echo hi", "token": "t"}
desk.script["/desk/tool_result"] = {"type": "reply", "text": "Got [exit 0] and hi."}
session, out = make_session(answers=["y"])
session.handle("say hi")
assert console.FABRICATION_WARNING not in out.getvalue()
def test_fabricated_matches_the_shapes_seen_live():
assert console.fabricated("→ Command result: [filectl] {...}", 0)
assert console.fabricated("-> Deliver files result: Files delivered to C:/x", 0)
assert console.fabricated("[filectl] wrote 1200 chars to C:/x", 0)
assert not console.fabricated("I wrote the plan below.", 0)
assert not console.fabricated("→ Command result: [filectl] wrote 3 chars", 1)
def test_the_context_note_spells_out_the_command_marker():
"""'use filectl and shell commands' made the live model write filectl as
prose; the explicit `command:` form got real relayed commands."""
note = console.annotate("hi", "/work", "Windows 10")
assert "`command: filectl {" in note
assert "`command: <shell command>`" in note
assert "Never write a command's output yourself" in note
def test_new_conversation_switches_and_remembers_the_session(desk, tmp_path, monkeypatch):
monkeypatch.setattr(config, "STATE_FILE", tmp_path / "state" / "session.json")
monkeypatch.delenv("DESK_SESSION_ID", raising=False)
session, out = make_session()
session._correct_next = True
session.handle("/new")
new_id = config.SESSION_ID
assert new_id.startswith("pet-console-") and new_id != "pet-console-test"
assert new_id in out.getvalue()
assert not session._correct_next # nothing to correct in an empty history
assert config._remembered_session() == new_id # survives a restart
session.handle("hello")
assert converse_bodies(desk)[-1]["session_id"] == new_id
def test_a_corrupt_state_file_means_the_default_session(tmp_path, monkeypatch):
state = tmp_path / "session.json"
state.write_text("{not json", encoding="utf-8")
monkeypatch.setattr(config, "STATE_FILE", state)
assert config._remembered_session() == ""
-203
View File
@@ -1,203 +0,0 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import numpy as np
import pytest
from PySide6.QtWidgets import QApplication
from bolt_pet import controller as controller_mod
from bolt_pet.state import PetState
# A QApplication is required before any QObject with signals can be built.
_app = QApplication.instance() or QApplication(["test"])
@pytest.fixture(autouse=True)
def no_screen_probes(monkeypatch):
# The real ones shell out to xprop/osascript — irrelevant here, and slow
# (or hung) on a headless box.
monkeypatch.setattr(controller_mod.screen_context, "context_for", lambda text: text)
monkeypatch.setattr(controller_mod.screen_context, "is_fullscreen_active", lambda: False)
@pytest.fixture(autouse=True)
def _no_streaming(monkeypatch):
"""Most tests drive the plain request/response path; leaving streaming on
would have them attempt a real HTTP call, fail, and fall back — passing,
slowly, for the wrong reason. The streaming path has its own tests."""
monkeypatch.setattr(controller_mod.config, "STREAMING_REPLIES", False)
@pytest.fixture
def ctrl():
return controller_mod.PetController()
def _capture(signal):
events = []
signal.connect(lambda *a: events.append(a[0] if len(a) == 1 else a))
return events
# ── conversation turn ────────────────────────────────────────────────────────
def test_full_turn_happy_path(monkeypatch, ctrl):
states = _capture(ctrl.state_changed)
said = _capture(ctrl.said)
monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: np.zeros(10, dtype=np.int16))
monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "what's the weather")
monkeypatch.setattr(controller_mod.server_client, "converse", lambda text, on_command=None, on_say=None: controller_mod.server_client.Reply("sunny and 72"))
spoken = []
monkeypatch.setattr(controller_mod.tts, "speak",
lambda text, on_error=None, should_stop=None, voice_id=None, on_level=None: (spoken.append(text), True)[1])
ctrl._handle_conversation_turn()
assert states == ["listening", "thinking", "talking", "idle"]
assert said == ["sunny and 72"]
assert spoken == ["sunny and 72"]
assert ctrl._state.state == PetState.IDLE
def test_turn_with_nothing_heard_returns_to_idle_without_calling_server(monkeypatch, ctrl):
states = _capture(ctrl.state_changed)
monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: None)
called = {"n": 0}
monkeypatch.setattr(controller_mod.server_client, "converse",
lambda text, on_command=None, on_say=None: called.__setitem__("n", called["n"] + 1))
ctrl._handle_conversation_turn()
assert states == ["listening", "idle"]
assert called["n"] == 0
def test_turn_with_empty_transcript_returns_to_idle(monkeypatch, ctrl):
states = _capture(ctrl.state_changed)
monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: np.zeros(10, dtype=np.int16))
monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "")
ctrl._handle_conversation_turn()
assert states == ["listening", "thinking", "idle"]
def test_turn_with_stt_error_flashes_error_then_idle(monkeypatch, ctrl):
states = _capture(ctrl.state_changed)
logs = _capture(ctrl.log)
monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: np.zeros(10, dtype=np.int16))
def boom(pcm):
raise controller_mod.stt.SttError("deepgram is down")
monkeypatch.setattr(controller_mod.stt, "transcribe", boom)
ctrl._handle_conversation_turn()
assert states == ["listening", "thinking", "error", "idle"]
assert any("deepgram is down" in msg for msg in logs)
def test_turn_with_server_error_flashes_error_then_idle(monkeypatch, ctrl):
states = _capture(ctrl.state_changed)
monkeypatch.setattr(controller_mod.mic, "record_utterance", lambda *a, **k: np.zeros(10, dtype=np.int16))
monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "hello")
def boom(text, on_command=None, on_say=None):
raise controller_mod.server_client.ServerError("server is down")
monkeypatch.setattr(controller_mod.server_client, "converse", boom)
ctrl._handle_conversation_turn()
assert states == ["listening", "thinking", "error", "idle"]
# ── mute / talk-now / wake-or-click disambiguation ──────────────────────────
def test_toggle_mute_flips_and_returns_new_state(ctrl):
assert ctrl.toggle_mute() is True
assert ctrl.toggle_mute() is False
def test_wait_for_wake_or_click_true_on_phrase_detection(monkeypatch, ctrl):
monkeypatch.setattr(controller_mod.wake_word, "listen_for_wake_word", lambda *a, **k: True)
ctrl._stream = object()
assert ctrl._wait_for_wake_or_click() is True
def test_wait_for_wake_or_click_true_on_manual_trigger(monkeypatch, ctrl):
# listen_for_wake_word returns False because should_continue() went
# false (the talk_now flag got set) — controller must still recognize
# this as "proceed", not "spurious wakeup".
def fake_listen(stream, should_continue, on_tick=None, **kwargs):
ctrl.request_talk_now()
should_continue() # simulate the loop noticing the flag
return False
monkeypatch.setattr(controller_mod.wake_word, "listen_for_wake_word", fake_listen)
ctrl._stream = object()
assert ctrl._wait_for_wake_or_click() is True
assert not ctrl._talk_now.is_set() # cleared after being consumed
def test_wait_for_wake_or_click_false_on_shutdown(monkeypatch, ctrl):
def fake_listen(stream, should_continue, on_tick=None, **kwargs):
ctrl.stop()
return False
monkeypatch.setattr(controller_mod.wake_word, "listen_for_wake_word", fake_listen)
ctrl._stream = object()
assert ctrl._wait_for_wake_or_click() is False
# ── heartbeat / proactive announcements ─────────────────────────────────────
def test_heartbeat_speaks_a_pending_announcement_when_idle(monkeypatch, ctrl):
monkeypatch.setattr(controller_mod.config, "HEARTBEAT_INTERVAL_SECONDS", 0)
monkeypatch.setattr(controller_mod.server_client, "report_status", lambda: "don't forget your 3pm")
spoken = []
monkeypatch.setattr(controller_mod.tts, "speak",
lambda text, on_error=None, should_stop=None, voice_id=None, on_level=None: (spoken.append(text), True)[1])
said = _capture(ctrl.said)
ctrl._maybe_heartbeat()
assert spoken == ["don't forget your 3pm"]
assert said == ["don't forget your 3pm"]
assert ctrl._state.state == PetState.IDLE
def test_heartbeat_does_nothing_when_not_idle(monkeypatch, ctrl):
monkeypatch.setattr(controller_mod.config, "HEARTBEAT_INTERVAL_SECONDS", 0)
ctrl._state.transition(PetState.LISTENING)
called = {"n": 0}
monkeypatch.setattr(controller_mod.server_client, "report_status",
lambda: called.__setitem__("n", called["n"] + 1))
ctrl._maybe_heartbeat()
assert called["n"] == 0
def test_heartbeat_respects_the_interval(monkeypatch, ctrl):
# Deterministic fake clock — time.monotonic()'s absolute value is
# arbitrary (often system uptime), so asserting behavior relative to it
# without controlling it would be flaky.
fake_now = {"t": 1000.0}
monkeypatch.setattr(controller_mod.time, "monotonic", lambda: fake_now["t"])
monkeypatch.setattr(controller_mod.config, "HEARTBEAT_INTERVAL_SECONDS", 60)
called = {"n": 0}
monkeypatch.setattr(controller_mod.server_client, "report_status",
lambda: called.__setitem__("n", called["n"] + 1))
ctrl._maybe_heartbeat() # last_heartbeat starts at 0.0 -> elapsed is huge -> runs
assert called["n"] == 1
fake_now["t"] += 10 # only 10s later — inside the 60s interval
ctrl._maybe_heartbeat()
assert called["n"] == 1 # skipped
fake_now["t"] += 60 # now well past the interval
ctrl._maybe_heartbeat()
assert called["n"] == 2
File diff suppressed because it is too large Load Diff
-236
View File
@@ -1,236 +0,0 @@
"""`dialoguectl` — multi-voice scene parsing, voice resolution, API limits,
and the request the ElevenLabs Text to Dialogue endpoint actually gets.
Pure logic plus one mocked HTTP call: no audio device, no network, no display.
"""
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import dialogue
from bolt_pet.audio import tts
SELF_ID = "aaorr6ZHIL88gEexu7dC"
NARRATOR_ID = "9BWtsMINqrJLrRacOk9x"
VILLAIN_ID = "IKne3meq5aSn9XLyUdCD"
VOICES = {"narrator": NARRATOR_ID, "villain": VILLAIN_ID}
def _scene(*lines):
return '{"lines": [' + ", ".join(lines) + "]}"
# ── parsing ─────────────────────────────────────────────────────────────────
def test_non_dialogue_commands_are_left_alone():
assert dialogue.parse("ls -la") is None
assert dialogue.parse('filectl {"op": "list"}') is None
assert dialogue.parse("") is None
# "dialogues" must not be mistaken for the "dialogue" prefix
assert dialogue.parse("dialogues --list") is None
def test_a_scene_parses_into_lines():
action = dialogue.parse(
'dialoguectl ' + _scene(
'{"voice": "self", "text": "[cheerfully] Hello, how are you?"}',
'{"voice": "villain", "text": "[stuttering] I am... fine."}',
)
)
assert action["action"] == "dialogue"
assert [line["voice"] for line in action["lines"]] == ["self", "villain"]
assert action["lines"][0]["text"].startswith("[cheerfully]")
def test_the_elevenlabs_field_names_are_accepted_too():
"""The model has read that API; copying its shape is the obvious thing to
try, so 'inputs'/'voice_id' work as well as 'lines'/'voice'."""
action = dialogue.parse(
'dialoguectl {"inputs": [{"voice_id": "%s", "text": "hi"}]}' % NARRATOR_ID
)
assert action["lines"] == [{"voice": NARRATOR_ID, "text": "hi"}]
def test_a_line_with_no_voice_defaults_to_the_pet_itself():
action = dialogue.parse('dialoguectl {"lines": [{"text": "just me talking"}]}')
assert action["lines"][0]["voice"] == "self"
def test_truncated_json_explains_the_one_line_rule():
"""The real failure mode: the server's command extractor stops at the
first newline, so a multi-line payload arrives cut in half. The error has
to name the cause, since Bolt is the one who has to fix it."""
with pytest.raises(dialogue.DialogueError, match="one line"):
dialogue.parse('dialoguectl {"lines": [{"voice": "self", "text": "hi"')
def test_an_empty_or_shapeless_payload_is_rejected():
with pytest.raises(dialogue.DialogueError, match="needs a JSON argument"):
dialogue.parse("dialoguectl")
with pytest.raises(dialogue.DialogueError, match="non-empty"):
dialogue.parse('dialoguectl {"lines": []}')
with pytest.raises(dialogue.DialogueError, match="no text"):
dialogue.parse('dialoguectl {"lines": [{"voice": "self", "text": " "}]}')
def test_optional_model_and_stability_ride_along():
action = dialogue.parse(
'dialoguectl {"model_id": "eleven_v3", "stability": 0.8, '
'"lines": [{"text": "hi"}]}'
)
assert action["model"] == "eleven_v3"
assert action["stability"] == 0.8
# ── voice resolution ────────────────────────────────────────────────────────
def test_named_voices_resolve_from_the_configured_cast():
action = dialogue.parse('dialoguectl ' + _scene(
'{"voice": "narrator", "text": "Once upon a time."}',
'{"voice": "villain", "text": "Not this again."}',
))
inputs = dialogue.resolve(action, voices=VOICES, self_voice=SELF_ID)
assert [entry["voice_id"] for entry in inputs] == [NARRATOR_ID, VILLAIN_ID]
def test_self_tracks_the_voice_the_pet_is_currently_using():
"""A scene featuring Bolt should sound like whoever Bolt currently is —
including a voice the server picked mid-conversation with speak_as."""
action = dialogue.parse('dialoguectl {"lines": [{"voice": "self", "text": "hi"}]}')
picked = "VOICEfromSPEAKas1234"
assert dialogue.resolve(action, self_voice=picked)[0]["voice_id"] == picked
def test_a_raw_voice_id_passes_straight_through():
action = dialogue.parse('dialoguectl {"lines": [{"voice": "%s", "text": "hi"}]}' % NARRATOR_ID)
assert dialogue.resolve(action, self_voice=SELF_ID)[0]["voice_id"] == NARRATOR_ID
def test_an_unknown_name_lists_what_is_available():
action = dialogue.parse('dialoguectl {"lines": [{"voice": "wizard", "text": "hi"}]}')
with pytest.raises(dialogue.DialogueError) as excinfo:
dialogue.resolve(action, voices=VOICES, self_voice=SELF_ID)
message = str(excinfo.value)
assert "wizard" in message and "narrator" in message and "villain" in message
def test_self_without_a_configured_voice_says_so():
action = dialogue.parse('dialoguectl {"lines": [{"voice": "self", "text": "hi"}]}')
with pytest.raises(dialogue.DialogueError, match="ELEVENLABS_VOICE_ID"):
dialogue.resolve(action, self_voice="")
def test_the_voice_map_parser_skips_typos_instead_of_dying():
voices = dialogue.parse_voice_map(f"narrator:{NARRATOR_ID}, broken-entry, villain:{VILLAIN_ID}")
assert voices == {"narrator": NARRATOR_ID, "villain": VILLAIN_ID}
assert dialogue.parse_voice_map("") == {}
# ── API limits, enforced before the request goes out ────────────────────────
def test_too_many_distinct_voices_is_refused_locally():
inputs = [{"text": "hi", "voice_id": f"voice{index:015d}"} for index in range(11)]
with pytest.raises(dialogue.DialogueError, match="limit is 10"):
dialogue.check_limits(inputs)
def test_an_over_long_scene_is_refused_with_advice():
inputs = [{"text": "x" * 1100, "voice_id": SELF_ID} for _ in range(2)]
with pytest.raises(dialogue.DialogueError) as excinfo:
dialogue.check_limits(inputs)
assert "Split it" in str(excinfo.value) # actionable, since Bolt reads this
# ── display / reporting ─────────────────────────────────────────────────────
def test_delivery_tags_are_stripped_from_what_the_bubble_shows():
action = dialogue.parse('dialoguectl ' + _scene(
'{"voice": "self", "text": "[cheerfully] Hello there!"}',
'{"voice": "narrator", "text": "[whispering] He is lying."}',
))
assert dialogue.spoken_text(action) == "Hello there! He is lying."
def test_the_relay_report_names_the_cast():
action = dialogue.parse('dialoguectl ' + _scene(
'{"voice": "self", "text": "one"}', '{"voice": "narrator", "text": "two"}',
))
assert dialogue.describe(action).startswith(
"[dialogue] played 2 lines in 2 voices: narrator, self")
def test_the_report_says_the_scene_was_already_heard():
"""Observed 2026-07-31: the scene played, then the final reply summarised
it, so the pet said the same thing twice with nothing in between. The
model cannot know the audio already happened unless it is told."""
action = dialogue.parse('dialoguectl {"lines": [{"text": "hello"}]}')
report = dialogue.describe(action)
assert "HEARD this already" in report
assert "Do not repeat" in report
# ── the HTTP request ────────────────────────────────────────────────────────
def _pcm_response(samples=(1, 2, 3, 4)):
response = MagicMock()
response.content = np.array(samples, dtype=np.int16).tobytes()
response.raise_for_status = MagicMock()
return response
def test_the_request_matches_the_text_to_dialogue_api(monkeypatch):
monkeypatch.setattr(tts.config, "ELEVENLABS_API_KEY", "test-key")
monkeypatch.setattr(tts.config, "TTS_SAMPLE_RATE", 24000)
monkeypatch.setattr(tts.config, "DIALOGUE_MODEL_ID", "eleven_v3")
inputs = [
{"text": "[cheerfully] Hello", "voice_id": NARRATOR_ID},
{"text": "[stuttering] H-hi", "voice_id": VILLAIN_ID},
]
with patch.object(tts.requests, "post", return_value=_pcm_response()) as post:
pcm, rate = tts.synthesize_dialogue(inputs)
assert rate == 24000 and pcm.tolist() == [1, 2, 3, 4]
args, kwargs = post.call_args
assert args[0] == "https://api.elevenlabs.io/v1/text-to-dialogue"
assert kwargs["params"] == {"output_format": "pcm_24000"}
assert kwargs["headers"] == {"xi-api-key": "test-key"}
assert kwargs["json"]["inputs"] == inputs
assert kwargs["json"]["model_id"] == "eleven_v3"
assert "settings" not in kwargs["json"] # omitted unless asked for
def test_stability_is_only_sent_when_given(monkeypatch):
monkeypatch.setattr(tts.config, "ELEVENLABS_API_KEY", "test-key")
with patch.object(tts.requests, "post", return_value=_pcm_response()) as post:
tts.synthesize_dialogue([{"text": "hi", "voice_id": SELF_ID}], stability=0.3)
assert post.call_args.kwargs["json"]["settings"] == {"stability": 0.3}
def test_a_rejected_request_surfaces_what_the_api_said(monkeypatch):
"""The API explains refusals in the body; Bolt reads this through the tool
relay, so it has to reach him rather than being flattened to '422'."""
monkeypatch.setattr(tts.config, "ELEVENLABS_API_KEY", "test-key")
failure = MagicMock()
failure.text = '{"detail": "voice_id not found"}'
error = Exception("422 Client Error")
error.response = failure
response = MagicMock()
response.raise_for_status = MagicMock(side_effect=error)
with patch.object(tts.requests, "post", return_value=response):
with pytest.raises(tts.TtsError, match="voice_id not found"):
tts.synthesize_dialogue([{"text": "hi", "voice_id": "nope"}])
def test_no_api_key_fails_before_the_request(monkeypatch):
monkeypatch.setattr(tts.config, "ELEVENLABS_API_KEY", "")
with patch.object(tts.requests, "post") as post:
with pytest.raises(tts.TtsError):
tts.synthesize_dialogue([{"text": "hi", "voice_id": SELF_ID}])
post.assert_not_called()
+295
View File
@@ -0,0 +1,295 @@
"""Dictation without a mic or a server: a fake PortAudio stream we push
frames into, and a fake /desk/stt socket that answers like Deepgram."""
import array
import json
import os
import queue
import sys
import threading
import time
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import pytest
from bolt_console import dictation
def frame(level: int, samples: int = 1280) -> bytes:
return array.array("h", [level, -level] * (samples // 2)).tobytes()
def result(text, final):
return json.dumps({"type": "Results", "is_final": final,
"channel": {"alternatives": [{"transcript": text}]}})
class FakeSocket:
"""Answers each audio chunk with the next scripted message; on CloseStream
sends whatever is left, then closes."""
def __init__(self, replies=()):
self.replies = list(replies)
self.audio = []
self.closed_stream = False
self._inbox = queue.Queue()
def send_binary(self, data):
self.audio.append(data)
if self.replies and len(self.audio) % 2 == 0:
self._inbox.put(self.replies.pop(0))
def send(self, text):
if json.loads(text).get("type") == "CloseStream":
self.closed_stream = True
for reply in self.replies:
self._inbox.put(reply)
self._inbox.put("")
def recv(self):
return self._inbox.get(timeout=5)
def close(self):
self._inbox.put("")
class FakeStream:
def __init__(self):
self.callback = None
self.stopped = False
def open(self, callback, _rate):
self.callback = callback
return self
def push(self, *chunks):
for chunk in chunks:
self.callback(chunk)
def stop(self):
self.stopped = True
def close(self):
pass
class Recorder:
def __init__(self):
self.updates, self.levels, self.done = [], [], threading.Event()
self.result = None
def kwargs(self):
return dict(on_update=lambda f, i: self.updates.append((f, i)),
on_level=self.levels.append,
on_done=lambda text, err: (setattr(self, "result", (text, err)), self.done.set()))
def make(sockets, stream, rec, **extra):
sockets = list(sockets)
def connect(_rate):
item = sockets.pop(0)
if isinstance(item, Exception):
raise item
return item
return dictation.Dictation(**rec.kwargs(), connect_fn=connect, open_stream_fn=stream.open, **extra)
def test_live_words_arrive_while_talking_and_stop_returns_the_text():
sock = FakeSocket([result("open the", False), result("open the readme", True),
result("please", True)])
stream, rec = FakeStream(), Recorder()
d = make([sock], stream, rec)
d.start()
assert d.live
stream.push(*[frame(3000)] * 4)
deadline = time.monotonic() + 3
while len(rec.updates) < 2 and time.monotonic() < deadline:
time.sleep(0.01)
assert rec.updates[0] == ("", "open the")
assert rec.updates[1] == ("open the readme", "")
d.stop()
assert rec.done.wait(5)
assert rec.result == ("open the readme please", None)
assert sock.closed_stream and stream.stopped
assert max(rec.levels) > 0 and rec.levels[-1] == 0.0
def test_no_live_socket_means_the_whole_recording_is_sent_on_stop():
late = FakeSocket([result("hello bolt", True)])
stream, rec = FakeStream(), Recorder()
d = make([ConnectionError("down"), late], stream, rec)
d.start()
assert not d.live
stream.push(frame(3000), frame(3000), frame(0))
time.sleep(0.3)
d.stop()
assert rec.done.wait(5)
assert rec.result == ("hello bolt", None)
assert b"".join(late.audio) == frame(3000) * 2 + frame(0)
def test_a_socket_dying_mid_recording_does_not_duplicate_words():
class Dying(FakeSocket):
def send_binary(self, data):
if len(self.audio) >= 1:
raise OSError("connection reset")
super().send_binary(data)
self._inbox.put(result("half", True))
retry = FakeSocket([result("the whole thing", True)])
stream, rec = FakeStream(), Recorder()
d = make([Dying(), retry], stream, rec)
d.start()
stream.push(frame(3000), frame(3000), frame(3000))
time.sleep(0.3)
d.stop()
assert rec.done.wait(5)
assert rec.result == ("the whole thing", None) # not "half the whole thing"
def test_pausing_after_speech_stops_on_its_own():
sock = FakeSocket([result("done talking", True)])
stream, rec = FakeStream(), Recorder()
stopping = threading.Event()
d = make([sock], stream, rec, silence_seconds=0.3, on_stopping=stopping.set)
d.start()
stream.push(frame(3000), frame(3000))
for _ in range(8):
stream.push(frame(10))
time.sleep(0.05)
assert rec.done.wait(5) # nobody called stop()
assert stopping.is_set()
assert rec.result == ("done talking", None)
def test_silence_alone_never_auto_stops():
stream, rec = FakeStream(), Recorder()
d = make([FakeSocket()], stream, rec, silence_seconds=0.2)
d.start()
for _ in range(8):
stream.push(frame(10))
time.sleep(0.05)
assert not rec.done.is_set() # still waiting for you to start talking
d.stop()
assert rec.done.wait(5)
assert rec.result[0] == "" and "Didn't catch anything" in rec.result[1]
def test_cancel_throws_the_result_away_without_a_fallback_upload():
stream, rec = FakeStream(), Recorder()
d = make([ConnectionError("down")], stream, rec) # a fallback would need a 2nd socket
d.start()
stream.push(frame(3000))
d.cancel()
assert rec.done.wait(5)
assert rec.result == ("", None)
def test_mic_failure_raises_from_start_and_closes_the_socket():
sock = FakeSocket()
rec = Recorder()
def broken(_cb, _rate):
raise OSError("no input device")
d = dictation.Dictation(**rec.kwargs(), connect_fn=lambda _r: sock, open_stream_fn=broken)
with pytest.raises(OSError):
d.start()
assert sock._inbox.get(timeout=1) == "" # closed
def test_parse_result_is_tolerant():
assert dictation.parse_result(result("hi", True)) == ("hi", True)
assert dictation.parse_result(json.dumps({"type": "Metadata"})) == ("", False)
assert dictation.parse_result(b"not json") == ("", False)
assert dictation.parse_result(json.dumps({"type": "Results", "speech_final": True,
"channel": {"alternatives": [{"transcript": "x"}]}})) == ("x", True)
def test_connect_url_carries_the_session_and_audio_format(monkeypatch):
import types
seen = {}
fake_ws = types.SimpleNamespace(create_connection=lambda url, header, timeout: seen.update(url=url, header=header))
monkeypatch.setitem(sys.modules, "websocket", fake_ws)
monkeypatch.setattr(dictation.config, "SERVER_URL", "https://bolt.example")
monkeypatch.setattr(dictation.config, "API_KEY", "k")
monkeypatch.setattr(dictation.config, "SESSION_ID", "pet-console-x y")
dictation.connect()
assert seen["url"].startswith("wss://bolt.example/desk/stt?")
assert "session_id=pet-console-x+y" in seen["url"] and "sample_rate=16000" in seen["url"]
assert seen["header"] == ["X-Desk-Api-Key: k"]
# ── the window ──────────────────────────────────────────────────────────────
class ScriptedDictation:
"""Stands in for Dictation in the window: the test drives its callbacks."""
last = None
def __init__(self, on_update, on_level, on_done, silence_seconds=3.0, on_stopping=None):
self.on_update, self.on_level, self.on_done = on_update, on_level, on_done
self.stopped = self.cancelled = False
self.live = True
ScriptedDictation.last = self
def start(self):
pass
def stop(self):
self.stopped = True
def cancel(self):
self.cancelled = True
def test_window_mic_fills_the_composer_and_blocks_send_until_done(monkeypatch):
from bolt_console import config
from bolt_console.gui.window import MainWindow, make_app
monkeypatch.setattr(config, "SERVER_URL", "http://127.0.0.1:9")
monkeypatch.setattr(config, "API_KEY", "k")
monkeypatch.setattr(dictation, "unavailable_reason", lambda: None)
app = make_app(["test"])
window = MainWindow(dictation_factory=ScriptedDictation)
try:
window.composer.edit.setPlainText("Please")
window.toggle_dictation()
rec = ScriptedDictation.last
assert window.composer.mic.state == "recording"
rec.on_update("open the readme", "and")
app.processEvents()
assert window.composer.edit.toPlainText() == "Please open the readme and"
assert window.composer.edit.isReadOnly() and not window.composer.send.isEnabled()
window.toggle_dictation() # click again: stop
assert rec.stopped and window.composer.mic.state == "finishing"
rec.on_done("open the readme and summarise it", None)
app.processEvents()
assert window.composer.edit.toPlainText() == "Please open the readme and summarise it"
assert not window.composer.edit.isReadOnly() and window.composer.send.isEnabled()
assert window.composer.mic.state == "idle"
finally:
window.close()
def test_window_shows_why_the_mic_is_unavailable(monkeypatch):
from bolt_console.gui.window import MainWindow, make_app
monkeypatch.setattr(dictation, "unavailable_reason", lambda: "Dictation needs sounddevice")
make_app(["test"])
window = MainWindow(dictation_factory=ScriptedDictation)
try:
ScriptedDictation.last = None
window.toggle_dictation()
assert ScriptedDictation.last is None
assert window.composer.mic.state == "unavailable"
assert "sounddevice" in window.hint.text()
finally:
window.close()
-187
View File
@@ -1,187 +0,0 @@
"""The preflight. Its one job is to never be the thing that's broken.
A doctor that raises on a broken install diagnoses the wrong patient, so the
tests that matter here are the ugly-input ones: no config at all, a check that
throws, a dependency missing. The individual diagnoses are simple enough to
read; that they *run* on a machine missing everything is the property worth
pinning down.
"""
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import config, doctor
from bolt_pet.doctor import FAIL, OK, WARN
def test_every_check_returns_a_verdict_on_a_bare_machine(monkeypatch):
"""Nothing configured, nothing installed — still a full report."""
for name in ("SERVER_URL", "API_KEY", "ELEVENLABS_API_KEY", "ELEVENLABS_VOICE_ID"):
monkeypatch.setattr(config, name, "")
monkeypatch.setattr(doctor, "_module", lambda _n: False)
results = doctor.run()
assert len(results) == len(doctor.CHECKS)
assert all(c.status in (OK, WARN, FAIL) for c in results)
assert all(c.name and c.detail for c in results)
def test_a_check_that_raises_does_not_hide_the_others():
"""One broken probe must not cost you the other eleven diagnoses."""
def explode(*_args):
raise RuntimeError("boom")
original = doctor.CHECKS
doctor.CHECKS = (("mic", explode),) + original[:2]
try:
results = doctor.run()
finally:
doctor.CHECKS = original
assert len(results) == 3
assert results[0].status == FAIL
assert "boom" in results[0].detail
def test_missing_server_config_is_a_failure_not_a_warning(monkeypatch):
"""Without it the controller exits its thread at startup — the pet looks
alive and simply never answers. That is the worst failure mode there is."""
monkeypatch.setattr(config, "missing_config", lambda: ["BOLT_SERVER_URL", "DESK_API_KEY"])
check = doctor.check_config()
assert check.status == FAIL
assert "BOLT_SERVER_URL" in check.detail
assert check.fix
def test_a_configured_server_passes_without_being_contacted(monkeypatch):
"""The shallow run must not need the network — it's the first thing you
reach for when the network is what's wrong."""
monkeypatch.setattr(config, "missing_config", lambda: [])
monkeypatch.setattr(config, "SERVER_URL", "http://bolt.local:8000")
assert doctor.check_config().status == OK
assert doctor.check_server(deep=False).status == OK
def test_every_problem_comes_with_something_to_do_about_it(monkeypatch):
""""screen reading: warn" is useless on its own; "apt install tesseract-ocr"
is the entire point of the tool."""
for name in ("SERVER_URL", "API_KEY"):
monkeypatch.setattr(config, name, "")
monkeypatch.setattr(doctor, "_module", lambda _n: False)
for check in doctor.run():
if check.status == FAIL:
assert check.fix, f"{check.name} says what's wrong but not what to do"
def test_the_exit_code_is_nonzero_only_for_real_failures(monkeypatch, capsys):
monkeypatch.setattr(doctor, "run", lambda deep=False: [
doctor.Check("a", OK, "fine"), doctor.Check("b", WARN, "degraded")])
assert doctor.main([]) == 0
monkeypatch.setattr(doctor, "run", lambda deep=False: [doctor.Check("a", FAIL, "broken")])
assert doctor.main([]) == 1
assert "Fix those first" in capsys.readouterr().out
def test_deep_is_off_unless_asked(monkeypatch):
seen = []
monkeypatch.setattr(doctor, "run", lambda deep=False: seen.append(deep) or [])
doctor.main([])
doctor.main(["--deep"])
assert seen == [False, True]
def test_a_slow_silence_timeout_is_flagged(monkeypatch):
"""The setting most likely to make it feel sluggish, and the least obvious
— it is pure dead air before anything at all starts happening."""
monkeypatch.setattr(config, "SILENCE_END_SEC", 2.0)
check = doctor.check_latency()
assert check.status == WARN
assert "2s" in check.detail or "2 " in check.detail
monkeypatch.setattr(config, "SILENCE_END_SEC", 0.9)
assert doctor.check_latency().status == OK
def test_a_check_line_renders_the_fix_only_when_there_is_a_problem():
assert "→" not in doctor.Check("x", OK, "all good", fix="unused").line()
assert "→ do the thing" in doctor.Check("x", WARN, "hmm", fix="do the thing").line()
# ── check_tts's --deep probe ────────────────────────────────────────────────
# check_streaming_endpoint already established the pattern: shallow only
# checks config (no network), --deep actually calls the server. TTS needs
# its own deep probe specifically because "configured but broken" (a
# rejected voice id, a dead credit balance) is otherwise indistinguishable
# from "working" until speak() silently falls back to the offline voice —
# which is exactly the bug report this was added to diagnose.
@pytest.fixture(autouse=True)
def _tts_configured(monkeypatch):
monkeypatch.setattr(config, "SERVER_URL", "http://test-server:5002")
monkeypatch.setattr(config, "API_KEY", "test-key")
monkeypatch.setattr(config, "SESSION_ID", "pet-test")
monkeypatch.setattr(config, "ELEVENLABS_VOICE_ID", "abc123voiceid")
def test_tts_shallow_does_not_touch_the_network():
with patch("requests.post") as post:
check = doctor.check_tts(deep=False)
post.assert_not_called()
assert check.status == OK
assert "not probed" in check.detail
def test_tts_deep_reports_success():
response = MagicMock(status_code=200, content=b"\x00\x01" * 100)
with patch("requests.post", return_value=response) as post:
check = doctor.check_tts(deep=True)
post.assert_called_once()
assert check.status == OK
assert "200 bytes" in check.detail
args, kwargs = post.call_args
assert args[0] == "http://test-server:5002/desk/tts"
assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"}
assert kwargs["json"]["voice_id"] == "abc123voiceid"
def test_tts_deep_surfaces_the_servers_error_message():
"""A 404 with 'voice not found' is exactly the shape of the reported bug:
a vb: clone owned by someone else, or a stale/mistyped id."""
response = MagicMock(status_code=404)
response.json.return_value = {"error": "voice not found"}
with patch("requests.post", return_value=response):
check = doctor.check_tts(deep=True)
assert check.status == FAIL
assert "404" in check.detail
assert "voice not found" in check.detail
assert check.fix
def test_tts_deep_handles_a_network_failure_without_raising():
with patch("requests.post", side_effect=OSError("no route to host")):
check = doctor.check_tts(deep=True)
assert check.status == FAIL
assert "no route to host" in check.detail
def test_tts_run_wires_deep_through_automatically(monkeypatch):
"""run()'s TypeError-fallback dance (doctor.py's CHECKS loop) must
actually reach check_tts's deep parameter, not silently call it shallow."""
seen = []
def fake_check_tts(deep=False):
seen.append(deep)
return doctor.Check("text-to-speech", OK, "stub")
monkeypatch.setattr(doctor, "CHECKS", (("tts", fake_check_tts),))
doctor.run(deep=True)
assert seen == [True]
+1 -1
View File
@@ -3,7 +3,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import file_delivery
from bolt_console import file_delivery
def test_sanitize_filename_strips_directory_components():
+18 -4
View File
@@ -4,7 +4,7 @@ read/write/edit local files instead of a raw shell heredoc.
filectl {"op": ...} is a single-line JSON envelope (not a multi-line
marker block) because it rides the "command" tool marker, which the main
repo's tool-call extractor only captures up to the next newline — see the
module docstring in bolt_pet/file_ops.py."""
module docstring in bolt_console/file_ops.py."""
import json
import sys
@@ -14,7 +14,7 @@ import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import file_ops
from bolt_console import file_ops
def _cmd(payload: dict) -> str:
@@ -305,7 +305,7 @@ def test_a_repair_is_reported_in_the_output_never_hidden(tmp_path):
tomorrow — the model has to be told while it still has the turn."""
(tmp_path / "a.txt").write_text("hello", encoding="utf-8")
action = file_ops.parse(
'filectl {"op":"list","path":"%s","recursive":false"}' % tmp_path
'filectl {"op":"list","path":%s,"recursive":false"}' % json.dumps(str(tmp_path))
)
output = file_ops.execute(action)
assert "a.txt" in output
@@ -315,7 +315,7 @@ def test_a_repair_is_reported_in_the_output_never_hidden(tmp_path):
def test_valid_json_gets_no_repair_note(tmp_path):
(tmp_path / "a.txt").write_text("hello", encoding="utf-8")
action = file_ops.parse('filectl {"op": "list", "path": "%s"}' % tmp_path)
action = file_ops.parse('filectl {"op": "list", "path": %s}' % json.dumps(str(tmp_path)))
assert "_repairs" not in action
assert "malformed" not in file_ops.execute(action)
@@ -327,3 +327,17 @@ def test_genuinely_unparseable_json_points_at_the_character():
message = str(excinfo.value)
assert "^" in message # caret under the offending character
assert '"extra"' in message # ...and the fragment around it
def test_a_raw_windows_path_is_repaired_and_reported():
r"""Models type Windows paths with single backslashes. `\U` isn't a JSON
escape, so strict parsing would reject the whole call."""
action = file_ops.parse(r'filectl {"op": "read", "path": "C:\Users\sam\data.txt"}')
assert action["path"] == r"C:\Users\sam\data.txt"
assert any("backslash" in repair for repair in action["_repairs"])
def test_the_backslash_repair_leaves_valid_escapes_alone():
# The escaped pair and the valid \n survive; only the lone \U is doubled.
action = file_ops.parse(r'filectl {"op": "read", "path": "C:\\ok\Users\n.txt"}')
assert action["path"] == "C:\\ok\\Users\n.txt"
+189
View File
@@ -0,0 +1,189 @@
"""The window: a whole turn through the real MainWindow, worker thread and
Bridge against the loopback desk server from test_console — only the mouse is
faked (by clicking the approval button programmatically)."""
import json
import os
import sys
import threading
import time
from pathlib import Path
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import pytest
from PySide6.QtWidgets import QApplication, QLabel
from bolt_console.gui import widgets
from bolt_console.gui.bridge import Bridge
from bolt_console.gui.window import MainWindow, make_app
from tests.test_console import desk # noqa: F401 (fixture)
app = make_app(["test"])
def pump_until(predicate, timeout=10.0):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
app.processEvents()
if predicate():
return True
time.sleep(0.01)
return False
def entries(window, kind):
layout = window.entries
return [layout.itemAt(i).widget() for i in range(layout.count())
if isinstance(layout.itemAt(i).widget(), kind)]
@pytest.fixture
def window(desk, monkeypatch): # noqa: F811
from bolt_console import config
monkeypatch.setattr(config, "STREAMING_REPLIES", True)
win = MainWindow(mode="ask")
yield win
win.close()
def test_confirm_blocks_the_worker_until_the_window_answers():
bridge = Bridge()
asked = []
bridge.approval_needed.connect(lambda tool_id, q: asked.append(q))
result = {}
worker = threading.Thread(target=lambda: result.setdefault("a", bridge.confirm("Run it?")))
worker.start()
assert pump_until(lambda: asked == ["Run it?"])
assert worker.is_alive() # still waiting on the user
bridge.answer("always")
worker.join(2)
assert result["a"] == "always"
def test_close_releases_a_pending_approval_as_no():
bridge = Bridge()
result = {}
worker = threading.Thread(target=lambda: result.setdefault("a", bridge.confirm("Run it?")))
worker.start()
time.sleep(0.05)
bridge.close()
worker.join(2)
assert result["a"] == "no"
assert bridge.confirm("again?") == "no" # and never blocks after close
def test_a_turn_with_an_approved_edit(window, desk, tmp_path): # noqa: F811
target = tmp_path / "app.py"
target.write_text("DEBUG = True\n", encoding="utf-8")
edit = "filectl " + json.dumps(
{"op": "edit", "path": str(target), "old": "DEBUG = True", "new": "DEBUG = False"})
desk.script["/desk/converse_stream"] = [
{"type": "say", "text": "On it."},
{"type": "command", "command": edit, "token": "t1"},
]
desk.script["/desk/tool_result"] = {"type": "reply", "text": "Debug is **off** now."}
window._send("turn debug off")
assert pump_until(lambda: entries(window, widgets.ToolCard)
and entries(window, widgets.ToolCard)[0].approval.isVisibleTo(window))
card = entries(window, widgets.ToolCard)[0]
assert card.pill.property("kind") == "waiting"
assert "DEBUG = True" in card.diff.text()
card.allow_button.click()
assert pump_until(lambda: not window._busy)
assert target.read_text(encoding="utf-8") == "DEBUG = False\n"
assert card.pill.property("kind") == "ok"
texts = [m.text() for m in entries(window, widgets.AssistantMessage)]
assert texts == ["On it.", "Debug is **off** now."]
assert len(entries(window, widgets.UserMessage)) == 1
def test_deny_reports_back_and_the_turn_finishes(window, desk, tmp_path): # noqa: F811
desk.script["/desk/converse"] = {"type": "command", "command": "echo hi > made.txt", "token": "t"}
desk.script["/desk/tool_result"] = {"type": "reply", "text": "Okay, I won't."}
window._send("make a file")
assert pump_until(lambda: entries(window, widgets.ToolCard)
and entries(window, widgets.ToolCard)[0].approval.isVisibleTo(window))
card = entries(window, widgets.ToolCard)[0]
card._answer("no")
assert pump_until(lambda: not window._busy)
assert not (tmp_path / "made.txt").exists()
assert card.pill.property("kind") == "declined"
posted = [r[2] for r in desk.requests if r[1] == "/desk/tool_result"]
assert "declined" in posted[0]["output"]
def test_always_allow_flips_the_header_switch_to_auto(window, desk, tmp_path): # noqa: F811
desk.script["/desk/converse"] = {"type": "command", "command": "echo hi", "token": "t"}
window._send("say hi")
assert pump_until(lambda: entries(window, widgets.ToolCard)
and entries(window, widgets.ToolCard)[0].approval.isVisibleTo(window))
entries(window, widgets.ToolCard)[0]._answer("always")
assert pump_until(lambda: not window._busy)
assert window.session.tools.mode == "auto"
assert window.mode.buttons["auto"].isChecked()
def test_clear_empties_the_transcript_but_keeps_the_scaffolding(window, desk): # noqa: F811
window._send("hello")
assert pump_until(lambda: not window._busy)
window.clear_transcript()
app.processEvents()
assert entries(window, widgets.UserMessage) == []
assert window.entries.indexOf(window.thinking) != -1
assert not window.empty.isHidden()
def test_markdown_code_uses_the_app_mono_font():
html = widgets.markdown_html("run `ls` now", QLabel().font())
assert "Courier New" not in html
assert widgets.theme.CODE in html
def test_align_columns_pads_tab_separated_rows():
rows = widgets.align_columns(["dir/", "a.txt\t5B", "longer.txt\t12B"])
assert rows[0] == "dir/"
assert rows[1].index("5B") == rows[2].index("12B")
def test_new_chat_clears_the_window_and_starts_a_fresh_session(window, desk, tmp_path, monkeypatch): # noqa: F811
from bolt_console import config
monkeypatch.setattr(config, "STATE_FILE", tmp_path / "session.json")
desk.script["/desk/chats"] = {"session_id": "chat-3f9a2b1c9e04"} # POST mints the id
window._send("hello")
assert pump_until(lambda: not window._busy)
window.start_new_chat()
assert pump_until(lambda: config.SESSION_ID == "chat-3f9a2b1c9e04")
assert ("POST", "/desk/chats", {}) in desk.requests # over real HTTP, off the UI thread
assert entries(window, widgets.UserMessage) == []
assert not window.empty.isHidden()
window._send("next")
assert pump_until(lambda: not window._busy)
converse = [r[2] for r in desk.requests if r[0] == "POST" and "converse" in r[1]]
assert converse[-1]["session_id"] == "chat-3f9a2b1c9e04" # the next turn uses the new chat
def test_write_ico_holds_every_size_the_exe_needs(tmp_path):
from PySide6.QtGui import QImageReader
from bolt_console.gui import icons
path = tmp_path / "bolt.ico"
icons.write_ico(str(path))
reader = QImageReader(str(path))
sizes = []
while True:
image = reader.read()
if image.isNull():
break
sizes.append(image.width())
if not reader.jumpToNextImage():
break
assert sizes == list(icons.APP_ICON_SIZES)
-92
View File
@@ -1,92 +0,0 @@
"""Conversation scrollback + push-to-talk hotkey parsing."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import history as history_mod
from bolt_pet.hotkey import GlobalHotkey, HotkeyError, to_pynput_spec
# ── history ─────────────────────────────────────────────────────────────────
def test_keeps_entries_in_order():
log = history_mod.ConversationHistory(limit=10)
log.add(history_mod.USER, "what's the weather")
log.add(history_mod.PET, "sunny and 72")
assert [e.text for e in log.entries()] == ["what's the weather", "sunny and 72"]
def test_drops_the_oldest_past_the_limit():
log = history_mod.ConversationHistory(limit=2)
for i in range(5):
log.add(history_mod.PET, f"line {i}")
assert [e.text for e in log.entries()] == ["line 3", "line 4"]
def test_blank_entries_are_ignored():
log = history_mod.ConversationHistory()
assert log.add(history_mod.PET, " ") is None
assert len(log) == 0
def test_last_can_filter_by_role():
log = history_mod.ConversationHistory()
log.add(history_mod.USER, "hello")
log.add(history_mod.PET, "hi there")
log.add(history_mod.USER, "still there?")
assert log.last().text == "still there?"
assert log.last(history_mod.PET).text == "hi there"
def test_as_text_is_copyable_transcript():
log = history_mod.ConversationHistory()
log.add(history_mod.USER, "ping")
log.add(history_mod.PET, "pong")
assert log.as_text() == "You: ping\nBolt: pong"
def test_timestamps_are_rendered_when_present():
log = history_mod.ConversationHistory()
log.add(history_mod.PET, "pong", timestamp=1710000000.0)
assert log.as_text(clock=lambda t: "12:00:00") == "[12:00:00] Bolt: pong"
def test_clear_empties_the_log():
log = history_mod.ConversationHistory()
log.add(history_mod.PET, "pong")
log.clear()
assert len(log) == 0
# ── hotkey ──────────────────────────────────────────────────────────────────
def test_translates_a_readable_spec_to_pynput_syntax():
assert to_pynput_spec("ctrl+alt+space") == "<ctrl>+<alt>+<space>"
assert to_pynput_spec("ctrl+shift+b") == "<ctrl>+<shift>+b"
def test_accepts_the_names_people_actually_type():
assert to_pynput_spec("Control+Option+Space") == "<ctrl>+<alt>+<space>"
assert to_pynput_spec("super+k") == "<cmd>+k"
def test_empty_spec_is_an_error_at_parse_time():
with pytest.raises(HotkeyError):
to_pynput_spec("")
def test_disabled_hotkey_starts_cleanly_and_reports_nothing():
hotkey = GlobalHotkey("", lambda: None)
assert hotkey.start() is None
assert hotkey.running is False
def test_invalid_hotkey_reports_instead_of_raising():
hotkey = GlobalHotkey("+++", lambda: None)
problem = hotkey.start()
assert problem and "invalid" in problem
assert hotkey.running is False
-135
View File
@@ -1,135 +0,0 @@
"""Amplitude → mouth openness, from PCM samples to the drawn frame.
Two halves, tested separately because only one of them needs a display:
`tts.level_of`/`envelope` turn samples into a 0..1 loudness, and
`PetWindow._mouth_frame` turns that loudness into a frame index. They meet at
the `mouth` signal, which the pipeline smoke test covers end to end.
"""
import sys
import time
from pathlib import Path
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet.audio import tts
def frame(amplitude: int, samples: int = 480) -> np.ndarray:
return np.full(samples, amplitude, dtype=np.int16)
# ── loudness ────────────────────────────────────────────────────────────────
def test_silence_closes_the_mouth():
assert tts.level_of(np.zeros(480, dtype=np.int16)) == 0.0
def test_a_loud_frame_opens_it_fully():
assert tts.level_of(frame(30000)) == 1.0
def test_the_level_never_leaves_zero_to_one():
"""It indexes a frame list; out of range is an IndexError on the UI thread."""
for amplitude in (0, 1, 500, 6000, 20000, 32767):
assert 0.0 <= tts.level_of(frame(amplitude)) <= 1.0
def test_it_rises_with_amplitude():
quiet, middling, loud = (tts.level_of(frame(a)) for a in (800, 4000, 12000))
assert quiet < middling < loud
def test_quiet_speech_still_moves_the_mouth_visibly():
"""The sqrt curve is the whole point. Speech spends most of its time well
below peak, so a linear map leaves the mouth barely open for normal talking
and the pet looks like it's mumbling."""
assert tts.level_of(frame(1500)) > 0.15
def test_an_empty_frame_is_silence_not_a_crash():
assert tts.level_of(np.zeros(0, dtype=np.int16)) == 0.0
# ── the envelope of a whole clip ────────────────────────────────────────────
def test_an_envelope_has_one_value_per_frame_at_the_requested_rate():
one_second = np.zeros(16000, dtype=np.int16)
assert len(tts.envelope(one_second, 16000, fps=30)) == pytest.approx(30, abs=1)
def test_an_envelope_tracks_loud_and_quiet_stretches():
pcm = np.concatenate([np.zeros(8000, dtype=np.int16), frame(20000, 8000)])
levels = tts.envelope(pcm, 16000, fps=10)
assert max(levels[:4]) == 0.0 # the silent half
assert min(levels[-4:]) > 0.5 # the loud half
def test_an_empty_clip_has_an_empty_envelope():
assert tts.envelope(np.zeros(0, dtype=np.int16), 16000) == []
# ── the drawn frame ─────────────────────────────────────────────────────────
@pytest.fixture
def window():
"""Needs a QApplication; run with QT_QPA_PLATFORM=offscreen."""
from PySide6.QtWidgets import QApplication
from bolt_pet.ui.pet_window import PetWindow
_app = QApplication.instance() or QApplication(["test"])
win = PetWindow()
yield win
win.close()
class FakeAnimation:
"""Stands in for sprite.Animation — _mouth_frame only wants `.frames`."""
def __init__(self, frames):
self.frames = list(frames)
def test_the_talking_frame_follows_the_level(window):
"""The talking frames are an openness ramp — closed first, widest last —
specifically so loudness can index them directly."""
animation = FakeAnimation(["closed", "a", "b", "c", "d", "open"])
assert window._mouth_frame(animation) is None # no level set yet
window.set_mouth(0.0)
assert window._mouth_frame(animation) == "closed"
window.set_mouth(1.0)
assert window._mouth_frame(animation) == "open"
window.set_mouth(0.5)
assert window._mouth_frame(animation) not in ("closed", "open")
def test_a_stale_level_gives_the_animation_back(window):
"""If the audio thread stops sending levels — TTS died, the clip ended
without a final zero — the mouth must not freeze half-open forever. After a
moment it falls back to the ordinary looping animation."""
animation = FakeAnimation(["a", "b", "c"])
window.set_mouth(0.9)
assert window._mouth_frame(animation) is not None
window._mouth_at = time.monotonic() - 5.0
assert window._mouth_frame(animation) is None
def test_a_single_frame_animation_falls_back_instead_of_indexing(window):
"""Art with one talking frame can't lip-sync; it must not try."""
window.set_mouth(1.0)
assert window._mouth_frame(FakeAnimation(["only"])) is None
assert window._mouth_frame(FakeAnimation([])) is None
def test_no_animation_at_all_is_handled(window):
window.set_mouth(0.5)
assert window._mouth_frame(None) is None
-140
View File
@@ -1,140 +0,0 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import numpy as np
import pytest
from bolt_pet.audio import mic
FRAME_LEN = 320 # small for fast tests
SAMPLE_RATE = 8000
class _ScriptedStream:
"""Replays a fixed list of frames, then quiet forever."""
def __init__(self, frames):
self._frames = list(frames)
def read(self, frames):
if self._frames:
frame = self._frames.pop(0)
else:
frame = np.zeros(FRAME_LEN, dtype=np.int16)
return frame.reshape(-1, 1), False
def _loud(n=1):
return [np.full(FRAME_LEN, 5000, dtype=np.int16) for _ in range(n)]
def _quiet(n=1):
return [np.zeros(FRAME_LEN, dtype=np.int16) for _ in range(n)]
def test_returns_none_when_nothing_ever_gets_loud():
stream = _ScriptedStream(_quiet(50))
calls = {"i": 0}
def should_continue():
calls["i"] += 1
return calls["i"] <= 50
result = mic.record_utterance(
stream, should_continue=should_continue,
rms_threshold=300, silence_end_sec=0.5, max_utterance_s=5, min_utterance_s=0.1,
frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE,
)
assert result is None
def test_captures_speech_and_stops_after_trailing_silence():
# speech, then enough silence to cross the silence_end_sec threshold
silence_end_sec = 0.5
silence_limit_frames = int(silence_end_sec * SAMPLE_RATE / FRAME_LEN)
frames = _loud(5) + _quiet(silence_limit_frames + 2)
stream = _ScriptedStream(frames)
result = mic.record_utterance(
stream, rms_threshold=300, silence_end_sec=silence_end_sec,
max_utterance_s=5, min_utterance_s=0.05,
frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE,
)
assert result is not None
# captured the loud frames plus the silence up to (and including) the
# frame that crossed the silence-end threshold, but not endless silence
assert len(result) < len(frames) * FRAME_LEN
def test_returns_none_if_utterance_shorter_than_minimum():
frames = _loud(1) + _quiet(2) # crosses silence limit almost immediately
stream = _ScriptedStream(frames)
result = mic.record_utterance(
stream, rms_threshold=300, silence_end_sec=0.05,
max_utterance_s=5, min_utterance_s=5.0, # impossible to satisfy
frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE,
)
assert result is None
def test_stops_at_max_utterance_even_without_silence():
max_utterance_s = 0.5
max_frames = int(max_utterance_s * SAMPLE_RATE / FRAME_LEN)
stream = _ScriptedStream(_loud(max_frames + 20)) # never goes quiet
result = mic.record_utterance(
stream, rms_threshold=300, silence_end_sec=10.0, # would never trigger
max_utterance_s=max_utterance_s, min_utterance_s=0.01,
frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE,
)
assert result is not None
assert len(result) == max_frames * FRAME_LEN
def test_returns_none_when_should_continue_stops_before_speech():
stream = _ScriptedStream(_quiet(100))
result = mic.record_utterance(stream, should_continue=lambda: False,
frame_len=FRAME_LEN, sample_rate=SAMPLE_RATE)
assert result is None
def test_list_input_devices_filters_to_input_capable(monkeypatch):
fake_devices = [
{"name": "Speakers", "max_input_channels": 0, "hostapi": 0},
{"name": "USB Mic", "max_input_channels": 2, "hostapi": 0},
{"name": "Line In", "max_input_channels": 1, "hostapi": 1},
]
fake_hostapis = [{"name": "MME"}, {"name": "WASAPI"}]
class _FakeSoundDevice:
@staticmethod
def query_devices():
return fake_devices
@staticmethod
def query_hostapis(index):
return fake_hostapis[index]
monkeypatch.setitem(sys.modules, "sounddevice", _FakeSoundDevice())
devices = mic.list_input_devices()
assert devices == [
{"index": 1, "name": "USB Mic", "hostapi": "MME"},
{"index": 2, "name": "Line In", "hostapi": "WASAPI"},
]
def test_pcm_to_wav_bytes_round_trips_via_wave_module():
import wave
import io
pcm = np.array([0, 100, -100, 32767, -32768], dtype=np.int16)
wav_bytes = mic.pcm_to_wav_bytes(pcm, sample_rate=16000)
with wave.open(io.BytesIO(wav_bytes), "rb") as wf:
assert wf.getnchannels() == 1
assert wf.getsampwidth() == 2
assert wf.getframerate() == 16000
frames = wf.readframes(wf.getnframes())
assert np.frombuffer(frames, dtype=np.int16).tolist() == pcm.tolist()
-169
View File
@@ -1,169 +0,0 @@
"""Screen layout logic — resolving `petctl jump` targets and describing the
setup. Pure: the monitor list is normally published by the UI, so none of this
needs a display."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import monitors as m
def grid():
"""The 2x2 setup this was built against: four 1080p screens.
[1 HDMI-0] [2 HDMI-1]
[3 DP-0 ] [4 DP-2 ]
"""
return [
m.Monitor(0, "HDMI-0", 0, 0, 1920, 1080, primary=False),
m.Monitor(1, "HDMI-1", 1920, 0, 1920, 1080, primary=False),
m.Monitor(2, "DP-0", 0, 1080, 1920, 1080, primary=True),
m.Monitor(3, "DP-2", 1920, 1080, 1920, 1080, primary=False),
]
def two():
return [
m.Monitor(0, "eDP-1", 0, 0, 1920, 1080, primary=True),
m.Monitor(1, "HDMI-1", 1920, 0, 2560, 1440),
]
def test_numbers_shown_to_humans_are_one_based():
left, right = two()
assert left.index == 0 and left.number == 1
assert right.index == 1 and right.number == 2
assert "1: eDP-1 1920x1080 (primary)" == left.label
def test_geometry_helpers():
screen = m.Monitor(1, "HDMI-1", 1920, 0, 1920, 1080)
assert screen.right == 3840 and screen.bottom == 1080
assert screen.center == (2880, 540)
assert screen.contains(1920, 0)
assert screen.contains(3839, 1079)
assert not screen.contains(3840, 0) # right edge is exclusive
assert not screen.contains(1919, 0)
def test_monitor_containing_and_nearest():
screens = grid()
assert m.monitor_containing(screens, 100, 100).name == "HDMI-0"
assert m.monitor_containing(screens, 2000, 1500).name == "DP-2"
assert m.monitor_containing(screens, -50, -50) is None
# off the desktop entirely still resolves to something
assert m.nearest_monitor(screens, -500, -500).name == "HDMI-0"
def test_resolve_by_number():
screens = grid()
assert m.resolve(screens, "3").name == "DP-0"
with pytest.raises(ValueError, match="no monitor 9"):
m.resolve(screens, "9")
with pytest.raises(ValueError):
m.resolve(screens, "0")
def test_resolve_next_and_prev_wrap():
screens = grid()
assert m.resolve(screens, "next", current=3).number == 1
assert m.resolve(screens, "prev", current=0).number == 4
assert m.resolve(screens, "next", current=0).number == 2
def test_resolve_primary_and_other():
screens = grid()
assert m.resolve(screens, "primary", current=0).name == "DP-0"
# "other" on a two-screen setup is genuinely the other one
pair = two()
assert m.resolve(pair, "other", current=0).number == 2
assert m.resolve(pair, "other", current=1).number == 1
def test_resolve_directions_on_a_grid():
screens = grid()
# from top-left (HDMI-0)
assert m.resolve(screens, "right", current=0).name == "HDMI-1"
assert m.resolve(screens, "down", current=0).name == "DP-0"
# from bottom-right (DP-2)
assert m.resolve(screens, "left", current=3).name == "DP-0"
assert m.resolve(screens, "up", current=3).name == "HDMI-1"
def test_direction_prefers_the_best_aligned_screen():
screens = grid()
# "right" from DP-0 (bottom-left) must pick DP-2 (same row), not HDMI-1,
# even though both are to the right.
assert m.resolve(screens, "right", current=2).name == "DP-2"
def test_resolve_direction_with_nothing_there():
screens = grid()
with pytest.raises(ValueError, match="no monitor to the left"):
m.resolve(screens, "left", current=0)
def test_resolve_by_name_is_fuzzy_but_refuses_ambiguity():
screens = grid()
assert m.resolve(screens, "dp-2").name == "DP-2"
assert m.resolve(screens, "HDMI-0").name == "HDMI-0"
with pytest.raises(ValueError, match="matches several"):
m.resolve(screens, "hdmi")
def test_resolve_unknown_spec_lists_the_options():
screens = two()
with pytest.raises(ValueError) as excinfo:
m.resolve(screens, "the big one")
assert "eDP-1" in str(excinfo.value) and "HDMI-1" in str(excinfo.value)
def test_resolve_without_a_current_screen_falls_back_to_primary():
screens = grid()
# primary is index 2, so "next" from nowhere is index 3
assert m.resolve(screens, "next", current=None).number == 4
# an out-of-range current is treated the same way rather than exploding
assert m.resolve(screens, "next", current=99).number == 4
def test_resolve_needs_monitors():
with pytest.raises(ValueError, match="no monitors"):
m.resolve([], "next")
with pytest.raises(ValueError, match="needs a target"):
m.resolve(grid(), "")
def test_random_always_moves_somewhere_else():
screens = grid()
for current in range(4):
assert m.resolve(screens, "random", current=current).index != current
def test_summary_is_one_line_and_marks_where_the_pet_is():
line = m.summary(grid(), current=1)
assert "\n" not in line
assert line.startswith("4 monitors:")
assert "Bolt is on 2" in line
assert m.summary([]) is None
assert "Bolt is on" not in m.summary(grid(), current=None)
def test_annotate_matches_the_screen_context_style():
out = m.annotate("what's on the other screen?", grid(), current=0)
assert out.startswith("what's on the other screen?")
assert "[4 monitors:" in out
# nothing to say, nothing added
assert m.annotate("hello", [], None) == "hello"
assert m.annotate("", grid(), 0) == ""
def test_describe_lists_every_screen_and_flags_the_pet():
text = m.describe(grid(), current=2)
assert text.count("\n") == 4 # header + 4 screens
assert "DP-0" in text and "+0+1080" in text
assert text.count("Bolt is here") == 1
assert m.describe([]) == "[pet] no monitor information available"
-90
View File
@@ -1,90 +0,0 @@
"""dbus-monitor parsing + the forward/rate-limit gate. No session bus
needed — the parser is fed canned dbus-monitor output."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet.notifications import Notification, NotificationGate, iter_notifications
SAMPLE = '''signal time=1710000000.1 sender=org.freedesktop.DBus -> destination=:1.7 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired
string ":1.7"
method call time=1710000001.2 sender=:1.72 -> destination=org.freedesktop.Notifications serial=88 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=Notify
string "Firefox"
uint32 0
string ""
string "Build finished"
string "All 42 tests passed"
array [
]
int32 -1
method call time=1710000002.3 sender=:1.80 -> destination=org.freedesktop.Notifications serial=91 path=/org/freedesktop/Notifications; interface=org.freedesktop.Notifications; member=Notify
string "Calendar"
uint32 0
string "calendar-icon"
string "Standup in 5 minutes"
string ""
array [
]
'''
def _parse(text=SAMPLE):
return list(iter_notifications(text.splitlines()))
def test_parses_each_notify_call():
parsed = _parse()
assert parsed == [
Notification(app="Firefox", summary="Build finished", body="All 42 tests passed"),
Notification(app="Calendar", summary="Standup in 5 minutes", body=""),
]
def test_unrelated_dbus_traffic_is_ignored():
noise = SAMPLE.split("method call")[0]
assert _parse(noise) == []
def test_trailing_notification_without_a_following_block_is_still_emitted():
assert _parse()[-1].summary == "Standup in 5 minutes"
def test_as_text_is_what_gets_sent_to_the_server():
assert _parse()[0].as_text() == "Firefox: Build finished — All 42 tests passed"
assert _parse()[1].as_text() == "Calendar: Standup in 5 minutes"
# ── gate ────────────────────────────────────────────────────────────────────
def _notification(summary="Build finished", app="Firefox"):
return Notification(app=app, summary=summary, body="")
def test_empty_filter_forwards_everything():
gate = NotificationGate("", min_interval=0)
assert gate.should_forward(_notification(), now=0) is True
def test_filter_regex_selects_what_is_worth_a_round_trip():
gate = NotificationGate(r"build|deploy", min_interval=0)
assert gate.should_forward(_notification("Build finished"), now=0) is True
assert gate.should_forward(_notification("New message from Dave"), now=1) is False
def test_rate_limit_drops_a_burst():
gate = NotificationGate("", min_interval=60)
assert gate.should_forward(_notification(), now=100) is True
assert gate.should_forward(_notification(), now=120) is False
assert gate.should_forward(_notification(), now=161) is True
def test_a_broken_regex_does_not_silence_the_bridge():
gate = NotificationGate("(unclosed", min_interval=0)
assert gate.should_forward(_notification(), now=0) is True
def test_empty_notifications_are_dropped():
gate = NotificationGate("", min_interval=0)
assert gate.should_forward(Notification(app="", summary="", body=""), now=0) is False
-85
View File
@@ -1,85 +0,0 @@
"""petctl parsing — the pseudo-commands the server can relay to drive the
pet's body instead of a shell."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import pet_actions
def test_non_pet_commands_are_left_alone():
assert pet_actions.parse("ls -la") is None
assert pet_actions.parse("systemctl restart nginx") is None
assert pet_actions.parse("") is None
# "petstore" must not be mistaken for the "pet" prefix
assert pet_actions.parse("petstore --list") is None
def test_move_to_an_anchor():
assert pet_actions.parse("petctl move top-left") == {"action": "move", "anchor": "top-left"}
assert pet_actions.parse("petctl move bottom_right") == {"action": "move", "anchor": "bottom-right"}
def test_move_to_coordinates():
assert pet_actions.parse("petctl move 300 120") == {"action": "move", "x": 300, "y": 120}
def test_move_rejects_nonsense_targets():
with pytest.raises(pet_actions.ActionError):
pet_actions.parse("petctl move sideways")
with pytest.raises(pet_actions.ActionError):
pet_actions.parse("petctl move")
def test_emotes():
assert pet_actions.parse("petctl emote wave") == {"action": "emote", "emote": "wave"}
with pytest.raises(pet_actions.ActionError):
pet_actions.parse("petctl emote moonwalk")
def test_say_keeps_the_whole_sentence():
assert pet_actions.parse('petctl say "build is green"') == {
"action": "say", "text": "build is green"
}
assert pet_actions.parse("petctl say build is green")["text"] == "build is green"
def test_wander_and_nap_toggles():
assert pet_actions.parse("petctl wander off") == {"action": "wander", "enabled": False}
assert pet_actions.parse("petctl nap on") == {"action": "nap", "enabled": True}
with pytest.raises(pet_actions.ActionError):
pet_actions.parse("petctl wander maybe")
def test_alternate_prefixes_and_verbs():
assert pet_actions.parse("bolt-pet goto center")["anchor"] == "center"
assert pet_actions.parse("pet do hop")["emote"] == "hop"
def test_unknown_verb_is_an_error_not_a_shell_command():
with pytest.raises(pet_actions.ActionError):
pet_actions.parse("petctl explode")
def test_describe_is_reported_back_to_the_server():
assert "top-left" in pet_actions.describe({"action": "move", "anchor": "top-left"})
assert "wave" in pet_actions.describe({"action": "emote", "emote": "wave"})
assert pet_actions.describe({"action": "help"}) == pet_actions.HELP
def test_voice_reset_parses_with_or_without_the_word_reset():
assert pet_actions.parse("petctl voice reset") == {"action": "voice", "voice": "default"}
assert pet_actions.parse("petctl voice default") == {"action": "voice", "voice": "default"}
assert pet_actions.parse("petctl voice") == {"action": "voice", "voice": "default"}
def test_petctl_cannot_be_used_to_pick_a_voice():
"""Choosing a voice is the server's job (speak_as) — it has the voice
library. petctl only ever undoes one, so an attempt to set a voice here
is pointed back at the marker that works."""
with pytest.raises(pet_actions.ActionError, match="speak_as"):
pet_actions.parse("petctl voice Terence")
-334
View File
@@ -1,334 +0,0 @@
"""Window-side behaviour for the newer features: emote curves, petctl
actions, edge snapping, napping, click-through.
Needs a QApplication — run with QT_QPA_PLATFORM=offscreen.
"""
import sys
from pathlib import Path
import pytest
from PySide6.QtCore import QPoint
from PySide6.QtWidgets import QApplication
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import config
from bolt_pet.state import PetState
from bolt_pet.ui.pet_window import (
_EMOTE_TICKS, _WALK_PIXELS_PER_FRAME, PetWindow, emote_transform,
)
from bolt_pet.ui.sprite import WALK
@pytest.fixture(scope="module")
def qt_app():
yield QApplication.instance() or QApplication([])
@pytest.fixture
def pet(qt_app):
window = PetWindow()
yield window
window.close()
# ── emote curves (pure maths) ───────────────────────────────────────────────
@pytest.mark.parametrize("emote", ["wave", "hop", "bounce", "spin", "nod", "shake", "wiggle"])
def test_every_emote_returns_the_sprite_to_rest(emote):
# Anything that doesn't land back at the identity transform leaves the pet
# permanently askew. (approx: the sine curves land on ~1e-16, not 0.0.)
rest = pytest.approx((0.0, 0.0, 0.0, 1.0), abs=1e-9)
assert emote_transform(emote, 1.0) == rest
assert emote_transform(emote, 0.0) == rest
def test_emotes_actually_move_the_sprite_mid_animation():
for emote in ("wave", "hop", "spin", "nod", "shake"):
samples = [emote_transform(emote, i / 20) for i in range(1, 20)]
assert any(sample != (0.0, 0.0, 0.0, 1.0) for sample in samples), emote
def test_an_unknown_emote_is_a_no_op_not_a_crash():
assert emote_transform("moonwalk", 0.5) == (0.0, 0.0, 0.0, 1.0)
def test_progress_is_clamped():
assert emote_transform("hop", 5.0) == emote_transform("hop", 1.0)
assert emote_transform("hop", -3.0) == emote_transform("hop", 0.0)
def test_an_emote_finishes_and_clears_itself(pet):
pet.start_emote("spin")
assert pet._emote == "spin"
for _ in range(_EMOTE_TICKS + 2):
pet._advance_emote()
assert pet._emote is None
# ── petctl actions ──────────────────────────────────────────────────────────
def test_move_action_sets_a_walk_target(pet):
pet.apply_action({"action": "move", "anchor": "top-left"})
assert pet._wander_target is not None
assert pet._commanded_move is True
def test_commanded_moves_happen_even_while_talking(pet, monkeypatch):
monkeypatch.setattr(config, "PET_EDGE_SNAP", False) # snapping would move it again on arrival
pet.set_state(PetState.TALKING)
pet.apply_action({"action": "move", "anchor": "top-left"})
target = pet._wander_target
if target is None:
pytest.skip("no usable screen geometry on this host")
for _ in range(2000):
pet._wander_tick()
if pet._wander_target is None:
break
assert pet.pos() == target
def test_a_commanded_move_survives_the_reply_arriving(pet):
# Real ordering: `petctl move` comes back as a tool call mid-turn, then
# the reply flips the pet to TALKING a moment later. That must not cancel
# the walk it was just told to make.
pet.apply_action({"action": "move", "anchor": "center"})
pet.set_state(PetState.TALKING)
assert pet._wander_target is not None
def test_move_to_explicit_coordinates_is_clamped_on_screen(pet):
pet.apply_action({"action": "move", "x": -5000, "y": -5000})
geo = pet._screen_geometry()
if geo is None:
pytest.skip("no usable screen geometry on this host")
assert pet._wander_target.x() >= geo.left()
assert pet._wander_target.y() >= geo.top()
def test_say_action_shows_the_bubble(pet):
pet.apply_action({"action": "say", "text": "build is green"})
assert pet._bubble.text == "build is green"
def test_wander_and_nap_actions(pet):
pet.apply_action({"action": "wander", "enabled": False})
assert pet._wander_enabled is False
pet.apply_action({"action": "nap", "enabled": True})
assert pet.napping is True
def test_emote_action_starts_the_emote(pet):
pet.apply_action({"action": "emote", "emote": "wave"})
assert pet._emote == "wave"
# ── napping ─────────────────────────────────────────────────────────────────
def test_napping_dims_the_pet_and_stops_it_wandering(pet):
pet.set_napping(True)
assert pet.windowOpacity() < 1.0
start = pet.pos()
pet.wander_now()
for _ in range(60):
pet._wander_tick()
assert pet.pos() == start
pet.set_napping(False)
assert pet.windowOpacity() == 1.0
# ── edge snapping ───────────────────────────────────────────────────────────
def test_snaps_flush_when_parked_near_an_edge(pet, monkeypatch):
geo = pet._screen_geometry()
if geo is None:
pytest.skip("no usable screen geometry on this host")
monkeypatch.setattr(config, "PET_EDGE_SNAP", True)
pet.move(geo.left() + 10, geo.top() + 10)
assert pet.snap_to_edge() is True
assert pet.pos() == QPoint(geo.left(), geo.top())
def test_does_not_snap_from_the_middle_of_the_screen(pet, monkeypatch):
geo = pet._screen_geometry()
if geo is None:
pytest.skip("no usable screen geometry on this host")
monkeypatch.setattr(config, "PET_EDGE_SNAP", True)
middle = QPoint(geo.left() + geo.width() // 2, geo.top() + geo.height() // 2)
pet.move(middle)
assert pet.snap_to_edge() is False
assert pet.pos() == middle
def test_snapping_can_be_turned_off(pet, monkeypatch):
geo = pet._screen_geometry()
if geo is None:
pytest.skip("no usable screen geometry on this host")
monkeypatch.setattr(config, "PET_EDGE_SNAP", False)
pet.move(geo.left() + 10, geo.top() + 10)
assert pet.snap_to_edge() is False
# ── click-through ───────────────────────────────────────────────────────────
def test_click_through_toggles_mouse_transparency(pet):
from PySide6.QtCore import Qt
pet.set_click_through(True)
assert pet.click_through is True
assert pet.testAttribute(Qt.WA_TransparentForMouseEvents) is True
pet.set_click_through(False)
assert pet.testAttribute(Qt.WA_TransparentForMouseEvents) is False
# ── monitors ────────────────────────────────────────────────────────────────
def test_window_publishes_a_monitor_list(pet):
"""Whatever the test host's screen setup is, the window must describe it
in the shape the controller expects."""
monitors = pet.monitors()
assert monitors, "offscreen Qt still reports at least one screen"
assert [m.index for m in monitors] == list(range(len(monitors)))
assert all(m.width > 0 and m.height > 0 for m in monitors)
assert all(m.name for m in monitors)
assert sum(1 for m in monitors if m.primary) <= 1
def test_publish_monitors_re_emits_when_forced(pet):
"""ui/app.py relies on this: the window is built before the controller
exists, so its constructor's publish reaches nobody and has to be redone."""
seen = []
pet.monitors_changed.connect(seen.append)
pet.publish_monitors() # force defaults to True
assert len(seen) == 1
pet.publish_monitors(force=False) # nothing changed -> stays quiet
assert len(seen) == 1
def test_pet_reports_which_monitor_it_is_on(pet):
seen = []
pet.pet_monitor_changed.connect(seen.append)
pet.publish_monitors()
assert seen and seen[-1] == pet.current_monitor_index()
assert 0 <= seen[-1] < len(pet.monitors())
def test_jump_moves_the_window_onto_the_target_screen(pet):
monitors = pet.monitors()
target = len(monitors) - 1
pet.apply_action({"action": "jump", "monitor": target})
assert pet.current_monitor_index() == target
# a jump lands with a hop rather than sliding there
assert pet._emote == "hop"
def test_jump_cancels_a_stroll_so_it_does_not_walk_back(pet):
pet.apply_action({"action": "move", "anchor": "top-left"})
assert pet._wander_target is not None
pet.apply_action({"action": "jump", "monitor": 0})
assert pet._wander_target is None
assert pet._commanded_move is False
def test_jump_to_a_bogus_index_is_a_no_op(pet):
before = pet.pos()
pet.apply_action({"action": "jump", "monitor": 99})
pet.apply_action({"action": "jump", "monitor": -1})
assert pet.pos() == before
# ── walk cycle ──────────────────────────────────────────────────────────────
def test_walk_art_loads_as_a_non_state_animation(pet):
"""Walking is a property of movement, not a PetState, so it lives outside
the state machine but still loads like any other animation."""
assert pet.sprites.has(WALK)
assert len(pet.sprites.get(WALK).frames) == 8
assert pet.sprites.get("nonsense") is pet.sprites.get(PetState.IDLE)
assert not pet.sprites.has("nonsense")
def test_walking_overrides_the_state_animation(pet):
assert pet._animation_key() == pet._current_state
pet._advance_walk(50, 0, 1.0)
assert pet._animation_key() == WALK
def test_walk_cycle_advances_by_distance_not_by_the_clock(pet):
"""The planted paw tracks backwards at the speed the window moves
forwards; drive it off the animation timer instead and the feet skate."""
anim = pet.sprites.get(WALK)
anim.reset()
pet._advance_walk(100, 0, _WALK_PIXELS_PER_FRAME * 3)
assert anim._index == 3
# a step too small to cross the threshold banks the distance instead
pet._advance_walk(100, 0, _WALK_PIXELS_PER_FRAME * 0.5)
assert anim._index == 3
pet._advance_walk(100, 0, _WALK_PIXELS_PER_FRAME * 0.5)
assert anim._index == 4
def test_the_animation_timer_does_not_double_step_the_walk(pet):
anim = pet.sprites.get(WALK)
pet._advance_walk(50, 0, 1.0)
anim.reset()
pet._advance_frame()
assert anim._index == 0
def test_facing_follows_horizontal_travel(pet):
pet._advance_walk(50, 0, 1.0)
assert pet._facing == 1
pet._advance_walk(-50, 0, 1.0)
assert pet._facing == -1
def test_a_near_vertical_stroll_does_not_flip_him(pet):
"""Rounding noise on dx would otherwise flip him back and forth every
tick on a straight-up walk."""
pet._facing = 1
pet._advance_walk(0.4, 60, 1.0)
assert pet._facing == 1
def test_walking_left_paints_a_mirrored_frame(pet):
frame = pet.sprites.get(WALK).current()
pet._facing = 1
assert pet._oriented(frame) is frame # art is drawn facing right
pet._facing = -1
flipped = pet._oriented(frame)
assert flipped is not frame
assert flipped.size() == frame.size()
assert pet._oriented(frame) is flipped # cached, not re-flipped per paint
def test_stopping_resets_the_cycle_to_a_standing_frame(pet):
pet._advance_walk(50, 0, _WALK_PIXELS_PER_FRAME * 2)
assert pet._walking
pet._stop_walking()
assert not pet._walking
assert pet._walk_distance == 0.0
assert pet.sprites.get(WALK)._index == 0
assert pet._animation_key() == pet._current_state
def test_walk_art_suppresses_the_hard_coded_bob(pet):
"""The frames carry their own weight shift — bobbing the window as well
would double it up."""
pet._advance_walk(50, 0, 5.0)
assert pet._bob_offset == 0
def test_without_walk_art_it_falls_back_to_the_old_bob(qt_app, tmp_path):
window = PetWindow(sprite_dir=tmp_path)
try:
assert not window.sprites.has(WALK)
window._advance_walk(50, 0, 5.0)
assert window._walking
assert window._animation_key() == window._current_state
assert window._bob_offset < 0 # still visibly moving
finally:
window.close()
-289
View File
@@ -1,289 +0,0 @@
"""End-to-end: microphone in, speech out, over real HTTP.
Every other test in this suite injects a fake at the seam it cares about, and
every one of them passed all week while these got through to production:
- notifications sitting unspoken for minutes (a clock stamped in the wrong
order, two correct units)
- the pet saying the same thing twice (server discarded prose, model repeated
it — both sides behaving as written)
- `[laughing]` read out loud (a tag that means something to one model and
nothing to the next one down the pipe)
- a device command written as prose (extractor fine, prompt fine, no marker)
They were all *interaction* bugs. So this one runs the actual pipeline against
a real socket: a threaded HTTP server that speaks the desk protocol, the real
`server_client` doing real requests (including NDJSON streaming), the real
controller loop and state machine. The only fakes are where the hardware is —
the mic stream and the speakers — because those are the two things a test
genuinely cannot have.
"""
import json
import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
import numpy as np
import pytest
from PySide6.QtWidgets import QApplication
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import controller as controller_mod
from bolt_pet.notifications import Notification
from bolt_pet.state import PetState
_app = QApplication.instance() or QApplication(["test"])
# ── a desk server that actually listens on a port ───────────────────────────
class FakeDesk:
"""Scripted responses, real HTTP. Set `.script` per test."""
def __init__(self):
self.script = {}
self.requests = []
self._server = ThreadingHTTPServer(("127.0.0.1", 0), self._handler())
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
self._thread.start()
@property
def url(self) -> str:
host, port = self._server.server_address[:2]
return f"http://{host}:{port}"
def stop(self) -> None:
self._server.shutdown()
self._server.server_close()
def _handler(self):
desk = self
class Handler(BaseHTTPRequestHandler):
def log_message(self, *_args):
pass # the test output is not an access log
def do_GET(self):
path = self.path.split("?")[0]
desk.requests.append(("GET", path))
self._json(desk.script.get(path, {"files": []}))
def do_POST(self):
length = int(self.headers.get("Content-Length") or 0)
body = json.loads(self.rfile.read(length) or b"{}")
path = self.path.split("?")[0]
desk.requests.append(("POST", path, body))
response = desk.script.get(path)
if callable(response):
response = response(body)
if path.endswith("converse_stream"):
self._ndjson(response or [])
else:
self._json(response if response is not None else {"type": "reply", "text": "ok"})
def _json(self, payload):
data = json.dumps(payload).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def _ndjson(self, events):
self.send_response(200)
self.send_header("Content-Type", "application/x-ndjson")
self.end_headers()
for event in events:
self.wfile.write((json.dumps(event) + "\n").encode())
self.wfile.flush()
return Handler
class FakeMic:
"""Loud frames then quiet ones, so the VAD ends the utterance on its own."""
def __init__(self, loud=8, quiet=60):
self.frames = ([np.full((320, 1), 4000, dtype=np.int16)] * loud
+ [np.zeros((320, 1), dtype=np.int16)] * quiet)
def read(self, _n):
return (self.frames.pop(0) if self.frames
else np.zeros((320, 1), dtype=np.int16)), None
def __enter__(self):
return self
def __exit__(self, *_a):
return False
@pytest.fixture
def pipeline(monkeypatch):
"""A controller wired to a real local server, with fake ears and mouth."""
desk = FakeDesk()
spoken: list[str] = []
levels: list[float] = []
monkeypatch.setattr(controller_mod.config, "SERVER_URL", desk.url)
monkeypatch.setattr(controller_mod.config, "API_KEY", "test-key")
monkeypatch.setattr(controller_mod.config, "SESSION_ID", "pet-smoke")
monkeypatch.setattr(controller_mod.config, "SILENCE_END_SEC", 0.2)
monkeypatch.setattr(controller_mod.config, "MIN_UTTERANCE_S", 0.0)
monkeypatch.setattr(controller_mod.config, "RECEIVE_FILES", False)
# Off by default so each test picks its own path; the streaming tests opt in.
monkeypatch.setattr(controller_mod.config, "STREAMING_REPLIES", False)
monkeypatch.setattr(controller_mod.screen_context, "context_for", lambda text: text)
monkeypatch.setattr(controller_mod.screen_context, "is_fullscreen_active", lambda: False)
# Deepgram and the speakers are the two things a test can't have.
monkeypatch.setattr(controller_mod.stt, "transcribe", lambda pcm: "what's on my disk?")
monkeypatch.setattr(controller_mod.stt_stream.StreamingTranscriber, "open",
classmethod(lambda cls, **kw: None))
def fake_speak(text, on_error=None, should_stop=None, voice_id=None, on_level=None):
spoken.append(text)
if on_level is not None:
on_level(0.8) # the mouth opens while a word plays...
levels.append(0.8)
return True
monkeypatch.setattr(controller_mod.tts, "speak", fake_speak)
ctrl = controller_mod.PetController()
ctrl._stream = FakeMic()
try:
yield ctrl, desk, spoken, levels
finally:
desk.stop()
# ── the whole path ──────────────────────────────────────────────────────────
def test_a_plain_turn_goes_mic_to_speaker(pipeline):
ctrl, desk, spoken, _levels = pipeline
desk.script["/desk/converse"] = {"type": "reply", "text": "About sixty percent full."}
ctrl._handle_conversation_turn()
assert spoken == ["About sixty percent full."]
assert ctrl._state.state == PetState.IDLE
posted = [r for r in desk.requests if r[0] == "POST"]
assert posted[0][1] == "/desk/converse"
assert "what's on my disk?" in posted[0][2]["text"]
assert ctrl.history.entries()[-1].text == "About sixty percent full."
def test_a_streamed_turn_speaks_each_sentence_as_it_lands(pipeline, monkeypatch):
"""The NDJSON is parsed by the real client over a real socket — the layer
that a mocked `converse` can never exercise."""
ctrl, desk, spoken, _levels = pipeline
monkeypatch.setattr(controller_mod.config, "STREAMING_REPLIES", True)
desk.script["/desk/converse_stream"] = [
{"type": "say", "text": "The disk is fine."},
{"type": "say", "text": "About sixty percent used."},
{"type": "reply", "text": "The disk is fine. About sixty percent used.",
"already_spoken": True},
]
ctrl._handle_conversation_turn()
assert spoken == ["The disk is fine.", "About sixty percent used."]
# The final reply must not be spoken a third time.
assert len(spoken) == 2
def test_a_tool_turn_says_give_me_a_sec_then_the_answer(pipeline, monkeypatch):
"""Holding line, relayed command, and the real answer — in that order."""
ctrl, desk, spoken, _levels = pipeline
ran = []
monkeypatch.setattr(controller_mod.server_client, "run_local_command",
lambda cmd: ran.append(cmd) or "[exit 0]\n60% used")
desk.script["/desk/converse"] = {
"type": "command", "command": "df -h /", "token": "tok",
"say": "Let me check that for you.",
}
desk.script["/desk/tool_result"] = {"type": "reply", "text": "Sixty percent used."}
ctrl._handle_conversation_turn()
assert spoken == ["Let me check that for you.", "Sixty percent used."]
assert ran == ["df -h /"]
relayed = next(r for r in desk.requests if r[0] == "POST" and r[1] == "/desk/tool_result")
assert relayed[2]["output"].endswith("60% used")
def test_a_device_command_never_reaches_the_shell(pipeline, monkeypatch):
ctrl, desk, spoken, _levels = pipeline
ran = []
monkeypatch.setattr(controller_mod.server_client, "run_local_command", lambda cmd: ran.append(cmd))
actions = []
ctrl.action.connect(actions.append)
desk.script["/desk/converse"] = {
"type": "command", "command": "petctl emote wave", "token": "tok"}
desk.script["/desk/tool_result"] = {"type": "reply", "text": "There you go."}
ctrl._handle_conversation_turn()
assert ran == []
assert actions == [{"action": "emote", "emote": "wave"}]
assert spoken == ["There you go."]
def test_the_mouth_moves_with_the_audio(pipeline):
"""Lip-sync is only real if the level actually reaches the window."""
ctrl, desk, _spoken, levels = pipeline
mouth = []
ctrl.mouth.connect(mouth.append)
desk.script["/desk/converse"] = {"type": "reply", "text": "Talking now."}
ctrl._handle_conversation_turn()
assert levels, "TTS was never given a level callback"
assert 0.8 in mouth # opened while speaking...
assert mouth[-1] == 0.0 # ...and closed at the end
def test_a_notification_is_forwarded_and_spoken(pipeline):
"""The path that was silently sitting for five to ten minutes."""
ctrl, desk, spoken, _levels = pipeline
ctrl._notification_gate = controller_mod.notifications.NotificationGate("", 0)
desk.script["/desk/converse"] = {"type": "reply", "text": "Harry says he's around."}
ctrl._queue_notification(Notification(app="Signal", summary="Harry", body="you there?"))
ctrl._maybe_heartbeat()
assert spoken == ["Harry says he's around."]
forwarded = next(r for r in desk.requests if r[0] == "POST" and r[1] == "/desk/converse")
assert "Harry" in forwarded[2]["text"]
def test_streaming_falls_back_when_the_server_is_older(pipeline, monkeypatch):
"""A server without /desk/converse_stream (or one that answers with nothing)
must not cost a turn — the client drops to the plain endpoint. This is how
the pet keeps working against a container that hasn't been updated yet."""
ctrl, desk, spoken, _levels = pipeline
monkeypatch.setattr(controller_mod.config, "STREAMING_REPLIES", True)
desk.script["/desk/converse_stream"] = [] # nothing streamed back
desk.script["/desk/converse"] = {"type": "reply", "text": "Still here."}
ctrl._handle_conversation_turn()
assert spoken == ["Still here."]
paths = [r[1] for r in desk.requests if r[0] == "POST"]
assert paths == ["/desk/converse_stream", "/desk/converse"]
def test_a_dead_server_leaves_the_pet_usable(pipeline):
"""It should flash an error and go back to listening, not wedge."""
ctrl, desk, spoken, _levels = pipeline
desk.stop() # the server disappears mid-session
states = []
ctrl.state_changed.connect(states.append)
ctrl._handle_conversation_turn()
assert states[-2:] == ["error", "idle"]
assert spoken == []
-54
View File
@@ -1,54 +0,0 @@
"""Quiet-hours parsing and matching, without waiting for 11pm."""
import sys
from datetime import time as dtime
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import quiet
def test_no_spec_means_never_quiet():
assert quiet.is_quiet("") is False
assert quiet.is_quiet(" ") is False
def test_simple_daytime_range():
ranges = quiet.parse_ranges("13:00-14:00")
assert quiet.in_ranges(dtime(13, 30), ranges) is True
assert quiet.in_ranges(dtime(12, 59), ranges) is False
assert quiet.in_ranges(dtime(14, 0), ranges) is False # end is exclusive
def test_range_wrapping_past_midnight():
ranges = quiet.parse_ranges("23:00-08:00")
for moment in (dtime(23, 0), dtime(23, 59), dtime(0, 0), dtime(7, 59)):
assert quiet.in_ranges(moment, ranges) is True, moment
for moment in (dtime(8, 0), dtime(12, 0), dtime(22, 59)):
assert quiet.in_ranges(moment, ranges) is False, moment
def test_multiple_ranges():
ranges = quiet.parse_ranges("23:00-08:00, 13:00-14:00")
assert quiet.in_ranges(dtime(13, 15), ranges) is True
assert quiet.in_ranges(dtime(2, 0), ranges) is True
assert quiet.in_ranges(dtime(16, 0), ranges) is False
def test_zero_length_range_is_not_all_day():
assert quiet.in_ranges(dtime(12, 0), quiet.parse_ranges("09:00-09:00")) is False
def test_malformed_specs_raise_when_parsed_directly():
for spec in ("nonsense", "25:00-26:00", "13:00", "13:60-14:00"):
with pytest.raises(quiet.QuietHoursError):
quiet.parse_ranges(spec)
def test_malformed_spec_is_reported_but_never_mutes_the_pet():
seen = []
assert quiet.is_quiet("nonsense", now=dtime(3, 0), on_error=seen.append) is False
assert len(seen) == 1
-56
View File
@@ -1,56 +0,0 @@
"""Screen-context parsing/annotation. The subprocess probes are platform
specific; the parsing they feed is not, so that's what's tested here."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import screen_context
def test_parses_the_active_window_id():
output = "_NET_ACTIVE_WINDOW(WINDOW): window id # 0x3c00007\n"
assert screen_context.parse_xprop_window_id(output) == "0x3c00007"
def test_no_active_window_id_when_nothing_is_focused():
assert screen_context.parse_xprop_window_id("_NET_ACTIVE_WINDOW(WINDOW): window id # 0x0") is None
assert screen_context.parse_xprop_window_id("") is None
def test_parses_the_window_title():
output = '_NET_WM_NAME(UTF8_STRING) = "bolt_pet/controller.py - Cursor"\n'
assert screen_context.parse_xprop_window_name(output) == "bolt_pet/controller.py - Cursor"
def test_unset_title_property():
assert screen_context.parse_xprop_window_name("_NET_WM_NAME: not found") is None
def test_fullscreen_state_detection():
assert screen_context.parse_xprop_fullscreen(
"_NET_WM_STATE(ATOM) = _NET_WM_STATE_FULLSCREEN, _NET_WM_STATE_FOCUSED") is True
assert screen_context.parse_xprop_fullscreen("_NET_WM_STATE(ATOM) = _NET_WM_STATE_FOCUSED") is False
def test_desktop_titles_are_treated_as_no_context():
assert screen_context.clean_title("Desktop") is None
assert screen_context.clean_title(" ") is None
def test_long_titles_are_truncated():
cleaned = screen_context.clean_title("x" * 500)
assert len(cleaned) <= 160 and cleaned.endswith("…")
def test_annotate_appends_context_as_an_aside():
annotated = screen_context.annotate("what's this error?", "app.py — Traceback")
assert annotated.startswith("what's this error?")
assert "[on screen right now: app.py — Traceback]" in annotated
def test_annotate_is_a_no_op_without_a_title_or_text():
assert screen_context.annotate("hello", None) == "hello"
assert screen_context.annotate("hello", "Desktop") == "hello"
assert screen_context.annotate("", "Firefox") == ""
-168
View File
@@ -1,168 +0,0 @@
"""OCR plumbing for `petctl read` — engine selection and output cleanup.
Only the pure half is covered, per the testing conventions: capture and the
OCR call itself need a real screen and a real engine. Engine probes are
injected so these pass on a machine with a different set installed (or none).
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import screen_text
from bolt_pet.monitors import Monitor
def probes(modules=(), binaries=()):
return (lambda name: name in modules), (
lambda name: f"/usr/bin/{name}" if name in binaries else None
)
def test_prefers_tesseract_when_fully_installed():
has_module, which = probes({"pytesseract"}, {"tesseract"})
assert screen_text.resolve_engine(has_module, which) == ("pytesseract", "")
def test_falls_back_to_rapidocr_when_tesseract_binary_is_absent():
has_module, which = probes({"pytesseract", "rapidocr_onnxruntime"}, set())
engine, reason = screen_text.resolve_engine(has_module, which)
assert engine == "rapidocr"
assert reason == ""
def test_pytesseract_without_the_binary_says_which_half_is_missing():
"""The commonest broken setup: `pip install pytesseract` and stop, not
realising the actual engine is a system package."""
has_module, which = probes({"pytesseract"}, set())
engine, reason = screen_text.resolve_engine(has_module, which)
assert engine is None
assert "tesseract binary" in reason
assert "apt install tesseract-ocr" in reason
def test_nothing_installed_explains_how_to_fix_it():
has_module, which = probes(set(), set())
engine, reason = screen_text.resolve_engine(has_module, which)
assert engine is None
assert "pip install" in reason
def test_capture_availability_follows_mss():
assert screen_text.capture_available(lambda name: name == "mss")
assert not screen_text.capture_available(lambda name: False)
def test_clean_drops_ocr_noise_and_blank_runs():
raw = "Firefox\n\n\n |\n .\nBuild failed\n~\n"
assert screen_text.clean_ocr_text(raw) == "Firefox\nBuild failed"
def test_clean_collapses_whitespace_but_keeps_line_structure():
raw = " File edit view \nline\ttwo "
assert screen_text.clean_ocr_text(raw) == "File edit view\nline two"
def test_clean_drops_consecutive_duplicates_only():
raw = "Terminal\nTerminal\nEditor\nTerminal"
assert screen_text.clean_ocr_text(raw) == "Terminal\nEditor\nTerminal"
def test_clean_keeps_short_but_real_tokens():
# two alphanumerics is the bar — "ok" and "42" survive, "-" doesn't
assert screen_text.clean_ocr_text("ok\n-\n42") == "ok\n42"
def test_clean_truncates_and_says_so():
out = screen_text.clean_ocr_text("word " * 500, max_chars=100)
assert out.endswith("[truncated]")
# the cap applies to the text, before the marker is appended
assert len(out.split("\n[truncated]")[0]) <= 100
def test_clean_handles_empty_input():
assert screen_text.clean_ocr_text("") == ""
assert screen_text.clean_ocr_text(None) == ""
def test_format_reading_names_the_monitor():
monitor = Monitor(1, "HDMI-1", 1920, 0, 1920, 1080)
out = screen_text.format_reading(monitor, "Build failed")
assert "monitor 2 (HDMI-1)" in out
assert out.endswith("Build failed")
def test_format_reading_when_nothing_was_recognised():
monitor = Monitor(0, "DP-0", 0, 0, 1920, 1080)
assert "no text recognised" in screen_text.format_reading(monitor, " ")
def test_read_monitor_never_raises_without_an_engine(monkeypatch):
"""Its return value goes back to the server as command output, so every
failure has to come back as a sentence rather than an exception."""
monkeypatch.setattr(screen_text, "capture_available", lambda *a, **k: True)
monkeypatch.setattr(
screen_text, "resolve_engine", lambda *a, **k: (None, "no engine here")
)
out = screen_text.read_monitor(Monitor(0, "DP-0", 0, 0, 1920, 1080))
assert out.startswith("[pet]")
assert "no engine here" in out
def test_read_monitor_reports_a_failed_capture(monkeypatch):
monkeypatch.setattr(screen_text, "capture_available", lambda *a, **k: True)
monkeypatch.setattr(screen_text, "resolve_engine", lambda *a, **k: ("pytesseract", ""))
monkeypatch.setattr(screen_text, "capture", lambda monitor: None)
out = screen_text.read_monitor(Monitor(0, "DP-0", 0, 0, 1920, 1080))
assert "couldn't capture" in out and "Wayland" in out
def test_read_monitor_survives_an_exploding_engine(monkeypatch):
monkeypatch.setattr(screen_text, "capture_available", lambda *a, **k: True)
monkeypatch.setattr(screen_text, "resolve_engine", lambda *a, **k: ("pytesseract", ""))
monkeypatch.setattr(screen_text, "capture", lambda monitor: object())
monkeypatch.setattr(
screen_text, "_ocr", lambda image, engine: (_ for _ in ()).throw(RuntimeError("boom"))
)
out = screen_text.read_monitor(Monitor(0, "DP-0", 0, 0, 1920, 1080))
assert "OCR failed" in out and "boom" in out
def test_read_monitors_splits_the_budget(monkeypatch):
seen = []
def fake_read(monitor, max_chars):
seen.append((monitor.number, max_chars))
return f"screen {monitor.number}"
monkeypatch.setattr(screen_text, "read_monitor", fake_read)
screens = [
Monitor(0, "A", 0, 0, 100, 100),
Monitor(1, "B", 100, 0, 100, 100),
Monitor(2, "C", 200, 0, 100, 100),
]
out = screen_text.read_monitors(screens, 3000)
assert [n for n, _ in seen] == [1, 2, 3]
assert all(limit == 1000 for _, limit in seen)
assert out.count("screen ") == 3
def test_read_monitors_keeps_a_floor_on_the_budget(monkeypatch):
monkeypatch.setattr(
screen_text, "read_monitor", lambda monitor, max_chars: str(max_chars)
)
screens = [Monitor(i, str(i), 0, 0, 10, 10) for i in range(20)]
# 100/20 would be 5 characters per screen, which is useless — floor wins
assert "400" in screen_text.read_monitors(screens, 100)
def test_read_monitors_with_one_screen_uses_the_whole_budget(monkeypatch):
monkeypatch.setattr(
screen_text, "read_monitor", lambda monitor, max_chars: str(max_chars)
)
assert screen_text.read_monitors([Monitor(0, "A", 0, 0, 10, 10)], 4000) == "4000"
def test_read_monitors_with_no_screens():
assert "no monitor information" in screen_text.read_monitors([], 4000)
-233
View File
@@ -1,233 +0,0 @@
"""End-to-end wiring for the multi-monitor features: `petctl jump`, `petctl
monitors`, `petctl read`, and the screen-layout note that rides along with
each utterance.
Needs a QApplication (signals), so run with QT_QPA_PLATFORM=offscreen.
"""
import sys
from pathlib import Path
import pytest
from PySide6.QtWidgets import QApplication
from bolt_pet import controller as controller_mod
from bolt_pet import pet_actions
from bolt_pet.monitors import Monitor
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
_app = QApplication.instance() or QApplication(["test"])
@pytest.fixture(autouse=True)
def no_screen_probes(monkeypatch):
monkeypatch.setattr(controller_mod.screen_context, "is_fullscreen_active", lambda: False)
monkeypatch.setattr(controller_mod.screen_context, "context_for", lambda text: text)
@pytest.fixture
def ctrl():
controller = controller_mod.PetController()
controller.set_monitors(
[
Monitor(0, "HDMI-0", 0, 0, 1920, 1080),
Monitor(1, "HDMI-1", 1920, 0, 1920, 1080),
Monitor(2, "DP-0", 0, 1080, 1920, 1080, primary=True),
]
)
controller.set_pet_monitor(0)
return controller
def _capture(signal):
events = []
signal.connect(lambda *a: events.append(a[0] if len(a) == 1 else a))
return events
# ── parsing ─────────────────────────────────────────────────────────────────
def test_jump_parses_without_validating_the_target():
"""Which monitors exist is a runtime fact, so the pure parser passes the
spec through and monitors.resolve() judges it later."""
assert pet_actions.parse("petctl jump 2") == {"action": "jump", "target": "2"}
assert pet_actions.parse("petctl jump next") == {"action": "jump", "target": "next"}
assert pet_actions.parse("petctl monitor left") == {"action": "jump", "target": "left"}
assert pet_actions.parse("petctl screen HDMI-1") == {"action": "jump", "target": "HDMI-1"}
# a nonsense target is still parsed — it fails at resolve time, with a
# message listing the real monitors
assert pet_actions.parse("petctl jump sideways") == {
"action": "jump", "target": "sideways",
}
def test_jump_needs_a_target():
with pytest.raises(pet_actions.ActionError, match="needs a monitor"):
pet_actions.parse("petctl jump")
def test_read_defaults_to_the_current_screen():
assert pet_actions.parse("petctl read") == {"action": "read", "target": "here"}
assert pet_actions.parse("petctl read all") == {"action": "read", "target": "all"}
assert pet_actions.parse("petctl look 2") == {"action": "read", "target": "2"}
assert pet_actions.parse("petctl see here") == {"action": "read", "target": "here"}
def test_monitors_verb():
for spelling in ("monitors", "screens", "displays"):
assert pet_actions.parse(f"petctl {spelling}") == {"action": "monitors"}
def test_help_mentions_the_new_verbs():
assert "petctl jump" in pet_actions.HELP
assert "petctl read" in pet_actions.HELP
assert "petctl monitors" in pet_actions.HELP
# ── jump ────────────────────────────────────────────────────────────────────
def test_jump_resolves_to_an_index_before_reaching_the_window(ctrl):
"""The controller resolves and emits a concrete index, so the window can't
re-resolve the spec against a different screen ordering."""
actions = _capture(ctrl.action)
out = ctrl._handle_command("petctl jump next")
assert actions == [{"action": "jump", "monitor": 1}]
assert "monitor 2: HDMI-1" in out
def test_jump_by_direction_uses_the_published_layout(ctrl):
actions = _capture(ctrl.action)
ctrl._handle_command("petctl jump down")
assert actions == [{"action": "jump", "monitor": 2}]
def test_jump_tracks_where_the_pet_actually_is(ctrl):
ctrl.set_pet_monitor(1)
actions = _capture(ctrl.action)
ctrl._handle_command("petctl jump left")
assert actions == [{"action": "jump", "monitor": 0}]
def test_jump_to_a_nonexistent_monitor_reports_back_and_moves_nothing(ctrl):
actions = _capture(ctrl.action)
out = ctrl._handle_command("petctl jump 7")
assert actions == []
assert "no monitor 7" in out
assert "you have 3" in out
def test_jump_never_reaches_the_shell(monkeypatch, ctrl):
ran = []
monkeypatch.setattr(controller_mod.server_client, "run_local_command", ran.append)
ctrl._handle_command("petctl jump 2")
ctrl._handle_command("petctl monitors")
ctrl._handle_command("petctl read")
assert ran == []
# ── monitors ────────────────────────────────────────────────────────────────
def test_monitors_lists_the_layout_and_where_the_pet_is(ctrl):
ctrl.set_pet_monitor(2)
out = ctrl._handle_command("petctl monitors")
assert "3 monitor(s)" in out
assert "HDMI-0" in out and "DP-0" in out
assert out.count("Bolt is here") == 1
def test_monitors_before_the_ui_has_published_anything():
fresh = controller_mod.PetController()
assert "no monitor information" in fresh._handle_command("petctl monitors")
# ── read ────────────────────────────────────────────────────────────────────
def test_read_here_uses_the_pets_own_screen(monkeypatch, ctrl):
seen = []
monkeypatch.setattr(
controller_mod.screen_text, "read_monitor",
lambda monitor, limit: seen.append(monitor.number) or "text",
)
ctrl.set_pet_monitor(1)
assert ctrl._handle_command("petctl read") == "text"
assert seen == [2]
def test_read_a_named_screen(monkeypatch, ctrl):
seen = []
monkeypatch.setattr(
controller_mod.screen_text, "read_monitor",
lambda monitor, limit: seen.append(monitor.name) or "text",
)
ctrl._handle_command("petctl read DP-0")
assert seen == ["DP-0"]
def test_read_all_goes_through_the_multi_screen_path(monkeypatch, ctrl):
seen = []
monkeypatch.setattr(
controller_mod.screen_text, "read_monitors",
lambda monitors, limit: seen.append(len(monitors)) or "everything",
)
assert ctrl._handle_command("petctl read all") == "everything"
assert seen == [3]
def test_read_an_unknown_screen_explains_rather_than_raising(ctrl):
out = ctrl._handle_command("petctl read 9")
assert out.startswith("[pet]")
assert "no monitor 9" in out
def test_read_respects_the_kill_switch(monkeypatch, ctrl):
monkeypatch.setattr(controller_mod.config, "SCREEN_TEXT", False)
called = []
monkeypatch.setattr(
controller_mod.screen_text, "read_monitor",
lambda *a, **k: called.append(1) or "text",
)
out = ctrl._handle_command("petctl read")
assert called == []
assert "disabled" in out and "SCREEN_TEXT" in out
def test_read_passes_the_character_cap_through(monkeypatch, ctrl):
monkeypatch.setattr(controller_mod.config, "SCREEN_TEXT_MAX_CHARS", 123)
seen = []
monkeypatch.setattr(
controller_mod.screen_text, "read_monitor",
lambda monitor, limit: seen.append(limit) or "text",
)
ctrl._handle_command("petctl read")
assert seen == [123]
# ── per-turn context ────────────────────────────────────────────────────────
def test_layout_rides_along_with_each_utterance(monkeypatch, ctrl):
monkeypatch.setattr(controller_mod.config, "MONITOR_CONTEXT", True)
out = ctrl._with_context("what's on the other screen?")
assert out.startswith("what's on the other screen?")
assert "3 monitors:" in out
assert "Bolt is on 1" in out
def test_layout_context_can_be_switched_off(monkeypatch, ctrl):
monkeypatch.setattr(controller_mod.config, "MONITOR_CONTEXT", False)
assert ctrl._with_context("hello") == "hello"
def test_screen_text_never_rides_along_automatically(monkeypatch, ctrl):
"""The layout is free; the *contents* cost an OCR pass and a lot of
privacy, so they only ever move on an explicit petctl read."""
monkeypatch.setattr(controller_mod.config, "MONITOR_CONTEXT", True)
called = []
monkeypatch.setattr(
controller_mod.screen_text, "read_monitor", lambda *a, **k: called.append(1)
)
monkeypatch.setattr(
controller_mod.screen_text, "read_monitors", lambda *a, **k: called.append(1)
)
ctrl._with_context("hello")
assert called == []
-158
View File
@@ -1,158 +0,0 @@
"""`petctl self_restart` — the pet restarting itself and remembering why.
Everything here runs against a temp context file and a fake subprocess runner,
so the tests exercise the arming/preflight/report logic without any process
actually dying.
"""
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import pet_actions, self_restart
@pytest.fixture
def state(tmp_path):
return tmp_path / "restart_context.json"
def _ok_run(*args, **kwargs):
return SimpleNamespace(returncode=0, stdout="", stderr="")
def _broken_run(*args, **kwargs):
return SimpleNamespace(
returncode=1, stdout="",
stderr=' File "bolt_pet/controller.py", line 42\n def _speak(\nSyntaxError: invalid syntax',
)
# ── parsing ─────────────────────────────────────────────────────────────────
def test_self_restart_parses_with_a_free_text_reason():
action = pet_actions.parse("petctl self_restart check the new walk cycle loads")
assert action == {
"action": "self_restart", "reason": "check the new walk cycle loads",
}
def test_self_restart_needs_no_reason_and_accepts_aliases():
assert pet_actions.parse("petctl self_restart")["reason"] == ""
assert pet_actions.parse("petctl restart")["action"] == "self_restart"
assert pet_actions.parse("petctl reboot")["action"] == "self_restart"
def test_self_restart_is_listed_in_the_help():
assert "self_restart" in pet_actions.HELP
# ── preflight ───────────────────────────────────────────────────────────────
def test_preflight_passes_when_the_code_imports():
self_restart.preflight(run=_ok_run) # no exception
def test_preflight_hands_back_the_traceback_instead_of_dying(state):
"""The whole point: a syntax error Bolt just introduced comes back as
something he can read and fix, in the same turn, with the pet still up."""
with pytest.raises(self_restart.RestartError) as excinfo:
self_restart.preflight(run=_broken_run)
message = str(excinfo.value)
assert "does not import" in message
assert "SyntaxError" in message and "controller.py" in message
def test_preflight_runs_the_import_in_a_subprocess_not_here():
"""This process holds the *old* modules, so an in-process import would
pass on a file that no longer parses."""
seen = {}
def capture(cmd, **kwargs):
seen["cmd"], seen["kwargs"] = cmd, kwargs
return SimpleNamespace(returncode=0, stdout="", stderr="")
self_restart.preflight(run=capture)
assert seen["cmd"][0] == sys.executable
assert "import bolt_pet" in seen["cmd"][2]
assert seen["kwargs"]["env"]["QT_QPA_PLATFORM"] == "offscreen" # imports need no display
def test_a_subprocess_that_cannot_even_run_is_reported(monkeypatch):
def explode(*args, **kwargs):
raise OSError("no python here")
with pytest.raises(self_restart.RestartError, match="couldn't run the preflight"):
self_restart.preflight(run=explode)
# ── context across the restart ──────────────────────────────────────────────
def test_arming_persists_the_reason_for_the_next_process(state):
self_restart.arm("check the sprite frames load", version="0.2.3",
session="pet-desktop", recent=["you: reload the sprites"],
path=state, now=1000.0)
revived = self_restart.load(state)
assert revived.reason == "check the sprite frames load"
assert revived.version == "0.2.3"
assert revived.recent == ["you: reload the sprites"]
assert revived.restarts == [1000.0]
def test_no_context_means_a_normal_start(state):
assert self_restart.load(state) is None
def test_a_corrupt_context_file_is_ignored_not_fatal(state):
state.write_text("{not json at all", encoding="utf-8")
assert self_restart.load(state) is None
def test_clearing_the_context_stops_it_being_re_announced(state):
self_restart.arm("once", path=state, now=1000.0)
self_restart.clear(state)
assert self_restart.load(state) is None
self_restart.clear(state) # clearing twice is not an error
def test_the_report_says_what_happened_and_what_to_check(state):
context = self_restart.arm(
"verify the dialogue command works", verify="verify the dialogue command works",
version="0.2.3", recent=["you: try a scene"], path=state, now=1000.0,
)
text = self_restart.report(context, version="0.2.4", now=1004.5)
assert "I restarted myself" in text
assert "verify the dialogue command works" in text
assert "4.5s" in text
assert "0.2.4" in text and "was 0.2.3" in text
assert "you: try a scene" in text
# ── loop guard ──────────────────────────────────────────────────────────────
def test_restart_history_accumulates_across_restarts(state):
self_restart.arm("one", path=state, now=1000.0)
self_restart.arm("two", path=state, now=1100.0)
assert self_restart.load(state).restarts == [1000.0, 1100.0]
def test_too_many_restarts_in_the_window_is_refused(state):
now = 1000.0
for index in range(self_restart.MAX_RESTARTS):
self_restart.arm(f"attempt {index}", path=state, now=now + index)
with pytest.raises(self_restart.RestartError, match="looping"):
self_restart.check_loop_guard(self_restart.load(state), now=now + 10)
def test_old_restarts_fall_out_of_the_window(state):
now = 1000.0
for index in range(self_restart.MAX_RESTARTS):
self_restart.arm(f"attempt {index}", path=state, now=now + index)
later = now + self_restart.WINDOW_SECONDS + 60
self_restart.check_loop_guard(self_restart.load(state), now=later) # no exception
assert self_restart.recent_restarts(self_restart.load(state), now=later) == []
+136 -33
View File
@@ -6,14 +6,14 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import pytest
from bolt_pet import server_client
from bolt_console import server_client
@pytest.fixture(autouse=True)
def _configure(monkeypatch):
monkeypatch.setattr(server_client.config, "SERVER_URL", "http://test-server:5002")
monkeypatch.setattr(server_client.config, "API_KEY", "test-key")
monkeypatch.setattr(server_client.config, "SESSION_ID", "pet-test")
monkeypatch.setattr(server_client.config, "SESSION_ID", "pet-console-test")
def _mock_response(json_data, ok=True):
@@ -28,11 +28,10 @@ def test_converse_returns_reply_directly():
post.return_value = _mock_response({"type": "reply", "text": "hello there"})
result = server_client.converse("hi")
assert result.text == "hello there"
assert result.voice_id == "" # no speak_as on this reply
post.assert_called_once()
args, kwargs = post.call_args
assert args[0] == "http://test-server:5002/desk/converse"
assert kwargs["json"] == {"session_id": "pet-test", "text": "hi"}
assert kwargs["json"] == {"session_id": "pet-console-test", "text": "hi"}
assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"}
@@ -50,23 +49,10 @@ def test_converse_relays_a_command_then_returns_reply():
second_call = post.call_args_list[1]
assert second_call.args[0] == "http://test-server:5002/desk/tool_result"
assert second_call.kwargs["json"] == {
"session_id": "pet-test", "token": "tok1", "output": "[exit 0]\nhi",
"session_id": "pet-console-test", "token": "tok1", "output": "[exit 0]\nhi",
}
def test_converse_carries_a_speak_as_voice_back_with_the_reply():
"""The server tags a reply with the voice it picked (speak_as); this
client is what actually speaks in it, so the id has to survive the
return trip rather than being dropped with the rest of the payload."""
with patch.object(server_client.requests, "post") as post:
post.return_value = _mock_response({
"type": "reply", "text": "Ahoy there.",
"voice_id": "hnhGxwvHP8fc469w51rM", "voice_name": "Terence",
})
result = server_client.converse("talk like a pirate")
assert result == server_client.Reply("Ahoy there.", "hnhGxwvHP8fc469w51rM", "Terence")
def test_converse_raises_server_error_on_error_payload():
with patch.object(server_client.requests, "post") as post:
post.return_value = _mock_response({"type": "error", "error": "unauthorized"})
@@ -80,19 +66,6 @@ def test_converse_raises_server_error_when_unreachable():
server_client.converse("hi")
def test_report_status_returns_reply_text_when_present():
with patch.object(server_client.requests, "post") as post:
post.return_value = _mock_response({"reply": "don't forget your 3pm"})
result = server_client.report_status()
assert result == "don't forget your 3pm"
def test_report_status_returns_none_when_nothing_pending():
with patch.object(server_client.requests, "post") as post:
post.return_value = _mock_response({"ok": True})
assert server_client.report_status() is None
def test_check_health_returns_parsed_json():
with patch.object(server_client.requests, "get") as get:
get.return_value = _mock_response({"ok": True, "service": "bolt-desk-api"})
@@ -109,7 +82,7 @@ def test_list_outbox_files_returns_the_queue():
assert result == [{"id": "abc", "name": "report.pdf", "size": 9}]
args, kwargs = get.call_args
assert args[0] == "http://test-server:5002/desk/files"
assert kwargs["params"] == {"session_id": "pet-test"}
assert kwargs["params"] == {"session_id": "pet-console-test"}
assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"}
@@ -134,7 +107,7 @@ def test_download_outbox_file_returns_raw_bytes():
assert result == b"%PDF fake bytes"
args, kwargs = get.call_args
assert args[0] == "http://test-server:5002/desk/files/abc"
assert kwargs["params"] == {"session_id": "pet-test"}
assert kwargs["params"] == {"session_id": "pet-console-test"}
def test_download_outbox_file_raises_server_error_on_http_failure():
@@ -142,3 +115,133 @@ def test_download_outbox_file_raises_server_error_on_http_failure():
get.return_value = _mock_response({}, ok=False)
with pytest.raises(server_client.ServerError, match="abc"):
server_client.download_outbox_file("abc")
class _StreamResponse:
def __init__(self, events):
self._lines = [__import__("json").dumps(e) for e in events]
self.closed = False
def raise_for_status(self):
pass
def iter_lines(self, decode_unicode=True):
return iter(self._lines)
def close(self):
self.closed = True
def test_converse_stream_hands_each_sentence_over_and_marks_the_reply_streamed():
said = []
events = [
{"type": "say", "text": "First."},
{"type": "say", "text": "Second."},
{"type": "reply", "text": "First. Second.", "already_spoken": True},
]
with patch.object(server_client.requests, "post", return_value=_StreamResponse(events)):
reply = server_client.converse_stream("hi", on_say=said.append)
assert said == ["First.", "Second."]
assert reply == server_client.Reply("First. Second.", streamed=True)
def test_converse_stream_finishes_a_tool_call_through_the_plain_relay():
events = [{"type": "command", "command": "filectl {}", "token": "t1"}]
posts = [_StreamResponse(events), _mock_response({"type": "reply", "text": "done"})]
on_command = MagicMock(return_value="out")
with patch.object(server_client.requests, "post", side_effect=posts) as post:
reply = server_client.converse_stream("hi", on_say=lambda _s: None, on_command=on_command)
on_command.assert_called_once_with("filectl {}")
assert post.call_args_list[1].args[0].endswith("/desk/tool_result")
assert reply == server_client.Reply("done", streamed=False)
def test_converse_stream_failing_before_any_text_raises_for_the_fallback():
with patch.object(server_client.requests, "post", side_effect=ConnectionError("down")):
with pytest.raises(server_client.ServerError):
server_client.converse_stream("hi", on_say=lambda _s: None)
def test_run_local_command_runs_in_the_current_directory(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
(tmp_path / "marker.txt").write_text("x")
output = server_client.run_local_command("dir /b" if sys.platform == "win32" else "ls")
assert output.startswith("[exit 0]")
assert "marker.txt" in output
def test_a_final_reply_repeating_the_streamed_sentences_is_not_shown_twice():
"""Seen live: sentences streamed, then the same text again as the reply,
without already_spoken — with line breaks the stream had dropped."""
events = [
{"type": "say", "text": "Let me check that."},
{"type": "say", "text": "The folder contains:"},
{"type": "reply", "text": "Let me check that.\n\nThe folder contains:\n- a\n- b"},
]
with patch.object(server_client.requests, "post", return_value=_StreamResponse(events)):
reply = server_client.converse_stream("hi", on_say=lambda _s: None)
assert reply == server_client.Reply("- a\n- b", streamed=False)
def test_an_exact_repeat_counts_as_streamed():
events = [{"type": "say", "text": "Done."}, {"type": "reply", "text": "Done."}]
with patch.object(server_client.requests, "post", return_value=_StreamResponse(events)):
reply = server_client.converse_stream("hi", on_say=lambda _s: None)
assert reply.streamed and reply.text == ""
def test_a_different_final_reply_is_shown_in_full():
events = [{"type": "say", "text": "Checking."}, {"type": "reply", "text": "All good."}]
with patch.object(server_client.requests, "post", return_value=_StreamResponse(events)):
reply = server_client.converse_stream("hi", on_say=lambda _s: None)
assert reply == server_client.Reply("All good.", streamed=False)
def test_list_chats_returns_the_list_with_the_api_key():
chats = [{"session_id": "chat-3f9a2b1c9e04", "label": "Fix the build",
"last_message": "try again", "last_active": "2026-09-25T23:46:31+00:00"}]
with patch.object(server_client.requests, "get") as get:
get.return_value = _mock_response({"chats": chats})
assert server_client.list_chats() == chats
args, kwargs = get.call_args
assert args[0] == "http://test-server:5002/desk/chats"
assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"}
def test_list_chats_defaults_to_empty_and_wraps_failures():
with patch.object(server_client.requests, "get") as get:
get.return_value = _mock_response({})
assert server_client.list_chats() == []
with patch.object(server_client.requests, "get") as get:
get.return_value = _mock_response({}, ok=False) # e.g. a 401
with pytest.raises(server_client.ServerError, match="couldn't list chats"):
server_client.list_chats()
def test_create_chat_posts_and_returns_the_new_session_id():
with patch.object(server_client.requests, "post") as post:
post.return_value = _mock_response({"session_id": "chat-3f9a2b1c9e04"})
assert server_client.create_chat() == "chat-3f9a2b1c9e04"
args, kwargs = post.call_args
assert args[0] == "http://test-server:5002/desk/chats"
assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"}
def test_create_chat_without_a_session_id_is_an_error():
with patch.object(server_client.requests, "post") as post:
post.return_value = _mock_response({})
with pytest.raises(server_client.ServerError, match="session_id"):
server_client.create_chat()
with patch.object(server_client.requests, "post", side_effect=ConnectionError("down")):
with pytest.raises(server_client.ServerError):
server_client.create_chat()
def test_every_call_follows_a_reassigned_session_id(monkeypatch):
"""Switching chats is just `config.SESSION_ID = new_id`; the next request
must carry it, with no call site holding on to the old one."""
monkeypatch.setattr(server_client.config, "SESSION_ID", "chat-new")
with patch.object(server_client.requests, "post") as post:
post.return_value = _mock_response({"type": "reply", "text": "hi"})
server_client.converse("hi")
assert post.call_args.kwargs["json"]["session_id"] == "chat-new"
-260
View File
@@ -1,260 +0,0 @@
"""The rules about talking — the four kinds of utterance and the mic policy.
These were four near-copies in the controller before, and the copies had
drifted: one didn't arm barge-in, one skipped the follow-up rule. The value of
having one `Speaker` is only real if the differences between the kinds stay
*visible*, so this asserts on the differences rather than on the machinery.
"""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import config, speech
from bolt_pet.speech import Speaker, Utterance
from bolt_pet.state import PetState, PetStateMachine
class FakeTts:
def __init__(self, completed=True):
self.completed = completed
self.calls = []
def speak(self, text, on_error=None, should_stop=None, voice_id=None, on_level=None):
self.calls.append({"text": text, "voice_id": voice_id,
"interruptible": should_stop is not None})
if on_level is not None:
on_level(0.7)
return self.completed
def play_pcm(self, pcm, sample_rate, should_stop=None, on_level=None):
self.calls.append({"pcm": pcm, "rate": sample_rate,
"interruptible": should_stop is not None})
return self.completed
class FakeBargeIn:
def __init__(self):
self.resets = 0
def reset(self):
self.resets += 1
def check(self, _frame=None):
return False
def build(**kwargs):
"""A speaker plus the things worth asserting on."""
state = PetStateMachine()
tts = kwargs.pop("tts", None) or FakeTts()
said, logged, recorded, levels = [], [], [], []
speaker = Speaker(
state=state, tts=tts,
history=recorded.append,
on_said=said.append,
on_log=logged.append,
on_level=levels.append,
**kwargs,
)
return speaker, state, tts, said, logged, recorded, levels
# ── what distinguishes the four kinds ───────────────────────────────────────
def test_a_reply_is_recorded_but_a_holding_line_is_not():
"""Filler must not push the actual answer out of the transcript."""
speaker, state, _tts, _said, _logged, recorded, _levels = build()
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.holding("Give me a sec."))
speaker.say(Utterance.reply("Sixty percent used."))
assert recorded == ["Sixty percent used."]
def test_a_holding_line_resumes_the_turn_it_interrupted():
"""The turn isn't over — a tool is still running — so it must go back to
THINKING, not drop to IDLE and end the turn."""
speaker, state, _tts, *_ = build()
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.holding("Let me check."))
assert state.state == PetState.THINKING
def test_a_holding_line_cannot_be_talked_over():
"""Cutting off "give me a sec" strands the tool that's already running."""
detector = FakeBargeIn()
speaker, state, tts, *_ = build(barge_in=lambda: detector)
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.holding("One sec."))
assert tts.calls[-1]["interruptible"] is False
speaker.say(Utterance.reply("Done."))
assert tts.calls[-1]["interruptible"] is True
def test_a_streamed_sentence_stays_talking_between_sentences():
"""Otherwise the sprite flickers idle-talking-idle down a long answer."""
speaker, state, *_ = build()
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.stream_chunk("The disk is fine."))
assert state.state == PetState.TALKING
def test_a_scene_is_recorded_because_the_user_heard_it():
speaker, state, _tts, _said, _logged, recorded, _levels = build()
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say_pcm(Utterance.scene("Once upon a time."), pcm=b"\x00\x00", sample_rate=44100)
assert recorded == ["Once upon a time."]
assert state.state == PetState.THINKING
# ── barge-in ordering, which is what the copies got wrong ───────────────────
def test_the_detector_is_reset_after_playback_not_only_before():
"""Playback fed the pet's own voice into the wake model's window. If it
isn't cleared afterwards, the idle listener re-hears the last sentence and
the pet answers itself."""
detector = FakeBargeIn()
speaker, state, *_ = build(barge_in=lambda: detector)
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.reply("Hello there."))
assert detector.resets == 2 # armed before, cleared after
def test_the_barge_in_detail_is_captured_before_the_reset():
"""Read it after the reset and every interruption reports zeroed counters —
which reads like hard evidence and is nothing of the sort."""
detector = FakeBargeIn()
details = ["score 0.81 at frame 12", "score 0.000 at frame 0"]
speaker, state, *_ = build(barge_in=lambda: detector,
detail_of=lambda: details.pop(0))
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.reply("Hello there."))
assert speaker.last_detail == "score 0.81 at frame 12"
def test_a_detector_that_appears_late_is_still_used():
"""The detector is built after the speaker — it needs the mic stream — so
it's read through a callable. Holding a copy is how the two drift apart."""
detector = None
speaker, state, tts, *_ = build(barge_in=lambda: detector)
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.reply("Before."))
assert tts.calls[-1]["interruptible"] is False
detector = FakeBargeIn()
speaker.say(Utterance.reply("After."))
assert tts.calls[-1]["interruptible"] is True
# ── the mouth ───────────────────────────────────────────────────────────────
def test_the_mouth_is_closed_when_the_line_ends():
"""A pet left mid-vowel after the audio stops looks broken."""
speaker, state, _tts, _said, _logged, _recorded, levels = build()
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.reply("Talking."))
assert levels[0] > 0
assert levels[-1] == 0.0
# ── text handling ───────────────────────────────────────────────────────────
def test_an_empty_line_is_not_spoken_at_all():
"""A reply that is nothing but an audio tag reduces to '' — and a silent
bubble with no audio is better than the pet announcing "laughing"."""
speaker, state, tts, said, *_ = build()
assert speaker.say(Utterance.reply("[laughing]")) is True
assert tts.calls == []
assert said == []
assert state.state == PetState.IDLE
def test_the_bubble_gets_display_text_and_tts_gets_the_original():
"""The bubble keeps emoji and drops markdown; TTS does its own stripping
(inside speak(), so every path is covered) and needs the real text."""
speaker, state, tts, said, *_ = build()
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.reply("**Sixty** percent 🎉"))
assert said == ["Sixty percent 🎉"]
assert tts.calls[-1]["text"] == "**Sixty** percent 🎉"
def test_a_voice_override_reaches_tts():
speaker, state, tts, *_ = build(voice_id=lambda: "voice-abc")
state.transition(PetState.LISTENING)
state.transition(PetState.THINKING)
speaker.say(Utterance.reply("In character."))
assert tts.calls[-1]["voice_id"] == "voice-abc"
# ── the follow-up rule ──────────────────────────────────────────────────────
@pytest.fixture
def follow_ups_on(monkeypatch):
monkeypatch.setattr(config, "FOLLOW_UP_LISTEN", True)
monkeypatch.setattr(config, "FOLLOW_UP_MAX_TURNS", 3)
def test_an_interruption_always_reopens_the_mic(follow_ups_on):
"""You talked over it — you are mid-sentence, so it has to listen."""
keep, why = speech.follow_up_decision("Anything else?", completed=False, follow_ups=99)
assert keep is True
assert why == "interrupted"
def test_a_question_keeps_the_mic_open_without_the_wake_word(follow_ups_on):
keep, why = speech.follow_up_decision("Want me to check?", completed=True, follow_ups=0)
assert (keep, why) == (True, "question")
def test_a_statement_ends_the_turn(follow_ups_on):
keep, _why = speech.follow_up_decision("Sixty percent used.", completed=True, follow_ups=0)
assert keep is False
def test_the_cap_stops_a_server_that_ends_every_reply_with_a_question(follow_ups_on):
"""Otherwise mic noise loops it forever."""
assert speech.follow_up_decision("Ok?", completed=True, follow_ups=2)[0] is True
keep, why = speech.follow_up_decision("Ok?", completed=True, follow_ups=3)
assert keep is False
assert "cap" in why
def test_the_rule_can_be_switched_off(monkeypatch):
monkeypatch.setattr(config, "FOLLOW_UP_LISTEN", False)
assert speech.follow_up_decision("Ok?", completed=True, follow_ups=0)[0] is False
-111
View File
@@ -1,111 +0,0 @@
"""Sanitizing chat-formatted replies into speakable prose. Pure string logic
— no audio hardware, no Qt (see the testing conventions in CLAUDE.md)."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet.speech_text import for_display, for_speech, is_question
def test_bold_markers_are_not_spoken():
assert "*" not in for_speech("Here's the **maji-desktop** snapshot")
assert for_speech("Here's the **maji-desktop** snapshot") == "Here's the maji-desktop snapshot"
def test_italics_and_underscores_dropped():
assert for_speech("that is _really_ odd") == "that is really odd"
assert for_speech("***everything*** is fine") == "everything is fine"
def test_bullet_list_becomes_sentences():
spoken = for_speech(
"System status:\n"
"* **OS:** Linux 7.0.0\n"
"* **Uptime:** 1 day\n"
)
assert "*" not in spoken
assert spoken == "System status: OS: Linux 7.0.0. Uptime: 1 day."
def test_emoji_and_symbols_removed():
assert for_speech("✅ Done 🚀 — all good") == "Done all good"
assert for_speech("→ next step") == "to next step"
def test_urls_and_code_are_not_read_out_character_by_character():
assert for_speech("see https://example.com/x?y=1 for docs") == "see link for docs"
assert for_speech("run `sudo reboot` now") == "run sudo reboot now"
assert for_speech("here:\n```\nls -la\n```\n") == "here: (code)."
def test_headings_quotes_and_rules_stripped():
assert for_speech("## Summary\n---\n> quoted bit") == "Summary. quoted bit."
def test_ampersand_and_percent_are_spoken_as_words():
assert for_speech("R&D at 50% capacity") == "R and D at 50 percent capacity"
def test_blank_and_symbol_only_input():
assert for_speech("") == ""
assert for_speech(None) == ""
assert for_speech("***") == ""
def test_display_keeps_emoji_but_drops_markdown():
assert for_display("**Done** ✅") == "Done ✅"
assert for_display("* one\n* two") == "• one • two"
def test_is_question_only_fires_on_a_trailing_question():
assert is_question("Ready to run a command or start a project?")
assert is_question("It's 7:15 AM. Want me to set a timer?")
assert not is_question("It's 7:15 AM on July 23, 2026.")
assert not is_question("What time is it? It's 7:15 AM.") # asked in passing
def test_is_question_ignores_trailing_decoration():
assert is_question("Ready to go? 🚀")
assert is_question('Shall I continue?"')
assert is_question("Want me to fix it? **")
def test_is_question_ignores_question_marks_that_are_not_spoken():
# The '?' here is inside a URL query string, which for_speech strips.
assert not is_question("Docs are at https://example.com/x?y=1")
assert not is_question("")
assert not is_question(None)
# ── ElevenLabs v3 delivery tags (observed live 2026-07-31) ──────────────────
# "[laughing] That one came through clean" was spoken as "laughing That one
# came through clean": the tags mean something to the dialogue model inside a
# dialoguectl scene, and nothing at all to the ordinary reply voice.
def test_delivery_tags_are_never_spoken_aloud():
spoken = for_speech("[laughing] That one came through clean.")
assert "laughing" not in spoken
assert spoken.startswith("That one came through clean")
def test_the_bubble_does_not_caption_a_laugh_nobody_heard():
assert "[laughing]" not in for_display("[laughing] All good.")
@pytest.mark.parametrize("tag", [
"[whispering]", "[cheerfully]", "[sighs]", "[laughs]", "[clears throat]",
"[nervously]", "[excited]", "[pause]", "[shouting]",
])
def test_the_common_tags_are_all_covered(tag):
assert "" == for_speech(tag).strip()
def test_ordinary_bracketed_text_survives():
"""The tags are matched narrowly on purpose — real bracketed content is
part of what the user asked to hear."""
assert "1" in for_speech("See reference [1] for details.")
assert "docs" in for_speech("It's in [the docs] somewhere.")
-73
View File
@@ -1,73 +0,0 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import pytest
from bolt_pet.state import InvalidTransition, PetState, PetStateMachine
def test_starts_idle():
sm = PetStateMachine()
assert sm.state == PetState.IDLE
def test_happy_path_transitions():
sm = PetStateMachine()
sm.transition(PetState.LISTENING)
sm.transition(PetState.THINKING)
sm.transition(PetState.TALKING)
sm.transition(PetState.IDLE)
assert sm.state == PetState.IDLE
def test_idle_to_talking_is_allowed_for_proactive_announcements():
# The heartbeat poll can make the pet speak unprompted (a reminder
# firing, a nudge from the server) with no preceding listen/think leg.
sm = PetStateMachine()
sm.transition(PetState.TALKING)
assert sm.state == PetState.TALKING
def test_invalid_transition_raises():
sm = PetStateMachine()
with pytest.raises(InvalidTransition):
sm.transition(PetState.THINKING) # can't skip straight to thinking with no utterance
def test_same_state_transition_is_a_noop():
calls = []
sm = PetStateMachine(on_change=lambda old, new: calls.append((old, new)))
sm.transition(PetState.IDLE) # already idle
assert calls == []
def test_on_change_callback_fires_with_old_and_new():
calls = []
sm = PetStateMachine(on_change=lambda old, new: calls.append((old, new)))
sm.transition(PetState.LISTENING)
assert calls == [(PetState.IDLE, PetState.LISTENING)]
def test_every_state_can_reach_error_and_recover():
for path in (
[PetState.LISTENING],
[PetState.LISTENING, PetState.THINKING],
[PetState.LISTENING, PetState.THINKING, PetState.TALKING],
):
sm = PetStateMachine()
for step in path:
sm.transition(step)
sm.transition(PetState.ERROR)
sm.transition(PetState.IDLE)
assert sm.state == PetState.IDLE
def test_force_recovers_from_talking_directly_to_idle_without_validation():
sm = PetStateMachine()
sm.transition(PetState.LISTENING)
sm.transition(PetState.THINKING)
sm.transition(PetState.TALKING)
sm.force(PetState.LISTENING) # not in TALKING's allowed set, but force skips the check
assert sm.state == PetState.LISTENING
-103
View File
@@ -1,103 +0,0 @@
"""stt.transcribe() — the "guaranteed" one-shot fallback.
There is no separate REST endpoint server-side any more: /desk/stt is a
websocket relay only, so this connects the exact same way stt_stream.py's
opportunistic streaming path does (via stt_stream.connect(), unconditionally
— not gated by STT_STREAMING, since there's nothing left to fall back to).
"""
import json
import sys
import time
from pathlib import Path
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet.audio import stt, stt_stream
class FakeSocket:
def __init__(self, messages=(), fail_to_connect=False):
self.sent = []
self.closed = False
self._fail_to_connect = fail_to_connect
self._messages = list(messages)
def send_binary(self, data):
self.sent.append(data)
def send(self, text):
self.sent.append(text)
def recv(self):
if self._messages:
return self._messages.pop(0)
time.sleep(0.01)
raise ConnectionError("closed")
def close(self):
self.closed = True
def _results(transcript, is_final=True):
return json.dumps({
"type": "Results", "is_final": is_final,
"channel": {"alternatives": [{"transcript": transcript}]},
})
@pytest.fixture(autouse=True)
def _configured(monkeypatch):
monkeypatch.setattr(stt.config, "SERVER_URL", "http://test-server:5002")
monkeypatch.setattr(stt.config, "API_KEY", "test-key")
def test_transcribe_raises_when_not_configured(monkeypatch):
monkeypatch.setattr(stt.config, "API_KEY", "")
with pytest.raises(stt.SttError, match="BOLT_SERVER_URL"):
stt.transcribe(np.zeros(320, dtype=np.int16))
def test_transcribe_connects_and_feeds_the_whole_utterance(monkeypatch):
socket = FakeSocket([_results("turn on the lights")])
monkeypatch.setattr(stt_stream, "connect", lambda: socket)
pcm = np.full(3200, 500, dtype=np.int16)
text = stt.transcribe(pcm)
assert text == "turn on the lights"
# one binary frame (the whole utterance) plus the close message
assert len(socket.sent) == 2
assert socket.closed
def test_transcribe_raises_when_the_server_is_unreachable(monkeypatch):
def refuse():
raise OSError("no route to host")
monkeypatch.setattr(stt_stream, "connect", refuse)
with pytest.raises(stt.SttError, match="couldn't reach"):
stt.transcribe(np.zeros(320, dtype=np.int16))
def test_transcribe_returns_empty_string_for_silence_not_an_error(monkeypatch):
"""No speech recognized is a legitimate outcome, not a failure — callers
(controller.py) treat "" as "say nothing" rather than logging an error."""
socket = FakeSocket([]) # never says anything back
monkeypatch.setattr(stt_stream, "connect", lambda: socket)
assert stt.transcribe(np.zeros(320, dtype=np.int16)) == ""
def test_transcribe_is_unaffected_by_stt_streaming_being_off(monkeypatch):
"""The opportunistic accelerator and the guaranteed fallback share a
connector, but STT_STREAMING must only gate the former."""
monkeypatch.setattr(stt.config, "STT_STREAMING", False)
socket = FakeSocket([_results("still works")])
monkeypatch.setattr(stt_stream, "connect", lambda: socket)
assert stt.transcribe(np.zeros(320, dtype=np.int16)) == "still works"
-224
View File
@@ -1,224 +0,0 @@
"""Streaming speech-to-text: the protocol, and the fallback that makes it safe
to switch on at all.
A fake websocket throughout — no network, no Deepgram account, no audio.
"""
import json
import sys
import time
from pathlib import Path
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet.audio import mic
from bolt_pet.audio.stt_stream import StreamingTranscriber
class FakeSocket:
"""Records what was sent; replays scripted Deepgram frames."""
def __init__(self, messages=(), fail_on_send=False):
self.sent = []
self.closed = False
self.fail_on_send = fail_on_send
self._messages = list(messages)
def send_binary(self, data):
if self.fail_on_send:
raise ConnectionError("socket died")
self.sent.append(data)
def send(self, text):
self.sent.append(text)
def recv(self):
if self._messages:
return self._messages.pop(0)
time.sleep(0.01)
raise ConnectionError("closed")
def close(self):
self.closed = True
def _results(transcript, is_final=True):
return json.dumps({
"type": "Results", "is_final": is_final,
"channel": {"alternatives": [{"transcript": transcript}]},
})
def _frame(value=1000):
return np.full(320, value, dtype=np.int16)
# ── the protocol ────────────────────────────────────────────────────────────
def test_frames_go_up_as_they_are_captured():
socket = FakeSocket([_results("what's the weather")])
session = StreamingTranscriber(socket)
for _ in range(3):
session.feed(_frame())
text = session.finish()
assert len(socket.sent) == 4 # three frames plus the close message
assert text == "what's the weather"
assert socket.closed
def test_only_final_results_are_kept():
"""Interim hypotheses change under you; concatenating them would produce
"what what's what's the what's the weather"."""
socket = FakeSocket([
_results("what's", is_final=False),
_results("what's the", is_final=False),
_results("what's the weather", is_final=True),
])
session = StreamingTranscriber(socket)
assert session.finish() == "what's the weather"
def test_several_final_segments_are_joined():
socket = FakeSocket([_results("turn the lights on"), _results("in the kitchen")])
session = StreamingTranscriber(socket)
assert session.finish() == "turn the lights on in the kitchen"
def test_junk_frames_are_ignored_rather_than_killing_the_reader():
"""An exception on the reader thread would silently end transcription for
the rest of the utterance."""
socket = FakeSocket(["not json at all", '{"type":"Metadata"}',
_results("still works")])
session = StreamingTranscriber(socket)
assert session.finish() == "still works"
def test_an_empty_frame_means_the_socket_closed():
"""websocket-client returns "" from recv() on a closed connection, so it
ends the read loop rather than being treated as a blank transcript."""
socket = FakeSocket([_results("heard this much"), "", _results("never arrives")])
session = StreamingTranscriber(socket)
assert session.finish() == "heard this much"
def test_a_socket_that_dies_mid_utterance_gives_up_quietly():
socket = FakeSocket([], fail_on_send=True)
session = StreamingTranscriber(socket)
session.feed(_frame()) # must not raise — recording carries on
assert session.finish() == ""
# ── opening: failure is an ordinary outcome ────────────────────────────────
def test_open_returns_none_when_it_cannot_connect():
"""None means "the one-shot path will do it", not an error."""
def refuse():
raise OSError("no network")
assert StreamingTranscriber.open(connect=refuse) is None
def test_open_returns_a_session_when_it_can():
session = StreamingTranscriber.open(connect=lambda: FakeSocket([_results("hi")]))
assert session is not None
assert session.finish() == "hi"
def test_streaming_is_off_without_the_switch_or_server_config(monkeypatch):
from bolt_pet.audio import stt_stream
monkeypatch.setattr(stt_stream.config, "STT_STREAMING", False)
monkeypatch.setattr(stt_stream.config, "SERVER_URL", "http://test-server:5002")
monkeypatch.setattr(stt_stream.config, "API_KEY", "test-key")
assert stt_stream.available() is False
monkeypatch.setattr(stt_stream.config, "STT_STREAMING", True)
assert stt_stream.available() is True
monkeypatch.setattr(stt_stream.config, "API_KEY", "")
assert stt_stream.available() is False
# ── connecting to the server relay ──────────────────────────────────────────
def test_connect_builds_the_server_relay_url(monkeypatch):
"""No local Deepgram account any more — the pet connects to its own
server's /desk/stt, authenticated with its own desk key."""
from bolt_pet.audio import stt_stream
monkeypatch.setattr(stt_stream.config, "SERVER_URL", "http://my-server:5002")
monkeypatch.setattr(stt_stream.config, "API_KEY", "my-desk-key")
monkeypatch.setattr(stt_stream.config, "SESSION_ID", "pet-test")
captured = {}
class _FakeWebsocketModule:
@staticmethod
def create_connection(url, header=None, timeout=None):
captured["url"] = url
captured["header"] = header
return "a-socket"
monkeypatch.setitem(sys.modules, "websocket", _FakeWebsocketModule())
result = stt_stream.connect(sample_rate=16000)
assert result == "a-socket"
assert captured["url"] == (
"ws://my-server:5002/desk/stt?session_id=pet-test"
"&encoding=linear16&sample_rate=16000"
)
assert captured["header"] == ["X-Desk-Api-Key: my-desk-key"]
def test_open_uses_connect_by_default(monkeypatch):
"""StreamingTranscriber.open() with no injected connect() goes through
the real server-relay connector."""
from bolt_pet.audio import stt_stream
monkeypatch.setattr(stt_stream.config, "STT_STREAMING", True)
monkeypatch.setattr(stt_stream.config, "SERVER_URL", "http://my-server:5002")
monkeypatch.setattr(stt_stream.config, "API_KEY", "my-desk-key")
monkeypatch.setattr(stt_stream, "_connect", lambda rate: FakeSocket([_results("hi")]))
session = stt_stream.StreamingTranscriber.open()
assert session is not None
assert session.finish() == "hi"
# ── the capture hook ────────────────────────────────────────────────────────
class _Stream:
"""Loud frames, then quiet ones, so the VAD ends the utterance."""
def __init__(self, loud=6, quiet=40):
self.frames = ([np.full((320, 1), 3000, dtype=np.int16)] * loud
+ [np.zeros((320, 1), dtype=np.int16)] * quiet)
def read(self, n):
return (self.frames.pop(0) if self.frames
else np.zeros((320, 1), dtype=np.int16)), None
def test_recording_hands_every_speech_frame_to_the_listener():
seen = []
pcm = mic.record_utterance(_Stream(), on_frame=seen.append,
silence_end_sec=0.2, min_utterance_s=0.0)
assert pcm is not None
assert len(seen) >= 6 # every frame of speech was streamed
def test_a_listener_that_throws_cannot_break_the_recording():
"""The fallback is about to need this audio — a dead stream must not cost
the recording too."""
def explode(frame):
raise RuntimeError("stream died")
pcm = mic.record_utterance(_Stream(), on_frame=explode,
silence_end_sec=0.2, min_utterance_s=0.0)
assert pcm is not None and len(pcm) > 0
-122
View File
@@ -1,122 +0,0 @@
"""Graphical sudo prompts: command rewriting and helper resolution.
Pure logic — no display, no sudo, no password."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import sudo_askpass
# ── rewriting ────────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
"command,expected",
[
("sudo apt update", "sudo -A apt update"),
("sudo apt update", "sudo -A apt update"), # spacing preserved
("apt update && sudo apt upgrade", "apt update && sudo -A apt upgrade"),
("ls; sudo reboot", "ls; sudo -A reboot"),
("echo hi | sudo tee /etc/motd", "echo hi | sudo -A tee /etc/motd"),
("sudo systemctl restart x\nsudo systemctl status x",
"sudo -A systemctl restart x\nsudo -A systemctl status x"),
],
)
def test_bare_sudo_gets_the_askpass_flag(command, expected):
assert sudo_askpass.add_askpass_flag(command) == expected
@pytest.mark.parametrize(
"command",
[
"sudo -n apt update", # explicitly non-interactive
"sudo -A apt update", # already asking
"sudo -u bob whoami", # the caller was explicit
"ls -la", # no sudo at all
"echo 'run sudo later'", # inside a quoted string
"pseudo --version", # not the word sudo
],
)
def test_commands_that_must_not_be_rewritten(command):
assert sudo_askpass.add_askpass_flag(command) == command
def test_blank_input():
assert sudo_askpass.add_askpass_flag("") == ""
assert sudo_askpass.add_askpass_flag(None) == ""
@pytest.mark.parametrize(
"command,expected",
[
("sudo apt update", True),
("ls && sudo reboot", True),
("ls -la", False),
("echo 'sudo'", False),
("", False),
],
)
def test_which_commands_get_the_longer_timeout(command, expected):
assert sudo_askpass.needs_password_prompt(command) is expected
# ── helper resolution ────────────────────────────────────────────────────────
def test_a_configured_helper_wins():
found = sudo_askpass.find_helper(
configured="/opt/my-askpass", is_executable=lambda p: p == "/opt/my-askpass",
)
assert found == "/opt/my-askpass"
def test_a_configured_helper_that_is_not_executable_is_not_silently_replaced():
"""Better to have sudo fail than to quietly prompt with something the
user didn't choose."""
assert sudo_askpass.find_helper(
configured="/opt/typo", is_executable=lambda p: False, which=lambda t: "/usr/bin/zenity",
) is None
def test_a_real_askpass_binary_beats_a_generated_wrapper():
found = sudo_askpass.find_helper(
configured="", is_executable=lambda p: p == "/usr/bin/ksshaskpass",
which=lambda tool: "/usr/bin/zenity",
)
assert found == "/usr/bin/ksshaskpass"
def test_falls_back_to_wrapping_a_dialog_tool(tmp_path):
found = sudo_askpass.find_helper(
configured="", is_executable=lambda p: False,
which=lambda tool: "/usr/bin/zenity" if tool == "zenity" else None,
cache_dir=tmp_path,
)
script = tmp_path / "askpass.sh"
assert found == str(script)
assert "zenity --password" in script.read_text()
assert script.stat().st_mode & 0o777 == 0o700 # nobody else edits the password box
def test_no_helper_available_at_all():
assert sudo_askpass.find_helper(
configured="", is_executable=lambda p: False, which=lambda tool: None,
) is None
def test_the_wrapper_passes_sudos_prompt_through():
"""sudo hands the helper its prompt as $1 — it names the account the
password is for, which is worth showing in the dialog."""
script = sudo_askpass.helper_script("/usr/bin/zenity")
assert script.startswith("#!/bin/sh")
assert '"$1"' in script
def test_environment_points_sudo_at_the_helper():
env = sudo_askpass.environment("/tmp/askpass.sh", base={"PATH": "/usr/bin"})
assert env["SUDO_ASKPASS"] == "/tmp/askpass.sh"
assert env["PATH"] == "/usr/bin" # the rest of the environment survives
+164
View File
@@ -0,0 +1,164 @@
"""The relayed-command dispatcher: what runs, what asks first, what's refused."""
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_console import tools as tools_mod
from bolt_console.tools import Tools
class FakeUI:
def __init__(self, answers=()):
self.answers = list(answers)
self.calls = []
self.results = []
self.questions = []
def tool_call(self, name, argument):
self.calls.append((name, argument))
def tool_result(self, text, error=False):
self.results.append((text, error))
def diff(self, old, new):
self.calls.append(("diff", old, new))
def confirm(self, question):
self.questions.append(question)
return self.answers.pop(0) if self.answers else "no"
class FakeShell:
def __init__(self, output="[exit 0]\nhello"):
self.output = output
self.ran = []
def __call__(self, command):
self.ran.append(command)
return self.output
def filectl(**payload) -> str:
return "filectl " + json.dumps(payload)
def test_shell_command_asks_first_and_runs_on_yes():
ui, shell = FakeUI(["yes"]), FakeShell()
output = Tools(ui, run_shell=shell).handle("echo hello")
assert shell.ran == ["echo hello"]
assert output == "[exit 0]\nhello"
assert ui.calls == [("Shell", "echo hello")]
assert ui.results == [("hello", False)]
def test_declined_command_never_runs_and_tells_the_model_why():
ui, shell = FakeUI(["no"]), FakeShell()
output = Tools(ui, run_shell=shell).handle("rm -rf build")
assert shell.ran == []
assert "declined" in output and "did not run" in output
def test_always_switches_the_session_to_auto():
ui, shell = FakeUI(["always"]), FakeShell()
tools = Tools(ui, run_shell=shell)
tools.handle("echo one")
tools.handle("echo two")
assert tools.mode == "auto"
assert len(ui.questions) == 1
assert shell.ran == ["echo one", "echo two"]
def test_auto_mode_never_asks():
ui, shell = FakeUI(), FakeShell()
Tools(ui, mode="auto", run_shell=shell).handle("echo hi")
assert ui.questions == []
assert shell.ran == ["echo hi"]
def test_failed_command_is_shown_as_an_error_with_its_status():
ui = FakeUI(["yes"])
Tools(ui, run_shell=FakeShell("[exit 2]\nno such file")).handle("cat nope")
assert ui.results == [("[exit 2]\nno such file", True)]
def test_reads_and_lists_never_ask(tmp_path):
(tmp_path / "a.txt").write_text("one\ntwo\n", encoding="utf-8")
ui = FakeUI()
tools = Tools(ui, run_shell=FakeShell())
read = tools.handle(filectl(op="read", path=str(tmp_path / "a.txt")))
listed = tools.handle(filectl(op="list", path=str(tmp_path)))
assert ui.questions == []
assert "one" in read and "two" in read
assert "a.txt" in listed
assert ui.calls[0] == ("Read", str(tmp_path / "a.txt"))
assert ui.results[0] == ("Read 2 lines", False)
def test_write_asks_and_declining_leaves_the_disk_alone(tmp_path):
target = tmp_path / "new.txt"
ui = FakeUI(["no"])
output = Tools(ui).handle(filectl(op="write", path=str(target), content="x"))
assert not target.exists()
assert "declined" in output
def test_edit_shows_the_diff_then_applies_on_yes(tmp_path):
target = tmp_path / "f.py"
target.write_text("x = 1\n", encoding="utf-8")
ui = FakeUI(["yes"])
output = Tools(ui).handle(filectl(op="edit", path=str(target), old="x = 1", new="x = 2"))
assert target.read_text(encoding="utf-8") == "x = 2\n"
assert ("diff", "x = 1", "x = 2") in ui.calls
assert "edited" in output
def test_filectl_errors_go_back_to_the_model_not_up_the_stack(tmp_path):
ui = FakeUI()
output = Tools(ui).handle(filectl(op="read", path=str(tmp_path / "missing.txt")))
assert output.startswith("[filectl] no such file")
assert ui.results[-1][1] is True
def test_pet_commands_are_explained_rather_than_run():
ui, shell = FakeUI(), FakeShell()
output = Tools(ui, mode="auto", run_shell=shell).handle("petctl emote wave")
assert shell.ran == []
assert output == tools_mod.PET_UNAVAILABLE.format(name="petctl")
def test_dialogue_scenes_are_explained_rather_than_run():
shell = FakeShell()
output = Tools(FakeUI(), mode="auto", run_shell=shell).handle('dialoguectl {"lines": []}')
assert shell.ran == []
assert "isn't available" in output
def test_a_stray_command_label_is_stripped_and_reported():
"""Seen live: the server relayed `command: powershell -Command "..."` and
cmd.exe answered "'command:' is not recognized"."""
ui, shell = FakeUI(["yes"]), FakeShell()
output = Tools(ui, run_shell=shell).handle('command: powershell -Command "Get-PnpDevice"')
assert shell.ran == ['powershell -Command "Get-PnpDevice"']
assert ui.calls[0] == ("Shell", 'powershell -Command "Get-PnpDevice"')
assert 'removed a leading "command:" label' in output
def test_wrapping_backticks_are_stripped():
shell = FakeShell()
Tools(FakeUI(), mode="auto", run_shell=shell).handle("`git status`")
assert shell.ran == ["git status"]
def test_the_label_is_stripped_before_filectl_is_recognised(tmp_path):
(tmp_path / "a.txt").write_text("hi", encoding="utf-8")
output = Tools(FakeUI()).handle(
"command: filectl " + json.dumps({"op": "read", "path": str(tmp_path / "a.txt")}))
assert "hi" in output and "command:" in output # read ran; note explains the fix
def test_a_clean_command_gets_no_note():
output = Tools(FakeUI(), mode="auto", run_shell=FakeShell()).handle("echo hi")
assert "note" not in output
-175
View File
@@ -1,175 +0,0 @@
"""Streaming-TTS chunk reassembly and the near-miss log. Both are pure —
no network, no audio device, no ONNX model."""
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import config as tts_config
from bolt_pet.audio import tts
from bolt_pet.audio.tts import TtsError, chunks_to_int16, voice_for
from bolt_pet.audio.wake_word import NearMissLog
def _pcm(*values):
return np.array(values, dtype=np.int16).tobytes()
def test_whole_samples_pass_straight_through():
chunks = list(chunks_to_int16([_pcm(1, 2), _pcm(3, 4)]))
assert np.concatenate(chunks).tolist() == [1, 2, 3, 4]
def test_a_sample_split_across_two_http_chunks_is_rejoined():
# The killer bug this exists to prevent: an odd byte at a chunk boundary
# shifts everything after it by one byte and plays as static.
raw = _pcm(100, -200, 300, -400)
chunks = list(chunks_to_int16([raw[:3], raw[3:]]))
assert np.concatenate(chunks).tolist() == [100, -200, 300, -400]
def test_many_odd_boundaries_in_a_row():
raw = _pcm(*range(1, 21))
pieces = [raw[i:i + 3] for i in range(0, len(raw), 3)] # every boundary odd
assert np.concatenate(list(chunks_to_int16(pieces))).tolist() == list(range(1, 21))
def test_empty_chunks_are_skipped():
assert list(chunks_to_int16([b"", b""])) == []
def test_a_dangling_byte_at_the_end_is_dropped_not_played():
raw = _pcm(7, 8) + b"\x01"
assert np.concatenate(list(chunks_to_int16([raw]))).tolist() == [7, 8]
# ── wake-word near misses ───────────────────────────────────────────────────
def test_scores_just_under_the_threshold_are_recorded():
log = NearMissLog(limit=10, margin=0.2)
assert log.observe(0.45, threshold=0.5, timestamp=1.0) is True
assert log.entries() == [(1.0, 0.45, 0.5)]
def test_detections_and_background_noise_are_not_near_misses():
log = NearMissLog(limit=10, margin=0.2)
assert log.observe(0.90, threshold=0.5, timestamp=1.0) is False # it fired
assert log.observe(0.05, threshold=0.5, timestamp=2.0) is False # just noise
assert log.entries() == []
def test_peak_tracks_every_score_not_just_near_misses():
log = NearMissLog(limit=10, margin=0.2)
log.observe(0.30, threshold=0.5, timestamp=1.0)
log.observe(0.95, threshold=0.5, timestamp=2.0)
log.observe(0.10, threshold=0.5, timestamp=3.0)
assert log.peak == 0.95
def test_the_log_is_bounded():
log = NearMissLog(limit=3, margin=0.2)
for i in range(10):
log.observe(0.45, threshold=0.5, timestamp=float(i))
assert len(log.entries()) == 3
assert log.entries()[-1][0] == 9.0
def test_clear_resets_peak_and_entries():
log = NearMissLog(limit=3, margin=0.2)
log.observe(0.45, threshold=0.5, timestamp=1.0)
log.clear()
assert log.entries() == [] and log.peak == 0.0
# ── voice selection (server speak_as) ───────────────────────────────────────
def test_the_override_voice_wins_over_the_configured_one(monkeypatch):
monkeypatch.setattr(tts_config, "ELEVENLABS_VOICE_ID", "DEFAULT")
assert voice_for("VOICE1") == "VOICE1"
assert voice_for("") == "DEFAULT"
assert voice_for(None) == "DEFAULT"
# ── server-hosted synthesis (/desk/tts) ─────────────────────────────────────
# No local ElevenLabs account: both the whole-clip and streaming paths post
# to this pet's own server, same as server_client.py's other endpoints.
@pytest.fixture(autouse=True)
def _configured(monkeypatch):
monkeypatch.setattr(tts_config, "SERVER_URL", "http://test-server:5002")
monkeypatch.setattr(tts_config, "API_KEY", "test-key")
monkeypatch.setattr(tts_config, "SESSION_ID", "pet-test")
monkeypatch.setattr(tts_config, "ELEVENLABS_VOICE_ID", "default-voice")
def _mock_response(content=b"", ok=True):
resp = MagicMock()
resp.content = content
resp.raise_for_status = MagicMock() if ok else MagicMock(side_effect=Exception("boom"))
return resp
def test_synthesize_pcm_posts_to_the_servers_tts_endpoint():
pcm_bytes = np.array([1, 2, 3], dtype=np.int16).tobytes()
with patch.object(tts.requests, "post") as post:
post.return_value = _mock_response(pcm_bytes)
pcm, rate = tts.synthesize_pcm("hello there")
post.assert_called_once()
args, kwargs = post.call_args
assert args[0] == "http://test-server:5002/desk/tts"
assert kwargs["headers"] == {"X-Desk-Api-Key": "test-key"}
assert kwargs["json"] == {
"session_id": "pet-test", "text": "hello there", "voice_id": "default-voice",
}
assert pcm.tolist() == [1, 2, 3]
assert rate == tts_config.TTS_SAMPLE_RATE
def test_synthesize_pcm_uses_the_override_voice():
with patch.object(tts.requests, "post") as post:
post.return_value = _mock_response(b"\x01\x00")
tts.synthesize_pcm("hi", voice_id="picked-voice")
assert post.call_args.kwargs["json"]["voice_id"] == "picked-voice"
def test_synthesize_pcm_raises_when_not_configured(monkeypatch):
monkeypatch.setattr(tts_config, "API_KEY", "")
with pytest.raises(TtsError, match="BOLT_SERVER_URL"):
tts.synthesize_pcm("hi")
def test_synthesize_pcm_raises_on_empty_audio():
with patch.object(tts.requests, "post") as post:
post.return_value = _mock_response(b"")
with pytest.raises(TtsError, match="no audio"):
tts.synthesize_pcm("hi")
def test_synthesize_pcm_raises_when_the_request_fails():
with patch.object(tts.requests, "post") as post:
post.return_value = _mock_response(b"", ok=False)
with pytest.raises(TtsError, match="server tts request failed"):
tts.synthesize_pcm("hi")
def test_stream_pcm_posts_to_the_same_endpoint_with_stream_true():
with patch.object(tts.requests, "post") as post:
response = MagicMock()
response.raise_for_status = MagicMock()
response.iter_content.return_value = [np.array([4, 5], dtype=np.int16).tobytes()]
post.return_value = response
chunks = list(tts.stream_pcm("hi"))
assert post.call_args.kwargs["stream"] is True
assert post.call_args[0][0] == "http://test-server:5002/desk/tts"
assert np.concatenate(chunks).tolist() == [4, 5]
def test_stream_pcm_raises_when_not_configured(monkeypatch):
monkeypatch.setattr(tts_config, "SERVER_URL", "")
with pytest.raises(TtsError, match="BOLT_SERVER_URL"):
list(tts.stream_pcm("hi"))
-237
View File
@@ -1,237 +0,0 @@
"""Auto-updater: version comparison, release parsing, and the apply/rollback
dance driven by a fake git (no network, no real repo, nothing checked out)."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import updater
# ── version comparison ───────────────────────────────────────────────────────
@pytest.mark.parametrize(
"tag,expected",
[
("v1.2.3", (1, 2, 3)),
("1.2.3", (1, 2, 3)),
("V0.1.0", (0, 1, 0)),
("1.2", (1, 2)),
("1.2.3-beta1", (1, 2, 3)), # suffix ends the parse
("", ()),
("nightly", ()),
],
)
def test_parse_version(tag, expected):
assert updater.parse_version(tag) == expected
@pytest.mark.parametrize(
"candidate,current",
[("0.2.0", "0.1.0"), ("1.0.0", "0.9.9"), ("0.1.1", "0.1"), ("v2.0", "1.9.9")],
)
def test_is_newer_accepts_newer_versions(candidate, current):
assert updater.is_newer(candidate, current)
@pytest.mark.parametrize(
"candidate,current",
[
("0.1.0", "0.1.0"),
("0.1.0", "0.2.0"),
("0.1", "0.1.0"), # zero-padded: equal, not newer
("", "0.1.0"),
("nightly", "0.1.0"), # unparseable is never newer
],
)
def test_is_newer_rejects_same_or_older(candidate, current):
assert not updater.is_newer(candidate, current)
def test_release_parsing_skips_drafts_and_untagged():
assert updater.release_from_payload({"tag_name": "v1.0.0", "draft": True}) is None
assert updater.release_from_payload({"name": "no tag"}) is None
release = updater.release_from_payload({"tag_name": "v1.0.0", "name": "One", "prerelease": True})
assert (release.tag, release.name, release.prerelease) == ("v1.0.0", "One", True)
# ── fake git ─────────────────────────────────────────────────────────────────
class FakeGit:
"""Records every git invocation and answers from a canned script.
*failures* maps a leading-args tuple to the (code, output) it should
return, so a test can make exactly one command fail."""
def __init__(self, ref="main", dirty=False, failures=None, requirements_changed=False,
head="abc1234", tag_commit="def5678"):
self.calls = []
self._ref = ref
self._dirty = dirty
self._failures = failures or {}
self._requirements_changed = requirements_changed
self._head = head
self._tag_commit = tag_commit # equal to head = "already on this tag"
def __call__(self, args):
self.calls.append(list(args))
for prefix, result in self._failures.items():
if tuple(args[: len(prefix)]) == prefix:
return result
head = args[0]
if head == "rev-parse" and args[1] == "--git-dir":
return 0, ".git"
if head == "status":
return 0, " M bolt_pet/config.py" if self._dirty else ""
if head == "symbolic-ref":
return (0, self._ref) if self._ref else (1, "")
if head == "rev-parse":
return 0, self._tag_commit if args[1].startswith("tags/") else self._head
if head == "diff":
return 0, "requirements.txt" if self._requirements_changed else ""
return 0, ""
def commands(self):
"""Just the verbs, for asserting on the sequence."""
return [call[0] for call in self.calls]
def test_apply_update_checks_out_the_tag(tmp_path):
git = FakeGit()
previous = updater.apply_update(
"v1.0.0", run=git, repo=tmp_path, install_deps=False, verify=lambda repo: None
)
assert previous == "main"
assert ["fetch", "--tags", "--prune", "origin"] in git.calls
assert ["checkout", "--force", "tags/v1.0.0"] in git.calls
def test_a_dirty_working_tree_is_left_completely_alone(tmp_path):
git = FakeGit(dirty=True)
with pytest.raises(updater.UpdateError, match="local changes"):
updater.apply_update("v1.0.0", run=git, repo=tmp_path, install_deps=False)
assert "fetch" not in git.commands()
assert "checkout" not in git.commands()
def test_a_non_git_install_refuses_before_touching_anything(tmp_path):
git = FakeGit(failures={("rev-parse", "--git-dir"): (128, "not a repository")})
with pytest.raises(updater.UpdateError, match="not a git clone"):
updater.apply_update("v1.0.0", run=git, repo=tmp_path, install_deps=False)
assert "checkout" not in git.commands()
def test_a_failed_smoke_test_rolls_back_to_the_previous_ref(tmp_path):
git = FakeGit(ref="main")
def broken(repo):
raise updater.UpdateError("the new version failed to import: boom")
with pytest.raises(updater.UpdateError, match="rolled back to main"):
updater.apply_update("v1.0.0", run=git, repo=tmp_path, install_deps=False, verify=broken)
checkouts = [call for call in git.calls if call[0] == "checkout"]
assert checkouts == [["checkout", "--force", "tags/v1.0.0"], ["checkout", "--force", "main"]]
def test_rollback_targets_the_commit_when_head_is_detached(tmp_path):
# No branch to go back to (symbolic-ref fails) — the SHA is the ref.
git = FakeGit(ref="")
with pytest.raises(updater.UpdateError):
updater.apply_update(
"v1.0.0", run=git, repo=tmp_path, install_deps=False,
verify=lambda repo: (_ for _ in ()).throw(RuntimeError("nope")),
)
assert ["checkout", "--force", "abc1234"] in git.calls
def test_a_verify_that_raises_something_unexpected_still_rolls_back(tmp_path):
git = FakeGit()
def exploding(repo):
raise ValueError("not even an UpdateError")
with pytest.raises(updater.UpdateError, match="rolled back"):
updater.apply_update(
"v1.0.0", run=git, repo=tmp_path, install_deps=False, verify=exploding
)
assert ["checkout", "--force", "main"] in git.calls
def test_a_release_tagged_without_bumping_the_version_does_not_loop(tmp_path):
"""Cut a release but forget to bump __version__ in the tagged commit and
every check would see the same "newer" tag: check out (a no-op), restart,
read the old version, repeat — a restart loop every check interval."""
git = FakeGit(head="same1234", tag_commit="same1234")
with pytest.raises(updater.UpdateError, match="bump it in the tagged commit"):
updater.apply_update("v0.2.1", run=git, repo=tmp_path, install_deps=False,
verify=lambda repo: None)
assert "checkout" not in git.commands() # nothing moved, so nothing to restart into
def test_an_unknown_tag_is_not_mistaken_for_being_already_on_it(tmp_path):
git = FakeGit(failures={("rev-parse", "tags/"): (128, "unknown revision")})
# The failure key matches by prefix, so make it explicit that a tag we
# can't resolve means "not there yet" rather than "already applied".
assert updater.already_at_tag(git, "v9.9.9") is False
def test_a_failed_fetch_never_moves_the_checkout(tmp_path):
git = FakeGit(failures={("fetch",): (1, "could not resolve host")})
with pytest.raises(updater.UpdateError, match="git fetch failed"):
updater.apply_update("v1.0.0", run=git, repo=tmp_path, install_deps=False)
assert "checkout" not in git.commands()
def test_a_stranded_checkout_is_logged_loudly(tmp_path):
"""Rollback itself failing is the one case a human has to fix by hand."""
git = FakeGit(failures={("checkout", "--force", "main"): (1, "index locked")})
logs = []
with pytest.raises(updater.UpdateError):
updater.apply_update(
"v1.0.0", run=git, repo=tmp_path, install_deps=False, on_log=logs.append,
verify=lambda repo: (_ for _ in ()).throw(updater.UpdateError("bad build")),
)
assert any("ROLLBACK FAILED" in line and "git checkout main" in line for line in logs)
def test_deps_are_only_reinstalled_when_requirements_actually_changed(tmp_path, monkeypatch):
installs = []
monkeypatch.setattr(updater, "_install_deps", lambda repo: installs.append(repo))
updater.apply_update(
"v1.0.0", run=FakeGit(requirements_changed=False), repo=tmp_path,
install_deps=True, verify=lambda repo: None,
)
assert installs == []
updater.apply_update(
"v1.0.0", run=FakeGit(requirements_changed=True), repo=tmp_path,
install_deps=True, verify=lambda repo: None,
)
assert installs == [tmp_path]
# ── release checking ─────────────────────────────────────────────────────────
def test_check_for_update_returns_nothing_when_current(monkeypatch):
monkeypatch.setattr(
updater, "fetch_latest_release",
lambda **kwargs: updater.Release("v0.1.0", "0.1.0", "", False),
)
assert updater.check_for_update(current_version="0.1.0") is None
assert updater.check_for_update(current_version="0.0.9").tag == "v0.1.0"
def test_no_releases_yet_is_not_an_error(monkeypatch):
monkeypatch.setattr(updater, "fetch_latest_release", lambda **kwargs: None)
assert updater.check_for_update(current_version="0.1.0") is None
-148
View File
@@ -1,148 +0,0 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import numpy as np
import pytest
from bolt_pet.audio import wake_word
class _FakeStream:
"""Yields a fixed sequence of frames, then silence forever."""
def __init__(self, frames, frame_len):
self._frames = list(frames)
self._frame_len = frame_len
def read(self, frames):
if self._frames:
frame = self._frames.pop(0)
else:
frame = np.zeros(self._frame_len, dtype=np.int16)
return frame.reshape(-1, 1), False
def _frame(frame_len):
return np.zeros(frame_len, dtype=np.int16)
class _FakeModel:
"""Reports the given score sequence (one dict per predict() call, then
repeats the last entry) and records reset() calls."""
def __init__(self, score_sequence):
self._scores = list(score_sequence)
self.reset_calls = 0
def predict(self, frame):
if self._scores:
return self._scores.pop(0)
return {"thunderbolt": 0.0}
def reset(self):
self.reset_calls += 1
def test_returns_true_and_resets_on_detection(monkeypatch):
frame_len = 1280
monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len)
monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000)
monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08) # 1 frame
monkeypatch.setattr(wake_word.config, "WAKE_WORD_THRESHOLD", 0.5)
stream = _FakeStream([_frame(frame_len)] * 3, frame_len)
model = _FakeModel([{"thunderbolt": 0.1}, {"thunderbolt": 0.9}])
detected = wake_word.listen_for_wake_word(stream, model=model)
assert detected is True
assert model.reset_calls == 1
def test_returns_false_when_should_continue_goes_false_first():
frame_len = 1280
stream = _FakeStream([_frame(frame_len)] * 5, frame_len)
model = _FakeModel([{"thunderbolt": 0.0}] * 5)
calls = {"n": 0}
def should_continue():
calls["n"] += 1
return calls["n"] <= 3
detected = wake_word.listen_for_wake_word(
stream, should_continue=should_continue, model=model,
)
assert detected is False
assert model.reset_calls == 0
def test_custom_threshold_is_respected(monkeypatch):
frame_len = 1280
monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len)
monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000)
monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08)
stream = _FakeStream([_frame(frame_len)] * 3, frame_len)
model = _FakeModel([{"thunderbolt": 0.6}] * 3)
calls = {"n": 0}
def should_continue():
calls["n"] += 1
return calls["n"] <= 3
detected = wake_word.listen_for_wake_word(
stream, should_continue=should_continue, model=model, threshold=0.7,
)
assert detected is False
def test_on_tick_fires_once_per_check_interval(monkeypatch):
frame_len = 1280
monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len)
monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000)
monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.08) # 1 frame/check
frames = [_frame(frame_len) for _ in range(5)]
stream = _FakeStream(frames, frame_len)
model = _FakeModel([{"thunderbolt": 0.0}] * 5)
ticks = {"n": 0}
state = {"i": 0}
def should_continue():
state["i"] += 1
return state["i"] <= 5
wake_word.listen_for_wake_word(
stream,
should_continue=should_continue,
model=model,
on_tick=lambda: ticks.__setitem__("n", ticks["n"] + 1),
)
assert ticks["n"] == 5 # one check-interval per frame at this config
def test_on_tick_interval_can_span_multiple_frames(monkeypatch):
frame_len = 1280
monkeypatch.setattr(wake_word.config, "FRAME_LEN", frame_len)
monkeypatch.setattr(wake_word.config, "SAMPLE_RATE", 16000)
monkeypatch.setattr(wake_word.config, "WAKE_CHECK_INTERVAL_SECONDS", 0.24) # 3 frames/check
frames = [_frame(frame_len) for _ in range(6)]
stream = _FakeStream(frames, frame_len)
model = _FakeModel([{"thunderbolt": 0.0}] * 6)
ticks = {"n": 0}
state = {"i": 0}
def should_continue():
state["i"] += 1
return state["i"] <= 6
wake_word.listen_for_wake_word(
stream,
should_continue=should_continue,
model=model,
on_tick=lambda: ticks.__setitem__("n", ticks["n"] + 1),
)
assert ticks["n"] == 2 # 6 frames / 3 frames-per-check
-89
View File
@@ -1,89 +0,0 @@
"""Autonomous wandering. Needs a QApplication, so run with
QT_QPA_PLATFORM=offscreen (same as test_controller.py)."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from PySide6.QtWidgets import QApplication
from bolt_pet import config
from bolt_pet.state import PetState
from bolt_pet.ui.pet_window import PetWindow
@pytest.fixture(scope="module")
def qt_app():
app = QApplication.instance() or QApplication([])
yield app
@pytest.fixture
def pet(qt_app):
window = PetWindow()
window.set_wander_enabled(True)
yield window
window.close()
def _walk_until_done(pet, max_ticks=2000):
for _ in range(max_ticks):
pet._wander_tick()
if pet._wander_target is None:
return True
return False
def test_wanders_when_idle_and_reaches_its_target(pet, monkeypatch):
# Snapping is tested separately; here it would tug the pet off its target
# the moment it arrives near an edge.
monkeypatch.setattr(config, "PET_EDGE_SNAP", False)
pet.wander_now()
pet._wander_tick()
target = pet._wander_target
if target is None:
pytest.skip("no usable screen geometry for a stroll on this host")
assert _walk_until_done(pet), "pet never reached its wander target"
assert pet.pos() == target
def test_stays_put_while_talking(pet):
pet.set_state(PetState.TALKING)
start = pet.pos()
pet.wander_now()
for _ in range(50):
pet._wander_tick()
assert pet.pos() == start
assert pet._wander_target is None
def test_stays_put_while_bubble_is_up(pet):
pet.say("hello there", duration_ms=60000)
start = pet.pos()
pet.wander_now()
for _ in range(50):
pet._wander_tick()
assert pet.pos() == start
def test_disabling_wander_stops_movement(pet):
pet.set_wander_enabled(False)
start = pet.pos()
pet.wander_now()
for _ in range(50):
pet._wander_tick()
assert pet.pos() == start
def test_stroll_stays_on_screen(pet):
geo = pet._screen_geometry()
if geo is None:
pytest.skip("no usable screen geometry on this host")
for _ in range(10):
pet.wander_now()
pet._wander_tick()
assert _walk_until_done(pet)
assert geo.contains(pet.geometry())
-84
View File
@@ -1,84 +0,0 @@
"""Remembering where the pet was left — and refusing to when that's a trap."""
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from bolt_pet import window_state
def test_a_saved_position_comes_back(tmp_path):
path = tmp_path / "window.json"
window_state.save(1200, 640, path)
assert window_state.load(path) == (1200, 640)
def test_no_file_yet_is_not_an_error(tmp_path):
assert window_state.load(tmp_path / "nope.json") is None
def test_a_corrupt_file_falls_back_to_the_default_corner(tmp_path):
"""Whatever is in there, the pet still has to start."""
path = tmp_path / "window.json"
for junk in ("", "{", "null", "[]", '{"x": "left"}', '{"y": 3}'):
path.write_text(junk)
assert window_state.load(path) is None
def test_saving_creates_the_cache_directory(tmp_path):
path = tmp_path / "deep" / "nested" / "window.json"
window_state.save(10, 20, path)
assert window_state.load(path) == (10, 20)
def test_an_unwritable_location_is_swallowed(tmp_path):
"""A read-only cache dir is a reason to forget the position, not to crash
on every drag."""
window_state.save(1, 2, tmp_path / "no" / "\0bad" / "window.json")
def test_the_write_is_atomic_and_leaves_no_litter(tmp_path):
path = tmp_path / "window.json"
window_state.save(5, 5, path)
window_state.save(7, 7, path)
assert json.loads(path.read_text()) == {"x": 7, "y": 7}
assert [p.name for p in tmp_path.iterdir()] == ["window.json"]
# ── validating against the screens that exist *now* ─────────────────────────
LAPTOP = (0, 0, 1920, 1080)
EXTERNAL = (1920, 0, 4480, 1440)
def test_a_position_on_a_connected_screen_is_kept():
assert window_state.is_visible_on(1700, 900, 128, [LAPTOP])
def test_a_position_on_an_unplugged_monitor_is_refused():
"""The dangerous case: restoring it faithfully puts the pet somewhere you
can't see or reach."""
assert not window_state.is_visible_on(3000, 700, 128, [LAPTOP])
assert window_state.is_visible_on(3000, 700, 128, [LAPTOP, EXTERNAL])
def test_mostly_off_screen_counts_as_gone():
"""A few pixels of ear poking onto the desktop is not "reachable"."""
assert not window_state.is_visible_on(1910, 500, 128, [LAPTOP])
assert window_state.is_visible_on(1830, 500, 128, [LAPTOP])
def test_touching_an_edge_is_not_overlapping():
assert not window_state.is_visible_on(1920, 0, 128, [LAPTOP])
def test_negative_coordinates_are_fine_when_a_screen_is_there():
"""Monitors left of or above the primary have negative origins."""
left_of_primary = (-1920, 0, 0, 1080)
assert window_state.is_visible_on(-900, 400, 128, [left_of_primary, LAPTOP])
def test_no_screens_at_all_is_not_visible():
assert not window_state.is_visible_on(100, 100, 128, [])