mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 16:15:33 +00:00
Rename MemoriesManager -> MemoryManager
This commit is contained in:
1 parent
14e939ee1c
commit
eb43c5be1a
11 files changed
+101
-102
No files matched your search
+3
-3
@@ -41,7 +41,7 @@ from serena.config.serena_config import (
|
||||
)
|
||||
from serena.dashboard import SerenaDashboardAPI, SerenaDashboardTrayManager, SerenaDashboardViewer, open_url_in_browser
|
||||
from serena.ls_manager import LanguageServerManager
|
||||
from serena.memories.memory_manager import MemoriesManager
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
from serena.project import Project
|
||||
from serena.prompt_factory import SerenaPromptFactory
|
||||
from serena.task_executor import TaskExecutor
|
||||
@@ -919,7 +919,7 @@ class SerenaAgent:
|
||||
"""
|
||||
available_tools = self._active_tools
|
||||
available_markers = available_tools.tool_marker_names
|
||||
global_memories = MemoriesManager(
|
||||
global_memories = MemoryManager(
|
||||
serena_data_folder=None, read_only_memory_patterns=self.serena_config.read_only_memory_patterns
|
||||
).list_global_memories()
|
||||
global_memories_str = dict_string(global_memories.to_dict()) if len(global_memories) > 0 else ""
|
||||
@@ -977,7 +977,7 @@ class SerenaAgent:
|
||||
# add list of memories (if memories are enabled)
|
||||
include_memories = self._active_tools.contains_tool_class(ReadMemoryTool)
|
||||
if include_memories:
|
||||
project_memories = proj.memories_manager.list_project_memories()
|
||||
project_memories = proj.memory_manager.list_project_memories()
|
||||
if project_memories:
|
||||
msg += (
|
||||
f"\n{json.dumps(project_memories.to_dict())}\n"
|
||||
|
||||
+3
-3
@@ -44,7 +44,7 @@ from solidlsp.ls_types import SymbolKind
|
||||
from solidlsp.util.subprocess_util import subprocess_kwargs
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.project import MemoriesManager
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -1127,7 +1127,7 @@ class MemoryCommands(AutoRegisteringGroup):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _load_memories_manager(project: str) -> "MemoriesManager":
|
||||
def _load_memories_manager(project: str) -> "MemoryManager":
|
||||
"""
|
||||
Load the project at ``project`` and return its memories manager.
|
||||
|
||||
@@ -1145,7 +1145,7 @@ class MemoryCommands(AutoRegisteringGroup):
|
||||
raise click.UsageError(
|
||||
f"{e}\nNo Serena project found at {project_path}. Create one first with:\n serena project create {project_path}"
|
||||
) from e
|
||||
return proj.memories_manager
|
||||
return proj.memory_manager
|
||||
|
||||
@staticmethod
|
||||
@click.command(
|
||||
|
||||
@@ -566,7 +566,7 @@ class SerenaDashboardAPI:
|
||||
# Get available memories if ReadMemoryTool is active
|
||||
available_memories = None
|
||||
if self._agent.tool_is_active("read_memory") and project is not None:
|
||||
available_memories = project.memories_manager.list_memories().get_full_list()
|
||||
available_memories = project.memory_manager.list_memories().get_full_list()
|
||||
|
||||
# Get list of languages for the active project
|
||||
languages = []
|
||||
@@ -620,7 +620,7 @@ class SerenaDashboardAPI:
|
||||
if project is None:
|
||||
raise ValueError("No active project")
|
||||
|
||||
content = project.memories_manager.load_memory(request_get_memory.memory_name)
|
||||
content = project.memory_manager.load_memory(request_get_memory.memory_name)
|
||||
return ResponseGetMemory(content=content, memory_name=request_get_memory.memory_name)
|
||||
|
||||
return self._agent.execute_task(run, logged=False)
|
||||
@@ -630,7 +630,7 @@ class SerenaDashboardAPI:
|
||||
project = self._agent.get_active_project()
|
||||
if project is None:
|
||||
raise ValueError("No active project")
|
||||
project.memories_manager.save_memory(request_save_memory.memory_name, request_save_memory.content, is_tool_context=False)
|
||||
project.memory_manager.save_memory(request_save_memory.memory_name, request_save_memory.content, is_tool_context=False)
|
||||
|
||||
self._agent.execute_task(run, logged=True, name="SaveMemory")
|
||||
|
||||
@@ -639,7 +639,7 @@ class SerenaDashboardAPI:
|
||||
project = self._agent.get_active_project()
|
||||
if project is None:
|
||||
raise ValueError("No active project")
|
||||
project.memories_manager.delete_memory(request_delete_memory.memory_name, is_tool_context=False)
|
||||
project.memory_manager.delete_memory(request_delete_memory.memory_name, is_tool_context=False)
|
||||
|
||||
self._agent.execute_task(run, logged=True, name="DeleteMemory")
|
||||
|
||||
@@ -649,9 +649,7 @@ class SerenaDashboardAPI:
|
||||
if project is None:
|
||||
raise ValueError("No active project")
|
||||
|
||||
return project.memories_manager.move_memory(
|
||||
request_rename_memory.old_name, request_rename_memory.new_name, is_tool_context=False
|
||||
)
|
||||
return project.memory_manager.move_memory(request_rename_memory.old_name, request_rename_memory.new_name, is_tool_context=False)
|
||||
|
||||
return self._agent.execute_task(run, logged=True, name="RenameMemory")
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from .memory_reference_analysis import (
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MemoriesManager:
|
||||
class MemoryManager:
|
||||
GLOBAL_TOPIC = "global"
|
||||
_global_memory_dir = SerenaPaths().global_memories_path
|
||||
_MEMORY_REF_PREFIX = "mem:"
|
||||
@@ -70,7 +70,7 @@ class MemoriesManager:
|
||||
return name == self.GLOBAL_TOPIC or name.startswith(self.GLOBAL_TOPIC + "/")
|
||||
|
||||
@classmethod
|
||||
def _prepare_name(cls, name: str) -> str:
|
||||
def _sanitize_name(cls, name: str) -> str:
|
||||
"""Corrects the name for common mistakes made by LLMs (``mem:`` prefix, ``.md`` suffix, OS-specific separators)."""
|
||||
name = name.removeprefix(cls._MEMORY_REF_PREFIX)
|
||||
if name.endswith(".md"):
|
||||
@@ -79,7 +79,7 @@ class MemoriesManager:
|
||||
|
||||
@classmethod
|
||||
def _add_reference_prefix(cls, name: str) -> str:
|
||||
name = cls._prepare_name(name)
|
||||
name = cls._sanitize_name(name)
|
||||
return cls._MEMORY_REF_PREFIX + name
|
||||
|
||||
MEMORY_MAINTENANCE_NAME: str = "memory_maintenance"
|
||||
@@ -147,7 +147,7 @@ class MemoriesManager:
|
||||
return re.subn(pattern, lambda _m: ref_new, content)
|
||||
|
||||
def get_memory_file_path(self, name: str) -> Path:
|
||||
name = self._prepare_name(name)
|
||||
name = self._sanitize_name(name)
|
||||
parts = name.split("/")
|
||||
if ".." in parts:
|
||||
raise ValueError(f"Memory name cannot contain '..' segments for security reasons. Got: {name}")
|
||||
@@ -186,7 +186,7 @@ class MemoriesManager:
|
||||
raise PermissionError(f"Attempted to write to read_only memory: '{name}')")
|
||||
|
||||
def load_memory(self, name: str) -> str:
|
||||
name = self._prepare_name(name)
|
||||
name = self._sanitize_name(name)
|
||||
self._check_not_ignored(name)
|
||||
memory_file_path = self.get_memory_file_path(name)
|
||||
if not memory_file_path.exists():
|
||||
@@ -195,7 +195,7 @@ class MemoriesManager:
|
||||
return f.read()
|
||||
|
||||
def save_memory(self, name: str, content: str, is_tool_context: bool) -> str:
|
||||
name = self._prepare_name(name)
|
||||
name = self._sanitize_name(name)
|
||||
self._check_not_ignored(name)
|
||||
self._check_write_access(name, is_tool_context)
|
||||
memory_file_path = self.get_memory_file_path(name)
|
||||
@@ -217,7 +217,7 @@ class MemoriesManager:
|
||||
else:
|
||||
self.memories.append(memory_name)
|
||||
|
||||
def extend(self, other: "MemoriesManager.MemoriesList") -> None:
|
||||
def extend(self, other: "MemoryManager.MemoriesList") -> None:
|
||||
self.memories.extend(other.memories)
|
||||
self.read_only_memories.extend(other.read_only_memories)
|
||||
|
||||
@@ -262,7 +262,7 @@ class MemoriesManager:
|
||||
Lists all memories, optionally filtered by topic.
|
||||
If the topic is omitted, both global and project-specific memories are returned.
|
||||
"""
|
||||
memories: MemoriesManager.MemoriesList
|
||||
memories: MemoryManager.MemoriesList
|
||||
|
||||
if topic:
|
||||
if self._is_global(topic):
|
||||
@@ -278,7 +278,7 @@ class MemoriesManager:
|
||||
return memories
|
||||
|
||||
def delete_memory(self, name: str, is_tool_context: bool) -> str:
|
||||
name = self._prepare_name(name)
|
||||
name = self._sanitize_name(name)
|
||||
self._check_not_ignored(name)
|
||||
self._check_write_access(name, is_tool_context)
|
||||
memory_file_path = self.get_memory_file_path(name)
|
||||
@@ -292,8 +292,8 @@ class MemoriesManager:
|
||||
Rename or move a memory file.
|
||||
Moving between global and project scope (e.g. "global/foo" -> "bar") is supported.
|
||||
"""
|
||||
old_name = self._prepare_name(old_name)
|
||||
new_name = self._prepare_name(new_name)
|
||||
old_name = self._sanitize_name(old_name)
|
||||
new_name = self._sanitize_name(new_name)
|
||||
self._check_not_ignored(old_name)
|
||||
self._check_not_ignored(new_name)
|
||||
self._check_write_access(new_name, is_tool_context)
|
||||
@@ -357,7 +357,7 @@ class MemoriesManager:
|
||||
:param is_tool_context: whether the call originates from a tool invocation (affects write-access checks)
|
||||
:param regex_multiline: whether to apply multi-line regex matching, enabling the flags re.DOTALL and re.MULTILINE
|
||||
"""
|
||||
name = self._prepare_name(name)
|
||||
name = self._sanitize_name(name)
|
||||
self._check_not_ignored(name)
|
||||
self._check_write_access(name, is_tool_context)
|
||||
memory_file_path = self.get_memory_file_path(name)
|
||||
|
||||
@@ -22,7 +22,7 @@ from typing import TYPE_CHECKING
|
||||
from sensai.util.string import TextBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from serena.project import MemoriesManager
|
||||
from .memory_manager import MemoryManager
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -516,7 +516,7 @@ class MemoryReferenceAnalyzer:
|
||||
independent of project-management concerns.
|
||||
"""
|
||||
|
||||
def __init__(self, manager: MemoriesManager):
|
||||
def __init__(self, manager: MemoryManager):
|
||||
self._manager = manager
|
||||
|
||||
def validate_referential_integrity(
|
||||
|
||||
@@ -13,7 +13,7 @@ from serena.config.serena_config import (
|
||||
SerenaConfig,
|
||||
)
|
||||
from serena.ls_manager import LanguageServerFactory, LanguageServerManager
|
||||
from serena.memories.memory_manager import MemoriesManager
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
from serena.util.file_system import GitignoreParser, match_path
|
||||
from serena.util.text_utils import MatchedConsecutiveLines, search_files
|
||||
from solidlsp import SolidLanguageServer
|
||||
@@ -44,7 +44,7 @@ class Project(ToStringMixin):
|
||||
|
||||
read_only_memory_patterns = serena_config.read_only_memory_patterns + project_config.read_only_memory_patterns
|
||||
ignored_memory_patterns = serena_config.ignored_memory_patterns + project_config.ignored_memory_patterns
|
||||
self.memories_manager = MemoriesManager(
|
||||
self.memory_manager = MemoryManager(
|
||||
self._serena_data_folder,
|
||||
read_only_memory_patterns=read_only_memory_patterns,
|
||||
ignored_memory_patterns=ignored_memory_patterns,
|
||||
|
||||
@@ -32,7 +32,7 @@ class WriteMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
f"Content for {memory_name} is too long. Max length is {max_chars} characters. " + "Please make the content shorter."
|
||||
)
|
||||
|
||||
return self.memories_manager.save_memory(memory_name, content, is_tool_context=True)
|
||||
return self.memory_manager.save_memory(memory_name, content, is_tool_context=True)
|
||||
|
||||
|
||||
class ReadMemoryTool(Tool):
|
||||
@@ -44,7 +44,7 @@ class ReadMemoryTool(Tool):
|
||||
"""
|
||||
Use to read a memory that is likely to be relevant to the current task, inferring relevance e.g. from the name.
|
||||
"""
|
||||
return self.memories_manager.load_memory(memory_name)
|
||||
return self.memory_manager.load_memory(memory_name)
|
||||
|
||||
|
||||
class ListMemoriesTool(Tool):
|
||||
@@ -56,7 +56,7 @@ class ListMemoriesTool(Tool):
|
||||
"""
|
||||
Lists available memories, optionally filtered by topic.
|
||||
"""
|
||||
return self._to_json(self.memories_manager.list_memories(topic).to_dict())
|
||||
return self._to_json(self.memory_manager.list_memories(topic).to_dict())
|
||||
|
||||
|
||||
class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
@@ -68,7 +68,7 @@ class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
"""
|
||||
Delete a memory, only call if instructed explicitly or permission was granted by the user.
|
||||
"""
|
||||
return self.memories_manager.delete_memory(memory_name, is_tool_context=True)
|
||||
return self.memory_manager.delete_memory(memory_name, is_tool_context=True)
|
||||
|
||||
|
||||
class RenameMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
@@ -83,7 +83,7 @@ class RenameMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
References to other memories that are marked with the `mem:` prefix will be updated accordingly.
|
||||
References in read-only memories are not affected.
|
||||
"""
|
||||
renaming_message, n_references_updated = self.memories_manager.rename_memory_and_propagate_references(
|
||||
renaming_message, n_references_updated = self.memory_manager.rename_memory_and_propagate_references(
|
||||
old_name, new_name, is_tool_context=True
|
||||
)
|
||||
if n_references_updated > 0:
|
||||
@@ -117,6 +117,6 @@ class EditMemoryTool(Tool, ToolMarkerCanEdit):
|
||||
:param allow_multiple_occurrences: whether to allow matching and replacing multiple occurrences.
|
||||
If false and multiple occurrences are found, an error will be returned.
|
||||
"""
|
||||
return self.memories_manager.edit_memory(
|
||||
return self.memory_manager.edit_memory(
|
||||
memory_name, needle, repl, mode, allow_multiple_occurrences, is_tool_context=True, regex_multiline=True
|
||||
)
|
||||
@@ -14,7 +14,8 @@ from sensai.util import logging
|
||||
from sensai.util.string import dict_string
|
||||
|
||||
from serena.config.serena_config import LanguageBackend
|
||||
from serena.project import MemoriesManager, Project
|
||||
from serena.memories.memory_manager import MemoryManager
|
||||
from serena.project import Project
|
||||
from serena.prompt_factory import PromptFactory
|
||||
from serena.util.class_decorators import singleton
|
||||
from serena.util.inspection import iter_subclasses
|
||||
@@ -46,8 +47,8 @@ class Component(ABC):
|
||||
return self.agent.prompt_factory
|
||||
|
||||
@property
|
||||
def memories_manager(self) -> "MemoriesManager":
|
||||
return self.project.memories_manager
|
||||
def memory_manager(self) -> "MemoryManager":
|
||||
return self.project.memory_manager
|
||||
|
||||
def create_language_server_symbol_retriever(self) -> "LanguageServerSymbolRetriever":
|
||||
from serena.symbol import LanguageServerSymbolRetriever
|
||||
|
||||
@@ -25,7 +25,7 @@ class OnboardingTool(Tool):
|
||||
system = platform.system()
|
||||
# seed the project-local memory-maintenance memory (or detect a global override) so
|
||||
# the prompt can point the agent at the conventions before it writes anything
|
||||
memory_maintenance_name = self.memories_manager.ensure_memory_maintenance_memory()
|
||||
memory_maintenance_name = self.memory_manager.ensure_memory_maintenance_memory()
|
||||
return self.prompt_factory.create_onboarding_prompt(system=system, memory_maintenance_name=memory_maintenance_name)
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from serena.config.serena_config import (
|
||||
SerenaConfigError,
|
||||
)
|
||||
from serena.constants import PROJECT_TEMPLATE_FILE, SERENA_MANAGED_DIR_NAME
|
||||
from serena.project import MemoriesManager, Project
|
||||
from serena.project import MemoryManager, Project
|
||||
from solidlsp.ls_config import Language
|
||||
from test.conftest import create_default_serena_config
|
||||
|
||||
@@ -567,17 +567,17 @@ class TestMemoriesManagerCustomPath:
|
||||
|
||||
def test_memories_subdir_is_created(self):
|
||||
assert not self.data_folder.exists()
|
||||
MemoriesManager(str(self.data_folder))
|
||||
MemoryManager(str(self.data_folder))
|
||||
assert (self.data_folder / "memories").exists()
|
||||
|
||||
def test_save_and_load_memory(self):
|
||||
manager = MemoriesManager(str(self.data_folder))
|
||||
manager = MemoryManager(str(self.data_folder))
|
||||
manager.save_memory("test_topic", "test content", is_tool_context=False)
|
||||
content = manager.load_memory("test_topic")
|
||||
assert content == "test content"
|
||||
|
||||
def test_list_memories(self):
|
||||
manager = MemoriesManager(str(self.data_folder))
|
||||
manager = MemoryManager(str(self.data_folder))
|
||||
manager.save_memory("topic_a", "content a", is_tool_context=False)
|
||||
manager.save_memory("topic_b", "content b", is_tool_context=False)
|
||||
memories = manager.list_project_memories()
|
||||
|
||||
@@ -15,13 +15,13 @@ from serena.memories.memory_reference_analysis import (
|
||||
compute_name_similarity,
|
||||
find_stale_reference_candidates,
|
||||
)
|
||||
from serena.project import MemoriesManager
|
||||
from serena.project import MemoryManager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def manager() -> MemoriesManager:
|
||||
def manager() -> MemoryManager:
|
||||
# serena_data_folder=None: the tested helpers do not access the filesystem
|
||||
return MemoriesManager(serena_data_folder=None)
|
||||
return MemoryManager(serena_data_folder=None)
|
||||
|
||||
|
||||
class TestPrepareName:
|
||||
@@ -40,9 +40,9 @@ class TestPrepareName:
|
||||
":colon_prefixed_unlikely",
|
||||
],
|
||||
)
|
||||
def test_does_not_strip_partial_prefix_chars_from_real_names(self, manager: MemoriesManager, name: str) -> None:
|
||||
def test_does_not_strip_partial_prefix_chars_from_real_names(self, manager: MemoryManager, name: str) -> None:
|
||||
"""A regression guard: previously used ``str.lstrip("mem:")`` which strips any leading m/e/:."""
|
||||
assert manager._prepare_name(name) == name
|
||||
assert manager._sanitize_name(name) == name
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("input_name", "expected"),
|
||||
@@ -53,8 +53,8 @@ class TestPrepareName:
|
||||
("mem:", ""),
|
||||
],
|
||||
)
|
||||
def test_strips_mem_prefix_only_when_actually_a_prefix(self, manager: MemoriesManager, input_name: str, expected: str) -> None:
|
||||
assert manager._prepare_name(input_name) == expected
|
||||
def test_strips_mem_prefix_only_when_actually_a_prefix(self, manager: MemoryManager, input_name: str, expected: str) -> None:
|
||||
assert manager._sanitize_name(input_name) == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("input_name", "expected"),
|
||||
@@ -66,14 +66,14 @@ class TestPrepareName:
|
||||
(".md", ""),
|
||||
],
|
||||
)
|
||||
def test_strips_md_only_as_suffix(self, manager: MemoriesManager, input_name: str, expected: str) -> None:
|
||||
assert manager._prepare_name(input_name) == expected
|
||||
def test_strips_md_only_as_suffix(self, manager: MemoryManager, input_name: str, expected: str) -> None:
|
||||
assert manager._sanitize_name(input_name) == expected
|
||||
|
||||
def test_normalizes_os_separator(self, manager: MemoriesManager) -> None:
|
||||
assert manager._prepare_name(f"topic{os.sep}sub{os.sep}name") == "topic/sub/name"
|
||||
def test_normalizes_os_separator(self, manager: MemoryManager) -> None:
|
||||
assert manager._sanitize_name(f"topic{os.sep}sub{os.sep}name") == "topic/sub/name"
|
||||
|
||||
def test_combined_normalizations(self, manager: MemoriesManager) -> None:
|
||||
assert manager._prepare_name(f"mem:topic{os.sep}foo.md") == "topic/foo"
|
||||
def test_combined_normalizations(self, manager: MemoryManager) -> None:
|
||||
assert manager._sanitize_name(f"mem:topic{os.sep}foo.md") == "topic/foo"
|
||||
|
||||
|
||||
class TestRenameReferencesToMemory:
|
||||
@@ -127,7 +127,7 @@ class TestRenameReferencesToMemory:
|
||||
)
|
||||
def test_rename(
|
||||
self,
|
||||
manager: MemoriesManager,
|
||||
manager: MemoryManager,
|
||||
content: str,
|
||||
old_name: str,
|
||||
new_name: str,
|
||||
@@ -150,7 +150,7 @@ class TestRenameReferencesToMemory:
|
||||
("", ""), # raw, no delimiters
|
||||
],
|
||||
)
|
||||
def test_replacement_inside_various_delimiters(self, manager: MemoriesManager, delimiter_pair: tuple[str, str]) -> None:
|
||||
def test_replacement_inside_various_delimiters(self, manager: MemoryManager, delimiter_pair: tuple[str, str]) -> None:
|
||||
left, right = delimiter_pair
|
||||
content = f"{left}mem:foo{right}"
|
||||
result, n = manager.rename_references_to_memory(content, "foo", "bar")
|
||||
@@ -176,7 +176,7 @@ class TestNameSimilarity:
|
||||
pytest.param("auth/login", "auth/v2/login", 0.75, id="basename_match_shared_prefix"),
|
||||
],
|
||||
)
|
||||
def test_high_similarity_cases(self, manager: MemoriesManager, a: str, b: str, expected: float) -> None:
|
||||
def test_high_similarity_cases(self, manager: MemoryManager, a: str, b: str, expected: float) -> None:
|
||||
assert compute_name_similarity(a, b) == pytest.approx(expected)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -195,7 +195,7 @@ class TestNameSimilarity:
|
||||
pytest.param("topic1/topic2/core", "topic3/topic5/core", id="same_basename_disjoint_prefix_tokens"),
|
||||
],
|
||||
)
|
||||
def test_below_threshold_cases(self, manager: MemoriesManager, a: str, b: str) -> None:
|
||||
def test_below_threshold_cases(self, manager: MemoryManager, a: str, b: str) -> None:
|
||||
assert compute_name_similarity(a, b) < NAME_SIMILARITY_THRESHOLD
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -210,14 +210,14 @@ class TestNameSimilarity:
|
||||
pytest.param("user_login_flow", "login_flow", id="partial_token_overlap"),
|
||||
],
|
||||
)
|
||||
def test_above_threshold_cases(self, manager: MemoriesManager, a: str, b: str) -> None:
|
||||
def test_above_threshold_cases(self, manager: MemoryManager, a: str, b: str) -> None:
|
||||
assert compute_name_similarity(a, b) >= NAME_SIMILARITY_THRESHOLD
|
||||
|
||||
def test_symmetry(self, manager: MemoriesManager) -> None:
|
||||
def test_symmetry(self, manager: MemoryManager) -> None:
|
||||
# similarity is order-independent
|
||||
assert compute_name_similarity("auth/login", "security/login") == compute_name_similarity("security/login", "auth/login")
|
||||
|
||||
def test_cross_topic_suffix_coincidence_is_rejected(self, manager: MemoriesManager) -> None:
|
||||
def test_cross_topic_suffix_coincidence_is_rejected(self, manager: MemoryManager) -> None:
|
||||
"""
|
||||
Regression: when two long names live under *different* topic prefixes and share
|
||||
only a single trailing token (e.g. ``-subtleties``), the score must collapse to
|
||||
@@ -238,7 +238,7 @@ class TestNameSimilarity:
|
||||
same_prefix_score = compute_name_similarity(target, "frontend/routing-app-shell-subtleties")
|
||||
assert same_prefix_score > 0.0
|
||||
|
||||
def test_find_candidates_ranks_by_descending_similarity(self, manager: MemoriesManager) -> None:
|
||||
def test_find_candidates_ranks_by_descending_similarity(self, manager: MemoryManager) -> None:
|
||||
existing = ["auth/login", "security/login", "database", "authentication"]
|
||||
candidates = find_stale_reference_candidates("login", existing)
|
||||
# both auth/login and security/login share basename `login` -> tied at 1.0; expect them first, alphabetical tiebreak
|
||||
@@ -246,11 +246,11 @@ class TestNameSimilarity:
|
||||
# `database` is unrelated and must not appear
|
||||
assert "database" not in candidates
|
||||
|
||||
def test_find_candidates_returns_empty_below_threshold(self, manager: MemoriesManager) -> None:
|
||||
def test_find_candidates_returns_empty_below_threshold(self, manager: MemoryManager) -> None:
|
||||
existing = ["completely_unrelated", "another_thing"]
|
||||
assert find_stale_reference_candidates("foo", existing) == []
|
||||
|
||||
def test_find_candidates_custom_threshold(self, manager: MemoriesManager) -> None:
|
||||
def test_find_candidates_custom_threshold(self, manager: MemoryManager) -> None:
|
||||
# raising the threshold filters out borderline matches
|
||||
existing = ["authentication"]
|
||||
loose = find_stale_reference_candidates("auth", existing, threshold=0.2)
|
||||
@@ -260,24 +260,24 @@ class TestNameSimilarity:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fs_manager(tmp_path) -> MemoriesManager:
|
||||
def fs_manager(tmp_path) -> MemoryManager:
|
||||
"""A MemoriesManager backed by tmp_path with no read-only patterns."""
|
||||
return MemoriesManager(serena_data_folder=tmp_path)
|
||||
return MemoryManager(serena_data_folder=tmp_path)
|
||||
|
||||
|
||||
def _write(manager: MemoriesManager, name: str, content: str) -> None:
|
||||
def _write(manager: MemoryManager, name: str, content: str) -> None:
|
||||
manager.save_memory(name, content, is_tool_context=False)
|
||||
|
||||
|
||||
class TestValidateReferentialIntegrity:
|
||||
def test_clean_report_when_all_references_resolve(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_clean_report_when_all_references_resolve(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth", "# auth notes")
|
||||
_write(fs_manager, "login_flow", "see `mem:auth` for details")
|
||||
report = fs_manager.validate_referential_integrity()
|
||||
assert report.is_clean()
|
||||
assert "No referential integrity issues" in report.format()
|
||||
|
||||
def test_stale_reference_with_topic_move_candidate(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_stale_reference_with_topic_move_candidate(self, fs_manager: MemoryManager) -> None:
|
||||
# original memory `login` was moved to `auth/login`; a reference still points to bare `login`
|
||||
_write(fs_manager, "auth/login", "# moved here")
|
||||
_write(fs_manager, "docs", "old reference: `mem:login`")
|
||||
@@ -288,14 +288,14 @@ class TestValidateReferentialIntegrity:
|
||||
assert stale.referenced_name == "login"
|
||||
assert "auth/login" in stale.candidates
|
||||
|
||||
def test_stale_reference_with_no_candidate(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_stale_reference_with_no_candidate(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "completely_unrelated", "# content")
|
||||
_write(fs_manager, "docs", "broken: `mem:xyz_unknown_thing`")
|
||||
report = fs_manager.validate_referential_integrity()
|
||||
assert len(report.stale_references) == 1
|
||||
assert report.stale_references[0].candidates == []
|
||||
|
||||
def test_high_confidence_unmarked_warning_for_topic_path(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_high_confidence_unmarked_warning_for_topic_path(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "# notes")
|
||||
_write(fs_manager, "docs", "the auth/login process is documented elsewhere")
|
||||
report = fs_manager.validate_referential_integrity()
|
||||
@@ -306,7 +306,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert warning.occurrences == 1
|
||||
assert report.low_confidence_unmarked_memories == []
|
||||
|
||||
def test_low_confidence_unmarked_warning_for_short_flat_name(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_low_confidence_unmarked_warning_for_short_flat_name(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth", "# notes")
|
||||
_write(fs_manager, "docs", "you must use auth here")
|
||||
report = fs_manager.validate_referential_integrity()
|
||||
@@ -315,7 +315,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert report.low_confidence_unmarked_memories[0].suspected_name == "auth"
|
||||
assert report.low_confidence_unmarked_memories[0].is_high_confidence is False
|
||||
|
||||
def test_long_flat_name_is_high_confidence(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_long_flat_name_is_high_confidence(self, fs_manager: MemoryManager) -> None:
|
||||
# a flat name >= _HIGH_CONFIDENCE_NAME_LENGTH chars is unlikely to be coincidental prose
|
||||
long_name = "authentication_flow"
|
||||
assert len(long_name) >= HIGH_CONFIDENCE_NAME_LENGTH
|
||||
@@ -324,7 +324,7 @@ class TestValidateReferentialIntegrity:
|
||||
report = fs_manager.validate_referential_integrity()
|
||||
assert len(report.high_confidence_unmarked_memories) == 1
|
||||
|
||||
def test_fuzzy_near_miss_rejects_substring_only_matches(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_fuzzy_near_miss_rejects_substring_only_matches(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
Regression: fuzzy bare-text matching must require meaningful token overlap, not just
|
||||
substring containment. Without the token-Jaccard floor, generic words like
|
||||
@@ -353,7 +353,7 @@ class TestValidateReferentialIntegrity:
|
||||
f"low={report.low_confidence_unmarked_memories}, stale={report.stale_references}"
|
||||
)
|
||||
|
||||
def test_long_flat_name_near_miss_is_high_confidence(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_long_flat_name_near_miss_is_high_confidence(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
Regression: bare text that is a *near-match* of an existing long memory name
|
||||
(e.g. the same name with a missing suffix) should be flagged as a high-confidence
|
||||
@@ -380,7 +380,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert warning.suspected_name == existing
|
||||
assert warning.source_memory == "docs"
|
||||
|
||||
def test_fuzzy_accepts_bare_token_when_jaccard_just_above_floor(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_fuzzy_accepts_bare_token_when_jaccard_just_above_floor(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
Edge case: bare token is a clean substring of the existing memory name AND
|
||||
their tokenized Jaccard sits just above ``_FUZZY_BARE_TOKEN_JACCARD_FLOOR``
|
||||
@@ -398,7 +398,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert fuzzy[0].suspected_name == existing
|
||||
assert fuzzy[0].actual_token == bare
|
||||
|
||||
def test_fuzzy_rejects_bare_token_when_jaccard_just_below_floor(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_fuzzy_rejects_bare_token_when_jaccard_just_below_floor(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
Edge case: bare token is still a clean substring of the existing memory name,
|
||||
but their tokenized Jaccard is just below ``_FUZZY_BARE_TOKEN_JACCARD_FLOOR``
|
||||
@@ -414,7 +414,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert report.high_confidence_unmarked_memories == []
|
||||
assert report.low_confidence_unmarked_memories == []
|
||||
|
||||
def test_fuzzy_requires_substring_containment(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_fuzzy_requires_substring_containment(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
Edge case: bare token and existing name share several tokens (and even a topic
|
||||
prefix) but neither is a substring of the other. Without the containment
|
||||
@@ -430,7 +430,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert report.high_confidence_unmarked_memories == []
|
||||
assert report.low_confidence_unmarked_memories == []
|
||||
|
||||
def test_fuzzy_accepts_reverse_containment(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_fuzzy_accepts_reverse_containment(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
Edge case: the *existing* memory name is a substring of the longer bare token
|
||||
(rather than the other way around). The containment gate must hold in both
|
||||
@@ -447,7 +447,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert fuzzy[0].suspected_name == existing
|
||||
assert fuzzy[0].actual_token == bare
|
||||
|
||||
def test_unmarked_exact_match_suppressed_for_ignored_word(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_unmarked_exact_match_suppressed_for_ignored_word(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
Names in ``_WORDS_TO_IGNORE_AS_MEMORY_NAME_CANDIDATES`` (e.g. ``core``) are
|
||||
common-word collisions and must not produce bare-occurrence warnings even
|
||||
@@ -460,7 +460,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert all(w.suspected_name != "core" for w in report.high_confidence_unmarked_memories)
|
||||
assert all(w.suspected_name != "core" for w in report.low_confidence_unmarked_memories)
|
||||
|
||||
def test_unmarked_exact_match_suppressed_for_topic_with_ignored_basename(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_unmarked_exact_match_suppressed_for_topic_with_ignored_basename(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
The ignore filter applies to the *basename* of a candidate name, so a memory
|
||||
named ``frontend/core`` is also exempt from bare-occurrence warnings (the
|
||||
@@ -473,7 +473,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert all(w.suspected_name != "frontend/core" for w in report.high_confidence_unmarked_memories)
|
||||
assert all(w.suspected_name != "frontend/core" for w in report.low_confidence_unmarked_memories)
|
||||
|
||||
def test_unmarked_exact_match_not_suppressed_for_non_ignored_basename(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_unmarked_exact_match_not_suppressed_for_non_ignored_basename(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
Sanity: the ignore filter must not over-apply to names whose basename happens
|
||||
to share a prefix with an ignored word.
|
||||
@@ -483,7 +483,7 @@ class TestValidateReferentialIntegrity:
|
||||
report = fs_manager.validate_referential_integrity()
|
||||
assert any(w.suspected_name == "frontend/core-utils" for w in report.high_confidence_unmarked_memories)
|
||||
|
||||
def test_stale_reference_candidates_are_capped(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_stale_reference_candidates_are_capped(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
The candidate list proposed for an unresolved ``mem:`` reference is capped at
|
||||
:attr:`MAX_STALE_REFERENCE_CANDIDATES`. Beyond a small number the list stops
|
||||
@@ -502,7 +502,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert len(candidates) == cap
|
||||
assert candidates == candidate_names[:cap]
|
||||
|
||||
def test_include_unmarked_false_skips_both_scans(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_include_unmarked_false_skips_both_scans(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
With ``include_unmarked=False`` only stale references are reported; both the
|
||||
exact unmarked scan and the fuzzy near-miss scan are entirely skipped.
|
||||
@@ -521,7 +521,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert len(report.stale_references) == 1
|
||||
assert report.stale_references[0].referenced_name == "nope"
|
||||
|
||||
def test_include_fuzzy_matching_false_keeps_exact_only(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_include_fuzzy_matching_false_keeps_exact_only(self, fs_manager: MemoryManager) -> None:
|
||||
"""
|
||||
With ``include_unmarked=True`` but ``include_fuzzy_matching=False``, exact
|
||||
bare-occurrence warnings still fire but fuzzy near-misses are suppressed.
|
||||
@@ -542,7 +542,7 @@ class TestValidateReferentialIntegrity:
|
||||
# fuzzy near-miss for `playwright-gestures-guide` is suppressed
|
||||
assert fuzzy == []
|
||||
|
||||
def test_self_reference_is_ignored(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_self_reference_is_ignored(self, fs_manager: MemoryManager) -> None:
|
||||
# the basename `login` appears in `auth/login`'s own content -> must not be flagged
|
||||
_write(fs_manager, "auth/login", "this login flow ...")
|
||||
_write(fs_manager, "auth", "# auth notes")
|
||||
@@ -553,14 +553,14 @@ class TestValidateReferentialIntegrity:
|
||||
for w in report.high_confidence_unmarked_memories + report.low_confidence_unmarked_memories:
|
||||
assert not (w.source_memory == "auth/login" and w.suspected_name in ("login", "auth/login"))
|
||||
|
||||
def test_marked_reference_does_not_produce_unmarked_warning(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_marked_reference_does_not_produce_unmarked_warning(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "# notes")
|
||||
_write(fs_manager, "docs", "see `mem:auth/login` for details")
|
||||
report = fs_manager.validate_referential_integrity()
|
||||
# the marked reference is valid; no warning should fire on that occurrence
|
||||
assert report.is_clean()
|
||||
|
||||
def test_mixed_marked_and_bare_in_same_content(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_mixed_marked_and_bare_in_same_content(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "# notes")
|
||||
_write(fs_manager, "docs", "we have `mem:auth/login` plus a bare auth/login mention")
|
||||
report = fs_manager.validate_referential_integrity()
|
||||
@@ -568,7 +568,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert len(report.high_confidence_unmarked_memories) == 1
|
||||
assert report.high_confidence_unmarked_memories[0].occurrences == 1
|
||||
|
||||
def test_empty_memory_skipped(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_empty_memory_skipped(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "")
|
||||
_write(fs_manager, "docs", "# nothing relevant")
|
||||
# must not raise even though one memory is empty
|
||||
@@ -576,7 +576,7 @@ class TestValidateReferentialIntegrity:
|
||||
assert report.is_clean()
|
||||
|
||||
def test_read_only_status_propagated(self, tmp_path) -> None:
|
||||
manager = MemoriesManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
|
||||
manager = MemoryManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
|
||||
_write(manager, "frozen/notes", "see bare auth/login mention")
|
||||
_write(manager, "auth/login", "# notes")
|
||||
report = manager.validate_referential_integrity()
|
||||
@@ -587,7 +587,7 @@ class TestValidateReferentialIntegrity:
|
||||
|
||||
|
||||
class TestAutoPrefixBareReferences:
|
||||
def test_prefixes_high_confidence_warning_in_writable_memory(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_prefixes_high_confidence_warning_in_writable_memory(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "# notes")
|
||||
_write(fs_manager, "docs", "the auth/login process is documented here")
|
||||
result = fs_manager.auto_prefix_bare_references()
|
||||
@@ -596,7 +596,7 @@ class TestAutoPrefixBareReferences:
|
||||
# verify the file content was actually mutated
|
||||
assert fs_manager.load_memory("docs") == "the mem:auth/login process is documented here"
|
||||
|
||||
def test_skips_low_confidence_by_default(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_skips_low_confidence_by_default(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth", "# notes")
|
||||
_write(fs_manager, "docs", "use auth here")
|
||||
result = fs_manager.auto_prefix_bare_references()
|
||||
@@ -605,7 +605,7 @@ class TestAutoPrefixBareReferences:
|
||||
# content unchanged
|
||||
assert fs_manager.load_memory("docs") == "use auth here"
|
||||
|
||||
def test_include_flat_names_widens_scope(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_include_flat_names_widens_scope(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth", "# notes")
|
||||
_write(fs_manager, "docs", "use auth here")
|
||||
result = fs_manager.auto_prefix_bare_references(include_flat_names=True)
|
||||
@@ -613,7 +613,7 @@ class TestAutoPrefixBareReferences:
|
||||
assert fs_manager.load_memory("docs") == "use mem:auth here"
|
||||
|
||||
def test_skips_read_only_source_by_default(self, tmp_path) -> None:
|
||||
manager = MemoriesManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
|
||||
manager = MemoryManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
|
||||
_write(manager, "frozen/notes", "the auth/login process")
|
||||
_write(manager, "auth/login", "# notes")
|
||||
result = manager.auto_prefix_bare_references()
|
||||
@@ -623,21 +623,21 @@ class TestAutoPrefixBareReferences:
|
||||
assert manager.load_memory("frozen/notes") == "the auth/login process"
|
||||
|
||||
def test_include_read_only_modifies_read_only_source(self, tmp_path) -> None:
|
||||
manager = MemoriesManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
|
||||
manager = MemoryManager(serena_data_folder=tmp_path, read_only_memory_patterns=[r"frozen/.*"])
|
||||
_write(manager, "frozen/notes", "the auth/login process")
|
||||
_write(manager, "auth/login", "# notes")
|
||||
result = manager.auto_prefix_bare_references(include_read_only=True)
|
||||
assert result.total_replacements == 1
|
||||
assert manager.load_memory("frozen/notes") == "the mem:auth/login process"
|
||||
|
||||
def test_multiple_occurrences_in_same_memory(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_multiple_occurrences_in_same_memory(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "# notes")
|
||||
_write(fs_manager, "docs", "auth/login here and auth/login there")
|
||||
result = fs_manager.auto_prefix_bare_references()
|
||||
assert result.total_replacements == 2
|
||||
assert fs_manager.load_memory("docs") == "mem:auth/login here and mem:auth/login there"
|
||||
|
||||
def test_marked_references_left_alone(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_marked_references_left_alone(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "# notes")
|
||||
_write(fs_manager, "docs", "see `mem:auth/login` and also auth/login bare")
|
||||
result = fs_manager.auto_prefix_bare_references()
|
||||
@@ -645,7 +645,7 @@ class TestAutoPrefixBareReferences:
|
||||
assert result.total_replacements == 1
|
||||
assert fs_manager.load_memory("docs") == "see `mem:auth/login` and also mem:auth/login bare"
|
||||
|
||||
def test_dry_run_does_not_modify_files(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_dry_run_does_not_modify_files(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "# notes")
|
||||
_write(fs_manager, "docs", "the auth/login process is documented here")
|
||||
result = fs_manager.auto_prefix_bare_references(dry_run=True)
|
||||
@@ -660,7 +660,7 @@ class TestAutoPrefixBareReferences:
|
||||
assert result.dry_run is False
|
||||
assert fs_manager.load_memory("docs") == "the mem:auth/login process is documented here"
|
||||
|
||||
def test_fuzzy_warnings_are_routed_to_skipped_fuzzy(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_fuzzy_warnings_are_routed_to_skipped_fuzzy(self, fs_manager: MemoryManager) -> None:
|
||||
# set up a fuzzy near-miss case (token shares all parts with a longer existing name)
|
||||
existing = "adding_new_language_support_guide"
|
||||
bare = "adding_new_language_support"
|
||||
@@ -676,7 +676,7 @@ class TestAutoPrefixBareReferences:
|
||||
# source unchanged
|
||||
assert fs_manager.load_memory("docs") == f"See {bare} for details"
|
||||
|
||||
def test_no_double_prefix_on_repeated_runs(self, fs_manager: MemoriesManager) -> None:
|
||||
def test_no_double_prefix_on_repeated_runs(self, fs_manager: MemoryManager) -> None:
|
||||
_write(fs_manager, "auth/login", "# notes")
|
||||
_write(fs_manager, "docs", "the auth/login process")
|
||||
fs_manager.auto_prefix_bare_references()
|
||||
|
||||
Reference in new issue
Block a user