Files
wehub-resource-sync 75c67150d0
build / build (3.13) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build wheels (macos-aarch64) (push) Has been cancelled
release-please / build wheels (macos-x86_64) (push) Has been cancelled
release-please / build wheels (windows-x86_64) (push) Has been cancelled
release-please / build wheels (linux-aarch64) (push) Has been cancelled
release-please / build wheels (linux-x86_64) (push) Has been cancelled
release-please / build sdist (push) Has been cancelled
release-please / publish release artifacts (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:36:10 +08:00

248 lines
9.1 KiB
Python

"""Tests for the resource -> file workspace memorize path (ADR 0007 phase 1).
Exercises ``MemoryService._memorize_ws_synthesize_files`` — the two-step route +
per-file synthesis that replaces the entry plane for the chat/skill tracks — including
the ``RecallFile`` upsert and the ``resource -> file`` provenance link.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
from memu.app import MemoryService
# Router output (step a): which files to update/create for a source.
_SKILL_ROUTE = '[{"op": "create", "name": "pour-over", "description": "Brew pour-over coffee"}]'
_MEMORY_ROUTE = '[{"op": "create", "name": "Preferences", "description": "User preferences"}]'
# Synthesis output (step b): the file body.
_SKILL_BODY = "# Pour-over\nUse a 1:16 ratio."
class _FakeClient:
"""Fake LLM/embed client that answers the route step and the synthesis step.
The two steps are distinguished by a marker only the route prompt contains, so a
single client can serve both ``chat`` calls in the workflow.
"""
def __init__(self, route: str = _SKILL_ROUTE, body: str = _SKILL_BODY) -> None:
self._route = route
self._body = body
async def chat(self, prompt: str, system_prompt: str | None = None) -> str:
if "JSON array of operations" in prompt:
return self._route
return self._body
async def embed(self, texts: list[str]) -> list[list[float]]:
return [[0.1, 0.2, 0.3] for _ in texts]
def _service(tmp_path: Path) -> MemoryService:
return MemoryService(
llm_profiles={"default": {"api_key": "test-key"}},
database_config={"metadata_store": {"provider": "inmemory"}},
memory_files_config={"enabled": True, "output_dir": str(tmp_path), "synthesize": True},
)
def _seed_resource(service: MemoryService, *, track: str, user: dict[str, Any]) -> Any:
return service.database.resource_repo.create_resource(
url=f"/w/{track}/x.md",
modality="document",
local_path=f"/w/{track}/x.md",
caption=None,
embedding=None,
user_data=dict(user),
track=track,
)
async def _run_synthesize(
service: MemoryService,
client: _FakeClient,
*,
track: str,
user: dict[str, Any],
text: str = "I brewed pour-over at a 1:16 ratio.",
resource: Any | None = None,
) -> dict:
service._get_step_llm_client = lambda *a, **k: client # type: ignore[method-assign]
service._get_step_embedding_client = lambda *a, **k: client # type: ignore[method-assign]
res = resource if resource is not None else _seed_resource(service, track=track, user=user)
state = {
"resources": [res],
"preprocessed_resources": [{"text": text, "caption": None}],
"resource_track": track,
"store": service.database,
"user": user,
}
return await service._memorize_ws_synthesize_files(state, None)
async def test_skill_track_synthesizes_file_and_links_resource(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user = {"user_id": "u1"}
res = _seed_resource(service, track="skill", user=user)
result = await _run_synthesize(service, _FakeClient(), track="skill", user=user, resource=res)
files = list(result["files"])
assert len(files) == 1
skill = files[0]
assert skill.name == "pour-over"
assert skill.track == "skill"
assert skill.description == "Brew pour-over coffee"
assert skill.content == _SKILL_BODY
# Persisted as a skill-track RecallFile, isolated from the memory track.
skill_files = store.recall_file_repo.list_categories(where={"user_id": "u1", "track": "skill"})
assert [f.name for f in skill_files.values()] == ["pour-over"]
assert store.recall_file_repo.list_categories(where={"user_id": "u1", "track": "memory"}) == {}
# A resource -> file provenance link was recorded.
links = store.recall_file_resource_repo.list_relations(where=user)
assert len(links) == 1
assert links[0].resource_id == res.id
assert links[0].file_id == skill.id
async def test_chat_track_routes_to_memory_track_file(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user = {"user_id": "u1"}
result = await _run_synthesize(
service,
_FakeClient(route=_MEMORY_ROUTE, body="## Preferences\nLikes strong coffee."),
track="chat",
user=user,
text="I really like strong coffee.",
)
files = list(result["files"])
assert len(files) == 1
assert files[0].name == "Preferences"
assert files[0].track == "memory"
assert store.recall_file_repo.list_categories(where={"user_id": "u1", "track": "skill"}) == {}
async def test_update_op_revises_existing_file_by_name(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user: dict[str, Any] = {}
await _run_synthesize(service, _FakeClient(), track="skill", user=user)
# A second source updates the same skill by exact name.
revised = _FakeClient(route='[{"op": "update", "name": "pour-over"}]', body="# Pour-over\nUpdated.")
await _run_synthesize(service, revised, track="skill", user=user)
skill_files = store.recall_file_repo.list_categories(where={"track": "skill"})
assert len(skill_files) == 1 # revised in place, not duplicated
assert next(iter(skill_files.values())).content == "# Pour-over\nUpdated."
async def test_workspace_track_is_resource_only_noop(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user = {"user_id": "u1"}
result = await _run_synthesize(service, _FakeClient(), track="workspace", user=user)
assert result["files"] == []
assert store.recall_file_repo.list_categories(where={"user_id": "u1"}) == {}
assert store.recall_file_resource_repo.list_relations(where=user) == []
async def test_empty_source_is_noop(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user = {"user_id": "u1"}
result = await _run_synthesize(service, _FakeClient(), track="skill", user=user, text=" ")
assert result["files"] == []
assert store.recall_file_repo.list_categories(where={"user_id": "u1", "track": "skill"}) == {}
async def test_skill_track_creates_single_name_description_segment(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user = {"user_id": "u1"}
result = await _run_synthesize(service, _FakeClient(), track="skill", user=user)
skill = next(iter(result["files"]))
segments = store.recall_file_segment_repo.list_segments_for_file(skill.id)
assert len(segments) == 1
assert segments[0].text == "name: pour-over\ndescription: Brew pour-over coffee"
assert segments[0].embedding == [0.1, 0.2, 0.3]
# Segment track mirrors the owning file's track (denormalized for filtering).
assert segments[0].track == "skill"
async def test_memory_track_segments_are_lines_skipping_headings(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user = {"user_id": "u1"}
result = await _run_synthesize(
service,
_FakeClient(route=_MEMORY_ROUTE, body="## Preferences\nLikes strong coffee.\n\nDrinks it black."),
track="chat",
user=user,
)
file = next(iter(result["files"]))
segments = store.recall_file_segment_repo.list_segments_for_file(file.id)
assert [s.text for s in segments] == ["Likes strong coffee.", "Drinks it black."]
# Segment track mirrors the owning file's track (chat routes to the "memory" track).
assert all(s.track == "memory" for s in segments)
async def test_memory_segments_drop_and_add_on_update(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user = {"user_id": "u1"}
first = await _run_synthesize(
service,
_FakeClient(route=_MEMORY_ROUTE, body="## P\nline a\nline b"),
track="chat",
user=user,
)
file = next(iter(first["files"]))
before = {s.text: s.id for s in store.recall_file_segment_repo.list_segments_for_file(file.id)}
assert set(before) == {"line a", "line b"}
# An update changes only one line: "line b" -> "line c".
await _run_synthesize(
service,
_FakeClient(route='[{"op": "update", "name": "Preferences"}]', body="## P\nline a\nline c"),
track="chat",
user=user,
)
after = {s.text: s.id for s in store.recall_file_segment_repo.list_segments_for_file(file.id)}
assert set(after) == {"line a", "line c"}
# Unchanged line keeps its original segment (not re-embedded); changed line is fresh.
assert after["line a"] == before["line a"]
assert "line b" not in after
async def test_update_op_for_unknown_file_is_dropped(tmp_path: Path) -> None:
service = _service(tmp_path)
store = service.database
user = {"user_id": "u1"}
result = await _run_synthesize(
service,
_FakeClient(route='[{"op": "update", "name": "does-not-exist"}]'),
track="skill",
user=user,
)
assert result["files"] == []
assert store.recall_file_repo.list_categories(where={"user_id": "u1", "track": "skill"}) == {}