fischer-agentkit/tests/unit/test_memory_profile.py

250 lines
9.8 KiB
Python

"""Tests for MemoryFile + MemoryStore — 记忆文件读写与多文件管理 (U1+U2)."""
import tempfile
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from agentkit.memory.profile import MemoryFile, MemoryStore, MemorySnapshot
class TestMemoryFileBasicIO:
"""MemoryFile 基本读写测试."""
def test_read_nonexistent_file_returns_empty(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "no_such.md")
assert mf.read() == ""
def test_write_and_read_back(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "test.md")
mf.write("hello world")
assert mf.read() == "hello world"
def test_write_creates_parent_dirs(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "deep" / "nested" / "test.md")
mf.write("content")
assert mf.read() == "content"
def test_overwrite_existing(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "test.md")
mf.write("first")
mf.write("second")
assert mf.read() == "second"
class TestMemoryFileSections:
"""MemoryFile section 级别操作测试."""
def _make_file(self, tmp_path: Path, content: str) -> MemoryFile:
mf = MemoryFile(tmp_path / "test.md")
mf.write(content)
return mf
def test_read_section_from_empty_file(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "empty.md")
assert mf.read_section("身份") == ""
def test_read_section_returns_content(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王\n## 性格\n友好耐心")
assert mf.read_section("身份") == "我是小王"
def test_read_section_not_found_returns_empty(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王")
assert mf.read_section("不存在") == ""
def test_add_section_creates_new(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王")
mf.add_section("性格", "友好耐心")
assert mf.read_section("性格") == "友好耐心"
assert mf.read_section("身份") == "我是小王"
def test_add_section_appends_to_existing(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王")
mf.add_section("身份", "也是AI助手")
content = mf.read_section("身份")
assert "我是小王" in content
assert "也是AI助手" in content
def test_replace_section_text(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王\n## 性格\n友好耐心")
mf.replace_section("身份", "我是小王", "我是大王")
assert mf.read_section("身份") == "我是大王"
assert mf.read_section("性格") == "友好耐心"
def test_replace_section_old_not_found_returns_false(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王")
result = mf.replace_section("身份", "不存在", "新内容")
assert result is False
def test_remove_section(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王\n## 性格\n友好耐心")
mf.remove_section("身份")
assert mf.read_section("身份") == ""
assert mf.read_section("性格") == "友好耐心"
def test_remove_nonexistent_section_no_error(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王")
mf.remove_section("不存在") # 不抛异常
assert mf.read_section("身份") == "我是小王"
def test_list_sections(self, tmp_path: Path):
mf = self._make_file(tmp_path, "## 身份\n我是小王\n## 性格\n友好耐心")
sections = mf.list_sections()
assert sections == ["身份", "性格"]
class TestMemoryFileCapacity:
"""MemoryFile 容量管理测试."""
def test_trim_to_budget_keeps_content_within_limit(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "test.md", char_budget=20)
mf.write("## 身份\n我是小王一个专业的AI助手") # 超过 20 字符
mf.trim_to_budget()
content = mf.read()
assert len(content) <= 20
def test_trim_preserves_earlier_sections(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "test.md", char_budget=30)
mf.write("## 身份\n我是小王\n## 性格\n友好耐心注重细节") # 性格部分超限
mf.trim_to_budget()
content = mf.read()
assert "身份" in content # 保留前面的 section
def test_no_trim_when_within_budget(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "test.md", char_budget=1000)
mf.write("## 身份\n我是小王")
mf.trim_to_budget()
assert mf.read() == "## 身份\n我是小王"
def test_write_auto_trims(self, tmp_path: Path):
mf = MemoryFile(tmp_path / "test.md", char_budget=15)
mf.write("## 身份\n我是小王一个专业的AI助手非常长")
content = mf.read()
assert len(content) <= 15
class TestMemoryStoreInit:
"""MemoryStore 初始化测试."""
def test_init_creates_base_dir(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path / "new_dir")
assert (tmp_path / "new_dir").exists()
def test_init_creates_memories_subdir(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
assert (tmp_path / "memories").exists()
def test_init_creates_daily_subdir(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
assert (tmp_path / "memories" / "daily").exists()
class TestMemoryStoreLoadAll:
"""MemoryStore load_all 测试."""
def test_load_all_returns_snapshot(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
snapshot = store.load_all()
assert isinstance(snapshot, MemorySnapshot)
def test_load_all_empty_when_no_files(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
snapshot = store.load_all()
assert snapshot.is_empty()
def test_load_all_with_content(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
store.get_file("soul").write("## 身份\n我是小王")
store.get_file("user").write("## 称呼\n叫我老板")
snapshot = store.load_all()
assert "小王" in snapshot.soul
assert "老板" in snapshot.user
assert snapshot.total_chars > 0
class TestMemoryStoreBuildPrompt:
"""MemoryStore build_system_prompt 测试."""
def test_build_prompt_injects_all_sections(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
store.get_file("soul").write("## 身份\n我是小王")
store.get_file("user").write("## 称呼\n叫我老板")
snapshot = store.load_all()
prompt = store.build_system_prompt(snapshot, "Be helpful.")
assert "<agent-identity>" in prompt
assert "小王" in prompt
assert "<user-profile>" in prompt
assert "老板" in prompt
assert "Be helpful." in prompt
def test_build_prompt_no_memory_returns_base_only(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
snapshot = store.load_all()
prompt = store.build_system_prompt(snapshot, "Be helpful.")
assert prompt == "Be helpful."
def test_build_prompt_empty_base_with_memory(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
store.get_file("soul").write("## 身份\n我是小王")
snapshot = store.load_all()
prompt = store.build_system_prompt(snapshot)
assert "<agent-identity>" in prompt
assert "小王" in prompt
class TestMemoryStoreDefaults:
"""MemoryStore ensure_defaults 测试."""
def test_ensure_defaults_creates_soul(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
store.ensure_defaults()
soul = store.get_file("soul").read()
assert "AgentKit" in soul
def test_ensure_defaults_no_overwrite(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
store.get_file("soul").write("## 身份\n自定义内容")
store.ensure_defaults()
soul = store.get_file("soul").read()
assert "自定义内容" in soul
assert "AgentKit" not in soul
class TestMemoryStoreDailyLogs:
"""MemoryStore 日志管理测试."""
def test_load_daily_logs_empty(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
assert store.load_daily_logs() == ""
def test_load_daily_logs_with_today(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
daily_file = MemoryFile(tmp_path / "memories" / "daily" / f"{today}.md")
daily_file.write("讨论了项目架构")
logs = store.load_daily_logs()
assert "讨论了项目架构" in logs
def test_archive_old_dailies(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
# 创建一个旧日志
old_date = (datetime.now(timezone.utc) - timedelta(days=5)).strftime("%Y-%m-%d")
old_file = tmp_path / "memories" / "daily" / f"{old_date}.md"
old_file.parent.mkdir(parents=True, exist_ok=True)
old_file.write_text("旧日志", encoding="utf-8")
count = store.archive_old_dailies(keep_days=2)
assert count == 1
assert not old_file.exists()
def test_get_file_daily_returns_today(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
daily = store.get_file("daily")
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
assert today in str(daily.path)
def test_get_file_invalid_key_raises(self, tmp_path: Path):
store = MemoryStore(base_dir=tmp_path)
with pytest.raises(ValueError, match="Invalid file_key"):
store.get_file("invalid")