From 5b78f7cf80c5de4ff5ae272619b1e7bdd8062fa0 Mon Sep 17 00:00:00 2001 From: hc Date: Wed, 15 Apr 2026 16:01:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BAmemory=20=E5=92=8Cdream=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_dream.py | 140 +++++++++++++++++++++++++++++++++++++ tests/test_memory.py | 159 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 299 insertions(+) create mode 100644 tests/test_dream.py create mode 100644 tests/test_memory.py diff --git a/tests/test_dream.py b/tests/test_dream.py new file mode 100644 index 0000000..a98aa61 --- /dev/null +++ b/tests/test_dream.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +import io +import sys +from pathlib import Path + +import pytest +from rich.console import Console + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from cc_slim.commands import handle_command +from cc_slim.engine import Agent, Config +from cc_slim.memory import MemoryStore +from cc_slim.mode import ModeState +from cc_slim.permissions import PermissionChecker +from cc_slim.session import SessionStore +from cc_slim.tools import build_default_tools + + +def make_workspace(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[Path, MemoryStore]: + home = tmp_path / "home" + home.mkdir() + monkeypatch.setattr("cc_slim.memory.Path.home", lambda: home) + monkeypatch.setattr("cc_slim.session.Path.home", lambda: home) + + workspace = tmp_path / "workspace" + workspace.mkdir() + (workspace / "AGENTS.md").write_text("# Workspace Rules\nworkspace-agents\n", encoding="utf-8") + skills = workspace / "SKILLS" + skills.mkdir() + (skills / "a.md").write_text("skill-a\n", encoding="utf-8") + memory = MemoryStore(workspace) + return workspace, memory + + +def make_agent(monkeypatch: pytest.MonkeyPatch, workspace: Path) -> Agent: + monkeypatch.setattr(Agent, "_build_client", lambda self: object()) + config = Config(provider="openai", model="test-model", api_key="key", base_url=None) + permissions = PermissionChecker(workspace) + return Agent(config=config, tools=build_default_tools(workspace), workspace=workspace, permission_checker=permissions) + + +def test_dream_command_is_routed(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + workspace, memory = make_workspace(monkeypatch, tmp_path) + output = io.StringIO() + console = Console(file=output, force_terminal=False, color_system=None) + + class DummyAgent: + def __init__(self) -> None: + self.called = False + + def dream(self, store: MemoryStore) -> str: + self.called = True + assert store is memory + return "已完成 dream,memory 已更新" + + agent = DummyAgent() + result = handle_command( + "/dream", + console=console, + root=workspace, + config=type("ConfigObj", (), {"model": "test-model"})(), + store=SessionStore(workspace), + memory=memory, + mode=ModeState(), + permissions=PermissionChecker(workspace), + agent=agent, # type: ignore[arg-type] + build_agent=lambda *args, **kwargs: agent, + render_history=lambda store: None, + ) + + assert agent.called is True + assert result is agent + assert "已完成 dream,memory 已更新" in output.getvalue() + + +def test_agent_dream_updates_memory_and_refreshes_prompt(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + workspace, memory = make_workspace(monkeypatch, tmp_path) + agent = make_agent(monkeypatch, workspace) + agent.history = [{"role": "user", "content": "请记住这个项目运行在 Windows 上"}] + + original_prompt = agent.system_prompt + monkeypatch.setattr( + agent, + "_run_dream_model", + lambda current_memory, session_text: """## Constraints +- 项目默认运行在 Windows 上 + +## Consolidated Facts +- 需要优先考虑 PowerShell 兼容性 +""", + ) + + result = agent.dream(memory) + sections = memory.read_sections() + + assert result == "已完成 dream,memory 已更新" + assert sections["Constraints"] == "- 项目默认运行在 Windows 上" + assert sections["Consolidated Facts"] == "- 需要优先考虑 PowerShell 兼容性" + assert agent.system_prompt != original_prompt + assert "项目默认运行在 Windows 上" in agent.system_prompt + + +def test_dream_does_not_modify_workspace_agents(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + workspace, memory = make_workspace(monkeypatch, tmp_path) + agent = make_agent(monkeypatch, workspace) + agent.history = [{"role": "user", "content": "记住这个项目使用 uv run"}] + agents_before = (workspace / "AGENTS.md").read_text(encoding="utf-8") + + monkeypatch.setattr( + agent, + "_run_dream_model", + lambda current_memory, session_text: """## Project Memory +- 使用 uv run 执行命令 +""", + ) + + agent.dream(memory) + + assert (workspace / "AGENTS.md").read_text(encoding="utf-8") == agents_before + + +def test_system_prompt_layers_remain_in_order(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + workspace, memory = make_workspace(monkeypatch, tmp_path) + memory.apply_dream( + """## Project Memory +- project-memory-marker +""" + ) + agent = make_agent(monkeypatch, workspace) + prompt = agent.system_prompt + + system_index = prompt.index("# cc-slim System") + runtime_index = prompt.index("## 运行环境") + agents_index = prompt.index("workspace-agents") + skills_index = prompt.index("skill-a") + memory_index = prompt.index("# Memory") + + assert system_index < runtime_index < agents_index < skills_index < memory_index diff --git a/tests/test_memory.py b/tests/test_memory.py new file mode 100644 index 0000000..3ef1978 --- /dev/null +++ b/tests/test_memory.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from cc_slim.memory import MemoryStore + + +def make_store(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> tuple[MemoryStore, Path]: + home = tmp_path / "home" + home.mkdir() + monkeypatch.setattr("cc_slim.memory.Path.home", lambda: home) + workspace = tmp_path / "workspace" + workspace.mkdir() + return MemoryStore(workspace), workspace + + +def test_memory_initializes_structured_template(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + store, _ = make_store(monkeypatch, tmp_path) + content = store.read() + + assert content.startswith("# Memory") + assert "## User Memory" in content + assert "## Project Memory" in content + assert "## Constraints" in content + assert "## Consolidated Facts" in content + assert "## Scratch Notes" in content + + +def test_memory_migrates_v1_text_into_scratch_notes(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + store, workspace = make_store(monkeypatch, tmp_path) + store.path().write_text("old note\nsecond line\n", encoding="utf-8") + + migrated = MemoryStore(workspace) + sections = migrated.read_sections() + + assert sections["Scratch Notes"] == "- old note\n second line" + + +def test_read_sections_parses_structured_memory(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + store, _ = make_store(monkeypatch, tmp_path) + store.path().write_text( + """# Memory + +## User Memory +- prefers concise output + +## Project Memory +- use uv run + +## Constraints +- windows first + +## Consolidated Facts +- workspace is repo root + +## Scratch Notes +- temporary note +""", + encoding="utf-8", + ) + + sections = store.read_sections() + + assert sections["User Memory"] == "- prefers concise output" + assert sections["Project Memory"] == "- use uv run" + assert sections["Constraints"] == "- windows first" + assert sections["Consolidated Facts"] == "- workspace is repo root" + assert sections["Scratch Notes"] == "- temporary note" + + +def test_apply_dream_updates_structured_sections_only(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + store, _ = make_store(monkeypatch, tmp_path) + store.path().write_text( + """# Memory + +## User Memory +- old user + +## Project Memory +- old project + +## Constraints +- old constraint + +## Consolidated Facts +- old fact + +## Scratch Notes +- keep me +""", + encoding="utf-8", + ) + + store.apply_dream( + """## User Memory +- new user + +## Project Memory +- new project + +## Constraints +- new constraint + +## Consolidated Facts +- new fact +""" + ) + sections = store.read_sections() + + assert sections["User Memory"] == "- new user" + assert sections["Project Memory"] == "- new project" + assert sections["Constraints"] == "- new constraint" + assert sections["Consolidated Facts"] == "- new fact" + assert sections["Scratch Notes"] == "- keep me" + + +def test_apply_dream_keeps_unmentioned_sections(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + store, _ = make_store(monkeypatch, tmp_path) + store.path().write_text( + """# Memory + +## User Memory +- old user + +## Project Memory +- old project + +## Constraints +- old constraint + +## Consolidated Facts +- old fact + +## Scratch Notes +- keep me +""", + encoding="utf-8", + ) + + store.apply_dream( + """## Project Memory +- updated project + +## Consolidated Facts +- updated fact +""" + ) + sections = store.read_sections() + + assert sections["User Memory"] == "- old user" + assert sections["Project Memory"] == "- updated project" + assert sections["Constraints"] == "- old constraint" + assert sections["Consolidated Facts"] == "- updated fact" + assert sections["Scratch Notes"] == "- keep me"