feat(memory): add workspace Dream prompt override

This commit is contained in:
chengyongru
2026-07-03 00:41:51 +08:00
committed by Xubin Ren
parent 5af22042ec
commit f38fd7d5d3
22 changed files with 295 additions and 10 deletions
+28
View File
@@ -61,6 +61,34 @@ class TestBuildDreamPrompt:
prompt, _ = result
assert "skill-creator" in prompt
def test_workspace_dream_prompt_overrides_default(self, store):
store.dream_prompt_file.parent.mkdir(parents=True)
store.dream_prompt_file.write_text(
"Custom Dream prompt.",
encoding="utf-8",
)
store.append_history("keep this fact")
result = store.build_dream_prompt()
assert result is not None
prompt, _ = result
assert prompt.startswith("Custom Dream prompt.")
assert "memory consolidation engine" not in prompt
assert "## Conversation History" in prompt
assert "keep this fact" in prompt
def test_empty_workspace_dream_prompt_uses_default(self, store):
store.dream_prompt_file.parent.mkdir(parents=True)
store.dream_prompt_file.write_text(" \n", encoding="utf-8")
store.append_history("test")
result = store.build_dream_prompt()
assert result is not None
prompt, _ = result
assert "memory consolidation engine" in prompt
def test_truncates_long_entries(self, store):
long_content = "x" * 2000
store.append_history(long_content)
+9
View File
@@ -375,6 +375,15 @@ class TestSyncWorkspaceTemplates:
assert (workspace / "memory").exists() or (workspace / "skills").exists()
def test_creates_prompt_readme_without_dream_override(self, tmp_path):
workspace = tmp_path / "workspace"
added = sync_workspace_templates(workspace, silent=True)
assert "prompts/README.md" in {path.replace("\\", "/") for path in added}
assert (workspace / "prompts" / "README.md").exists()
assert not (workspace / "prompts" / "dream.md").exists()
def test_returns_list_of_added_files(self, tmp_path):
"""Should return list of relative paths for added files."""
workspace = tmp_path / "workspace"
+11
View File
@@ -280,6 +280,7 @@ async def test_start_creates_separate_pools_with_proxy(monkeypatch) -> None:
assert any(cmd.command == "dream" for cmd in app.bot.commands)
assert any(cmd.command == "dream_log" for cmd in app.bot.commands)
assert any(cmd.command == "dream_restore" for cmd in app.bot.commands)
assert any(cmd.command == "dream_prompt" for cmd in app.bot.commands)
@pytest.mark.asyncio
@@ -1570,6 +1571,14 @@ async def test_forward_command_normalizes_telegram_safe_dream_aliases() -> None:
assert len(handled) == 1
assert handled[0]["content"] == "/dream-restore deadbeef"
handled.clear()
update = _make_telegram_update(text="/dream_prompt@nanobot_test init", reply_to_message=None)
await channel._forward_command(update, None)
assert len(handled) == 1
assert handled[0]["content"] == "/dream-prompt init"
def test_telegram_bus_slash_command_regex_matches_agent_loop_commands() -> None:
"""Bus-routed slash commands must match the Telegram handler regex (see builtin router)."""
@@ -1588,6 +1597,7 @@ def test_telegram_bus_slash_command_regex_matches_agent_loop_commands() -> None:
assert pat.fullmatch("/trigger@nanobot_bot CI summary")
assert pat.fullmatch("/dream-log deadbeef") is None
assert pat.fullmatch("/dream-restore deadbeef") is None
assert pat.fullmatch("/dream-prompt init") is None
@pytest.mark.asyncio
@@ -1608,6 +1618,7 @@ async def test_on_help_includes_restart_command() -> None:
assert "/skill" in help_text
assert "/dream" in help_text
assert "/dream-log" in help_text
assert "/dream-prompt" in help_text
assert "/goal" in help_text
assert "/trigger" in help_text
assert "/pairing" in help_text
+71 -1
View File
@@ -5,8 +5,16 @@ from types import SimpleNamespace
import pytest
from nanobot.agent.memory import MemoryStore
from nanobot.bus.events import InboundMessage, OutboundMessage
from nanobot.command.builtin import cmd_dream, cmd_dream_log, cmd_dream_restore
from nanobot.command.builtin import (
build_help_text,
builtin_command_palette,
cmd_dream,
cmd_dream_log,
cmd_dream_prompt,
cmd_dream_restore,
)
from nanobot.command.router import CommandContext
from nanobot.utils.gitstore import CommitInfo
@@ -94,6 +102,12 @@ def _make_dream_ctx(tmp_path) -> tuple[CommandContext, _FakeBus]:
return ctx, bus
def _make_dream_prompt_ctx(tmp_path, raw: str = "/dream-prompt", args: str = "") -> CommandContext:
msg = InboundMessage(channel="cli", sender_id="u1", chat_id="direct", content=raw)
loop = SimpleNamespace(context=SimpleNamespace(memory=MemoryStore(tmp_path)))
return CommandContext(msg=msg, session=None, key=msg.session_key, raw=raw, args=args, loop=loop)
@pytest.mark.asyncio
async def test_dream_no_history_explains_how_to_create_input(tmp_path) -> None:
ctx, bus = _make_dream_ctx(tmp_path)
@@ -109,6 +123,7 @@ async def test_dream_no_history_explains_how_to_create_input(tmp_path) -> None:
assert "idle auto-compact" in content
assert "Dream cursor" in content
assert "agents.defaults.idleCompactAfterMinutes" in content
assert "/dream-prompt" in content
@pytest.mark.asyncio
@@ -185,6 +200,61 @@ async def test_dream_log_before_first_run_is_clear() -> None:
assert "Dream has not run yet." in out.content
assert "Run `/dream`" in out.content
assert "/dream-prompt" in out.content
@pytest.mark.asyncio
async def test_dream_log_without_saved_versions_mentions_prompt_command() -> None:
git = _FakeGit(initialized=True, commits=[])
out = await cmd_dream_log(_make_ctx("/dream-log", git))
assert "Dream memory has no saved versions yet." in out.content
assert "/dream-prompt" in out.content
@pytest.mark.asyncio
async def test_dream_prompt_reports_default_prompt(tmp_path) -> None:
out = await cmd_dream_prompt(_make_dream_prompt_ctx(tmp_path))
assert "Dream memory instructions: nanobot default" in out.content
assert "prompts" in out.content
assert "dream.md" in out.content
assert "/dream-prompt init" in out.content
@pytest.mark.asyncio
async def test_dream_prompt_init_copies_default_prompt(tmp_path) -> None:
ctx = _make_dream_prompt_ctx(tmp_path, "/dream-prompt init", "init")
out = await cmd_dream_prompt(ctx)
prompt_file = tmp_path / "prompts" / "dream.md"
assert "Created Dream memory instructions" in out.content
assert prompt_file.read_text(encoding="utf-8") == MemoryStore.default_dream_prompt() + "\n"
@pytest.mark.asyncio
async def test_dream_prompt_init_does_not_overwrite_existing_prompt(tmp_path) -> None:
prompt_file = tmp_path / "prompts" / "dream.md"
prompt_file.parent.mkdir()
prompt_file.write_text("custom", encoding="utf-8")
ctx = _make_dream_prompt_ctx(tmp_path, "/dream-prompt init", "init")
out = await cmd_dream_prompt(ctx)
assert "already exist" in out.content
assert prompt_file.read_text(encoding="utf-8") == "custom"
def test_dream_prompt_command_in_help_and_palette() -> None:
palette = builtin_command_palette()
assert any(
item["command"] == "/dream-prompt" and item["arg_hint"] == "[init]"
for item in palette
)
assert "/dream-prompt [init]" in build_help_text()
@pytest.mark.asyncio
@@ -26,12 +26,14 @@ class TestIsDispatchableCommand:
assert router.is_dispatchable_command("/dream")
assert router.is_dispatchable_command("/dream-log")
assert router.is_dispatchable_command("/dream-restore")
assert router.is_dispatchable_command("/dream-prompt")
assert router.is_dispatchable_command("/goal")
assert router.is_dispatchable_command("/pairing")
def test_prefix_commands_match(self, router: CommandRouter) -> None:
assert router.is_dispatchable_command("/dream-log abc123")
assert router.is_dispatchable_command("/dream-restore def456")
assert router.is_dispatchable_command("/dream-prompt init")
assert router.is_dispatchable_command("/model fast")
assert router.is_dispatchable_command("/goal migrate the database")
assert router.is_dispatchable_command("/pairing list")