Files
hermes-agent/tests/agent/test_live_db_preview_locks.py
kshitijk4poor 48196b0ceb fix(files): refuse live-DB downloads and share one sidecar-aware liveness check
FileResponse opens and closes the file in the dashboard process, so
downloading a live state.db (or its -shm/-wal) via /api/fs/download or
the managed-file read/download/media routes still cancelled the
connection's POSIX locks. Both now return 409 via is_live_database_file;
the registry lock is not held across the streamed response.

The main-or-WAL-sidecar rule now lives in one _live_main_key helper used
by offline_file_access, has_live_connection and read_header_bytes_preopen,
and the sidecar refusal names the main database the connection is open on.

@file previews hold _live_lock only for the raw read; token counting and
formatting run after release. The Linux lock test gains requires_wal
(Hermes uses DELETE mode on WAL-reset-vulnerable SQLite), covers the
download refusal, and drops an ambiguous conditional assert.

Co-authored-by: Benjamin PERRY <benjaminperry6@yahoo.fr>
2026-09-27 20:45:56 +05:30

119 lines
5.5 KiB
Python

"""File previews may not cancel SQLite's live POSIX locks (including WAL sidecars)."""
import asyncio
import os
from pathlib import Path
import sqlite3
import subprocess
import sys
import pytest
from fastapi import HTTPException
from agent.context_references import _expand_path_reference, parse_context_references
from hermes_state import SessionDB
from hermes_cli.web_routers.files import fs_download, fs_read_text
@pytest.mark.platforms("linux")
@pytest.mark.requires_wal
@pytest.mark.parametrize("route,target_kind", [
("file", "main"), ("folder", "directory"), ("desktop", "main"), ("desktop", "shm"),
])
def test_preview_preserves_live_database_locks(tmp_path, route, target_kind):
path = tmp_path / "state.db"
text = tmp_path / "normal.txt"
text.write_text("ordinary readable text", encoding="utf-8")
db = SessionDB(path)
lock_fd = None
try:
db.create_session("preview-test", "cli")
db.append_message("preview-test", "user", "before preview")
shm = Path(str(path) + "-shm")
target = {"main": path, "shm": shm, "directory": tmp_path}[target_kind]
conn = db._conn
assert isinstance(conn, sqlite3.Connection)
conn.execute("CREATE TABLE preview_markers (value TEXT)")
conn.commit()
conn.execute("BEGIN IMMEDIATE")
conn.execute("INSERT INTO preview_markers VALUES ('first process')")
# WAL writers reliably hold -shm locks, but not always a main-file lock.
# Hold our own POSIX main-file lock to prove that preview close() does
# not cancel any locks on that inode, regardless of SQLite's WAL timing.
import fcntl
lock_fd = os.open(path, os.O_RDONLY)
fcntl.lockf(lock_fd, fcntl.LOCK_SH, 1, 4096)
def posix_locks(file):
inode = file.stat().st_ino
return sorted(line.split(": ", 1)[1] for line in Path("/proc/locks").read_text(encoding="utf-8").splitlines()
if f":{inode} " in line and "POSIX ADVISORY" in line
and f" {os.getpid()} " in line)
def rival_locked():
code = ("import sqlite3,sys; c=sqlite3.connect(sys.argv[1], timeout=0); "
"c.execute('BEGIN IMMEDIATE'); c.rollback(); c.close()")
result = subprocess.run([sys.executable, "-c", code, str(path)],
capture_output=True, text=True, timeout=10)
return result.returncode != 0 and "database is locked" in result.stderr
before = (posix_locks(path), posix_locks(shm))
assert all(before), "fixture must hold POSIX main and WAL-sidecar locks"
assert rival_locked(), "second process must be excluded before the preview"
if route == "desktop":
with pytest.raises(HTTPException) as refused:
asyncio.run(fs_read_text(str(target)))
assert refused.value.status_code == 409
if target_kind == "shm":
assert "main database" in refused.value.detail
# FileResponse opens/closes in-process too, so a download must be refused as well.
with pytest.raises(HTTPException) as refused:
asyncio.run(fs_download(str(target)))
assert refused.value.status_code == 409
assert asyncio.run(fs_read_text(str(text)))["text"] == "ordinary readable text"
else:
ref = parse_context_references(f"@{route}:{target}")[0]
warning, block = _expand_path_reference(ref, tmp_path.parent)
assert warning is None
assert block is not None
if route == "file":
assert "not previewed" in block
ordinary = parse_context_references(f"@file:{text}")[0]
warning, block = _expand_path_reference(ordinary, tmp_path.parent)
assert warning is None and block is not None and "ordinary readable text" in block
assert (posix_locks(path), posix_locks(shm)) == before
assert rival_locked(), "second process entered a still-open write transaction"
conn.commit()
code = (
"import sqlite3,sys; c=sqlite3.connect(sys.argv[1], timeout=2); "
"assert c.execute('SELECT value FROM preview_markers').fetchall() == [('first process',)]; "
"c.execute(\"INSERT INTO preview_markers VALUES ('second process')\"); "
"c.commit(); c.close()"
)
rival = subprocess.run([sys.executable, "-c", code, str(path)],
capture_output=True, text=True, timeout=10)
assert rival.returncode == 0, rival.stderr
assert [row[0] for row in conn.execute(
"SELECT value FROM preview_markers ORDER BY rowid"
)] == ["first process", "second process"]
db.append_message("preview-test", "assistant", "after preview")
assert len(db.get_messages("preview-test")) == 2
finally:
db.close()
if lock_fd is not None:
os.close(lock_fd)
def test_closed_database_can_still_be_previewed(tmp_path):
path = tmp_path / "offline.db"
db = SessionDB(path)
db.create_session("offline", "cli")
db.close()
ref = parse_context_references(f"@file:{path}")[0]
warning, block = _expand_path_reference(ref, tmp_path.parent)
assert warning is None and block is not None and "binary file" in block
preview = asyncio.run(fs_read_text(str(path)))
assert preview["binary"] is True and preview["byteSize"] == path.stat().st_size
assert asyncio.run(fs_download(str(path))).path == str(path)