"""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: `" 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() == ""