mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-09-29 01:41:38 +00:00
* refactor(steps): update naming conventions in components and configuration Updated naming conventions across multiple files, changing colon-separated names to underscore-separated format, and added new step definitions along with documentation updates. Key changes: - Replaced `Synchronizer` with `AutoMemory` as the counterpart component for cold-write operations - Updated naming conventions in all related configuration files (e.g., `frontmatter:read` → `frontmatter_read`) - Added new step definitions such as `submit_slug_updates` and `auto_memory` - Updated relevant documentation - Modified log output format for improved readability * refactor(evolve): Refactor the auto-memory module and update related configurations - Remove the old slug update commit step file - Add new auto-memory planner and writer steps - Update __init__.py to export the new step classes - Modify the auto_memory configuration structure in default.yaml - Update the slug field description for clearer explanation of its purpose * up * up * refactor(tests): Move unit test directory from `tests4/unittest` to `tests4/unit` Additionally, the assertion logic in test files has been updated: direct comparisons of `payload["notes"]` have been replaced with checks verifying the presence of paths and metadata within the response content. Furthermore, some test expectations have been simplified—for example, using `count` instead of asserting against specific note lists. Specific changes include: - Updating workflow configurations to align with the new test directory structure - Modifying assertions across multiple test methods to make them more flexible and maintainable - Cleaning up and optimizing parts of the test code structure This is a comprehensive test refactoring effort aimed at improving test readability and robustness. * Refactor(steps): Update memory writing logic and optimize JSON schema structure Improved the write strategy description in `auto_memory_writer.yaml` to emphasize using `edit` over `write`. Adjusted the `json_schema` structure in `base_step.py` to support the new function definition format. Also corrected grammatical issues in the related documentation. * Fix: Improve frontend data parsing error handling and update test files Added capture and handling logic for YAML parsing exceptions, providing more detailed error messages when frontend data format issues occur. Also corrected the description text in a test file.
349 lines
12 KiB
Python
349 lines
12 KiB
Python
"""Tests for reme4 common steps.
|
|
|
|
Two surfaces share this file:
|
|
|
|
* **In-process job tests** (top half) build an ``Application`` from the
|
|
default config and call ``run_job`` directly — no subprocess, no HTTP.
|
|
Each test uses an isolated cwd so the vault (``.reme`` by default)
|
|
does not collide.
|
|
* **Direct unit tests** (bottom half) exercise ``TraverseStep``
|
|
(registered as ``traverse_step``) — BFS over wikilink edges from a
|
|
seed file, forward / backward / both — against a freshly built
|
|
``LocalFileStore`` (embedding disabled).
|
|
"""
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
import asyncio
|
|
import os
|
|
import tempfile
|
|
import warnings
|
|
|
|
from reme4 import Application, __version__ as REME_VERSION
|
|
from reme4.components.file_store import LocalFileStore
|
|
from reme4.config import resolve_app_config
|
|
from reme4.schema import FileLink, FileNode
|
|
from reme4.steps.index import traverse as traverse_mod
|
|
from reme4.utils import load_env
|
|
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning, module="jieba")
|
|
warnings.filterwarnings("ignore", category=DeprecationWarning, module="pkg_resources")
|
|
|
|
|
|
class _temp_chdir:
|
|
"""chdir to path for the duration of the block; 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 _run(coro):
|
|
"""Run an async coroutine on a fresh isolated event loop."""
|
|
asyncio.run(coro)
|
|
|
|
|
|
async def _make_app() -> Application:
|
|
"""Build and start an Application with the default config, logging silenced."""
|
|
load_env()
|
|
cfg = resolve_app_config(log_to_console=False, log_to_file=False, enable_logo=False)
|
|
app = Application(**cfg)
|
|
await app.start()
|
|
return app
|
|
|
|
|
|
def _node(path: str, links: list[tuple[str, str | None, str | None]] | None = None) -> FileNode:
|
|
"""Build a FileNode with (target_path, target_anchor, predicate) outgoing edges."""
|
|
return FileNode(
|
|
path=path,
|
|
st_mtime=1.0,
|
|
links=[FileLink(source_path=path, target_path=t, target_anchor=a, predicate=p) for t, a, p in (links or [])],
|
|
)
|
|
|
|
|
|
async def _make_store(nodes: list[FileNode]) -> LocalFileStore:
|
|
"""LocalFileStore seeded with the given graph nodes (no files on disk)."""
|
|
store = LocalFileStore(name="t", embedding_model="")
|
|
await store.start()
|
|
if nodes:
|
|
await store.file_graph.upsert_nodes(nodes)
|
|
return store
|
|
|
|
|
|
def _edges(step) -> list[dict]:
|
|
return step.context.response.metadata.get("edges", [])
|
|
|
|
|
|
# ===========================================================================
|
|
# In-process job tests: version / help / health_check / search / reindex
|
|
# ===========================================================================
|
|
|
|
|
|
def test_version_job():
|
|
"""version job should return the package version string."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
app = await _make_app()
|
|
try:
|
|
resp = await app.run_job("version")
|
|
assert resp.success is True
|
|
assert resp.answer == REME_VERSION
|
|
assert resp.metadata.get("version") == REME_VERSION
|
|
finally:
|
|
await app.close()
|
|
print("✓ test_version_job passed")
|
|
|
|
_run(run())
|
|
|
|
|
|
def test_help_job():
|
|
"""help job should list jobs except itself."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
app = await _make_app()
|
|
try:
|
|
resp = await app.run_job("help")
|
|
assert resp.success is True
|
|
assert isinstance(resp.answer, str)
|
|
assert resp.metadata.get("job_count", 0) > 0
|
|
assert "`help`" not in resp.answer
|
|
for expected_job in ("version", "health_check", "search"):
|
|
assert expected_job in resp.answer, f"help missing {expected_job!r}: {resp.answer!r}"
|
|
finally:
|
|
await app.close()
|
|
print("✓ test_help_job passed")
|
|
|
|
_run(run())
|
|
|
|
|
|
def test_search_job_empty_store():
|
|
"""search on an empty store should return successfully with zero results."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
app = await _make_app()
|
|
try:
|
|
resp = await app.run_job("search", query="hello world", limit=5)
|
|
assert resp.success is True
|
|
counts = resp.metadata.get("counts", {})
|
|
assert isinstance(counts, dict)
|
|
assert counts.get("returned", -1) == 0
|
|
finally:
|
|
await app.close()
|
|
print("✓ test_search_job_empty_store passed")
|
|
|
|
_run(run())
|
|
|
|
|
|
def test_search_job_missing_query():
|
|
"""search with empty query returns success=False and a query-related error in answer."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
app = await _make_app()
|
|
try:
|
|
resp = await app.run_job("search", query="")
|
|
assert resp.success is False
|
|
assert "query" in str(resp.answer).lower()
|
|
finally:
|
|
await app.close()
|
|
print("✓ test_search_job_missing_query passed")
|
|
|
|
_run(run())
|
|
|
|
|
|
def test_all_jobs_single_app():
|
|
"""Run every common job against one shared in-process Application for efficiency."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
app = await _make_app()
|
|
try:
|
|
resp = await app.run_job("version")
|
|
assert resp.answer == REME_VERSION
|
|
|
|
resp = await app.run_job("help")
|
|
assert resp.metadata.get("job_count", 0) > 0
|
|
|
|
resp = await app.run_job("health_check")
|
|
assert isinstance(resp.metadata.get("health"), dict)
|
|
|
|
resp = await app.run_job("search", query="anything")
|
|
assert resp.success is True
|
|
|
|
resp = await app.run_job("reindex")
|
|
assert isinstance(resp.metadata.get("counts"), dict)
|
|
finally:
|
|
await app.close()
|
|
print("✓ test_all_jobs_single_app passed")
|
|
|
|
_run(run())
|
|
|
|
|
|
# ===========================================================================
|
|
# Direct unit tests: TraverseStep
|
|
# (LocalFileStore, no HTTP server — BFS over wikilink edges)
|
|
# ===========================================================================
|
|
|
|
|
|
def test_traverse_forward_depth_1():
|
|
"""depth=1 forward returns direct outbound neighbors."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
store = await _make_store(
|
|
[
|
|
_node("a.md", [("b.md", None, None), ("c.md", "intro", "ref")]),
|
|
_node("b.md"),
|
|
_node("c.md"),
|
|
],
|
|
)
|
|
step = traverse_mod.TraverseStep(file_store=store)
|
|
await step(path="a.md", direction="forward", depth=1)
|
|
results = _edges(step)
|
|
paths = {r["path"] for r in results}
|
|
assert paths == {"b.md", "c.md"}
|
|
# The 'ref' edge should report its predicate/anchor.
|
|
c_edge = next(r for r in results if r["path"] == "c.md")
|
|
assert c_edge["predicate"] == "ref"
|
|
assert c_edge["anchor"] == "intro"
|
|
assert c_edge["via"] == "a.md"
|
|
assert c_edge["depth"] == 1
|
|
await store.close()
|
|
print("✓ test_traverse_forward_depth_1 passed")
|
|
|
|
asyncio.run(run())
|
|
|
|
|
|
def test_traverse_backward_returns_inlinks():
|
|
"""direction=backward walks inbound edges."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
store = await _make_store(
|
|
[
|
|
_node("a.md", [("b.md", None, None)]),
|
|
_node("c.md", [("b.md", None, None)]),
|
|
_node("b.md"),
|
|
],
|
|
)
|
|
step = traverse_mod.TraverseStep(file_store=store)
|
|
await step(path="b.md", direction="backward", depth=1)
|
|
results = _edges(step)
|
|
assert {r["path"] for r in results} == {"a.md", "c.md"}
|
|
await store.close()
|
|
print("✓ test_traverse_backward_returns_inlinks passed")
|
|
|
|
asyncio.run(run())
|
|
|
|
|
|
def test_traverse_depth_2_expands():
|
|
"""depth=2 traverses one hop beyond direct neighbors."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
store = await _make_store(
|
|
[
|
|
_node("a.md", [("b.md", None, None)]),
|
|
_node("b.md", [("c.md", None, None)]),
|
|
_node("c.md"),
|
|
],
|
|
)
|
|
step = traverse_mod.TraverseStep(file_store=store)
|
|
await step(path="a.md", direction="forward", depth=2)
|
|
results = _edges(step)
|
|
depth_map = {r["path"]: r["depth"] for r in results}
|
|
assert depth_map.get("b.md") == 1
|
|
assert depth_map.get("c.md") == 2
|
|
await store.close()
|
|
print("✓ test_traverse_depth_2_expands passed")
|
|
|
|
asyncio.run(run())
|
|
|
|
|
|
def test_traverse_short_seed_yields_empty():
|
|
"""A short (not relative to the vault) seed isn't resolved anymore — BFS simply
|
|
finds no edges from a path that doesn't match any graph node."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
store = await _make_store(
|
|
[
|
|
_node("topics/Bob.md"),
|
|
_node("people/Bob.md"),
|
|
],
|
|
)
|
|
step = traverse_mod.TraverseStep(file_store=store)
|
|
await step(path="Bob", direction="forward", depth=1)
|
|
payload = _edges(step)
|
|
# No error, just empty results because "Bob" isn't a graph key.
|
|
assert payload == []
|
|
await store.close()
|
|
print("✓ test_traverse_short_seed_yields_empty passed")
|
|
|
|
asyncio.run(run())
|
|
|
|
|
|
def test_traverse_not_found_seed():
|
|
"""A seed not in the graph returns an empty list (no error)."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
store = await _make_store([_node("a.md")])
|
|
step = traverse_mod.TraverseStep(file_store=store)
|
|
await step(path="topics/ghost.md", direction="forward", depth=1)
|
|
payload = _edges(step)
|
|
assert payload == []
|
|
await store.close()
|
|
print("✓ test_traverse_not_found_seed passed")
|
|
|
|
asyncio.run(run())
|
|
|
|
|
|
def test_traverse_both_directions():
|
|
"""direction=both walks out- and in-bound; depth=1 returns one hop in each direction."""
|
|
|
|
async def run():
|
|
with tempfile.TemporaryDirectory() as tmp, _temp_chdir(tmp):
|
|
store = await _make_store(
|
|
[
|
|
_node("upstream.md", [("center.md", None, None)]),
|
|
_node("center.md", [("downstream.md", None, None)]),
|
|
_node("downstream.md"),
|
|
],
|
|
)
|
|
step = traverse_mod.TraverseStep(file_store=store)
|
|
await step(path="center.md", direction="both", depth=1)
|
|
results = _edges(step)
|
|
assert {r["path"] for r in results} == {"upstream.md", "downstream.md"}
|
|
await store.close()
|
|
print("✓ test_traverse_both_directions passed")
|
|
|
|
asyncio.run(run())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("\n=== reme4 common steps in-process tests ===")
|
|
test_version_job()
|
|
test_help_job()
|
|
test_search_job_empty_store()
|
|
test_search_job_missing_query()
|
|
test_all_jobs_single_app()
|
|
print("\n=== traverse step tests ===")
|
|
test_traverse_forward_depth_1()
|
|
test_traverse_backward_returns_inlinks()
|
|
test_traverse_depth_2_expands()
|
|
test_traverse_short_seed_yields_empty()
|
|
test_traverse_not_found_seed()
|
|
test_traverse_both_directions()
|
|
print("\n所有测试通过!")
|