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
22 changes: 12 additions & 10 deletions python/packages/core/agent_framework/_harness/_file_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -1113,6 +1113,7 @@ async def list_children(self, directory: str = "") -> list[FileStoreEntry]:
prefix = _normalize_relative_path(directory, is_directory=True).lower()
if prefix and not prefix.endswith("/"):
prefix += "/"
prefix_depth = prefix.count("/")
async with self._lock:
entries = [(key, display) for key, (display, _) in self._files.items()]
files: list[str] = []
Expand All @@ -1121,18 +1122,18 @@ async def list_children(self, directory: str = "") -> list[FileStoreEntry]:
for key, display in entries:
if not key.startswith(prefix):
continue
remainder = key[len(prefix) :]
separator_index = remainder.find("/")
if separator_index == -1:
# ``display`` is the original-case normalized path; strip the
# directory prefix using the same length we matched on ``key``.
files.append(display[len(prefix) :])
elif separator_index > 0:
segment_key = remainder[:separator_index]
# Unicode lowercasing can change character counts, so key offsets
# cannot be used to slice the original display path.
remainder = display.split("/", prefix_depth)[-1]
segment, separator, _ = remainder.partition("/")
if not separator:
files.append(remainder)
elif segment:
segment_key = segment.lower()
if segment_key in seen_dirs:
continue
seen_dirs.add(segment_key)
directories.append(display[len(prefix) : len(prefix) + separator_index])
directories.append(segment)
results: list[FileStoreEntry] = [FileStoreEntry(name, FileStoreEntry.DIRECTORY) for name in directories]
results.extend(FileStoreEntry(name, FileStoreEntry.FILE) for name in files)
return results
Expand Down Expand Up @@ -1165,6 +1166,7 @@ async def search(
prefix = _normalize_relative_path(directory, is_directory=True).lower()
if prefix and not prefix.endswith("/"):
prefix += "/"
prefix_depth = prefix.count("/")
search_pattern = _compile_search_regex(regex_pattern)

async with self._lock:
Expand All @@ -1178,7 +1180,7 @@ def scan() -> list[FileSearchResult]:
relative_key = key[len(prefix) :]
if not recursive and "/" in relative_key:
continue
relative_display = display[len(prefix) :]
relative_display = display.split("/", prefix_depth)[-1]
if not _matches_glob(relative_display, glob_pattern):
continue
result = AgentFileStore.scan_content(relative_display, file_content, search_pattern)
Expand Down
32 changes: 32 additions & 0 deletions python/packages/core/tests/core/test_harness_file_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,38 @@ async def test_in_memory_store_round_trips_files() -> None:
assert sorted(await _list_files(store)) == []


@pytest.mark.parametrize("directory", ["İstanbul", "i\u0307stanbul", "İSTANBUL/"])
async def test_in_memory_store_lists_unicode_paths_without_truncation(directory: str) -> None:
"""Length-changing lowercasing must not alter the displayed path components."""
store = InMemoryAgentFileStore()
await store.write("İstanbul/Notes.md", "memory")
await store.write("İstanbul/İzmir/plan.md", "nested memory")
await store.write("i\u0307stanbul/i\u0307zmir/other.md", "another memory")

assert await _list_dirs(store) == ["İstanbul"]
assert await _list_files(store, directory) == ["Notes.md"]
assert await _list_dirs(store, directory) == ["İzmir"]
assert sorted(await _list_files(store, f"{directory.rstrip('/')}/İzmir")) == ["other.md", "plan.md"]
assert await store.read(f"{directory.rstrip('/')}/notes.MD") == "memory"


@pytest.mark.parametrize("directory", ["İstanbul", "i\u0307stanbul", "İSTANBUL/"])
async def test_in_memory_store_search_preserves_unicode_relative_paths(directory: str) -> None:
"""Search results must retain readable paths after matching a Unicode directory."""
store = InMemoryAgentFileStore()
await store.write("İstanbul/Notes.md", "memory")
await store.write("İstanbul/İzmir/plan.md", "memory")
await store.write("other/notes.md", "memory")

direct = await store.search(directory, "memory", "Notes.md")
assert [result.file_name for result in direct] == ["Notes.md"]

recursive = await store.search(directory, "memory", recursive=True)
assert {result.file_name for result in recursive} == {"Notes.md", "İzmir/plan.md"}
for result in recursive:
assert await store.read(f"{directory.rstrip('/')}/{result.file_name}") == "memory"


async def test_in_memory_store_search_returns_matches_with_snippets() -> None:
"""The in-memory store should search file content case-insensitively and respect glob filters."""
store = InMemoryAgentFileStore()
Expand Down
Loading