mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-08 03:10:24 +00:00
fix(file_io): auto appending suffix for all related steps (#430)
* fix(file_io): auto appending suffix for all related steps * fix(file_io): covering boundary cases of potential directory input
This commit is contained in:
parent
3095564313
commit
58276f740b
7 changed files with 370 additions and 29 deletions
|
|
@ -66,6 +66,17 @@ def is_relative_to(path: Path, parent: Path) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def display_path(workspace_path: Path, target: Path) -> str:
|
||||
"""Return ``target`` as a workspace-relative POSIX path for user-facing messages.
|
||||
|
||||
Falls back to the absolute string when ``target`` is not under ``workspace_path``.
|
||||
"""
|
||||
try:
|
||||
return target.relative_to(workspace_path).as_posix()
|
||||
except ValueError:
|
||||
return str(target)
|
||||
|
||||
|
||||
# pylint: disable=too-many-return-statements
|
||||
def resolve_path(
|
||||
workspace_path: Path,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from pathlib import Path
|
|||
import frontmatter
|
||||
|
||||
from ._file_io import get_path_lock
|
||||
from ._path import resolve_path
|
||||
from ._path import display_path, gate_md, resolve_path
|
||||
from ..base_step import BaseStep
|
||||
from ...components import R
|
||||
|
||||
|
|
@ -37,11 +37,13 @@ class FrontmatterDeleteStep(BaseStep):
|
|||
if err or target is None:
|
||||
payload: dict = {"path": path, "error": err or "invalid path"}
|
||||
else:
|
||||
original_target = target
|
||||
target, is_md = gate_md(target)
|
||||
lock = await get_path_lock(target)
|
||||
async with lock:
|
||||
if not target.is_file():
|
||||
payload = {"path": path, "error": "not found"}
|
||||
elif target.suffix != ".md":
|
||||
payload = {"path": path, "error": f"{display_path(workspace_dir, target)} not found"}
|
||||
elif not is_md:
|
||||
payload = {"path": path, "error": "not markdown"}
|
||||
elif not keys:
|
||||
payload = {"path": path, "error": "keys is empty"}
|
||||
|
|
@ -63,6 +65,8 @@ class FrontmatterDeleteStep(BaseStep):
|
|||
"missing": missing,
|
||||
"frontmatter": dict(post.metadata),
|
||||
}
|
||||
if target != original_target:
|
||||
payload["resolved_path"] = display_path(workspace_dir, target)
|
||||
|
||||
if "error" in payload:
|
||||
self.context.response.success = False
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from pathlib import Path
|
|||
import frontmatter
|
||||
import yaml
|
||||
|
||||
from ._path import resolve_path
|
||||
from ._path import display_path, gate_md, resolve_path
|
||||
from ..base_step import BaseStep
|
||||
from ...components import R
|
||||
|
||||
|
|
@ -35,13 +35,19 @@ class FrontmatterReadStep(BaseStep):
|
|||
self.context.response.metadata.update({"path": path, "exists": False, "error": err or "invalid path"})
|
||||
self.logger.info(f"[{self.name}] path={path} error={err!r}")
|
||||
return
|
||||
original_target = target
|
||||
target, is_md = gate_md(target)
|
||||
resolved: dict = {}
|
||||
if target != original_target:
|
||||
resolved["resolved_path"] = display_path(workspace_dir, target)
|
||||
probed = resolved.get("resolved_path", path)
|
||||
if not target.is_file():
|
||||
self.context.response.success = False
|
||||
self.context.response.answer = f"Error: {path} not found"
|
||||
self.context.response.metadata.update({"path": path, "exists": False})
|
||||
self.context.response.answer = f"Error: {probed} not found"
|
||||
self.context.response.metadata.update({"path": path, "exists": False, **resolved})
|
||||
self.logger.info(f"[{self.name}] path={path} exists=False")
|
||||
return
|
||||
if target.suffix != ".md":
|
||||
if not is_md:
|
||||
self.context.response.success = False
|
||||
self.context.response.answer = "Error: not markdown"
|
||||
self.context.response.metadata.update({"path": path, "error": "not markdown"})
|
||||
|
|
@ -52,11 +58,11 @@ class FrontmatterReadStep(BaseStep):
|
|||
meta = dict(frontmatter.loads(target.read_text(encoding="utf-8")).metadata)
|
||||
except yaml.YAMLError as exc:
|
||||
self.context.response.success = False
|
||||
self.context.response.answer = f"Error: failed to parse frontmatter in {path}: {exc}"
|
||||
self.context.response.metadata.update({"path": path, "exists": True, "error": str(exc)})
|
||||
self.context.response.answer = f"Error: failed to parse frontmatter in {probed}: {exc}"
|
||||
self.context.response.metadata.update({"path": path, "exists": True, "error": str(exc), **resolved})
|
||||
self.logger.info(f"[{self.name}] path={path} parse_error={exc!r}")
|
||||
return
|
||||
self.context.response.success = True
|
||||
self.context.response.answer = f"Read frontmatter from {path} ({len(meta)} key(s))"
|
||||
self.context.response.metadata.update({"path": path, "exists": True, "frontmatter": meta})
|
||||
self.context.response.metadata.update({"path": path, "exists": True, "frontmatter": meta, **resolved})
|
||||
self.logger.info(f"[{self.name}] path={path} keys={len(meta)}")
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from pathlib import Path
|
|||
import frontmatter
|
||||
|
||||
from ._file_io import get_path_lock
|
||||
from ._path import _check_path_permission, resolve_path
|
||||
from ._path import _check_path_permission, display_path, gate_md, resolve_path
|
||||
from ..base_step import BaseStep
|
||||
from ...components import R
|
||||
|
||||
|
|
@ -43,22 +43,27 @@ class FrontmatterUpdateStep(BaseStep):
|
|||
target, err = resolve_path(workspace_dir, path)
|
||||
if err or target is None:
|
||||
payload: dict = {"path": path, "error": err or "invalid path"}
|
||||
elif not _check_path_permission(workspace_dir, target, self.context.get("_allowed_paths")):
|
||||
payload = {"path": path, "error": "no permission to update this file"}
|
||||
else:
|
||||
lock = await get_path_lock(target)
|
||||
async with lock:
|
||||
if not target.is_file():
|
||||
payload = {"path": path, "error": "not found"}
|
||||
elif target.suffix != ".md":
|
||||
payload = {"path": path, "error": "not markdown"}
|
||||
elif not metadata:
|
||||
payload = {"path": path, "error": "no fields to update"}
|
||||
else:
|
||||
post = frontmatter.loads(target.read_text(encoding="utf-8"))
|
||||
post.metadata.update(metadata)
|
||||
target.write_text(frontmatter.dumps(post), encoding="utf-8")
|
||||
payload = {"path": path, "updated": metadata}
|
||||
original_target = target
|
||||
target, is_md = gate_md(target)
|
||||
if not _check_path_permission(workspace_dir, target, self.context.get("_allowed_paths")):
|
||||
payload = {"path": path, "error": "no permission to update this file"}
|
||||
else:
|
||||
lock = await get_path_lock(target)
|
||||
async with lock:
|
||||
if not target.is_file():
|
||||
payload = {"path": path, "error": f"{display_path(workspace_dir, target)} not found"}
|
||||
elif not is_md:
|
||||
payload = {"path": path, "error": "not markdown"}
|
||||
elif not metadata:
|
||||
payload = {"path": path, "error": "no fields to update"}
|
||||
else:
|
||||
post = frontmatter.loads(target.read_text(encoding="utf-8"))
|
||||
post.metadata.update(metadata)
|
||||
target.write_text(frontmatter.dumps(post), encoding="utf-8")
|
||||
payload = {"path": path, "updated": metadata}
|
||||
if target != original_target:
|
||||
payload["resolved_path"] = display_path(workspace_dir, target)
|
||||
|
||||
if "error" in payload:
|
||||
self.context.response.success = False
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from pathlib import Path
|
|||
|
||||
import frontmatter
|
||||
|
||||
from ._path import resolve_path
|
||||
from ._path import display_path, gate_md, resolve_path
|
||||
from ..base_step import BaseStep
|
||||
from ...components import R
|
||||
|
||||
|
|
@ -46,10 +46,24 @@ class StatStep(BaseStep):
|
|||
self.context.response.metadata.update({"path": path, "exists": False, "error": err or "invalid path"})
|
||||
self.logger.info(f"[{self.name}] path={path} error={err!r}")
|
||||
return
|
||||
# Auto-append .md for suffix-less paths (consistent with edit). An
|
||||
# existing directory wins top priority: the daily layout keeps
|
||||
# <daily_dir>/<date>/ and its <daily_dir>/<date>.md index side by
|
||||
# side, and directory probing is a supported use of this step.
|
||||
# Otherwise fall back to the original when the .md candidate is
|
||||
# missing but the original exists (e.g., an extension-less file).
|
||||
original_target = target
|
||||
target, _is_md = gate_md(target)
|
||||
if original_target.is_dir() or (not target.exists() and original_target.exists()):
|
||||
target = original_target
|
||||
if not target.exists():
|
||||
probed = display_path(workspace_dir, target)
|
||||
self.context.response.success = False
|
||||
self.context.response.answer = f"stat: {path} not found"
|
||||
self.context.response.metadata.update({"path": path, "exists": False})
|
||||
self.context.response.answer = f"stat: {probed} not found"
|
||||
not_found: dict = {"path": path, "exists": False}
|
||||
if target != original_target:
|
||||
not_found["resolved_path"] = probed
|
||||
self.context.response.metadata.update(not_found)
|
||||
self.logger.info(f"[{self.name}] path={path} exists=False")
|
||||
return
|
||||
|
||||
|
|
@ -62,6 +76,8 @@ class StatStep(BaseStep):
|
|||
"mtime": datetime.fromtimestamp(st.st_mtime).isoformat(),
|
||||
"ctime": datetime.fromtimestamp(st.st_ctime).isoformat(),
|
||||
}
|
||||
if target != original_target:
|
||||
payload["resolved_path"] = display_path(workspace_dir, target)
|
||||
if target.is_file():
|
||||
payload["size"] = st.st_size
|
||||
is_md = target.suffix.lower() == ".md"
|
||||
|
|
|
|||
|
|
@ -150,6 +150,107 @@ def test_stat_directory_fallback():
|
|||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_stat_directory_wins_over_same_name_index():
|
||||
"""stat prefers an existing directory over its same-name ``.md`` index.
|
||||
|
||||
The daily layout keeps ``<daily_dir>/<date>/`` and its
|
||||
``<daily_dir>/<date>.md`` index side by side; probing the bare date
|
||||
must report the directory, while the explicit ``.md`` still hits the file.
|
||||
"""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
store = await _make_store(
|
||||
{
|
||||
"daily/2026-08-10/note.md": "---\nname: n\n---\nbody",
|
||||
"daily/2026-08-10.md": "---\nname: index\n---\nindex body",
|
||||
},
|
||||
)
|
||||
step = crud_stat.StatStep(file_store=store)
|
||||
await step(path="daily/2026-08-10")
|
||||
payload = _metadata(step)
|
||||
assert payload["exists"] is True
|
||||
assert payload["type"] == "dir"
|
||||
assert payload["absolute_path"].endswith("2026-08-10")
|
||||
assert "resolved_path" not in payload
|
||||
|
||||
step = crud_stat.StatStep(file_store=store)
|
||||
await step(path="daily/2026-08-10.md")
|
||||
payload = _metadata(step)
|
||||
assert payload["exists"] is True
|
||||
assert payload["type"] == "file"
|
||||
assert payload["frontmatter"] == {"name": "index"}
|
||||
await store.close()
|
||||
print("✓ test_stat_directory_wins_over_same_name_index passed")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_stat_shadowed_extensionless_reports_resolved_path():
|
||||
"""When both ``LICENSE`` and ``LICENSE.md`` exist, ``stat path=LICENSE``
|
||||
resolves to the ``.md`` but discloses the substitution via ``resolved_path``.
|
||||
"""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
(Path(tmp) / "LICENSE").write_text("x" * 15, encoding="utf-8")
|
||||
(Path(tmp) / "LICENSE.md").write_text("y" * 12, encoding="utf-8")
|
||||
store = await _make_store()
|
||||
step = crud_stat.StatStep(file_store=store)
|
||||
await step(path="LICENSE")
|
||||
payload = _metadata(step)
|
||||
assert payload["exists"] is True
|
||||
assert payload["type"] == "file"
|
||||
assert payload["path"] == "LICENSE"
|
||||
assert payload["size"] == 12
|
||||
assert payload["mime"] == "text/markdown"
|
||||
assert payload["absolute_path"].endswith("LICENSE.md")
|
||||
assert payload["resolved_path"] == "LICENSE.md"
|
||||
await store.close()
|
||||
print("✓ test_stat_shadowed_extensionless_reports_resolved_path passed")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_stat_extensionless_file_without_md_sibling():
|
||||
"""An extension-less file with no ``.md`` sibling is reported as-is."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
(Path(tmp) / "LICENSE").write_text("x" * 15, encoding="utf-8")
|
||||
store = await _make_store()
|
||||
step = crud_stat.StatStep(file_store=store)
|
||||
await step(path="LICENSE")
|
||||
payload = _metadata(step)
|
||||
assert payload["exists"] is True
|
||||
assert payload["type"] == "file"
|
||||
assert payload["size"] == 15
|
||||
assert payload["absolute_path"].endswith("LICENSE")
|
||||
assert "resolved_path" not in payload
|
||||
await store.close()
|
||||
print("✓ test_stat_extensionless_file_without_md_sibling passed")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_stat_missing_reports_probed_md_path():
|
||||
"""stat on a missing suffix-less path reports the probed ``.md`` candidate."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
store = await _make_store()
|
||||
step = crud_stat.StatStep(file_store=store)
|
||||
await step(path="digest/missing")
|
||||
payload = _metadata(step)
|
||||
assert payload["exists"] is False
|
||||
assert payload["resolved_path"] == "digest/missing.md"
|
||||
assert "digest/missing.md" in str(step.context.response.answer)
|
||||
await store.close()
|
||||
print("✓ test_stat_missing_reports_probed_md_path passed")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
# -- list ----------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -1320,6 +1421,10 @@ if __name__ == "__main__":
|
|||
# stat / list / move / delete
|
||||
test_stat_indexed_file()
|
||||
test_stat_directory_fallback()
|
||||
test_stat_directory_wins_over_same_name_index()
|
||||
test_stat_shadowed_extensionless_reports_resolved_path()
|
||||
test_stat_extensionless_file_without_md_sibling()
|
||||
test_stat_missing_reports_probed_md_path()
|
||||
test_list_lists_files()
|
||||
test_list_respects_limit_and_non_recursive()
|
||||
test_move_relocates_within_workspace()
|
||||
|
|
|
|||
194
tests/unit/test_frontmatter_steps.py
Normal file
194
tests/unit/test_frontmatter_steps.py
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
"""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()
|
||||
Loading…
Add table
Reference in a new issue