feat(memory): add workspace Dream prompt override
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user