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>
55 lines
2.0 KiB
Python
55 lines
2.0 KiB
Python
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from bolt_console import file_delivery
|
|
|
|
|
|
def test_sanitize_filename_strips_directory_components():
|
|
assert file_delivery.sanitize_filename("../../etc/passwd") == "passwd"
|
|
assert file_delivery.sanitize_filename("/absolute/path/report.pdf") == "report.pdf"
|
|
assert file_delivery.sanitize_filename("plain.txt") == "plain.txt"
|
|
|
|
|
|
def test_sanitize_filename_falls_back_on_empty_or_dots():
|
|
assert file_delivery.sanitize_filename("") == "delivered_file"
|
|
assert file_delivery.sanitize_filename("..") == "delivered_file"
|
|
assert file_delivery.sanitize_filename(".") == "delivered_file"
|
|
assert file_delivery.sanitize_filename(None) == "delivered_file"
|
|
|
|
|
|
def test_unique_path_returns_the_plain_name_when_free(tmp_path):
|
|
path = file_delivery.unique_path(tmp_path, "report.pdf")
|
|
assert path == tmp_path / "report.pdf"
|
|
|
|
|
|
def test_unique_path_suffixes_on_collision(tmp_path):
|
|
(tmp_path / "report.pdf").write_bytes(b"existing")
|
|
path = file_delivery.unique_path(tmp_path, "report.pdf")
|
|
assert path == tmp_path / "report (1).pdf"
|
|
|
|
(tmp_path / "report (1).pdf").write_bytes(b"also existing")
|
|
path = file_delivery.unique_path(tmp_path, "report.pdf")
|
|
assert path == tmp_path / "report (2).pdf"
|
|
|
|
|
|
def test_unique_path_creates_the_directory(tmp_path):
|
|
target = tmp_path / "nested" / "dir"
|
|
file_delivery.unique_path(target, "a.txt")
|
|
assert target.is_dir()
|
|
|
|
|
|
def test_save_writes_bytes_and_sanitizes_the_name(tmp_path):
|
|
path = file_delivery.save(tmp_path, "../sneaky/report.pdf", b"hello")
|
|
assert path == tmp_path / "report.pdf"
|
|
assert path.read_bytes() == b"hello"
|
|
|
|
|
|
def test_save_never_overwrites_an_existing_download(tmp_path):
|
|
first = file_delivery.save(tmp_path, "notes.txt", b"first")
|
|
second = file_delivery.save(tmp_path, "notes.txt", b"second")
|
|
assert first != second
|
|
assert first.read_bytes() == b"first"
|
|
assert second.read_bytes() == b"second"
|