| 1 | """Tests for project memory and notepad tools.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import json |
| 6 | from pathlib import Path |
| 7 | |
| 8 | import pytest |
| 9 | |
| 10 | from loader.agent.loop import AgentConfig |
| 11 | from loader.llm.base import CompletionResponse, ToolCall |
| 12 | from loader.runtime.executor import ToolExecutionState, ToolExecutor |
| 13 | from loader.runtime.hooks import HookManager, MemoryLifecycleHook |
| 14 | from loader.runtime.permissions import PermissionMode, build_permission_policy |
| 15 | from loader.runtime.tracing import RuntimeTracer |
| 16 | from loader.tools.base import create_default_registry |
| 17 | from tests.helpers.runtime_harness import ScriptedBackend, run_scenario |
| 18 | |
| 19 | |
| 20 | @pytest.mark.asyncio |
| 21 | async def test_project_memory_tools_round_trip(temp_dir: Path) -> None: |
| 22 | registry = create_default_registry(temp_dir) |
| 23 | |
| 24 | write_result = await registry.execute( |
| 25 | "project_memory_write", |
| 26 | memory={"techStack": "Python", "build": "uv run pytest -q"}, |
| 27 | merge=True, |
| 28 | ) |
| 29 | read_result = await registry.execute("project_memory_read", section="all") |
| 30 | directive_result = await registry.execute( |
| 31 | "project_memory_add_directive", |
| 32 | directive="Use uv, never pip.", |
| 33 | priority="high", |
| 34 | ) |
| 35 | |
| 36 | assert not write_result.is_error |
| 37 | assert not read_result.is_error |
| 38 | assert not directive_result.is_error |
| 39 | memory = json.loads(read_result.output) |
| 40 | assert memory["techStack"] == "Python" |
| 41 | assert registry.get("project_memory_read").required_permission == PermissionMode.READ_ONLY |
| 42 | assert registry.get("project_memory_write").required_permission == PermissionMode.WORKSPACE_WRITE |
| 43 | |
| 44 | |
| 45 | @pytest.mark.asyncio |
| 46 | async def test_notepad_tools_round_trip(temp_dir: Path) -> None: |
| 47 | registry = create_default_registry(temp_dir) |
| 48 | |
| 49 | await registry.execute("notepad_write_priority", content="Keep Loader focused on runtime quality.") |
| 50 | await registry.execute("notepad_write_working", content="Audit session resume behavior.") |
| 51 | await registry.execute("notepad_write_manual", content="Never delete refs without asking.") |
| 52 | read_result = await registry.execute("notepad_read", section="all") |
| 53 | |
| 54 | assert not read_result.is_error |
| 55 | assert "Keep Loader focused on runtime quality." in read_result.output |
| 56 | assert "Audit session resume behavior." in read_result.output |
| 57 | assert "Never delete refs without asking." in read_result.output |
| 58 | |
| 59 | |
| 60 | @pytest.mark.asyncio |
| 61 | async def test_memory_lifecycle_hook_mirrors_directives_into_notepad(temp_dir: Path) -> None: |
| 62 | registry = create_default_registry(temp_dir) |
| 63 | policy = build_permission_policy( |
| 64 | active_mode=PermissionMode.WORKSPACE_WRITE, |
| 65 | workspace_root=temp_dir, |
| 66 | tool_requirements=registry.get_tool_requirements(), |
| 67 | ) |
| 68 | executor = ToolExecutor( |
| 69 | registry, |
| 70 | RuntimeTracer(), |
| 71 | policy, |
| 72 | hooks=HookManager([MemoryLifecycleHook()]), |
| 73 | ) |
| 74 | |
| 75 | outcome = await executor.execute_tool_call( |
| 76 | ToolCall( |
| 77 | id="directive-1", |
| 78 | name="project_memory_add_directive", |
| 79 | arguments={ |
| 80 | "directive": "Use uv, never pip.", |
| 81 | "priority": "high", |
| 82 | }, |
| 83 | ), |
| 84 | source="native", |
| 85 | skip_confirmation=True, |
| 86 | ) |
| 87 | |
| 88 | assert outcome.state == ToolExecutionState.EXECUTED |
| 89 | notepad = (temp_dir / ".loader" / "notepad.md").read_text() |
| 90 | assert "Remembered directive [high]: Use uv, never pip." in notepad |
| 91 | |
| 92 | |
| 93 | @pytest.mark.asyncio |
| 94 | async def test_definition_of_done_summary_is_captured_in_project_memory( |
| 95 | temp_dir: Path, |
| 96 | ) -> None: |
| 97 | target = temp_dir / "memory-proof.txt" |
| 98 | backend = ScriptedBackend( |
| 99 | completions=[ |
| 100 | CompletionResponse( |
| 101 | content="I'll create the file.", |
| 102 | tool_calls=[ |
| 103 | ToolCall( |
| 104 | id="write-1", |
| 105 | name="write", |
| 106 | arguments={"file_path": str(target), "content": "memory proof\n"}, |
| 107 | ) |
| 108 | ], |
| 109 | ), |
| 110 | CompletionResponse(content="The file is in place."), |
| 111 | ] |
| 112 | ) |
| 113 | |
| 114 | await run_scenario( |
| 115 | "Create memory-proof.txt in the workspace root.", |
| 116 | backend, |
| 117 | config=AgentConfig(auto_context=False, stream=False), |
| 118 | project_root=temp_dir, |
| 119 | ) |
| 120 | |
| 121 | memory = json.loads((temp_dir / ".loader" / "project-memory.json").read_text()) |
| 122 | notes = memory.get("notes", []) |
| 123 | assert any( |
| 124 | note.get("category") == "definition_of_done" |
| 125 | and "Verification:" in note.get("content", "") |
| 126 | for note in notes |
| 127 | ) |