Files
Bolt-Console/tests/test_tools.py
T
themajesticmagicianandClaude Opus 5.5 3977804ae3 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>
2026-09-25 19:50:10 -06:00

165 lines
5.5 KiB
Python

"""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