diff --git a/reme/steps/file_io/stat.py b/reme/steps/file_io/stat.py index 4ef31dd4..ca3c3f51 100644 --- a/reme/steps/file_io/stat.py +++ b/reme/steps/file_io/stat.py @@ -1,9 +1,9 @@ """``file_stat`` — peek at file metadata under the workspace without copying it. Cheap inspection alternative to ``file_download``: the agent gets -size, mtime, mime type, and (for markdown files) the parsed -frontmatter — enough to decide whether to download / parse / skip -without paying the copy cost. +size and mtime in the answer, with mime type and (for markdown files) +the parsed frontmatter alongside in metadata — enough to decide whether +to download / parse / skip without paying the copy cost. Returns a uniform envelope: @@ -68,12 +68,13 @@ class StatStep(BaseStep): return st = target.stat() + mtime = datetime.fromtimestamp(st.st_mtime).isoformat() payload: dict = { "path": path, "absolute_path": str(target), "exists": True, "type": "dir" if target.is_dir() else "file", - "mtime": datetime.fromtimestamp(st.st_mtime).isoformat(), + "mtime": mtime, "ctime": datetime.fromtimestamp(st.st_ctime).isoformat(), } if target != original_target: @@ -90,9 +91,9 @@ class StatStep(BaseStep): except Exception: meta = {} payload["frontmatter"] = meta - answer = f"stat: {path} (file, {st.st_size} bytes)" + answer = f"stat: {path} (file, {st.st_size} bytes, mtime {mtime})" else: - answer = f"stat: {path} (dir)" + answer = f"stat: {path} (dir, mtime {mtime})" self.context.response.success = True self.context.response.answer = answer diff --git a/tests/unit/test_crud_steps.py b/tests/unit/test_crud_steps.py index 498424bb..5d3a3184 100644 --- a/tests/unit/test_crud_steps.py +++ b/tests/unit/test_crud_steps.py @@ -150,6 +150,35 @@ def test_stat_directory_fallback(): asyncio.run(run()) +def test_stat_answer_reports_size_and_mtime(): + """The answer carries size and mtime, not just the path and type. + + The ``stat`` job description advertises "size, mtime, exists, is_dir, + is_file", but only ``answer`` reaches an MCP caller — mtime used to live + solely in metadata, so an agent could not tell how fresh a file was. + """ + + async def run(): + with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp): + store = await _make_store({"topics/n.md": "---\nname: T\n---\nbody"}) + step = crud_stat.StatStep(file_store=store) + await step(path="topics/n.md") + answer = step.context.response.answer + mtime = _metadata(step)["mtime"] + assert answer.startswith("stat: topics/n.md (file, ") + assert "bytes, mtime " in answer + assert answer.endswith(f"{mtime})") + + (Path(tmp) / "topics").mkdir(parents=True, exist_ok=True) + step = crud_stat.StatStep(file_store=store) + await step(path="topics") + answer = step.context.response.answer + assert answer == f"stat: topics (dir, mtime {_metadata(step)['mtime']})" + await store.close() + + asyncio.run(run()) + + def test_stat_directory_wins_over_same_name_index(): """stat prefers an existing directory over its same-name ``.md`` index.