mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-14 10:37:23 +00:00
b24b0335f7
Merged as v0.50.230. 2685 tests passing. Browser QA 21/21. Closes the orphan-files leg of #1171. `new_session()` no longer writes an empty session to disk — the first disk write is deferred until the session has real state. Verified live: `POST /api/session/new` creates no `.json` file; session is findable by GET from in-memory SESSIONS dict. Attribution: original PR #1184 by @nesquena (Claude Code).
131 lines
4.9 KiB
Python
131 lines
4.9 KiB
Python
"""
|
|
Regression tests for the "no disk write for empty sessions" follow-up to #1171.
|
|
|
|
Lifecycle contract:
|
|
1. ``new_session()`` adds the session to the in-memory ``SESSIONS`` dict but
|
|
does NOT write a JSON file to disk.
|
|
2. The first ``s.save()`` happens when the session has real state to persist
|
|
(a user message via ``/api/chat/start``, or a populated title/messages
|
|
for btw / background agents).
|
|
3. ``get_session(sid)`` is unchanged: it checks ``SESSIONS`` first, so an
|
|
unsaved session is still findable by ID for the brief window between
|
|
create and first message.
|
|
4. ``all_sessions()`` already filters Untitled + 0-message sessions (#1171),
|
|
so an unsaved in-memory session does not surface in the sidebar even
|
|
though it lives in the SESSIONS dict.
|
|
|
|
Crash-safety: if the process exits between create and first message, the
|
|
session is lost. There were no messages to lose, so this is an explicit
|
|
trade-off documented in ``new_session``'s docstring.
|
|
"""
|
|
import json
|
|
import time
|
|
|
|
import pytest
|
|
|
|
import api.models as models
|
|
from api.models import (
|
|
SESSIONS,
|
|
Session,
|
|
all_sessions,
|
|
get_session,
|
|
new_session,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate(tmp_path, monkeypatch):
|
|
"""Redirect SESSION_DIR and SESSION_INDEX_FILE to a fresh tmp dir."""
|
|
session_dir = tmp_path / "sessions"
|
|
session_dir.mkdir()
|
|
index_file = session_dir / "_index.json"
|
|
monkeypatch.setattr(models, "SESSION_DIR", session_dir)
|
|
monkeypatch.setattr(models, "SESSION_INDEX_FILE", index_file)
|
|
SESSIONS.clear()
|
|
yield session_dir
|
|
SESSIONS.clear()
|
|
|
|
|
|
# ── 1. new_session does not write to disk ───────────────────────────────────
|
|
|
|
|
|
def test_new_session_does_not_write_to_disk(_isolate):
|
|
s = new_session()
|
|
assert not s.path.exists(), (
|
|
"new_session() must not eagerly persist an empty session — disk write "
|
|
"is deferred until the first message is appended (#1171 follow-up)"
|
|
)
|
|
|
|
|
|
def test_new_session_lives_in_memory(_isolate):
|
|
s = new_session()
|
|
assert s.session_id in SESSIONS
|
|
assert SESSIONS[s.session_id] is s
|
|
|
|
|
|
def test_get_session_finds_unsaved_session_by_id(_isolate):
|
|
"""The brief window between create and first message must still allow
|
|
/api/chat/start to look up the session by its returned session_id."""
|
|
s = new_session()
|
|
found = get_session(s.session_id)
|
|
assert found is s, (
|
|
"get_session must return the in-memory unsaved session — _handle_chat_start "
|
|
"depends on this for the very first message in a fresh session."
|
|
)
|
|
|
|
|
|
# ── 2. unsaved sessions never surface in the sidebar ─────────────────────────
|
|
|
|
|
|
def test_unsaved_empty_session_hidden_from_sidebar(_isolate):
|
|
"""all_sessions filters Untitled+0-message regardless of save state (#1171)."""
|
|
s = new_session()
|
|
ids = {row["session_id"] for row in all_sessions()}
|
|
assert s.session_id not in ids, (
|
|
"An unsaved empty Untitled session must not appear in /api/sessions"
|
|
)
|
|
|
|
|
|
# ── 3. save() materialises the file when state is real ─────────────────────
|
|
|
|
|
|
def test_save_writes_to_disk_when_first_invoked(_isolate):
|
|
"""The first save() (typically from _handle_chat_start after appending a
|
|
user message) creates the JSON file."""
|
|
s = new_session()
|
|
assert not s.path.exists()
|
|
s.messages.append({"role": "user", "content": "hello"})
|
|
s.save()
|
|
assert s.path.exists(), "save() must create the file once it's called"
|
|
content = json.loads(s.path.read_text(encoding="utf-8"))
|
|
assert content["session_id"] == s.session_id
|
|
assert content["messages"] and content["messages"][0]["role"] == "user"
|
|
|
|
|
|
def test_btw_background_pattern_still_persists(_isolate):
|
|
"""btw / background agents at api/routes.py call save() right after
|
|
populating title/messages — that path must continue to write to disk
|
|
even though new_session itself no longer saves."""
|
|
s = new_session()
|
|
s.title = "btw: question"
|
|
s.messages = [{"role": "user", "content": "hi"}]
|
|
s.save() # mirrors api/routes.py:_handle_btw / _handle_background
|
|
assert s.path.exists()
|
|
on_disk = json.loads(s.path.read_text(encoding="utf-8"))
|
|
assert on_disk["title"] == "btw: question"
|
|
|
|
|
|
# ── 4. crash-safety semantics: no orphan files accumulate on the new path ──
|
|
|
|
|
|
def test_repeated_new_session_creates_no_disk_files(_isolate, tmp_path):
|
|
"""Five news in a row produce zero disk files. Pre-fix this would have
|
|
written five orphan JSON files to SESSION_DIR."""
|
|
session_dir = tmp_path / "sessions"
|
|
for _ in range(5):
|
|
new_session()
|
|
on_disk_jsons = [p for p in session_dir.glob("*.json") if not p.name.startswith("_")]
|
|
assert on_disk_jsons == [], (
|
|
f"new_session() produced disk files: {[p.name for p in on_disk_jsons]}"
|
|
)
|