| import json |
| from pathlib import Path |
| from unittest.mock import MagicMock |
|
|
| import pytest |
|
|
| import reachy_mini_conversation_rmscript.prompts as prompts_mod |
| from reachy_mini_conversation_rmscript.config import config |
| from reachy_mini_conversation_rmscript.memory import ( |
| MAX_FACTS, |
| MAX_FACT_CHARS, |
| MemoryFact, |
| add_memory_fact, |
| list_memory_facts, |
| clear_memory_facts, |
| forget_memory_fact, |
| format_memory_for_prompt, |
| memory_path_for_instance, |
| ) |
| from reachy_mini_conversation_rmscript.tools.forget import Forget |
| from reachy_mini_conversation_rmscript.tools.remember import Remember |
| from reachy_mini_conversation_rmscript.tools.core_tools import ToolDependencies |
|
|
|
|
| def test_memory_store_adds_dedupes_caps_and_formats(tmp_path: Path) -> None: |
| """Memory facts should be normalized, deduplicated, capped, and prompt-formatted.""" |
| first = add_memory_fact(tmp_path, " Likes jazz ") |
| duplicate = add_memory_fact(tmp_path, "likes jazz") |
|
|
| assert first is not None |
| assert duplicate == first |
| assert [fact.text for fact in list_memory_facts(tmp_path)] == ["Likes jazz"] |
|
|
| long_text = "x" * (MAX_FACT_CHARS + 20) |
| stored_long = add_memory_fact(tmp_path, long_text) |
|
|
| assert stored_long is not None |
| assert len(stored_long.text) == MAX_FACT_CHARS |
| assert stored_long.text.endswith("...") |
|
|
| for index in range(MAX_FACTS + 5): |
| add_memory_fact(tmp_path, f"Fact {index}") |
|
|
| facts = list_memory_facts(tmp_path) |
| assert len(facts) == MAX_FACTS |
| assert facts[0].text == f"Fact {MAX_FACTS + 4}" |
| assert "Likes jazz" not in [fact.text for fact in facts] |
|
|
| prompt = format_memory_for_prompt(tmp_path) |
| assert prompt.startswith("Things you remember about the user") |
| assert f"- Fact {MAX_FACTS + 4}" in prompt |
|
|
|
|
| def test_memory_store_reads_mobile_json_shape_and_forgets_by_query(tmp_path: Path) -> None: |
| """The Python store should preserve the mobile app JSON envelope shape.""" |
| path = memory_path_for_instance(tmp_path) |
| path.write_text( |
| json.dumps( |
| { |
| "version": 1, |
| "facts": [ |
| {"id": "m_1", "text": "Likes jazz", "createdAt": 1000}, |
| {"id": "m_2", "text": "Likes jazz piano", "createdAt": 900}, |
| {"id": "bad"}, |
| ], |
| } |
| ), |
| encoding="utf-8", |
| ) |
|
|
| result = forget_memory_fact(tmp_path, query="jazz") |
|
|
| assert result.removed == MemoryFact(id="m_1", text="Likes jazz", created_at=1000) |
| assert [candidate.text for candidate in result.candidates] == ["Likes jazz", "Likes jazz piano"] |
| assert [fact.text for fact in list_memory_facts(tmp_path)] == ["Likes jazz piano"] |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_memory_tools_use_instance_storage(tmp_path: Path) -> None: |
| """Remember and forget tools should read/write through ToolDependencies.instance_path.""" |
| deps = ToolDependencies( |
| reachy_mini=MagicMock(), |
| movement_manager=MagicMock(), |
| instance_path=tmp_path, |
| ) |
|
|
| remember_result = await Remember()(deps, fact="Has a dog named Mochi") |
| forget_result = await Forget()(deps, query="Mochi") |
|
|
| assert remember_result["saved"] == "Has a dog named Mochi" |
| assert forget_result["removed"] == "Has a dog named Mochi" |
| assert list_memory_facts(tmp_path) == [] |
|
|
|
|
| def test_prompt_includes_memory_fragment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: |
| """Session instructions should prepend saved memories for the active app instance.""" |
| monkeypatch.setattr(config, "REACHY_MINI_CUSTOM_PROFILE", None) |
| clear_memory_facts(tmp_path) |
| add_memory_fact(tmp_path, "Prefers concise answers") |
|
|
| instructions = prompts_mod.get_session_instructions(instance_path=tmp_path) |
|
|
| assert instructions.startswith("Things you remember about the user") |
| assert "- Prefers concise answers" in instructions |
| assert "## IDENTITY" in instructions |
|
|