LazyBoy2/tests/cli_flow.py

108 lines
5.1 KiB
Python
Raw Normal View History

2026-09-13 16:38:32 +00:00
"""Offline CLI integration: python3 tests/cli_flow.py (cargo build -p grokboy first)."""
import json
import os
from pathlib import Path
import subprocess
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
BINARY = Path(__file__).resolve().parents[1] / "target/debug/grokboy"
def tool(name, args):
return {"role": "assistant", "tool_calls": [{"id": "call", "type": "function", "function": {
"name": name, "arguments": json.dumps(args)}}]}
def response(message, finish=None):
return {"choices": [{"message": message, "finish_reason": finish or (
"tool_calls" if message.get("tool_calls") else "stop")}]}
def paired(messages):
for i, message in enumerate(messages):
for offset, call in enumerate(message.get("tool_calls", [])):
result = messages[i + offset + 1]
assert result["role"] == "tool" and result["tool_call_id"] == call["id"]
class Provider(BaseHTTPRequestHandler):
def do_POST(self):
body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
self.server.requests.append(body)
try:
paired(body["messages"])
reply = self.server.replies.pop(0)
encoded = json.dumps(reply).encode()
self.send_response(200)
except Exception as exc:
self.server.errors.append(str(exc))
encoded = b'{"error":{"message":"unexpected request or unpaired history"}}'
self.send_response(500)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
def log_message(self, *_):
pass
def main():
with tempfile.TemporaryDirectory(prefix="grokboy-cli-") as temp, ThreadingHTTPServer(("127.0.0.1", 0), Provider) as server:
server.requests, server.replies, server.errors = [], [], []
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
env = {**os.environ, "GROKBOY_API_KEY": "offline-test", "GROKBOY_MODEL": "mock",
"GROKBOY_BASE_URL": f"http://127.0.0.1:{server.server_port}/v1",
"GROKBOY_SESSIONS_DIR": str(Path(temp) / "sessions"),
"GROKBOY_MAX_ROUNDS": "1", "GROKBOY_MAX_ROUNDS_TOTAL": "6",
"GROKBOY_CONTEXT_CHARS": "100000", "GROKBOY_PROGRESS": "1"}
def run(replies, args, verdict, input_text=None):
server.requests.clear()
server.replies = list(replies)
result = subprocess.run([str(BINARY), *args], cwd=temp, env=env, input=input_text,
capture_output=True, text=True, timeout=15)
assert result.returncode == (0 if verdict in ("done", "answer") or args[0] == "agent" else 1), result.stderr
assert not server.replies and not server.errors, (server.replies, server.errors, result.stderr)
sessions = list((Path(temp) / "sessions").glob("*.json"))
session = json.loads(max(sessions, key=lambda p: p.stat().st_mtime_ns).read_text())
assert session["last_verdict"] == verdict, session
paired(session["messages"])
assert len(server.requests) == len(replies), "hidden summary request"
return session, result
try:
run([response(tool("write_file", {"path": "note.txt", "content": "ok"})),
response(tool("read_file", {"path": "note.txt"})),
response(tool("report_done", {"message": "verified note.txt"}))], ["run", "write and verify"], "done")
assert Path(temp, "note.txt").read_text() == "ok"
print("PASS write -> read -> done across progress intervals")
env["GROKBOY_MAX_ROUNDS_TOTAL"] = "1"
saved, _ = run([response(tool("read_file", {"path": "note.txt"}))], ["run", "inspect"], "budget_exhausted")
run([response({"role": "assistant", "content": "continued"})], ["run", "--session", saved["id"], "continue"], "answer")
print("PASS hard request budget -> saved session -> resume")
env["GROKBOY_MAX_ROUNDS_TOTAL"] = "6"
saved, _ = run([response(tool("read_file", {"path": "note.txt"}))] * 3, ["run", "repeat"], "blocked")
run([response({"role": "assistant", "content": "recovered"})], ["run", "--session", saved["id"], "change approach"], "answer")
print("PASS repeated observations -> paired history -> resume")
run([response(tool("write_file", {"path": "bad.txt", "content": "bad"}), "length")], ["run", "truncated"], "failed")
assert not Path(temp, "bad.txt").exists()
print("PASS truncated tool response never executes")
run([{"error": {"message": "mock provider failure"}}, response({"role": "assistant", "content": "retry ok"})],
["agent"], "answer", "first\nretry\n/exit\n")
print("PASS provider failure keeps REPL alive for next input")
finally:
server.shutdown()
thread.join(timeout=5)
if __name__ == "__main__":
main()