mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
test(agents): cover live filesystem middleware, retire dead twin
The single-agent-era filesystem middleware (app/agents/shared/middleware/ filesystem.py, ~2000 lines) was never instantiated in production, yet three unit suites validated it — an illusory guardrail while the live decomposed middleware (multi_agent_chat/middleware/shared/filesystem) was unguarded. Close the gap before reorganizing the agents module: - Add 14 integration tests driving live B's tools in desktop mode (real on-disk effects) and cloud mode (in-state staging, namespace policy). - Port all high-value dead-twin assertions onto the live path: cloud rm/rmdir staging + guard rails, KBPostgresBackend delete-view filter, mode-scoped system prompt, cwd/relative/namespace resolution, multi-root mount normalization. - Delete dead twin filesystem.py, drop its __init__ re-export, and retire its 3 dead-twin tests. Verified: test_import_all + middleware unit + FS integration all green.
This commit is contained in:
parent
f3484f5a24
commit
1acde6a470
9 changed files with 960 additions and 2492 deletions
|
|
@ -21,9 +21,6 @@ from app.agents.shared.middleware.doom_loop import DoomLoopMiddleware
|
|||
from app.agents.shared.middleware.file_intent import (
|
||||
FileIntentMiddleware,
|
||||
)
|
||||
from app.agents.shared.middleware.filesystem import (
|
||||
SurfSenseFilesystemMiddleware,
|
||||
)
|
||||
from app.agents.shared.middleware.flatten_system import (
|
||||
FlattenSystemMessageMiddleware,
|
||||
)
|
||||
|
|
@ -78,7 +75,6 @@ __all__ = [
|
|||
"SpillToBackendEdit",
|
||||
"SpillingContextEditingMiddleware",
|
||||
"SurfSenseCompactionMiddleware",
|
||||
"SurfSenseFilesystemMiddleware",
|
||||
"ToolCallNameRepairMiddleware",
|
||||
"build_skills_backend_factory",
|
||||
"commit_staged_filesystem_state",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,198 @@
|
|||
"""Real-behavior tests for the LIVE knowledge-base filesystem middleware (B) in
|
||||
cloud mode.
|
||||
|
||||
Cloud mode is the default production filesystem for web chat. Unlike desktop,
|
||||
cloud writes/edits/moves/deletes are *staged* into LangGraph state during the
|
||||
turn and committed to Postgres at end-of-turn by the persistence middleware.
|
||||
These tests drive the production ``build_filesystem_mw`` cloud tools through a
|
||||
real ``create_agent`` graph and assert the staging contract (namespace policy,
|
||||
read-from-stage, mkdir staging, duplicate rejection) — all deterministic and
|
||||
DB-free because cloud ``awrite`` is pure in-state staging.
|
||||
|
||||
The end-of-turn DB commit (``commit_staged_filesystem_state``) is covered
|
||||
separately; here we lock the per-tool behavior that the reorg could break.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from langchain.agents import create_agent
|
||||
from langchain_core.messages import HumanMessage, ToolMessage
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
from app.agents.multi_agent_chat.middleware.shared.filesystem import (
|
||||
build_filesystem_mw,
|
||||
)
|
||||
from app.agents.shared.filesystem_backends import build_backend_resolver
|
||||
from app.agents.shared.filesystem_selection import FilesystemMode, FilesystemSelection
|
||||
from tests.integration.harness import ScriptedTurn, build_scripted_harness
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
|
||||
|
||||
_SEARCH_SPACE_ID = 1
|
||||
|
||||
|
||||
def _build_cloud_fs_mw():
|
||||
"""Build the production filesystem middleware in cloud mode.
|
||||
|
||||
A non-None ``search_space_id`` makes the resolver hand out a
|
||||
``KBPostgresBackend``, exactly as production does. Staging operations never
|
||||
touch the DB, so a dummy id is sufficient for these tests.
|
||||
"""
|
||||
selection = FilesystemSelection(mode=FilesystemMode.CLOUD)
|
||||
resolver = build_backend_resolver(selection, search_space_id=_SEARCH_SPACE_ID)
|
||||
return build_filesystem_mw(
|
||||
backend_resolver=resolver,
|
||||
filesystem_mode=FilesystemMode.CLOUD,
|
||||
search_space_id=_SEARCH_SPACE_ID,
|
||||
user_id="00000000-0000-0000-0000-000000000001",
|
||||
thread_id=_SEARCH_SPACE_ID,
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
|
||||
async def _run(turns: list[ScriptedTurn], thread: str):
|
||||
harness = build_scripted_harness(turns=turns)
|
||||
agent = create_agent(
|
||||
harness.model,
|
||||
tools=[],
|
||||
middleware=[_build_cloud_fs_mw()],
|
||||
checkpointer=InMemorySaver(),
|
||||
)
|
||||
return await agent.ainvoke(
|
||||
{"messages": [HumanMessage(content="do kb work")]},
|
||||
config={"configurable": {"thread_id": thread}},
|
||||
)
|
||||
|
||||
|
||||
def _tool_text(result, name: str) -> str:
|
||||
for m in result["messages"]:
|
||||
if isinstance(m, ToolMessage) and m.name == name:
|
||||
return str(m.content)
|
||||
raise AssertionError(f"no ToolMessage from {name!r}")
|
||||
|
||||
|
||||
def _write(path: str, content: str, call_id: str) -> ScriptedTurn:
|
||||
return ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "write_file",
|
||||
"args": {"file_path": path, "content": content},
|
||||
"id": call_id,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def test_cloud_write_then_read_returns_staged_content():
|
||||
"""A cloud write stages into state and a later read returns that content."""
|
||||
result = await _run(
|
||||
[
|
||||
_write("/documents/note.md", "cloud CANARY-CLD-1", "c1"),
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "read_file",
|
||||
"args": {"file_path": "/documents/note.md"},
|
||||
"id": "c2",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-cloud-write-read",
|
||||
)
|
||||
|
||||
assert "Updated file /documents/note.md" in _tool_text(result, "write_file")
|
||||
assert "CANARY-CLD-1" in _tool_text(result, "read_file")
|
||||
|
||||
|
||||
async def test_cloud_write_outside_documents_is_rejected():
|
||||
"""Cloud namespace policy: writes must target /documents (non-temp paths)."""
|
||||
result = await _run(
|
||||
[
|
||||
_write("/scratch/note.md", "nope", "c1"),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-cloud-namespace",
|
||||
)
|
||||
|
||||
msg = _tool_text(result, "write_file")
|
||||
assert "must target /documents" in msg
|
||||
|
||||
|
||||
async def test_cloud_temp_prefixed_write_is_allowed_anywhere():
|
||||
"""A ``temp_`` basename escapes the /documents namespace restriction."""
|
||||
result = await _run(
|
||||
[
|
||||
_write("/temp_scratch.md", "ephemeral", "c1"),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-cloud-temp",
|
||||
)
|
||||
|
||||
msg = _tool_text(result, "write_file")
|
||||
assert "must target /documents" not in msg
|
||||
assert "Updated file" in msg
|
||||
|
||||
|
||||
async def test_cloud_mkdir_stages_directory():
|
||||
"""Cloud mkdir stages the directory for end-of-turn creation (no immediate IO)."""
|
||||
result = await _run(
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "mkdir",
|
||||
"args": {"path": "/documents/projects"},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-cloud-mkdir",
|
||||
)
|
||||
|
||||
msg = _tool_text(result, "mkdir")
|
||||
assert "Staged directory" in msg
|
||||
assert "/documents/projects" in msg
|
||||
|
||||
|
||||
async def test_cloud_mkdir_outside_documents_is_rejected():
|
||||
"""Cloud mkdir is also restricted to the /documents namespace."""
|
||||
result = await _run(
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{"name": "mkdir", "args": {"path": "/elsewhere"}, "id": "c1"}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-cloud-mkdir-bad",
|
||||
)
|
||||
|
||||
assert "must target a path under /documents" in _tool_text(result, "mkdir")
|
||||
|
||||
|
||||
async def test_cloud_duplicate_write_is_rejected():
|
||||
"""Writing to a path already staged this turn is rejected (use edit instead)."""
|
||||
result = await _run(
|
||||
[
|
||||
_write("/documents/dup.md", "first", "c1"),
|
||||
_write("/documents/dup.md", "second", "c2"),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-cloud-dup",
|
||||
)
|
||||
|
||||
# Two write ToolMessages: first succeeds, second is rejected.
|
||||
write_msgs = [
|
||||
str(m.content)
|
||||
for m in result["messages"]
|
||||
if isinstance(m, ToolMessage) and m.name == "write_file"
|
||||
]
|
||||
assert len(write_msgs) == 2
|
||||
assert "Updated file" in write_msgs[0]
|
||||
assert "already exists" in write_msgs[1]
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
"""Real-behavior tests for the LIVE knowledge-base filesystem middleware (B).
|
||||
|
||||
These exercise ``app.agents.multi_agent_chat.middleware.shared.filesystem`` —
|
||||
the decomposed middleware + tools that production actually mounts on the
|
||||
knowledge_base subagent (via ``build_filesystem_mw``). The previous
|
||||
``tests/unit/middleware/test_filesystem_*.py`` suite asserts a *dead twin*
|
||||
(``app.agents.shared.middleware.filesystem``) that is never instantiated, so the
|
||||
live tool path had no real coverage.
|
||||
|
||||
Strategy: mount the production ``build_filesystem_mw`` on a minimal
|
||||
``create_agent`` graph and drive its tools with the scripted harness. Desktop
|
||||
mode binds a ``MultiRootLocalFolderBackend`` to a real ``tmp_path`` directory,
|
||||
so every write/edit/move/rm is asserted against the real on-disk filesystem —
|
||||
no mocks, only the LLM is scripted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from langchain.agents import create_agent
|
||||
from langchain_core.messages import HumanMessage, ToolMessage
|
||||
from langgraph.checkpoint.memory import InMemorySaver
|
||||
|
||||
from app.agents.multi_agent_chat.middleware.shared.filesystem import (
|
||||
build_filesystem_mw,
|
||||
)
|
||||
from app.agents.shared.filesystem_backends import build_backend_resolver
|
||||
from app.agents.shared.filesystem_selection import (
|
||||
FilesystemMode,
|
||||
FilesystemSelection,
|
||||
LocalFilesystemMount,
|
||||
)
|
||||
from tests.integration.harness import ScriptedTurn, build_scripted_harness
|
||||
|
||||
pytestmark = [pytest.mark.integration, pytest.mark.asyncio]
|
||||
|
||||
_MOUNT_ID = "workspace"
|
||||
|
||||
|
||||
def _build_desktop_fs_mw(root: Path):
|
||||
"""Build the production filesystem middleware bound to a real local folder."""
|
||||
selection = FilesystemSelection(
|
||||
mode=FilesystemMode.DESKTOP_LOCAL_FOLDER,
|
||||
local_mounts=(
|
||||
LocalFilesystemMount(mount_id=_MOUNT_ID, root_path=str(root)),
|
||||
),
|
||||
)
|
||||
resolver = build_backend_resolver(selection)
|
||||
return build_filesystem_mw(
|
||||
backend_resolver=resolver,
|
||||
filesystem_mode=FilesystemMode.DESKTOP_LOCAL_FOLDER,
|
||||
search_space_id=1,
|
||||
user_id="00000000-0000-0000-0000-000000000001",
|
||||
thread_id=1,
|
||||
read_only=False,
|
||||
)
|
||||
|
||||
|
||||
async def _run(root: Path, turns: list[ScriptedTurn], thread: str):
|
||||
"""Assemble a 1-middleware agent and drive the scripted turns to completion."""
|
||||
harness = build_scripted_harness(turns=turns)
|
||||
fs_mw = _build_desktop_fs_mw(root)
|
||||
agent = create_agent(
|
||||
harness.model,
|
||||
tools=[],
|
||||
middleware=[fs_mw],
|
||||
checkpointer=InMemorySaver(),
|
||||
)
|
||||
return await agent.ainvoke(
|
||||
{"messages": [HumanMessage(content="do filesystem work")]},
|
||||
config={"configurable": {"thread_id": thread}},
|
||||
)
|
||||
|
||||
|
||||
def _tool_messages(result) -> list[ToolMessage]:
|
||||
return [m for m in result["messages"] if isinstance(m, ToolMessage)]
|
||||
|
||||
|
||||
def _tool_text(result, name: str) -> str:
|
||||
for m in _tool_messages(result):
|
||||
if m.name == name:
|
||||
return str(m.content)
|
||||
raise AssertionError(f"no ToolMessage from {name!r} in {_tool_messages(result)}")
|
||||
|
||||
|
||||
async def test_write_then_read_round_trip(tmp_path: Path):
|
||||
"""write_file persists to the real folder and read_file returns the content."""
|
||||
result = await _run(
|
||||
tmp_path,
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "write_file",
|
||||
"args": {
|
||||
"file_path": f"/{_MOUNT_ID}/notes.md",
|
||||
"content": "hello FS-CANARY-001",
|
||||
},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "read_file",
|
||||
"args": {"file_path": f"/{_MOUNT_ID}/notes.md"},
|
||||
"id": "c2",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-desktop-write-read",
|
||||
)
|
||||
|
||||
# Real on-disk effect, not a mock.
|
||||
assert (tmp_path / "notes.md").read_text() == "hello FS-CANARY-001"
|
||||
# The tool actually returned the file content.
|
||||
assert "FS-CANARY-001" in _tool_text(result, "read_file")
|
||||
|
||||
|
||||
async def test_write_then_ls_lists_file(tmp_path: Path):
|
||||
"""ls reflects a freshly written file in the real folder."""
|
||||
result = await _run(
|
||||
tmp_path,
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "write_file",
|
||||
"args": {
|
||||
"file_path": f"/{_MOUNT_ID}/report.md",
|
||||
"content": "x",
|
||||
},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{"name": "ls", "args": {"path": f"/{_MOUNT_ID}"}, "id": "c2"}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-desktop-ls",
|
||||
)
|
||||
|
||||
assert (tmp_path / "report.md").exists()
|
||||
assert "report.md" in _tool_text(result, "ls")
|
||||
|
||||
|
||||
async def test_edit_file_rewrites_on_disk(tmp_path: Path):
|
||||
"""edit_file applies a real string replacement to the on-disk file."""
|
||||
result = await _run(
|
||||
tmp_path,
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "write_file",
|
||||
"args": {
|
||||
"file_path": f"/{_MOUNT_ID}/doc.md",
|
||||
"content": "the quick brown fox",
|
||||
},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "edit_file",
|
||||
"args": {
|
||||
"file_path": f"/{_MOUNT_ID}/doc.md",
|
||||
"old_string": "brown",
|
||||
"new_string": "red",
|
||||
},
|
||||
"id": "c2",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-desktop-edit",
|
||||
)
|
||||
|
||||
assert (tmp_path / "doc.md").read_text() == "the quick red fox"
|
||||
|
||||
|
||||
async def test_write_into_existing_subdir(tmp_path: Path):
|
||||
"""A write into an EXISTING subdirectory lands on disk under that folder."""
|
||||
(tmp_path / "sub").mkdir()
|
||||
result = await _run(
|
||||
tmp_path,
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "write_file",
|
||||
"args": {
|
||||
"file_path": f"/{_MOUNT_ID}/sub/inner.md",
|
||||
"content": "nested",
|
||||
},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-desktop-subdir",
|
||||
)
|
||||
|
||||
assert "Error" not in _tool_text(result, "write_file")
|
||||
assert (tmp_path / "sub" / "inner.md").read_text() == "nested"
|
||||
|
||||
|
||||
async def test_write_to_missing_parent_dir_is_rejected(tmp_path: Path):
|
||||
"""Desktop write refuses to create a file under a non-existent directory.
|
||||
|
||||
Real current behavior: the local-folder backend requires the parent to
|
||||
exist (and ``mkdir`` is a no-op for this backend), so the agent cannot
|
||||
fabricate new nested folders via ``write_file``. Locking this guards against
|
||||
a silent behavior change during the agents-module reorg.
|
||||
"""
|
||||
result = await _run(
|
||||
tmp_path,
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "write_file",
|
||||
"args": {
|
||||
"file_path": f"/{_MOUNT_ID}/missing/inner.md",
|
||||
"content": "nested",
|
||||
},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-desktop-missing-parent",
|
||||
)
|
||||
|
||||
write_msg = _tool_text(result, "write_file")
|
||||
assert "parent directory" in write_msg.lower()
|
||||
assert not (tmp_path / "missing").exists()
|
||||
|
||||
|
||||
async def test_move_file_relocates_on_disk(tmp_path: Path):
|
||||
"""move_file relocates the real file from source to destination."""
|
||||
await _run(
|
||||
tmp_path,
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "write_file",
|
||||
"args": {
|
||||
"file_path": f"/{_MOUNT_ID}/src.md",
|
||||
"content": "movable",
|
||||
},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "move_file",
|
||||
"args": {
|
||||
"source_path": f"/{_MOUNT_ID}/src.md",
|
||||
"destination_path": f"/{_MOUNT_ID}/dst.md",
|
||||
},
|
||||
"id": "c2",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-desktop-move",
|
||||
)
|
||||
|
||||
assert not (tmp_path / "src.md").exists()
|
||||
assert (tmp_path / "dst.md").read_text() == "movable"
|
||||
|
||||
|
||||
async def test_rm_deletes_file_on_disk(tmp_path: Path):
|
||||
"""rm removes the real file (desktop deletes are immediate)."""
|
||||
await _run(
|
||||
tmp_path,
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "write_file",
|
||||
"args": {
|
||||
"file_path": f"/{_MOUNT_ID}/trash.md",
|
||||
"content": "bye",
|
||||
},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "rm",
|
||||
"args": {"path": f"/{_MOUNT_ID}/trash.md"},
|
||||
"id": "c2",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-desktop-rm",
|
||||
)
|
||||
|
||||
assert not (tmp_path / "trash.md").exists()
|
||||
|
||||
|
||||
async def test_rmdir_removes_empty_dir_on_disk(tmp_path: Path):
|
||||
"""rmdir removes a real empty directory."""
|
||||
(tmp_path / "gone").mkdir()
|
||||
assert (tmp_path / "gone").is_dir()
|
||||
|
||||
result = await _run(
|
||||
tmp_path,
|
||||
[
|
||||
ScriptedTurn(
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "rmdir",
|
||||
"args": {"path": f"/{_MOUNT_ID}/gone"},
|
||||
"id": "c1",
|
||||
}
|
||||
]
|
||||
),
|
||||
ScriptedTurn(text="done"),
|
||||
],
|
||||
"fs-desktop-rmdir",
|
||||
)
|
||||
|
||||
assert "Error" not in _tool_text(result, "rmdir")
|
||||
assert not (tmp_path / "gone").exists()
|
||||
|
|
@ -0,0 +1,287 @@
|
|||
"""Path/cwd/namespace + multi-root mount-normalization tests for LIVE filesystem.
|
||||
|
||||
Ported from the dead-twin suites:
|
||||
* ``tests/unit/middleware/test_filesystem_middleware.py`` (cwd defaults,
|
||||
relative resolution, cloud write-namespace policy)
|
||||
* ``tests/unit/middleware/test_filesystem_verification.py`` (desktop
|
||||
multi-root mount-prefix normalization)
|
||||
|
||||
Both exercised ``app.agents.shared.middleware.filesystem`` (dead). This drives
|
||||
the production free functions in
|
||||
``app.agents.multi_agent_chat.middleware.shared.filesystem.middleware`` instead.
|
||||
The functions only touch ``mw._filesystem_mode`` and ``mw._get_backend`` so we
|
||||
pass a lightweight fake ``mw`` rather than constructing the full middleware.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.agents.multi_agent_chat.middleware.shared.filesystem.middleware.mode import (
|
||||
default_cwd,
|
||||
)
|
||||
from app.agents.multi_agent_chat.middleware.shared.filesystem.middleware.namespace_policy import (
|
||||
check_cloud_write_namespace,
|
||||
)
|
||||
from app.agents.multi_agent_chat.middleware.shared.filesystem.middleware.path_resolution import (
|
||||
current_cwd,
|
||||
get_contract_suggested_path,
|
||||
normalize_local_mount_path,
|
||||
resolve_relative,
|
||||
)
|
||||
from app.agents.shared.filesystem_selection import FilesystemMode
|
||||
from app.agents.shared.middleware.multi_root_local_folder_backend import (
|
||||
MultiRootLocalFolderBackend,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _mw(mode: FilesystemMode = FilesystemMode.CLOUD, backend=None):
|
||||
return SimpleNamespace(_filesystem_mode=mode, _get_backend=lambda _rt: backend)
|
||||
|
||||
|
||||
def _runtime(state: dict | None = None) -> SimpleNamespace:
|
||||
return SimpleNamespace(state=state or {})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cwd defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCwdDefaults:
|
||||
def test_default_cwd_in_cloud_is_documents_root(self):
|
||||
assert default_cwd(FilesystemMode.CLOUD) == "/documents"
|
||||
|
||||
def test_default_cwd_in_desktop_is_root(self):
|
||||
assert default_cwd(FilesystemMode.DESKTOP_LOCAL_FOLDER) == "/"
|
||||
|
||||
def test_current_cwd_uses_state_when_set(self):
|
||||
assert (
|
||||
current_cwd(_mw(), _runtime({"cwd": "/documents/notes"}))
|
||||
== "/documents/notes"
|
||||
)
|
||||
|
||||
def test_current_cwd_falls_back_to_default(self):
|
||||
assert current_cwd(_mw(), _runtime({})) == "/documents"
|
||||
|
||||
def test_current_cwd_ignores_invalid(self):
|
||||
assert current_cwd(_mw(), _runtime({"cwd": "not-absolute"})) == "/documents"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# relative resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRelativePathResolution:
|
||||
def test_relative_path_resolves_against_cwd(self):
|
||||
assert (
|
||||
resolve_relative(_mw(), "notes.md", _runtime({"cwd": "/documents/projects"}))
|
||||
== "/documents/projects/notes.md"
|
||||
)
|
||||
|
||||
def test_relative_path_with_dotdot(self):
|
||||
assert (
|
||||
resolve_relative(_mw(), "../c.md", _runtime({"cwd": "/documents/a/b"}))
|
||||
== "/documents/a/c.md"
|
||||
)
|
||||
|
||||
def test_absolute_path_is_kept(self):
|
||||
assert (
|
||||
resolve_relative(_mw(), "/other/x.md", _runtime({"cwd": "/documents"}))
|
||||
== "/other/x.md"
|
||||
)
|
||||
|
||||
def test_empty_path_returns_cwd(self):
|
||||
assert (
|
||||
resolve_relative(_mw(), "", _runtime({"cwd": "/documents/projects"}))
|
||||
== "/documents/projects"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# contract suggested-path fallback
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestContractSuggestedPath:
|
||||
def test_falls_back_to_documents_notes_md_in_cloud(self):
|
||||
suggested = get_contract_suggested_path(
|
||||
_mw(FilesystemMode.CLOUD),
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert suggested == "/documents/notes.md"
|
||||
|
||||
def test_falls_back_to_root_notes_md_in_desktop(self):
|
||||
suggested = get_contract_suggested_path(
|
||||
_mw(FilesystemMode.DESKTOP_LOCAL_FOLDER),
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert suggested == "/notes.md"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cloud write-namespace policy
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCloudWriteNamespacePolicy:
|
||||
def test_documents_path_allowed(self):
|
||||
assert (
|
||||
check_cloud_write_namespace(_mw(), "/documents/foo.md", _runtime()) is None
|
||||
)
|
||||
|
||||
def test_documents_root_allowed(self):
|
||||
assert check_cloud_write_namespace(_mw(), "/documents", _runtime()) is None
|
||||
|
||||
def test_temp_basename_anywhere_allowed(self):
|
||||
assert (
|
||||
check_cloud_write_namespace(_mw(), "/temp_scratch.md", _runtime()) is None
|
||||
)
|
||||
assert check_cloud_write_namespace(_mw(), "/foo/temp_x.md", _runtime()) is None
|
||||
assert (
|
||||
check_cloud_write_namespace(_mw(), "/documents/temp_x.md", _runtime())
|
||||
is None
|
||||
)
|
||||
|
||||
def test_other_paths_rejected(self):
|
||||
err = check_cloud_write_namespace(_mw(), "/foo/bar.md", _runtime())
|
||||
assert err is not None
|
||||
assert "must target /documents" in err
|
||||
|
||||
def test_anon_doc_path_is_read_only(self):
|
||||
runtime = _runtime(
|
||||
{
|
||||
"kb_anon_doc": {
|
||||
"path": "/documents/uploaded.xml",
|
||||
"title": "uploaded",
|
||||
"content": "",
|
||||
"chunks": [],
|
||||
}
|
||||
}
|
||||
)
|
||||
err = check_cloud_write_namespace(_mw(), "/documents/uploaded.xml", runtime)
|
||||
assert err is not None
|
||||
assert "read-only" in err
|
||||
|
||||
def test_desktop_mode_skips_namespace_policy(self):
|
||||
assert (
|
||||
check_cloud_write_namespace(
|
||||
_mw(FilesystemMode.DESKTOP_LOCAL_FOLDER), "/random/path.md", _runtime()
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# desktop multi-root mount normalization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _desktop_mw(backend) -> SimpleNamespace:
|
||||
return _mw(FilesystemMode.DESKTOP_LOCAL_FOLDER, backend)
|
||||
|
||||
|
||||
class TestNormalizeLocalMountPath:
|
||||
def test_prefixes_default_mount(self, tmp_path: Path):
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
resolved = normalize_local_mount_path(
|
||||
_desktop_mw(backend),
|
||||
"/random-note.md",
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert resolved == "/pc_backups/random-note.md"
|
||||
|
||||
def test_keeps_explicit_mount(self, tmp_path: Path):
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
resolved = normalize_local_mount_path(
|
||||
_desktop_mw(backend),
|
||||
"/pc_backups/notes/random-note.md",
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert resolved == "/pc_backups/notes/random-note.md"
|
||||
|
||||
def test_windows_backslashes(self, tmp_path: Path):
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
resolved = normalize_local_mount_path(
|
||||
_desktop_mw(backend),
|
||||
r"\notes\random-note.md",
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert resolved == "/pc_backups/notes/random-note.md"
|
||||
|
||||
def test_normalizes_mixed_separators(self, tmp_path: Path):
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
resolved = normalize_local_mount_path(
|
||||
_desktop_mw(backend),
|
||||
r"\\notes//nested\\random-note.md",
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert resolved == "/pc_backups/notes/nested/random-note.md"
|
||||
|
||||
def test_keeps_explicit_mount_with_backslashes(self, tmp_path: Path):
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
resolved = normalize_local_mount_path(
|
||||
_desktop_mw(backend),
|
||||
r"\pc_backups\notes\random-note.md",
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert resolved == "/pc_backups/notes/random-note.md"
|
||||
|
||||
def test_prefixes_posix_absolute_path(self, tmp_path: Path):
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
resolved = normalize_local_mount_path(
|
||||
_desktop_mw(backend),
|
||||
"/var/log/app.log",
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert resolved == "/pc_backups/var/log/app.log"
|
||||
|
||||
def test_prefers_unique_existing_parent_mount(self, tmp_path: Path):
|
||||
root_a = tmp_path / "RootA"
|
||||
root_b = tmp_path / "RootB"
|
||||
(root_a / "other").mkdir(parents=True)
|
||||
(root_b / "nested" / "deep").mkdir(parents=True)
|
||||
backend = MultiRootLocalFolderBackend(
|
||||
(("root_a", str(root_a)), ("root_b", str(root_b)))
|
||||
)
|
||||
resolved = normalize_local_mount_path(
|
||||
_desktop_mw(backend),
|
||||
"/nested/deep/new-note.md",
|
||||
_runtime({"file_operation_contract": {}}),
|
||||
)
|
||||
assert resolved == "/root_b/nested/deep/new-note.md"
|
||||
|
||||
def test_uses_suggested_mount_when_ambiguous(self, tmp_path: Path):
|
||||
root_a = tmp_path / "RootA"
|
||||
root_b = tmp_path / "RootB"
|
||||
root_a.mkdir(parents=True)
|
||||
root_b.mkdir(parents=True)
|
||||
backend = MultiRootLocalFolderBackend(
|
||||
(("root_a", str(root_a)), ("root_b", str(root_b)))
|
||||
)
|
||||
resolved = normalize_local_mount_path(
|
||||
_desktop_mw(backend),
|
||||
"/brand-new-note.md",
|
||||
_runtime(
|
||||
{"file_operation_contract": {"suggested_path": "/root_b/notes/context.md"}}
|
||||
),
|
||||
)
|
||||
assert resolved == "/root_b/brand-new-note.md"
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
"""Cloud-mode behavior tests for the new ``rm`` and ``rmdir`` filesystem tools.
|
||||
"""Cloud-mode ``rm``/``rmdir`` staging tests for the LIVE filesystem middleware.
|
||||
|
||||
The tools build ``Command(update=...)`` payloads that the persistence
|
||||
middleware applies at end of turn. These tests stub out the backend and
|
||||
runtime to assert the staging payload shape:
|
||||
|
||||
* ``rm`` queues into ``pending_deletes`` and tombstones state files.
|
||||
* ``rm`` rejects directories, ``/documents``, root, and the anonymous doc.
|
||||
* ``rmdir`` queues into ``pending_dir_deletes`` and rejects non-empty dirs.
|
||||
* ``rmdir`` un-stages a same-turn ``mkdir`` rather than queuing a delete.
|
||||
* ``rmdir`` refuses to drop the cwd or any of its ancestors.
|
||||
* ``KBPostgresBackend`` view-helpers honor staged deletes.
|
||||
Ported from the former ``tests/unit/agents/new_chat/test_rm_rmdir_cloud.py``,
|
||||
which exercised the *dead twin* ``app.agents.shared.middleware.filesystem``.
|
||||
This drives the production decomposed tools
|
||||
(``app.agents.multi_agent_chat.middleware.shared.filesystem``) instead: it
|
||||
builds the real middleware via ``build_filesystem_mw``, pulls the real ``rm`` /
|
||||
``rmdir`` tools off it, and invokes their coroutines with a stubbed
|
||||
``KBPostgresBackend`` + runtime so we can assert the end-of-turn staging
|
||||
payloads (``pending_deletes`` / ``pending_dir_deletes``) and the destructive-op
|
||||
guard rails (root, /documents, anon doc, non-empty, cwd/ancestor, file vs dir).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -20,18 +19,31 @@ from unittest.mock import AsyncMock
|
|||
|
||||
import pytest
|
||||
|
||||
from app.agents.shared.filesystem_selection import FilesystemMode
|
||||
from app.agents.shared.middleware.filesystem import SurfSenseFilesystemMiddleware
|
||||
from app.agents.multi_agent_chat.middleware.shared.filesystem import (
|
||||
build_filesystem_mw,
|
||||
)
|
||||
from app.agents.shared.filesystem_backends import build_backend_resolver
|
||||
from app.agents.shared.filesystem_selection import FilesystemMode, FilesystemSelection
|
||||
from app.agents.shared.middleware.kb_postgres_backend import KBPostgresBackend
|
||||
from app.agents.shared.state_reducers import _CLEAR
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _make_middleware(mode: FilesystemMode = FilesystemMode.CLOUD):
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._filesystem_mode = mode
|
||||
middleware._custom_tool_descriptions = {}
|
||||
return middleware
|
||||
selection = FilesystemSelection(mode=mode)
|
||||
resolver = build_backend_resolver(selection, search_space_id=1)
|
||||
return build_filesystem_mw(
|
||||
backend_resolver=resolver,
|
||||
filesystem_mode=mode,
|
||||
search_space_id=1,
|
||||
user_id="00000000-0000-0000-0000-000000000001",
|
||||
thread_id=1,
|
||||
)
|
||||
|
||||
|
||||
def _tool(mw, name: str):
|
||||
return next(t for t in mw.tools if t.name == name)
|
||||
|
||||
|
||||
def _runtime(state: dict[str, Any] | None = None, *, tool_call_id: str = "tc-abc"):
|
||||
|
|
@ -41,13 +53,12 @@ def _runtime(state: dict[str, Any] | None = None, *, tool_call_id: str = "tc-abc
|
|||
|
||||
|
||||
class _KBBackendStub(KBPostgresBackend):
|
||||
"""Construct-able subclass of :class:`KBPostgresBackend` for tests.
|
||||
"""Construct-able ``KBPostgresBackend`` subclass for tests.
|
||||
|
||||
We bypass the real ``__init__`` (which expects a runtime + DB session)
|
||||
and inject just the methods the rm/rmdir tools touch. The class
|
||||
inheritance keeps ``isinstance(backend, KBPostgresBackend)`` checks
|
||||
inside the tools happy, which is what gates them from the desktop
|
||||
code path.
|
||||
Bypasses the real ``__init__`` (which expects a runtime + DB session) and
|
||||
injects only the async methods the rm/rmdir tools touch. The class
|
||||
inheritance keeps the ``isinstance(backend, KBPostgresBackend)`` checks in
|
||||
the tools on the cloud path.
|
||||
"""
|
||||
|
||||
def __init__(self, *, children=None, file_data=None) -> None:
|
||||
|
|
@ -61,9 +72,8 @@ def _make_backend_stub(*, children=None, file_data=None) -> KBPostgresBackend:
|
|||
return _KBBackendStub(children=children, file_data=file_data)
|
||||
|
||||
|
||||
def _bind_backend(middleware, backend):
|
||||
"""Inject a backend resolver onto the middleware test instance."""
|
||||
middleware._get_backend = lambda runtime: backend
|
||||
def _bind_backend(mw, backend):
|
||||
mw._get_backend = lambda runtime: backend
|
||||
return backend
|
||||
|
||||
|
||||
|
|
@ -86,8 +96,7 @@ class TestRmStaging:
|
|||
tool_call_id="tc-1",
|
||||
)
|
||||
|
||||
tool = m._create_rm_tool()
|
||||
result = await tool.coroutine("/documents/notes.md", runtime=runtime)
|
||||
result = await _tool(m, "rm").coroutine("/documents/notes.md", runtime=runtime)
|
||||
|
||||
assert hasattr(result, "update"), f"expected Command, got {result!r}"
|
||||
update = result.update
|
||||
|
|
@ -100,31 +109,22 @@ class TestRmStaging:
|
|||
@pytest.mark.asyncio
|
||||
async def test_rejects_documents_root(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime()
|
||||
tool = m._create_rm_tool()
|
||||
result = await tool.coroutine("/documents", runtime=runtime)
|
||||
result = await _tool(m, "rm").coroutine("/documents", runtime=_runtime())
|
||||
assert isinstance(result, str)
|
||||
assert "refusing to rm" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_root(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime()
|
||||
tool = m._create_rm_tool()
|
||||
result = await tool.coroutine("/", runtime=runtime)
|
||||
result = await _tool(m, "rm").coroutine("/", runtime=_runtime())
|
||||
assert isinstance(result, str)
|
||||
assert "refusing to rm" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_directory_via_staged_dirs(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime(
|
||||
{
|
||||
"staged_dirs": ["/documents/team-x"],
|
||||
}
|
||||
)
|
||||
tool = m._create_rm_tool()
|
||||
result = await tool.coroutine("/documents/team-x", runtime=runtime)
|
||||
runtime = _runtime({"staged_dirs": ["/documents/team-x"]})
|
||||
result = await _tool(m, "rm").coroutine("/documents/team-x", runtime=runtime)
|
||||
assert isinstance(result, str)
|
||||
assert "directory" in result.lower()
|
||||
assert "rmdir" in result
|
||||
|
|
@ -138,9 +138,7 @@ class TestRmStaging:
|
|||
children=[{"path": "/documents/foo/x.md", "is_dir": False}]
|
||||
),
|
||||
)
|
||||
runtime = _runtime()
|
||||
tool = m._create_rm_tool()
|
||||
result = await tool.coroutine("/documents/foo", runtime=runtime)
|
||||
result = await _tool(m, "rm").coroutine("/documents/foo", runtime=_runtime())
|
||||
assert isinstance(result, str)
|
||||
assert "directory" in result.lower()
|
||||
|
||||
|
|
@ -157,8 +155,9 @@ class TestRmStaging:
|
|||
}
|
||||
}
|
||||
)
|
||||
tool = m._create_rm_tool()
|
||||
result = await tool.coroutine("/documents/uploaded.xml", runtime=runtime)
|
||||
result = await _tool(m, "rm").coroutine(
|
||||
"/documents/uploaded.xml", runtime=runtime
|
||||
)
|
||||
assert isinstance(result, str)
|
||||
assert "read-only" in result
|
||||
|
||||
|
|
@ -173,12 +172,9 @@ class TestRmStaging:
|
|||
"dirty_paths": ["/documents/notes.md"],
|
||||
}
|
||||
)
|
||||
tool = m._create_rm_tool()
|
||||
result = await tool.coroutine("/documents/notes.md", runtime=runtime)
|
||||
update = result.update
|
||||
# First element is _CLEAR sentinel; the rest must NOT contain the
|
||||
# rm'd path.
|
||||
dirty = update.get("dirty_paths") or []
|
||||
result = await _tool(m, "rm").coroutine("/documents/notes.md", runtime=runtime)
|
||||
dirty = result.update.get("dirty_paths") or []
|
||||
# First element is the _CLEAR sentinel; the rm'd path must not survive.
|
||||
assert "/documents/notes.md" not in dirty[1:]
|
||||
|
||||
|
||||
|
|
@ -192,30 +188,19 @@ class TestRmdirStaging:
|
|||
async def test_stages_dir_delete_when_empty_and_db_backed(self):
|
||||
m = _make_middleware()
|
||||
backend = _bind_backend(m, _make_backend_stub(children=[]))
|
||||
# Override _load_file_data to return None (folder, not a file) and
|
||||
# parent listing to claim the folder exists.
|
||||
backend._load_file_data = AsyncMock(return_value=None)
|
||||
backend.als_info = AsyncMock(
|
||||
side_effect=[
|
||||
[], # children of /documents/proj
|
||||
[
|
||||
{"path": "/documents/proj", "is_dir": True},
|
||||
], # parent listing
|
||||
[{"path": "/documents/proj", "is_dir": True}], # parent listing
|
||||
]
|
||||
)
|
||||
runtime = _runtime(
|
||||
{
|
||||
"cwd": "/documents",
|
||||
},
|
||||
tool_call_id="tc-rd",
|
||||
)
|
||||
runtime = _runtime({"cwd": "/documents"}, tool_call_id="tc-rd")
|
||||
|
||||
tool = m._create_rmdir_tool()
|
||||
result = await tool.coroutine("/documents/proj", runtime=runtime)
|
||||
result = await _tool(m, "rmdir").coroutine("/documents/proj", runtime=runtime)
|
||||
|
||||
assert hasattr(result, "update")
|
||||
update = result.update
|
||||
assert update["pending_dir_deletes"] == [
|
||||
assert result.update["pending_dir_deletes"] == [
|
||||
{"path": "/documents/proj", "tool_call_id": "tc-rd"}
|
||||
]
|
||||
|
||||
|
|
@ -228,9 +213,9 @@ class TestRmdirStaging:
|
|||
children=[{"path": "/documents/proj/x.md", "is_dir": False}]
|
||||
),
|
||||
)
|
||||
runtime = _runtime()
|
||||
tool = m._create_rmdir_tool()
|
||||
result = await tool.coroutine("/documents/proj", runtime=runtime)
|
||||
result = await _tool(m, "rmdir").coroutine(
|
||||
"/documents/proj", runtime=_runtime()
|
||||
)
|
||||
assert isinstance(result, str)
|
||||
assert "not empty" in result
|
||||
|
||||
|
|
@ -239,30 +224,25 @@ class TestRmdirStaging:
|
|||
m = _make_middleware()
|
||||
_bind_backend(m, _make_backend_stub(children=[]))
|
||||
runtime = _runtime(
|
||||
{
|
||||
"cwd": "/documents",
|
||||
"staged_dirs": ["/documents/scratch"],
|
||||
},
|
||||
{"cwd": "/documents", "staged_dirs": ["/documents/scratch"]},
|
||||
tool_call_id="tc-rd",
|
||||
)
|
||||
tool = m._create_rmdir_tool()
|
||||
result = await tool.coroutine("/documents/scratch", runtime=runtime)
|
||||
result = await _tool(m, "rmdir").coroutine(
|
||||
"/documents/scratch", runtime=runtime
|
||||
)
|
||||
|
||||
assert hasattr(result, "update")
|
||||
update = result.update
|
||||
assert "pending_dir_deletes" not in update
|
||||
# _CLEAR sentinel + remaining items (in this case, none).
|
||||
staged_after = update["staged_dirs"]
|
||||
assert staged_after[0] == "\x00__SURFSENSE_FILESYSTEM_CLEAR__\x00"
|
||||
assert staged_after[0] == _CLEAR
|
||||
assert "/documents/scratch" not in staged_after[1:]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejects_root(self):
|
||||
async def test_rejects_root_and_documents(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime()
|
||||
tool = m._create_rmdir_tool()
|
||||
for victim in ("/", "/documents"):
|
||||
result = await tool.coroutine(victim, runtime=runtime)
|
||||
result = await _tool(m, "rmdir").coroutine(victim, runtime=_runtime())
|
||||
assert isinstance(result, str)
|
||||
assert "refusing to rmdir" in result
|
||||
|
||||
|
|
@ -270,8 +250,7 @@ class TestRmdirStaging:
|
|||
async def test_rejects_cwd(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({"cwd": "/documents/proj"})
|
||||
tool = m._create_rmdir_tool()
|
||||
result = await tool.coroutine("/documents/proj", runtime=runtime)
|
||||
result = await _tool(m, "rmdir").coroutine("/documents/proj", runtime=runtime)
|
||||
assert isinstance(result, str)
|
||||
assert "cwd" in result.lower()
|
||||
|
||||
|
|
@ -279,8 +258,7 @@ class TestRmdirStaging:
|
|||
async def test_rejects_ancestor_of_cwd(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({"cwd": "/documents/proj/sub"})
|
||||
tool = m._create_rmdir_tool()
|
||||
result = await tool.coroutine("/documents/proj", runtime=runtime)
|
||||
result = await _tool(m, "rmdir").coroutine("/documents/proj", runtime=runtime)
|
||||
assert isinstance(result, str)
|
||||
assert "cwd" in result.lower()
|
||||
|
||||
|
|
@ -288,34 +266,31 @@ class TestRmdirStaging:
|
|||
async def test_rejects_files(self):
|
||||
m = _make_middleware()
|
||||
_bind_backend(m, _make_backend_stub(children=[], file_data={"content": ["x"]}))
|
||||
runtime = _runtime()
|
||||
tool = m._create_rmdir_tool()
|
||||
result = await tool.coroutine("/documents/notes.md", runtime=runtime)
|
||||
result = await _tool(m, "rmdir").coroutine(
|
||||
"/documents/notes.md", runtime=_runtime()
|
||||
)
|
||||
assert isinstance(result, str)
|
||||
assert "is a file" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# KBPostgresBackend view filter
|
||||
# KBPostgresBackend staged-delete view filter (already the live backend)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestKBPostgresBackendDeleteFilter:
|
||||
"""als_info / glob / grep should suppress paths queued for delete."""
|
||||
"""``als_info`` / glob / grep must suppress paths queued for delete."""
|
||||
|
||||
def _make_backend(self, state: dict[str, Any]) -> KBPostgresBackend:
|
||||
runtime = SimpleNamespace(state=state)
|
||||
backend = KBPostgresBackend(search_space_id=1, runtime=runtime)
|
||||
return backend
|
||||
return KBPostgresBackend(search_space_id=1, runtime=runtime)
|
||||
|
||||
def test_pending_filesystem_view_returns_deleted_paths(self):
|
||||
backend = self._make_backend(
|
||||
{
|
||||
"pending_deletes": [
|
||||
{"path": "/documents/x.md", "tool_call_id": "t1"},
|
||||
],
|
||||
"pending_deletes": [{"path": "/documents/x.md", "tool_call_id": "t1"}],
|
||||
"pending_dir_deletes": [
|
||||
{"path": "/documents/d1", "tool_call_id": "t2"},
|
||||
{"path": "/documents/d1", "tool_call_id": "t2"}
|
||||
],
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
"""Mode-specific system-prompt assembly tests for the LIVE filesystem middleware.
|
||||
|
||||
Ported from ``TestModeSpecificPrompts`` in the former
|
||||
``tests/unit/middleware/test_filesystem_middleware.py`` (which exercised the
|
||||
dead twin ``app.agents.shared.middleware.filesystem._build_filesystem_system_prompt``).
|
||||
|
||||
These drive the production ``build_system_prompt`` so the prompt the model
|
||||
actually receives stays mode-scoped: cloud rules don't leak into desktop
|
||||
sessions and vice-versa, and the sandbox section appears only when available.
|
||||
|
||||
The per-tool *description* assertions from the old suite are intentionally NOT
|
||||
ported: they assert exact prompt copy (tightly coupled to the old wording) and
|
||||
guard prompt token hygiene rather than the code-movement refactor this suite
|
||||
protects.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.agents.multi_agent_chat.middleware.shared.filesystem.system_prompt import (
|
||||
build_system_prompt,
|
||||
)
|
||||
from app.agents.shared.filesystem_selection import FilesystemMode
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class TestModeSpecificPrompts:
|
||||
def test_cloud_prompt_omits_desktop_section(self):
|
||||
prompt = build_system_prompt(FilesystemMode.CLOUD, sandbox_available=False)
|
||||
assert "Local Folder Mode" not in prompt
|
||||
assert "mount-prefixed" not in prompt
|
||||
assert "Persistence Rules" in prompt
|
||||
assert "/documents" in prompt
|
||||
assert "temp_" in prompt
|
||||
|
||||
def test_desktop_prompt_omits_cloud_persistence_rules(self):
|
||||
prompt = build_system_prompt(
|
||||
FilesystemMode.DESKTOP_LOCAL_FOLDER, sandbox_available=False
|
||||
)
|
||||
assert "Persistence Rules" not in prompt
|
||||
assert "Workspace Tree" not in prompt
|
||||
assert "Local Folder Mode" in prompt
|
||||
assert "mount-prefixed" in prompt
|
||||
|
||||
def test_sandbox_addendum_appended_when_available(self):
|
||||
prompt = build_system_prompt(FilesystemMode.CLOUD, sandbox_available=True)
|
||||
assert "execute_code" in prompt
|
||||
assert "Code Execution" in prompt
|
||||
|
||||
def test_sandbox_addendum_absent_when_unavailable(self):
|
||||
prompt = build_system_prompt(FilesystemMode.CLOUD, sandbox_available=False)
|
||||
assert "execute_code" not in prompt
|
||||
|
|
@ -1,220 +0,0 @@
|
|||
"""Unit tests for the SurfSense filesystem middleware new behaviors.
|
||||
|
||||
Covers:
|
||||
* cloud cwd defaults to ``/documents`` and relative paths resolve under it
|
||||
* cloud writes outside ``/documents/`` are rejected unless basename starts
|
||||
with ``temp_``
|
||||
* cloud writes/edits to the anonymous document are rejected (read-only)
|
||||
* helper methods on the middleware (``_resolve_relative``,
|
||||
``_check_cloud_write_namespace``, ``_default_cwd``)
|
||||
|
||||
These tests use ``__new__`` to bypass the heavy ``__init__`` and exercise
|
||||
the helper methods directly so the test surface stays narrow and fast.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from app.agents.shared.filesystem_selection import FilesystemMode
|
||||
from app.agents.shared.middleware.filesystem import (
|
||||
SurfSenseFilesystemMiddleware,
|
||||
_build_filesystem_system_prompt,
|
||||
_build_tool_descriptions,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
def _make_middleware(mode: FilesystemMode = FilesystemMode.CLOUD):
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._filesystem_mode = mode
|
||||
return middleware
|
||||
|
||||
|
||||
def _runtime(state: dict | None = None) -> SimpleNamespace:
|
||||
return SimpleNamespace(state=state or {})
|
||||
|
||||
|
||||
class TestCloudCwdDefaults:
|
||||
def test_default_cwd_in_cloud_is_documents_root(self):
|
||||
m = _make_middleware()
|
||||
assert m._default_cwd() == "/documents"
|
||||
|
||||
def test_default_cwd_in_desktop_is_root(self):
|
||||
m = _make_middleware(FilesystemMode.DESKTOP_LOCAL_FOLDER)
|
||||
assert m._default_cwd() == "/"
|
||||
|
||||
def test_current_cwd_uses_state_when_set(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({"cwd": "/documents/notes"})
|
||||
assert m._current_cwd(runtime) == "/documents/notes"
|
||||
|
||||
def test_current_cwd_falls_back_to_default(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({})
|
||||
assert m._current_cwd(runtime) == "/documents"
|
||||
|
||||
def test_current_cwd_ignores_invalid(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({"cwd": "not-absolute"})
|
||||
assert m._current_cwd(runtime) == "/documents"
|
||||
|
||||
|
||||
class TestRelativePathResolution:
|
||||
def test_relative_path_resolves_against_cwd(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({"cwd": "/documents/projects"})
|
||||
assert (
|
||||
m._resolve_relative("notes.md", runtime) == "/documents/projects/notes.md"
|
||||
)
|
||||
|
||||
def test_relative_path_with_dotdot(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({"cwd": "/documents/a/b"})
|
||||
assert m._resolve_relative("../c.md", runtime) == "/documents/a/c.md"
|
||||
|
||||
def test_absolute_path_is_kept(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({"cwd": "/documents"})
|
||||
assert m._resolve_relative("/other/x.md", runtime) == "/other/x.md"
|
||||
|
||||
def test_empty_path_returns_cwd(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime({"cwd": "/documents/projects"})
|
||||
assert m._resolve_relative("", runtime) == "/documents/projects"
|
||||
|
||||
|
||||
class TestCloudWriteNamespacePolicy:
|
||||
def test_documents_path_allowed(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime()
|
||||
assert m._check_cloud_write_namespace("/documents/foo.md", runtime) is None
|
||||
|
||||
def test_documents_root_allowed(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime()
|
||||
assert m._check_cloud_write_namespace("/documents", runtime) is None
|
||||
|
||||
def test_temp_basename_anywhere_allowed(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime()
|
||||
assert m._check_cloud_write_namespace("/temp_scratch.md", runtime) is None
|
||||
assert m._check_cloud_write_namespace("/foo/temp_x.md", runtime) is None
|
||||
assert m._check_cloud_write_namespace("/documents/temp_x.md", runtime) is None
|
||||
|
||||
def test_other_paths_rejected(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime()
|
||||
err = m._check_cloud_write_namespace("/foo/bar.md", runtime)
|
||||
assert err is not None
|
||||
assert "must target /documents" in err
|
||||
|
||||
def test_anon_doc_path_is_read_only(self):
|
||||
m = _make_middleware()
|
||||
runtime = _runtime(
|
||||
{
|
||||
"kb_anon_doc": {
|
||||
"path": "/documents/uploaded.xml",
|
||||
"title": "uploaded",
|
||||
"content": "",
|
||||
"chunks": [],
|
||||
}
|
||||
}
|
||||
)
|
||||
err = m._check_cloud_write_namespace("/documents/uploaded.xml", runtime)
|
||||
assert err is not None
|
||||
assert "read-only" in err
|
||||
|
||||
def test_desktop_mode_skips_namespace_policy(self):
|
||||
m = _make_middleware(FilesystemMode.DESKTOP_LOCAL_FOLDER)
|
||||
runtime = _runtime()
|
||||
assert m._check_cloud_write_namespace("/random/path.md", runtime) is None
|
||||
|
||||
|
||||
class TestModeSpecificPrompts:
|
||||
"""The prompt and tool descriptions must only describe the active mode.
|
||||
|
||||
Cross-mode noise wastes tokens and confuses the model with rules it
|
||||
cannot use this session.
|
||||
"""
|
||||
|
||||
def test_cloud_prompt_omits_desktop_section(self):
|
||||
prompt = _build_filesystem_system_prompt(
|
||||
FilesystemMode.CLOUD, sandbox_available=False
|
||||
)
|
||||
assert "Local Folder Mode" not in prompt
|
||||
assert "mount-prefixed" not in prompt
|
||||
assert "Persistence Rules" in prompt
|
||||
assert "/documents" in prompt
|
||||
assert "temp_" in prompt
|
||||
|
||||
def test_desktop_prompt_omits_cloud_persistence_rules(self):
|
||||
prompt = _build_filesystem_system_prompt(
|
||||
FilesystemMode.DESKTOP_LOCAL_FOLDER, sandbox_available=False
|
||||
)
|
||||
assert "Persistence Rules" not in prompt
|
||||
assert "Workspace Tree" not in prompt
|
||||
assert "<priority_documents>" not in prompt
|
||||
assert "Local Folder Mode" in prompt
|
||||
assert "mount-prefixed" in prompt
|
||||
|
||||
def test_cloud_tool_descs_omit_desktop_phrases(self):
|
||||
descs = _build_tool_descriptions(FilesystemMode.CLOUD)
|
||||
for name in (
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"move_file",
|
||||
"mkdir",
|
||||
"rm",
|
||||
"rmdir",
|
||||
"list_tree",
|
||||
"grep",
|
||||
):
|
||||
text = descs[name]
|
||||
assert "Desktop" not in text, f"{name} leaks desktop hints"
|
||||
assert "Cloud mode:" not in text, f"{name} qualifies a cloud-only desc"
|
||||
|
||||
def test_desktop_tool_descs_omit_cloud_phrases(self):
|
||||
descs = _build_tool_descriptions(FilesystemMode.DESKTOP_LOCAL_FOLDER)
|
||||
for name in (
|
||||
"write_file",
|
||||
"edit_file",
|
||||
"move_file",
|
||||
"mkdir",
|
||||
"rm",
|
||||
"rmdir",
|
||||
"list_tree",
|
||||
"grep",
|
||||
):
|
||||
text = descs[name]
|
||||
assert "Cloud" not in text, f"{name} leaks cloud hints"
|
||||
assert "/documents/" not in text, f"{name} mentions cloud namespace"
|
||||
assert "temp_" not in text, f"{name} mentions cloud temp_ semantics"
|
||||
|
||||
def test_cloud_descs_include_rm_and_rmdir(self):
|
||||
descs = _build_tool_descriptions(FilesystemMode.CLOUD)
|
||||
assert "rm" in descs and "rmdir" in descs
|
||||
assert "Deletes a single file" in descs["rm"]
|
||||
assert "Deletes an empty directory" in descs["rmdir"]
|
||||
assert "rmdir" in descs["rmdir"] and "POSIX" in descs["rmdir"]
|
||||
|
||||
def test_desktop_descs_warn_about_irreversibility(self):
|
||||
descs = _build_tool_descriptions(FilesystemMode.DESKTOP_LOCAL_FOLDER)
|
||||
assert "NOT reversible" in descs["rm"]
|
||||
assert "NOT reversible" in descs["rmdir"]
|
||||
|
||||
def test_sandbox_addendum_appended_when_available(self):
|
||||
prompt = _build_filesystem_system_prompt(
|
||||
FilesystemMode.CLOUD, sandbox_available=True
|
||||
)
|
||||
assert "execute_code" in prompt
|
||||
assert "Code Execution" in prompt
|
||||
|
||||
def test_sandbox_addendum_absent_when_unavailable(self):
|
||||
prompt = _build_filesystem_system_prompt(
|
||||
FilesystemMode.CLOUD, sandbox_available=False
|
||||
)
|
||||
assert "execute_code" not in prompt
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.agents.shared.filesystem_selection import FilesystemMode
|
||||
from app.agents.shared.middleware.filesystem import SurfSenseFilesystemMiddleware
|
||||
from app.agents.shared.middleware.multi_root_local_folder_backend import (
|
||||
MultiRootLocalFolderBackend,
|
||||
)
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
class _RuntimeNoSuggestedPath:
|
||||
state = {"file_operation_contract": {}}
|
||||
|
||||
|
||||
class _RuntimeWithSuggestedPath:
|
||||
def __init__(self, suggested_path: str) -> None:
|
||||
self.state = {"file_operation_contract": {"suggested_path": suggested_path}}
|
||||
|
||||
|
||||
def test_contract_suggested_path_falls_back_to_documents_notes_md() -> None:
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._filesystem_mode = FilesystemMode.CLOUD
|
||||
suggested = middleware._get_contract_suggested_path(_RuntimeNoSuggestedPath()) # type: ignore[arg-type]
|
||||
# Cloud default cwd is /documents so the fallback lands in the KB.
|
||||
assert suggested == "/documents/notes.md"
|
||||
|
||||
|
||||
def test_contract_suggested_path_falls_back_to_root_notes_md_in_desktop() -> None:
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._filesystem_mode = FilesystemMode.DESKTOP_LOCAL_FOLDER
|
||||
suggested = middleware._get_contract_suggested_path(_RuntimeNoSuggestedPath()) # type: ignore[arg-type]
|
||||
assert suggested == "/notes.md"
|
||||
|
||||
|
||||
def test_normalize_local_mount_path_prefixes_default_mount(tmp_path: Path) -> None:
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
runtime = _RuntimeNoSuggestedPath()
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._get_backend = lambda _runtime: backend # type: ignore[method-assign]
|
||||
|
||||
resolved = middleware._normalize_local_mount_path("/random-note.md", runtime) # type: ignore[arg-type]
|
||||
|
||||
assert resolved == "/pc_backups/random-note.md"
|
||||
|
||||
|
||||
def test_normalize_local_mount_path_keeps_explicit_mount(tmp_path: Path) -> None:
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
runtime = _RuntimeNoSuggestedPath()
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._get_backend = lambda _runtime: backend # type: ignore[method-assign]
|
||||
|
||||
resolved = middleware._normalize_local_mount_path( # type: ignore[arg-type]
|
||||
"/pc_backups/notes/random-note.md",
|
||||
runtime,
|
||||
)
|
||||
|
||||
assert resolved == "/pc_backups/notes/random-note.md"
|
||||
|
||||
|
||||
def test_normalize_local_mount_path_windows_backslashes(tmp_path: Path) -> None:
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
runtime = _RuntimeNoSuggestedPath()
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._get_backend = lambda _runtime: backend # type: ignore[method-assign]
|
||||
|
||||
resolved = middleware._normalize_local_mount_path( # type: ignore[arg-type]
|
||||
r"\notes\random-note.md",
|
||||
runtime,
|
||||
)
|
||||
|
||||
assert resolved == "/pc_backups/notes/random-note.md"
|
||||
|
||||
|
||||
def test_normalize_local_mount_path_normalizes_mixed_separators(tmp_path: Path) -> None:
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
runtime = _RuntimeNoSuggestedPath()
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._get_backend = lambda _runtime: backend # type: ignore[method-assign]
|
||||
|
||||
resolved = middleware._normalize_local_mount_path( # type: ignore[arg-type]
|
||||
r"\\notes//nested\\random-note.md",
|
||||
runtime,
|
||||
)
|
||||
|
||||
assert resolved == "/pc_backups/notes/nested/random-note.md"
|
||||
|
||||
|
||||
def test_normalize_local_mount_path_keeps_explicit_mount_with_backslashes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
runtime = _RuntimeNoSuggestedPath()
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._get_backend = lambda _runtime: backend # type: ignore[method-assign]
|
||||
|
||||
resolved = middleware._normalize_local_mount_path( # type: ignore[arg-type]
|
||||
r"\pc_backups\notes\random-note.md",
|
||||
runtime,
|
||||
)
|
||||
|
||||
assert resolved == "/pc_backups/notes/random-note.md"
|
||||
|
||||
|
||||
def test_normalize_local_mount_path_prefixes_posix_absolute_path_for_linux_and_macos(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root = tmp_path / "PC Backups"
|
||||
root.mkdir()
|
||||
backend = MultiRootLocalFolderBackend((("pc_backups", str(root)),))
|
||||
runtime = _RuntimeNoSuggestedPath()
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._get_backend = lambda _runtime: backend # type: ignore[method-assign]
|
||||
|
||||
resolved = middleware._normalize_local_mount_path("/var/log/app.log", runtime) # type: ignore[arg-type]
|
||||
|
||||
assert resolved == "/pc_backups/var/log/app.log"
|
||||
|
||||
|
||||
def test_normalize_local_mount_path_prefers_unique_existing_parent_mount(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root_a = tmp_path / "RootA"
|
||||
root_b = tmp_path / "RootB"
|
||||
(root_a / "other").mkdir(parents=True)
|
||||
(root_b / "nested" / "deep").mkdir(parents=True)
|
||||
backend = MultiRootLocalFolderBackend(
|
||||
(("root_a", str(root_a)), ("root_b", str(root_b)))
|
||||
)
|
||||
runtime = _RuntimeNoSuggestedPath()
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._get_backend = lambda _runtime: backend # type: ignore[method-assign]
|
||||
|
||||
resolved = middleware._normalize_local_mount_path( # type: ignore[arg-type]
|
||||
"/nested/deep/new-note.md",
|
||||
runtime,
|
||||
)
|
||||
|
||||
assert resolved == "/root_b/nested/deep/new-note.md"
|
||||
|
||||
|
||||
def test_normalize_local_mount_path_uses_suggested_mount_when_ambiguous(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
root_a = tmp_path / "RootA"
|
||||
root_b = tmp_path / "RootB"
|
||||
root_a.mkdir(parents=True)
|
||||
root_b.mkdir(parents=True)
|
||||
backend = MultiRootLocalFolderBackend(
|
||||
(("root_a", str(root_a)), ("root_b", str(root_b)))
|
||||
)
|
||||
runtime = _RuntimeWithSuggestedPath("/root_b/notes/context.md")
|
||||
middleware = SurfSenseFilesystemMiddleware.__new__(SurfSenseFilesystemMiddleware)
|
||||
middleware._get_backend = lambda _runtime: backend # type: ignore[method-assign]
|
||||
|
||||
resolved = middleware._normalize_local_mount_path( # type: ignore[arg-type]
|
||||
"/brand-new-note.md",
|
||||
runtime,
|
||||
)
|
||||
|
||||
assert resolved == "/root_b/brand-new-note.md"
|
||||
Loading…
Add table
Add a link
Reference in a new issue