fix(sessions): escape LIKE wildcards in the cwd-prefix clause
_cwd_prefix_clause builds "cwd is this directory or under it" for session
listing, workspace resume and prune/archive. The two LIKE arms bound the
raw prefix, so `_` and `%` acted as wildcards on a value that is a path:
cwd_prefix="/home/me/my_project"
main -> ['sibling', 'target'] # /home/me/myXproject/src matched too
fix -> ['target']
`_` matches any single character, so a same-length sibling directory with
children falls inside the pattern. prune_sessions() deletes the rows it
matches (and their on-disk transcripts), so an unrelated project's history
goes with it.
Escape the needle and pair both arms with ESCAPE, the convention the rest
of this file already uses; the literal separator backslash in the Windows
pattern is escaped for the same reason. The `=` arm is an exact compare and
keeps the raw prefix, so directory-and-children matching is unchanged.
Follow-up to the *_like filters in #78681, kept separate because this helper
is shared by four call sites beyond prune.
This commit is contained in:
parent
1d2dabce56
commit
b37de01926
|
|
@ -189,7 +189,16 @@ def _escape_like(text: str) -> str:
|
|||
|
||||
def _cwd_prefix_clause(cwd_prefix: str) -> Tuple[str, List[str]]:
|
||||
prefix = cwd_prefix.rstrip("/\\") or cwd_prefix
|
||||
return "(s.cwd = ? OR s.cwd LIKE ? OR s.cwd LIKE ?)", [prefix, f"{prefix}/%", f"{prefix}\\%"]
|
||||
# ``_`` and ``%`` are LIKE wildcards but ordinary characters in a path
|
||||
# (``my_project``), so an unescaped prefix also matches sibling directories.
|
||||
# Escape the needle and pair it with ESCAPE; the literal separator
|
||||
# backslash in the Windows pattern needs escaping for the same reason. The
|
||||
# ``=`` arm is an exact compare and keeps the raw prefix.
|
||||
esc = prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
return (
|
||||
"(s.cwd = ? OR s.cwd LIKE ? ESCAPE '\\' OR s.cwd LIKE ? ESCAPE '\\')",
|
||||
[prefix, f"{esc}/%", f"{esc}\\\\%"],
|
||||
)
|
||||
|
||||
|
||||
def _workspace_key_clause(key: str) -> Tuple[str, List[str]]:
|
||||
|
|
|
|||
|
|
@ -1121,6 +1121,35 @@ class TestPruneSessionFilters:
|
|||
assert {r["id"] for r in db.list_prune_candidates(title_like="smoke")} == {"smoke"}
|
||||
assert {r["id"] for r in db.list_prune_candidates(title_like=r"c:\tmp")} == {"winpath"}
|
||||
|
||||
def test_cwd_prefix_underscore_is_literal_not_a_wildcard(self, db):
|
||||
"""``_`` is a LIKE wildcard but an ordinary character in a path, so an
|
||||
unescaped prefix also matched a same-length sibling directory — and
|
||||
prune_sessions deletes what it matches."""
|
||||
self._mk(db, "target", cwd="/home/me/my_project/src")
|
||||
self._mk(db, "sibling", cwd="/home/me/myXproject/src")
|
||||
|
||||
rows = db.list_prune_candidates(cwd_prefix="/home/me/my_project")
|
||||
assert {r["id"] for r in rows} == {"target"}
|
||||
|
||||
pruned = db.prune_sessions(older_than_days=None, cwd_prefix="/home/me/my_project")
|
||||
assert pruned == 1
|
||||
assert db.get_session("sibling") is not None
|
||||
|
||||
def test_cwd_prefix_percent_does_not_select_everything(self, db):
|
||||
self._mk(db, "a", cwd="/home/me/one")
|
||||
self._mk(db, "b", cwd="/home/me/two")
|
||||
|
||||
assert db.list_prune_candidates(cwd_prefix="/home/me/%") == []
|
||||
|
||||
def test_cwd_prefix_still_matches_the_directory_and_its_children(self, db):
|
||||
"""Control: the prefix must keep matching itself and anything under it."""
|
||||
self._mk(db, "root", cwd="/home/me/proj")
|
||||
self._mk(db, "child", cwd="/home/me/proj/src")
|
||||
self._mk(db, "outside", cwd="/home/me/other")
|
||||
|
||||
rows = db.list_prune_candidates(cwd_prefix="/home/me/proj")
|
||||
assert {r["id"] for r in rows} == {"root", "child"}
|
||||
|
||||
def test_unknown_filter_rejected(self, db):
|
||||
import pytest as _pytest
|
||||
with _pytest.raises(TypeError):
|
||||
|
|
|
|||
Loading…
Reference in New Issue