mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
* fix(file_io): auto appending suffix for all related steps * fix(file_io): covering boundary cases of potential directory input
194 lines
7.4 KiB
Python
194 lines
7.4 KiB
Python
"""Tests for the frontmatter-only CRUD steps (``frontmatter_read`` /
|
|
``frontmatter_update`` / ``frontmatter_delete``).
|
|
|
|
Covers the suffix-less path contract shared with ``read`` / ``write`` /
|
|
``edit``: a bare path with no suffix auto-appends ``.md``. All three
|
|
siblings must agree on path handling, disclose the substitution via
|
|
``resolved_path`` in the response metadata, and report the actually
|
|
probed path in not-found errors instead of the caller's raw input.
|
|
"""
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from reme.components.file_store import LocalFileStore
|
|
from reme.steps.file_io.frontmatter_delete import FrontmatterDeleteStep
|
|
from reme.steps.file_io.frontmatter_read import FrontmatterReadStep
|
|
from reme.steps.file_io.frontmatter_update import FrontmatterUpdateStep
|
|
|
|
|
|
class temp_chdir:
|
|
"""Context manager to temporarily chdir into a path and restore on exit."""
|
|
|
|
def __init__(self, path):
|
|
self.path = path
|
|
self.old = None
|
|
|
|
def __enter__(self):
|
|
self.old = os.getcwd()
|
|
os.chdir(self.path)
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
os.chdir(self.old)
|
|
|
|
|
|
def _seed(workspace: Path, rel: str, body: str) -> Path:
|
|
target = workspace / rel
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_text(body, encoding="utf-8")
|
|
return target
|
|
|
|
|
|
async def _make_store() -> LocalFileStore:
|
|
store = LocalFileStore(name="t_fm", embedding_store="")
|
|
await store.start()
|
|
return store
|
|
|
|
|
|
async def _run(step_cls, store: LocalFileStore, **kwargs):
|
|
step = step_cls(file_store=store)
|
|
await step(**kwargs)
|
|
return step.context.response
|
|
|
|
|
|
NOTE = "notes/post.md"
|
|
BODY = "---\nname: n\ntags:\n- a\n---\nbody\n"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_no_suffix_autoappends_md():
|
|
"""frontmatter_read on a suffix-less path resolves to the ``.md`` file."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
_seed(Path(tmp), NOTE, BODY)
|
|
store = await _make_store()
|
|
resp = await _run(FrontmatterReadStep, store, path="notes/post")
|
|
assert resp.success is True
|
|
assert resp.metadata["frontmatter"] == {"name": "n", "tags": ["a"]}
|
|
assert resp.metadata["path"] == "notes/post"
|
|
assert resp.metadata["resolved_path"] == "notes/post.md"
|
|
await store.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_no_suffix_autoappends_md():
|
|
"""frontmatter_update on a suffix-less path resolves to the ``.md`` file."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
note = _seed(Path(tmp), NOTE, BODY)
|
|
store = await _make_store()
|
|
resp = await _run(FrontmatterUpdateStep, store, path="notes/post", metadata={"name": "renamed"})
|
|
assert resp.success is True
|
|
assert resp.metadata["updated"] == {"name": "renamed"}
|
|
assert resp.metadata["resolved_path"] == "notes/post.md"
|
|
assert "name: renamed" in note.read_text(encoding="utf-8")
|
|
await store.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_no_suffix_autoappends_md():
|
|
"""frontmatter_delete on a suffix-less path resolves to the ``.md`` file."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
note = _seed(Path(tmp), NOTE, BODY)
|
|
store = await _make_store()
|
|
resp = await _run(FrontmatterDeleteStep, store, path="notes/post", keys=["tags"])
|
|
assert resp.success is True
|
|
assert resp.metadata["deleted"] == ["tags"]
|
|
assert resp.metadata["missing"] == []
|
|
assert resp.metadata["resolved_path"] == "notes/post.md"
|
|
assert "tags" not in note.read_text(encoding="utf-8")
|
|
await store.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_family_consistent_on_suffixless_path():
|
|
"""read / update / delete all succeed on the same suffix-less path."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
_seed(Path(tmp), NOTE, BODY)
|
|
store = await _make_store()
|
|
|
|
resp = await _run(FrontmatterReadStep, store, path="notes/post")
|
|
assert resp.success is True
|
|
|
|
resp = await _run(FrontmatterUpdateStep, store, path="notes/post", metadata={"status": "done"})
|
|
assert resp.success is True
|
|
|
|
resp = await _run(FrontmatterDeleteStep, store, path="notes/post", keys=["status"])
|
|
assert resp.success is True
|
|
assert resp.metadata["deleted"] == ["status"]
|
|
await store.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_missing_reports_probed_path():
|
|
"""not-found errors surface the probed ``.md`` path, not the raw input."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
store = await _make_store()
|
|
resp = await _run(FrontmatterReadStep, store, path="digest/missing")
|
|
assert resp.success is False
|
|
assert resp.metadata["exists"] is False
|
|
assert resp.metadata["path"] == "digest/missing"
|
|
assert resp.metadata["resolved_path"] == "digest/missing.md"
|
|
assert "digest/missing.md" in str(resp.answer)
|
|
await store.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_missing_reports_probed_path():
|
|
"""frontmatter_update not-found errors mention the probed ``.md`` path."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
store = await _make_store()
|
|
resp = await _run(FrontmatterUpdateStep, store, path="digest/missing", metadata={"x": 1})
|
|
assert resp.success is False
|
|
assert resp.metadata["error"] == "digest/missing.md not found"
|
|
assert resp.metadata["resolved_path"] == "digest/missing.md"
|
|
assert "digest/missing.md" in str(resp.answer)
|
|
await store.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_missing_reports_probed_path():
|
|
"""frontmatter_delete not-found errors mention the probed ``.md`` path."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
store = await _make_store()
|
|
resp = await _run(FrontmatterDeleteStep, store, path="digest/missing", keys=["x"])
|
|
assert resp.success is False
|
|
assert resp.metadata["error"] == "digest/missing.md not found"
|
|
assert resp.metadata["resolved_path"] == "digest/missing.md"
|
|
assert "digest/missing.md" in str(resp.answer)
|
|
await store.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_non_md_rejected_without_resolved_path():
|
|
"""Non-markdown targets are rejected by all three siblings; no substitution."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
_seed(Path(tmp), "notes/data.txt", "plain\n")
|
|
store = await _make_store()
|
|
|
|
for step_cls, kwargs in (
|
|
(FrontmatterReadStep, {}),
|
|
(FrontmatterUpdateStep, {"metadata": {"x": 1}}),
|
|
(FrontmatterDeleteStep, {"keys": ["x"]}),
|
|
):
|
|
resp = await _run(step_cls, store, path="notes/data.txt", **kwargs)
|
|
assert resp.success is False
|
|
assert "not markdown" in str(resp.answer).lower()
|
|
assert "resolved_path" not in resp.metadata
|
|
await store.close()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_explicit_md_path_has_no_resolved_path():
|
|
"""Explicit ``.md`` paths need no substitution, so no ``resolved_path``."""
|
|
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
|
_seed(Path(tmp), NOTE, BODY)
|
|
store = await _make_store()
|
|
resp = await _run(FrontmatterReadStep, store, path=NOTE)
|
|
assert resp.success is True
|
|
assert "resolved_path" not in resp.metadata
|
|
await store.close()
|