"""会话日志:JSONL 写读往返、帧哈希记录(不落图片本体)。""" import hashlib import json from quercus_core.providers.base import AssistantTurn, ToolCall from quercus_core.session.log import SessionLog, frame_sha256, replay from quercus_core.types import Action, ActionBatch, PngBytes, ToolResult PNG = PngBytes(b"\x89PNG\r\n\x1a\n" + bytes(range(64))) def test_write_read_roundtrip(tmp_path): path = tmp_path / "s1.jsonl" log = SessionLog(path, session_id="sess-1") log.log_user("你好") log.log_assistant( AssistantTurn( text="", tool_calls=(ToolCall(id="c1", name="get_frame", arguments={"time": {"num": 1, "den": 1}}),), ) ) log.log_tool_result(ToolResult(tool="get_frame", ok=True, summary="帧", images=(PNG,))) log.log_batch_decision( ActionBatch( label="AI 动作:get_frame", actions=[Action(tool="get_frame", params={"time": {"num": 1, "den": 1}}, call_id="c1")], id="b1", ), approved=True, ) events = replay(path) assert len(events) == 4 assert events[0].kind == "user_message" assert events[0].payload["text"] == "你好" assert events[0].session_id == "sess-1" assert events[1].kind == "assistant_turn" assert events[1].payload["tool_calls"][0]["name"] == "get_frame" assert events[2].kind == "tool_result" assert events[3].kind == "batch_decision" assert events[3].payload["approved"] is True def test_frame_hash_recorded_not_bytes(tmp_path): path = tmp_path / "s2.jsonl" log = SessionLog(path, session_id="s") log.log_tool_result(ToolResult(tool="get_frame", ok=True, summary="f", images=(PNG,))) events = replay(path) ev = events[0] assert ev.kind == "tool_result" assert ev.payload["image_hashes"] == [hashlib.sha256(PNG).hexdigest()] # 日志文件里绝不出现图片本体字节 assert PNG not in path.read_bytes() def test_jsonl_each_line_is_valid_json(tmp_path): path = tmp_path / "s3.jsonl" log = SessionLog(path) log.log_user("x") log.log_tool_result(ToolResult(tool="seek", ok=True, summary="ok")) for line in path.read_text(encoding="utf-8").splitlines(): json.loads(line) def test_frame_sha256_helper(fake_png): assert frame_sha256(fake_png) == hashlib.sha256(fake_png).hexdigest() def test_create_default_uses_home(tmp_home): log = SessionLog.create_default() assert log.path.parent == tmp_home / ".quercus" / "sessions" assert log.path.suffix == ".jsonl" assert log.path.name # .jsonl