| 1 | from __future__ import annotations |
| 2 | |
| 3 | import json |
| 4 | from pathlib import Path |
| 5 | from types import SimpleNamespace |
| 6 | |
| 7 | import pytest |
| 8 | |
| 9 | |
| 10 | def test_deserialize_log_preserves_item_id() -> None: |
| 11 | from helpers.log import Log |
| 12 | from helpers.persist_chat import _deserialize_log, _serialize_log |
| 13 | |
| 14 | log = Log() |
| 15 | log.log(type="user", heading="User message", content="hello", id="msg-123") |
| 16 | log.log(type="assistant", heading="Assistant", content="hi") |
| 17 | |
| 18 | serialized = _serialize_log(log) |
| 19 | restored = _deserialize_log(serialized) |
| 20 | |
| 21 | assert restored.logs[0].type == "user" |
| 22 | assert restored.logs[0].id == "msg-123" |
| 23 | assert restored.logs[1].type == "assistant" |
| 24 | assert restored.logs[1].id is None |
| 25 | |
| 26 | |
| 27 | def test_load_tmp_chats_skips_directories_without_chat_json(monkeypatch, capsys) -> None: |
| 28 | from helpers import persist_chat |
| 29 | |
| 30 | monkeypatch.setattr(persist_chat, "_convert_v080_chats", lambda: None) |
| 31 | monkeypatch.setattr( |
| 32 | persist_chat.files, |
| 33 | "get_abs_path", |
| 34 | lambda *parts: "/" + "/".join(str(part).strip("/") for part in parts if part), |
| 35 | ) |
| 36 | monkeypatch.setattr( |
| 37 | persist_chat.files, |
| 38 | "list_files", |
| 39 | lambda folder, pattern="*": ["orphan", "valid"], |
| 40 | ) |
| 41 | monkeypatch.setattr( |
| 42 | persist_chat.files, |
| 43 | "exists", |
| 44 | lambda path: str(path).endswith("/valid/chat.json"), |
| 45 | ) |
| 46 | monkeypatch.setattr( |
| 47 | persist_chat.files, |
| 48 | "read_file", |
| 49 | lambda path: json.dumps({"id": "valid"}), |
| 50 | ) |
| 51 | monkeypatch.setattr( |
| 52 | persist_chat, |
| 53 | "_deserialize_context", |
| 54 | lambda data: SimpleNamespace(id=data["id"], data={}), |
| 55 | ) |
| 56 | |
| 57 | assert persist_chat.load_tmp_chats() == ["valid"] |
| 58 | assert "Error loading chat" not in capsys.readouterr().out |
| 59 | |
| 60 | |
| 61 | def test_save_tmp_chat_preserves_existing_file_until_atomic_replace( |
| 62 | monkeypatch, tmp_path |
| 63 | ) -> None: |
| 64 | from agent import AgentContextType |
| 65 | from helpers import persist_chat |
| 66 | |
| 67 | path = tmp_path / "chat.json" |
| 68 | path.write_text("previous", encoding="utf-8") |
| 69 | context = SimpleNamespace(id="chat", type=AgentContextType.USER, data={}) |
| 70 | |
| 71 | monkeypatch.setattr(persist_chat, "_get_chat_file_path", lambda _ctxid: str(path)) |
| 72 | monkeypatch.setattr(persist_chat, "_serialize_context", lambda _context: {"new": True}) |
| 73 | real_replace = persist_chat.os.replace |
| 74 | |
| 75 | def interrupted_replace(_source: str, destination: str) -> None: |
| 76 | assert Path(destination).read_text(encoding="utf-8") == "previous" |
| 77 | raise OSError("simulated interruption") |
| 78 | |
| 79 | monkeypatch.setattr(persist_chat.os, "replace", interrupted_replace) |
| 80 | with pytest.raises(OSError, match="simulated interruption"): |
| 81 | persist_chat.save_tmp_chat(context) |
| 82 | |
| 83 | assert path.read_text(encoding="utf-8") == "previous" |
| 84 | assert list(tmp_path.glob("*.tmp")) == [] |
| 85 | |
| 86 | monkeypatch.setattr(persist_chat.os, "replace", real_replace) |
| 87 | persist_chat.save_tmp_chat(context) |
| 88 | |
| 89 | assert json.loads(path.read_text(encoding="utf-8")) == {"new": True} |
| 90 | assert context.data[persist_chat.SAVED_CHAT_CONTEXT_DATA_KEY] is True |