fix(session): serialize canonical file access (#5383)

This commit is contained in:
chengyongru
2026-08-14 10:32:17 +08:00
committed by GitHub
parent e3d1819a2b
commit e226242dfc
10 changed files with 262 additions and 77 deletions
+24 -13
View File
@@ -29,8 +29,11 @@ class TestPruneDreamSessions:
import os
import time
sessions_dir = tmp_path / "sessions"
sessions_dir.mkdir()
manager = SessionManager(
tmp_path / "workspace",
sessions_root=tmp_path / "runtime",
)
sessions_dir = manager.sessions_dir
base_time = time.time() - 100
dream_paths = []
@@ -50,7 +53,7 @@ class TestPruneDreamSessions:
normal_path = sessions_dir / "telegram_123.jsonl"
normal_path.write_text('{"_type": "metadata"}\n', encoding="utf-8")
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
MemoryStore.prune_dream_sessions(manager, keep=10)
assert [path.exists() for path in dream_paths] == [False] * 5 + [True] * 10
assert normal_path.exists()
@@ -59,8 +62,11 @@ class TestPruneDreamSessions:
import os
import time
sessions_dir = tmp_path / "sessions"
sessions_dir.mkdir()
manager = SessionManager(
tmp_path / "workspace",
sessions_root=tmp_path / "runtime",
)
sessions_dir = manager.sessions_dir
base_time = time.time() - 100
current_paths = []
@@ -81,24 +87,29 @@ class TestPruneDreamSessions:
)
os.utime(legacy_path, (base_time - 1, base_time - 1))
MemoryStore.prune_dream_sessions(sessions_dir, keep=1)
MemoryStore.prune_dream_sessions(manager, keep=1)
assert [path.exists() for path in current_paths] == [False, True]
assert legacy_path.exists()
def test_noop_when_under_limit(self, tmp_path):
sessions_dir = tmp_path / "sessions"
sessions_dir.mkdir()
manager = SessionManager(
tmp_path / "workspace",
sessions_root=tmp_path / "runtime",
)
sessions_dir = manager.sessions_dir
for i in range(3):
key = f"dream:20260528-{100000 + i:06d}"
path = sessions_dir / f"{SessionManager._storage_key(key)}.jsonl"
path.write_text("{}", encoding="utf-8")
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
MemoryStore.prune_dream_sessions(manager, keep=10)
assert len(list(sessions_dir.glob("*.jsonl"))) == 3
def test_empty_dir_noop(self, tmp_path):
sessions_dir = tmp_path / "sessions"
sessions_dir.mkdir()
MemoryStore.prune_dream_sessions(sessions_dir, keep=10)
assert list(sessions_dir.iterdir()) == []
manager = SessionManager(
tmp_path / "workspace",
sessions_root=tmp_path / "runtime",
)
MemoryStore.prune_dream_sessions(manager, keep=10)
assert list(manager.sessions_dir.glob("*.jsonl")) == []
+40 -9
View File
@@ -4,6 +4,9 @@ import json
from datetime import datetime
from pathlib import Path
import pytest
from filelock import Timeout
from nanobot.providers.base import ProviderConversationState
from nanobot.session.manager import Session, SessionManager
@@ -37,14 +40,23 @@ class TestAtomicSave:
tmp_files = list(mgr.sessions_dir.glob("*.tmp"))
assert tmp_files == []
def test_tmp_file_cleaned_up_on_write_failure(self, tmp_path: Path):
def test_unique_tmp_file_cleaned_up_on_write_failure(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
):
mgr = SessionManager(tmp_path)
session = Session(key="test:fail")
path = mgr._get_session_path("test:fail")
tmp_path_file = path.with_suffix(".jsonl.tmp")
stale_shared_tmp = path.with_suffix(".jsonl.tmp")
unique_tmp = path.with_name(f".{path.name}.save-failure.tmp")
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path_file.write_text("stale")
stale_shared_tmp.write_text("stale", encoding="utf-8")
monkeypatch.setattr(
"nanobot.session.manager.secrets.token_hex",
lambda _length: "save-failure",
)
class BadMessage:
def __init__(self, data):
@@ -64,13 +76,17 @@ class TestAtomicSave:
]
import unittest.mock
with unittest.mock.patch("nanobot.session.manager.json.dumps", side_effect=failing_dumps):
try:
mgr.save(session)
except OSError:
pass
with (
unittest.mock.patch(
"nanobot.session.manager.json.dumps",
side_effect=failing_dumps,
),
pytest.raises(OSError, match="simulated disk full"),
):
mgr.save(session)
assert not tmp_path_file.exists()
assert not unique_tmp.exists()
assert stale_shared_tmp.read_text(encoding="utf-8") == "stale"
def test_overwrite_preserves_latest_data(self, tmp_path: Path):
mgr = SessionManager(tmp_path)
@@ -102,6 +118,21 @@ class TestAtomicSave:
for i in range(5):
assert loaded.messages[i]["content"] == f"msg{i}"
def test_managers_for_same_directory_coordinate_saves(self, tmp_path: Path):
workspace = tmp_path / "workspace"
sessions_root = tmp_path / "runtime"
owner = SessionManager(workspace, sessions_root=sessions_root)
peer = SessionManager(workspace, sessions_root=sessions_root)
assert owner.sessions_dir == peer.sessions_dir
session = Session(key="test:peer-manager")
peer._jsonl_store._session_files_lock.timeout = 0
with owner.locked_session_files(), pytest.raises(Timeout):
peer.save(session)
peer.save(session)
assert peer._get_session_path(session.key).is_file()
def test_provider_state_round_trips_in_private_record_only(self, tmp_path: Path):
mgr = SessionManager(tmp_path)
secret = "encrypted-reasoning-blob"