Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/bub/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def fetch_all(self, query: TapeQuery) -> Iterable[TapeEntry]:
if not query._query:
result: Iterable[TapeEntry] = super().fetch_all(query)
return result
unlimited_query = replace(query, _limit=None)
unlimited_query = replace(query, _query=None, _limit=None)
entries: Iterable[TapeEntry] = super().fetch_all(unlimited_query)
return self._filter_entries(list(entries), query._query, query._limit or 20)

Expand Down
2 changes: 1 addition & 1 deletion src/bub/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def workspace_from_state(state: TurnState) -> Path:
def get_entry_text(entry: TapeEntry) -> str:
import yaml

return yaml.safe_dump(entry.payload)
return yaml.safe_dump(entry.payload, allow_unicode=True)


async def maybe_context_manager(obj: Any, stack: AsyncExitStack) -> Any:
Expand Down
85 changes: 85 additions & 0 deletions tests/test_file_tape_store_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from __future__ import annotations

from pathlib import Path

import pytest

from bub.store import FileTapeStore, TapeQuery
from bub.tape import TapeEntry


@pytest.mark.parametrize(
("content", "query", "matches"),
[
("deployment configuration", "deployment", True),
("Deployment configuration", "DEPLOYMENT", True),
("deployment configuration", "deploymnt", True),
("项目配置和部署说明", "项目配置", True),
("café configuration", "CAFÉ", True),
("deployment 🚀 ready", "🚀", True),
("deployment configuration", "zqxjkv", False),
("deployment configuration", "xy", False),
("deployment configuration", " ", False),
],
)
def test_file_search_matches_persisted_payload(tmp_path: Path, content: str, query: str, matches: bool) -> None:
store = FileTapeStore(tmp_path)
store.append("session", TapeEntry.message({"role": "user", "content": content}))

# Reopen the store so the query exercises persisted data rather than its write cache.
reopened = FileTapeStore(tmp_path)
search = TapeQuery("session", reopened).query(query)
entries = list(search.all())

assert [entry.payload["content"] for entry in entries] == ([content] if matches else [])
assert search._query == query


@pytest.mark.parametrize("limit", [1, 2, 10])
def test_fuzzy_search_preserves_filters_order_deduplication_and_limit(tmp_path: Path, limit: int) -> None:
store = FileTapeStore(tmp_path)
entries = [
TapeEntry.message({"content": "deployment before anchor"}),
TapeEntry.anchor("start"),
TapeEntry(0, "message", {"content": "deployment alpha"}, date="2026-01-02T00:00:00Z"),
TapeEntry(0, "message", {"content": "deployment alpha"}, date="2026-01-02T12:00:00Z"),
TapeEntry(0, "message", {"content": "deployment beta"}, date="2026-01-03T23:59:59Z"),
TapeEntry(0, "system", {"content": "deployment system"}, date="2026-01-03T00:00:00Z"),
TapeEntry(0, "message", {"content": "deployment outside dates"}, date="2026-01-01T00:00:00Z"),
TapeEntry.anchor("end"),
TapeEntry.message({"content": "deployment after anchor"}),
]
for entry in entries:
store.append("session", entry)
search = (
TapeQuery("session", store)
.between_anchors("start", "end")
.between_dates("2026-01-02", "2026-01-03")
.kinds("message")
.query("deploymnt")
.limit(limit)
)

assert [entry.id for entry in search.all()] == [5, 4][:limit]
assert search._query == "deploymnt"
assert search._limit == limit


@pytest.mark.parametrize("last", [False, True])
def test_fuzzy_search_preserves_after_anchor_selection(tmp_path: Path, last: bool) -> None:
store = FileTapeStore(tmp_path)
store.append("session", TapeEntry.message({"content": "deployment before"}))
store.append("session", TapeEntry.anchor("checkpoint"))
store.append("session", TapeEntry.message({"content": "deployment after"}))
search = TapeQuery("session", store).query("deploymnt")
search = search.last_anchor() if last else search.after_anchor("checkpoint")

assert [entry.id for entry in search.all()] == [3]


def test_unqueried_file_search_retains_chronological_order(tmp_path: Path) -> None:
store = FileTapeStore(tmp_path)
for content in ("first", "second", "third"):
store.append("session", TapeEntry.message({"content": content}))

assert [entry.id for entry in TapeQuery("session", store).limit(2).all()] == [1, 2]
Loading