Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions hermes_state_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,13 @@ def _cwd_prefix_clause(cwd_prefix: str) -> Tuple[str, List[str]]:
prefix = cwd_prefix.rstrip("/\\") or cwd_prefix
# ``_``/``%`` are LIKE wildcards but ordinary path characters: unescaped, a
# prefix also matches sibling directories. The ``=`` arm keeps the raw prefix.
esc = _escape_like(prefix)
# A filesystem root already ends with its separator: adding another would
# search for //repo (or \\repo) instead of its direct descendants.
children = [_escape_like(prefix if prefix.endswith(sep) else prefix + sep) + "%"
for sep in ("/", "\\")]
return (
"(s.cwd = ? OR s.cwd LIKE ? ESCAPE '\\' OR s.cwd LIKE ? ESCAPE '\\')",
[prefix, f"{esc}/%", f"{esc}\\\\%"],
[prefix, *children],
)


Expand Down
46 changes: 46 additions & 0 deletions tests/test_session_root_cwd_filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Root and ordinary directory filters share exact directory-boundary semantics."""

import pytest

from hermes_state import SessionDB


@pytest.mark.parametrize("prefix, descendants, outside", [
("/", ["/", "/repo", "/repo/src"], ["relative", "C:\\repo"]),
("\\", ["\\", "\\repo", "\\repo\\src"], ["relative", "/repo"]),
("C:\\", ["C:\\", "C:\\repo", "C:\\repo\\src"], ["D:\\repo", "relative"]),
("/work/a_%/", ["/work/a_%", "/work/a_%/src"], ["/work/aXX/src", "/work/a_%other"]),
])
@pytest.mark.parametrize("order_by_last_active", [False, True])
def test_directory_filter_agrees_across_list_count_and_resume(
tmp_path, prefix, descendants, outside, order_by_last_active,
):
# These are stored path strings, not a simulated host OS or real directories.
db = SessionDB(tmp_path / "state.db")
try:
for index, cwd in enumerate(descendants + outside):
db.create_session(f"s{index}", source="cli", cwd=cwd)
expected = {f"s{i}" for i in range(len(descendants))}
rows = db.list_sessions_rich(cwd_prefix=prefix, order_by_last_active=order_by_last_active)
assert {row["id"] for row in rows} == expected
assert db.session_count(cwd_prefix=prefix) == len(expected)
assert {row["id"] for row in db.search_sessions(workspace_key=prefix)} == expected
finally:
db.close()


def test_root_prune_matches_its_preview_and_preserves_outside_rows(tmp_path):
db = SessionDB(tmp_path / "state.db")
try:
for sid, cwd in [("root", "/"), ("child", "/repo/src"), ("outside", "relative")]:
db.create_session(sid, source="cli", cwd=cwd)
db.append_message(sid, role="user", content=sid)
db.end_session(sid, end_reason="completed")
candidates = db.list_prune_candidates(cwd_prefix="/")
assert {row["id"] for row in candidates} == {"root", "child"}
assert db.count_prune_matches(cwd_prefix="/") == len(candidates)
assert db.prune_sessions(older_than_days=None, cwd_prefix="/") == len(candidates)
assert {row["id"] for row in db.search_sessions()} == {"outside"}
assert db.get_messages("outside")[0]["content"] == "outside"
finally:
db.close()