"""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 "" in prompt assert "小王" in prompt assert "" 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 "" 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")