mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
feat: upgrade fastmcp 2.12.3 to 3.0.1 with tool annotations (#598)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -29,7 +29,7 @@ dependencies = [
|
||||
"alembic>=1.14.1",
|
||||
"pillow>=11.1.0",
|
||||
"pybars3>=0.9.7",
|
||||
"fastmcp==2.12.3", # Pinned - 2.14.x breaks MCP tools visibility (issue #463)
|
||||
"fastmcp>=3.0.1,<4",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-dotenv>=1.1.0",
|
||||
"pytest-aio>=1.9.0",
|
||||
|
||||
@@ -104,7 +104,7 @@ async def _write_note_json(
|
||||
) -> dict:
|
||||
"""Write a note and return structured JSON metadata."""
|
||||
# Use the MCP tool to create/update the entity (handles create-or-update logic)
|
||||
await mcp_write_note.fn(
|
||||
await mcp_write_note(
|
||||
title=title,
|
||||
content=content,
|
||||
directory=folder,
|
||||
@@ -155,7 +155,7 @@ async def _read_note_json(
|
||||
if entity_id is None:
|
||||
from basic_memory.mcp.tools.search import search_notes as mcp_search_tool
|
||||
|
||||
title_results = await mcp_search_tool.fn(
|
||||
title_results = await mcp_search_tool(
|
||||
query=identifier,
|
||||
search_type="title",
|
||||
project=project_name,
|
||||
@@ -387,7 +387,7 @@ def write_note(
|
||||
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
|
||||
else:
|
||||
note = run_with_cleanup(
|
||||
mcp_write_note.fn(
|
||||
mcp_write_note(
|
||||
title=title,
|
||||
content=content,
|
||||
directory=folder,
|
||||
@@ -468,13 +468,15 @@ def read_note(
|
||||
result["content"] = stripped_content
|
||||
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
|
||||
else:
|
||||
note = run_with_cleanup(
|
||||
mcp_read_note.fn(
|
||||
identifier=identifier,
|
||||
project=project_name,
|
||||
workspace=workspace,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
note = str(
|
||||
run_with_cleanup(
|
||||
mcp_read_note(
|
||||
identifier=identifier,
|
||||
project=project_name,
|
||||
workspace=workspace,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
)
|
||||
)
|
||||
if strip_frontmatter:
|
||||
@@ -560,16 +562,18 @@ def edit_note(
|
||||
)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
|
||||
else:
|
||||
result = run_with_cleanup(
|
||||
mcp_edit_note.fn(
|
||||
identifier=identifier,
|
||||
operation=operation,
|
||||
content=content,
|
||||
project=project_name,
|
||||
workspace=workspace,
|
||||
section=section,
|
||||
find_text=find_text,
|
||||
expected_replacements=expected_replacements,
|
||||
result = str(
|
||||
run_with_cleanup(
|
||||
mcp_edit_note(
|
||||
identifier=identifier,
|
||||
operation=operation,
|
||||
content=content,
|
||||
project=project_name,
|
||||
workspace=workspace,
|
||||
section=section,
|
||||
find_text=find_text,
|
||||
expected_replacements=expected_replacements,
|
||||
)
|
||||
)
|
||||
)
|
||||
rprint(result)
|
||||
@@ -629,7 +633,7 @@ def build_context(
|
||||
|
||||
with force_routing(local=local, cloud=cloud):
|
||||
result = run_with_cleanup(
|
||||
mcp_build_context.fn(
|
||||
mcp_build_context(
|
||||
project=project_name,
|
||||
workspace=workspace,
|
||||
url=url,
|
||||
@@ -712,10 +716,10 @@ def recent_activity(
|
||||
print(json.dumps(result, indent=2, ensure_ascii=True, default=str))
|
||||
else:
|
||||
result = run_with_cleanup(
|
||||
mcp_recent_activity.fn(
|
||||
type=type, # pyright: ignore [reportArgumentType]
|
||||
depth=depth,
|
||||
timeframe=timeframe,
|
||||
mcp_recent_activity(
|
||||
type=type, # pyright: ignore[reportArgumentType]
|
||||
depth=depth if depth is not None else 1,
|
||||
timeframe=timeframe if timeframe is not None else "7d",
|
||||
project=project_name,
|
||||
workspace=workspace,
|
||||
)
|
||||
@@ -862,11 +866,12 @@ def search_notes(
|
||||
|
||||
with force_routing(local=local, cloud=cloud):
|
||||
results = run_with_cleanup(
|
||||
mcp_search.fn(
|
||||
mcp_search(
|
||||
query=query or "",
|
||||
project=project_name,
|
||||
workspace=workspace,
|
||||
search_type=search_type,
|
||||
output_format="json",
|
||||
page=page,
|
||||
after_date=after_date,
|
||||
page_size=page_size,
|
||||
@@ -881,8 +886,7 @@ def search_notes(
|
||||
print(results)
|
||||
raise typer.Exit(1)
|
||||
|
||||
results_dict = results.model_dump(exclude_none=True)
|
||||
print(json.dumps(results_dict, indent=2, ensure_ascii=True, default=str))
|
||||
print(json.dumps(results, indent=2, ensure_ascii=True, default=str))
|
||||
except ValueError as e:
|
||||
typer.echo(f"Error: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
@@ -916,7 +920,7 @@ def continue_conversation(
|
||||
with force_routing(local=local, cloud=cloud):
|
||||
# Prompt functions return formatted strings directly
|
||||
session = run_with_cleanup(
|
||||
mcp_continue_conversation.fn(topic=topic, timeframe=timeframe) # type: ignore[arg-type]
|
||||
mcp_continue_conversation(topic=topic, timeframe=timeframe) # type: ignore[arg-type]
|
||||
)
|
||||
rprint(session)
|
||||
except ValueError as e:
|
||||
|
||||
@@ -99,11 +99,9 @@ def _workspace_choices(workspaces: list[WorkspaceInfo]) -> str:
|
||||
async def get_available_workspaces(context: Optional[Context] = None) -> list[WorkspaceInfo]:
|
||||
"""Load available cloud workspaces for the current authenticated user."""
|
||||
if context:
|
||||
cached_workspaces = context.get_state("available_workspaces")
|
||||
if isinstance(cached_workspaces, list) and all(
|
||||
isinstance(item, WorkspaceInfo) for item in cached_workspaces
|
||||
):
|
||||
return cached_workspaces
|
||||
cached_raw = await context.get_state("available_workspaces")
|
||||
if isinstance(cached_raw, list):
|
||||
return [WorkspaceInfo.model_validate(item) for item in cached_raw]
|
||||
|
||||
from basic_memory.mcp.async_client import get_cloud_control_plane_client
|
||||
from basic_memory.mcp.tools.utils import call_get
|
||||
@@ -113,7 +111,10 @@ async def get_available_workspaces(context: Optional[Context] = None) -> list[Wo
|
||||
workspace_list = WorkspaceListResponse.model_validate(response.json())
|
||||
|
||||
if context:
|
||||
context.set_state("available_workspaces", workspace_list.workspaces)
|
||||
await context.set_state(
|
||||
"available_workspaces",
|
||||
[ws.model_dump() for ws in workspace_list.workspaces],
|
||||
)
|
||||
|
||||
return workspace_list.workspaces
|
||||
|
||||
@@ -124,12 +125,12 @@ async def resolve_workspace_parameter(
|
||||
) -> WorkspaceInfo:
|
||||
"""Resolve workspace using explicit input, session cache, and cloud discovery."""
|
||||
if context:
|
||||
cached_workspace = context.get_state("active_workspace")
|
||||
if isinstance(cached_workspace, WorkspaceInfo) and (
|
||||
workspace is None or _workspace_matches_identifier(cached_workspace, workspace)
|
||||
):
|
||||
logger.debug(f"Using cached workspace from context: {cached_workspace.tenant_id}")
|
||||
return cached_workspace
|
||||
cached_raw = await context.get_state("active_workspace")
|
||||
if isinstance(cached_raw, dict):
|
||||
cached_workspace = WorkspaceInfo.model_validate(cached_raw)
|
||||
if workspace is None or _workspace_matches_identifier(cached_workspace, workspace):
|
||||
logger.debug(f"Using cached workspace from context: {cached_workspace.tenant_id}")
|
||||
return cached_workspace
|
||||
|
||||
workspaces = await get_available_workspaces(context=context)
|
||||
if not workspaces:
|
||||
@@ -164,7 +165,7 @@ async def resolve_workspace_parameter(
|
||||
)
|
||||
|
||||
if context:
|
||||
context.set_state("active_workspace", selected_workspace)
|
||||
await context.set_state("active_workspace", selected_workspace.model_dump())
|
||||
logger.debug(f"Cached workspace in context: {selected_workspace.tenant_id}")
|
||||
|
||||
return selected_workspace
|
||||
@@ -206,10 +207,12 @@ async def get_active_project(
|
||||
|
||||
# Check if already cached in context
|
||||
if context:
|
||||
cached_project = context.get_state("active_project")
|
||||
if cached_project and cached_project.name == project:
|
||||
logger.debug(f"Using cached project from context: {project}")
|
||||
return cached_project
|
||||
cached_raw = await context.get_state("active_project")
|
||||
if isinstance(cached_raw, dict):
|
||||
cached_project = ProjectItem.model_validate(cached_raw)
|
||||
if cached_project.name == project:
|
||||
logger.debug(f"Using cached project from context: {project}")
|
||||
return cached_project
|
||||
|
||||
# Validate project exists by calling API
|
||||
logger.debug(f"Validating project: {project}")
|
||||
@@ -230,7 +233,7 @@ async def get_active_project(
|
||||
|
||||
# Cache in context if available
|
||||
if context:
|
||||
context.set_state("active_project", active_project)
|
||||
await context.set_state("active_project", active_project.model_dump())
|
||||
logger.debug(f"Cached project in context: {project}")
|
||||
|
||||
logger.debug(f"Validated project: {active_project.name}")
|
||||
@@ -307,7 +310,7 @@ async def resolve_project_and_path(
|
||||
is_default=resolved.is_default,
|
||||
)
|
||||
if context:
|
||||
context.set_state("active_project", active_project)
|
||||
await context.set_state("active_project", active_project.model_dump())
|
||||
|
||||
resolved_path = f"{resolved.permalink}/{remainder}" if include_project else remainder
|
||||
return active_project, resolved_path, True
|
||||
|
||||
@@ -46,7 +46,7 @@ async def recent_activity_prompt(
|
||||
logger.info(f"Getting recent activity, timeframe: {timeframe}, project: {project}")
|
||||
|
||||
# Call the tool function - it returns a well-formatted string
|
||||
activity_summary = await recent_activity.fn(project=project, timeframe=timeframe)
|
||||
activity_summary = await recent_activity(project=project, timeframe=timeframe)
|
||||
|
||||
# Build the prompt response
|
||||
# The tool already returns formatted markdown, so we use it directly
|
||||
|
||||
@@ -192,6 +192,7 @@ def _format_context_markdown(graph: GraphContext, project: str) -> str:
|
||||
- "json" (default): Slimmed JSON with redundant fields removed
|
||||
- "text": Compact markdown text for LLM consumption
|
||||
""",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def build_context(
|
||||
url: MemoryUrl,
|
||||
|
||||
@@ -16,6 +16,7 @@ from basic_memory.mcp.tools.utils import call_put, call_post, resolve_entity_id
|
||||
|
||||
@mcp.tool(
|
||||
description="Create an Obsidian canvas file to visualize concepts and connections.",
|
||||
annotations={"destructiveHint": False, "idempotentHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def canvas(
|
||||
nodes: List[Dict[str, Any]],
|
||||
|
||||
@@ -92,7 +92,10 @@ def _format_document_for_chatgpt(
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool(description="Search for content across the knowledge base")
|
||||
@mcp.tool(
|
||||
description="Search for content across the knowledge base",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def search(
|
||||
query: str,
|
||||
context: Context | None = None,
|
||||
@@ -115,7 +118,7 @@ async def search(
|
||||
default_project = config.default_project
|
||||
|
||||
# Call underlying search_notes with sensible defaults for ChatGPT
|
||||
results = await search_notes.fn(
|
||||
results = await search_notes(
|
||||
query=query,
|
||||
project=default_project, # Use default project for ChatGPT
|
||||
page=1,
|
||||
@@ -156,7 +159,10 @@ async def search(
|
||||
return [{"type": "text", "text": json.dumps(error_results, ensure_ascii=False)}]
|
||||
|
||||
|
||||
@mcp.tool(description="Fetch the full contents of a search result document")
|
||||
@mcp.tool(
|
||||
description="Fetch the full contents of a search result document",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def fetch(
|
||||
id: str,
|
||||
context: Context | None = None,
|
||||
@@ -178,13 +184,15 @@ async def fetch(
|
||||
config = ConfigManager().config
|
||||
default_project = config.default_project
|
||||
|
||||
# Call underlying read_note function
|
||||
content = await read_note.fn(
|
||||
identifier=id,
|
||||
project=default_project, # Use default project for ChatGPT
|
||||
page=1,
|
||||
page_size=10, # Default pagination
|
||||
context=context,
|
||||
# Call underlying read_note function (default output_format="text" returns str)
|
||||
content = str(
|
||||
await read_note(
|
||||
identifier=id,
|
||||
project=default_project, # Use default project for ChatGPT
|
||||
page=1,
|
||||
page_size=10, # Default pagination
|
||||
context=context,
|
||||
)
|
||||
)
|
||||
|
||||
# Format the document for ChatGPT
|
||||
|
||||
@@ -5,7 +5,10 @@ from pathlib import Path
|
||||
from basic_memory.mcp.server import mcp
|
||||
|
||||
|
||||
@mcp.tool("cloud_info")
|
||||
@mcp.tool(
|
||||
"cloud_info",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
def cloud_info() -> str:
|
||||
"""Return optional Basic Memory Cloud information and setup guidance."""
|
||||
content_path = Path(__file__).parent.parent / "resources" / "cloud_info.md"
|
||||
|
||||
@@ -146,7 +146,10 @@ delete_note("{project}", "correct-identifier-from-search")
|
||||
If the note should be deleted but the operation keeps failing, send a message to support@basicmemory.com."""
|
||||
|
||||
|
||||
@mcp.tool(description="Delete a note or directory by title, permalink, or path")
|
||||
@mcp.tool(
|
||||
description="Delete a note or directory by title, permalink, or path",
|
||||
annotations={"destructiveHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def delete_note(
|
||||
identifier: str,
|
||||
is_directory: bool = False,
|
||||
|
||||
@@ -125,6 +125,7 @@ Error editing note '{identifier}': {error_message}
|
||||
|
||||
@mcp.tool(
|
||||
description="Edit an existing markdown note using various operations like append, prepend, find_replace, or replace_section.",
|
||||
annotations={"destructiveHint": False, "openWorldHint": False},
|
||||
)
|
||||
async def edit_note(
|
||||
identifier: str,
|
||||
|
||||
@@ -11,6 +11,7 @@ from basic_memory.mcp.server import mcp
|
||||
|
||||
@mcp.tool(
|
||||
description="List directory contents with filtering and depth control.",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def list_directory(
|
||||
dir_name: str = "/",
|
||||
|
||||
@@ -343,6 +343,7 @@ delete_note("{identifier}")
|
||||
|
||||
@mcp.tool(
|
||||
description="Move a note or directory to a new location, updating database and maintaining links.",
|
||||
annotations={"destructiveHint": False, "openWorldHint": False},
|
||||
)
|
||||
async def move_note(
|
||||
identifier: str,
|
||||
|
||||
@@ -14,7 +14,10 @@ from basic_memory.schemas.project_info import ProjectInfoRequest
|
||||
from basic_memory.utils import generate_permalink
|
||||
|
||||
|
||||
@mcp.tool("list_memory_projects")
|
||||
@mcp.tool(
|
||||
"list_memory_projects",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def list_memory_projects(
|
||||
output_format: Literal["text", "json"] = "text",
|
||||
context: Context | None = None,
|
||||
@@ -77,7 +80,10 @@ async def list_memory_projects(
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool("create_memory_project")
|
||||
@mcp.tool(
|
||||
"create_memory_project",
|
||||
annotations={"destructiveHint": False, "openWorldHint": False},
|
||||
)
|
||||
async def create_memory_project(
|
||||
project_name: str,
|
||||
project_path: str,
|
||||
@@ -193,7 +199,9 @@ async def create_memory_project(
|
||||
return result
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
annotations={"destructiveHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def delete_project(project_name: str, context: Context | None = None) -> str:
|
||||
"""Delete a Basic Memory project.
|
||||
|
||||
|
||||
@@ -148,7 +148,10 @@ def optimize_image(img, content_length, max_output_bytes=350000):
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
@mcp.tool(description="Read a file's raw content by path or permalink")
|
||||
@mcp.tool(
|
||||
description="Read a file's raw content by path or permalink",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def read_content(
|
||||
path: str,
|
||||
project: Optional[str] = None,
|
||||
|
||||
@@ -59,6 +59,7 @@ def _parse_opening_frontmatter(content: str) -> tuple[str, dict | None]:
|
||||
description="Read a markdown note by title or permalink.",
|
||||
# TODO: re-enable once MCP client rendering is working
|
||||
# meta={"ui/resourceUri": "ui://basic-memory/note-preview"},
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def read_note(
|
||||
identifier: str,
|
||||
@@ -228,7 +229,7 @@ async def read_note(
|
||||
|
||||
# Fallback 1: Try title search via API
|
||||
logger.info(f"Search title for: {identifier}")
|
||||
title_results = await search_notes.fn(
|
||||
title_results = await search_notes(
|
||||
query=identifier,
|
||||
search_type="title",
|
||||
project=active_project.name,
|
||||
@@ -280,7 +281,7 @@ async def read_note(
|
||||
|
||||
# Fallback 2: Text search as a last resort
|
||||
logger.info(f"Title search failed, trying text search for: {identifier}")
|
||||
text_results = await search_notes.fn(
|
||||
text_results = await search_notes(
|
||||
query=identifier,
|
||||
search_type="text",
|
||||
project=active_project.name,
|
||||
|
||||
@@ -35,6 +35,7 @@ from basic_memory.schemas.search import SearchItemType
|
||||
- "3 weeks ago"
|
||||
Or standard formats like "7d"
|
||||
""",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def recent_activity(
|
||||
type: Union[str, List[str]] = "",
|
||||
|
||||
@@ -5,7 +5,10 @@ from pathlib import Path
|
||||
from basic_memory.mcp.server import mcp
|
||||
|
||||
|
||||
@mcp.tool("release_notes")
|
||||
@mcp.tool(
|
||||
"release_notes",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
def release_notes() -> str:
|
||||
"""Return the latest product release notes for optional user review."""
|
||||
content_path = Path(__file__).parent.parent / "resources" / "release_notes.md"
|
||||
|
||||
@@ -71,6 +71,7 @@ def _no_schema_guidance(note_type: str, tool_name: str) -> str:
|
||||
|
||||
@mcp.tool(
|
||||
description="Validate notes against their Picoschema definitions.",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def schema_validate(
|
||||
note_type: Optional[str] = None,
|
||||
@@ -166,6 +167,7 @@ async def schema_validate(
|
||||
|
||||
@mcp.tool(
|
||||
description="Analyze existing notes and suggest a Picoschema definition.",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def schema_infer(
|
||||
note_type: str,
|
||||
@@ -267,6 +269,7 @@ async def schema_infer(
|
||||
|
||||
@mcp.tool(
|
||||
description="Detect drift between a schema definition and actual note usage.",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def schema_diff(
|
||||
note_type: str,
|
||||
|
||||
@@ -250,6 +250,7 @@ Error searching for '{query}': {error_message}
|
||||
description="Search across all content in the knowledge base with advanced syntax support.",
|
||||
# TODO: re-enable once MCP client rendering is working
|
||||
# meta={"ui/resourceUri": "ui://basic-memory/search-results"},
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def search_notes(
|
||||
query: str,
|
||||
@@ -514,6 +515,7 @@ async def search_notes(
|
||||
|
||||
@mcp.tool(
|
||||
description="Search entities by structured frontmatter metadata.",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def search_by_metadata(
|
||||
filters: Dict[str, Any],
|
||||
|
||||
@@ -20,6 +20,7 @@ def _text_block(message: str) -> List[ContentBlock]:
|
||||
@mcp.tool(
|
||||
description="Search notes and return an embedded MCP-UI resource (raw HTML).",
|
||||
output_schema=None,
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def search_notes_ui(
|
||||
query: str,
|
||||
@@ -36,7 +37,7 @@ async def search_notes_ui(
|
||||
context: Context | None = None,
|
||||
) -> List[ContentBlock]:
|
||||
"""Return a search results UI as an embedded MCP-UI resource."""
|
||||
result = await search_notes.fn(
|
||||
result = await search_notes(
|
||||
query=query,
|
||||
project=project,
|
||||
page=page,
|
||||
@@ -82,6 +83,7 @@ async def search_notes_ui(
|
||||
@mcp.tool(
|
||||
description="Read a note and return an embedded MCP-UI resource (raw HTML).",
|
||||
output_schema=None,
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def read_note_ui(
|
||||
identifier: str,
|
||||
@@ -91,7 +93,7 @@ async def read_note_ui(
|
||||
context: Context | None = None,
|
||||
) -> List[ContentBlock]:
|
||||
"""Return a note preview UI as an embedded MCP-UI resource."""
|
||||
content = await read_note.fn(
|
||||
content = await read_note(
|
||||
identifier=identifier,
|
||||
project=project,
|
||||
page=page,
|
||||
|
||||
@@ -12,6 +12,7 @@ from basic_memory.mcp.tools.read_note import read_note
|
||||
|
||||
@mcp.tool(
|
||||
description="View a note as a formatted artifact for better readability.",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def view_note(
|
||||
identifier: str,
|
||||
@@ -57,14 +58,16 @@ async def view_note(
|
||||
"""
|
||||
logger.info(f"Viewing note: {identifier} in project: {project}")
|
||||
|
||||
# Call the existing read_note logic
|
||||
content = await read_note.fn(
|
||||
identifier=identifier,
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
context=context,
|
||||
# Call the existing read_note logic (default output_format="text" returns str)
|
||||
content = str(
|
||||
await read_note(
|
||||
identifier=identifier,
|
||||
project=project,
|
||||
workspace=workspace,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
context=context,
|
||||
)
|
||||
)
|
||||
|
||||
# Check if this is an error message (note not found)
|
||||
|
||||
@@ -6,7 +6,10 @@ from basic_memory.mcp.project_context import get_available_workspaces
|
||||
from basic_memory.mcp.server import mcp
|
||||
|
||||
|
||||
@mcp.tool(description="List available cloud workspaces (tenant_id, type, role, and name).")
|
||||
@mcp.tool(
|
||||
description="List available cloud workspaces (tenant_id, type, role, and name).",
|
||||
annotations={"readOnlyHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def list_workspaces(context: Context | None = None) -> str:
|
||||
"""List workspaces available to the current cloud user."""
|
||||
workspaces = await get_available_workspaces(context=context)
|
||||
|
||||
@@ -16,6 +16,7 @@ TagType = Union[List[str], str, None]
|
||||
|
||||
@mcp.tool(
|
||||
description="Create or update a markdown note. Returns a markdown formatted summary of the semantic content.",
|
||||
annotations={"destructiveHint": False, "idempotentHint": True, "openWorldHint": False},
|
||||
)
|
||||
async def write_note(
|
||||
title: str,
|
||||
|
||||
@@ -96,14 +96,13 @@ def test_write_note_json_output(mock_write_json, mock_config_cls):
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@patch(
|
||||
"basic_memory.cli.commands.tool.mcp_write_note",
|
||||
new_callable=AsyncMock,
|
||||
return_value="Created note: Test Note",
|
||||
)
|
||||
def test_write_note_text_output(mock_mcp_write, mock_config_cls):
|
||||
"""write-note with default text format uses the MCP tool path."""
|
||||
mock_config_cls.return_value = _mock_config_manager()
|
||||
|
||||
# MCP tool .fn returns a formatted string
|
||||
mock_mcp_write.fn = AsyncMock(return_value="Created note: Test Note")
|
||||
|
||||
result = runner.invoke(
|
||||
cli_app,
|
||||
[
|
||||
@@ -120,7 +119,7 @@ def test_write_note_text_output(mock_mcp_write, mock_config_cls):
|
||||
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
assert "Created note: Test Note" in result.output
|
||||
mock_mcp_write.fn.assert_called_once()
|
||||
mock_mcp_write.assert_called_once()
|
||||
|
||||
|
||||
# --- read-note --format json ---
|
||||
@@ -156,15 +155,13 @@ def test_read_note_json_output(mock_read_json, mock_config_cls):
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@patch(
|
||||
"basic_memory.cli.commands.tool.mcp_read_note",
|
||||
new_callable=AsyncMock,
|
||||
return_value="---\ntitle: Test Note\n---\n# Test Note\n\nhello world",
|
||||
)
|
||||
def test_read_note_text_output(mock_mcp_read, mock_config_cls):
|
||||
"""read-note with default text format uses the MCP tool path."""
|
||||
mock_config_cls.return_value = _mock_config_manager()
|
||||
|
||||
mock_mcp_read.fn = AsyncMock(
|
||||
return_value="---\ntitle: Test Note\n---\n# Test Note\n\nhello world"
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
cli_app,
|
||||
["tool", "read-note", "test-note"],
|
||||
@@ -172,15 +169,18 @@ def test_read_note_text_output(mock_mcp_read, mock_config_cls):
|
||||
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
assert "---" in result.output
|
||||
mock_mcp_read.fn.assert_called_once()
|
||||
mock_mcp_read.assert_called_once()
|
||||
|
||||
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@patch("basic_memory.cli.commands.tool.mcp_read_note")
|
||||
@patch(
|
||||
"basic_memory.cli.commands.tool.mcp_read_note",
|
||||
new_callable=AsyncMock,
|
||||
return_value="# Test Note",
|
||||
)
|
||||
def test_read_note_workspace_passthrough(mock_mcp_read, mock_config_cls):
|
||||
"""read-note --workspace passes workspace through to the MCP tool call."""
|
||||
mock_config_cls.return_value = _mock_config_manager()
|
||||
mock_mcp_read.fn = AsyncMock(return_value="# Test Note")
|
||||
|
||||
result = runner.invoke(
|
||||
cli_app,
|
||||
@@ -188,8 +188,8 @@ def test_read_note_workspace_passthrough(mock_mcp_read, mock_config_cls):
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
mock_mcp_read.fn.assert_called_once()
|
||||
assert mock_mcp_read.fn.call_args.kwargs["workspace"] == "tenant-123"
|
||||
mock_mcp_read.assert_called_once()
|
||||
assert mock_mcp_read.call_args.kwargs["workspace"] == "tenant-123"
|
||||
|
||||
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@@ -220,15 +220,13 @@ def test_read_note_json_strip_frontmatter(mock_read_json, mock_config_cls):
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@patch(
|
||||
"basic_memory.cli.commands.tool.mcp_read_note",
|
||||
new_callable=AsyncMock,
|
||||
return_value="---\ntitle: Test Note\n---\n# Test Note\n\nhello world",
|
||||
)
|
||||
def test_read_note_text_strip_frontmatter(mock_mcp_read, mock_config_cls):
|
||||
"""read-note --strip-frontmatter strips opening frontmatter in text mode."""
|
||||
mock_config_cls.return_value = _mock_config_manager()
|
||||
|
||||
mock_mcp_read.fn = AsyncMock(
|
||||
return_value="---\ntitle: Test Note\n---\n# Test Note\n\nhello world"
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
cli_app,
|
||||
["tool", "read-note", "test-note", "--strip-frontmatter"],
|
||||
@@ -237,19 +235,19 @@ def test_read_note_text_strip_frontmatter(mock_mcp_read, mock_config_cls):
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
assert "---" not in result.output
|
||||
assert "# Test Note" in result.output
|
||||
mock_mcp_read.fn.assert_called_once()
|
||||
mock_mcp_read.assert_called_once()
|
||||
|
||||
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@patch(
|
||||
"basic_memory.cli.commands.tool.mcp_read_note",
|
||||
new_callable=AsyncMock,
|
||||
return_value="# Test Note\n\nhello world",
|
||||
)
|
||||
def test_read_note_text_strip_frontmatter_no_frontmatter(mock_mcp_read, mock_config_cls):
|
||||
"""read-note --strip-frontmatter keeps notes unchanged when no frontmatter exists."""
|
||||
mock_config_cls.return_value = _mock_config_manager()
|
||||
|
||||
mock_mcp_read.fn = AsyncMock(return_value="# Test Note\n\nhello world")
|
||||
|
||||
result = runner.invoke(
|
||||
cli_app,
|
||||
["tool", "read-note", "test-note", "--strip-frontmatter"],
|
||||
@@ -257,7 +255,7 @@ def test_read_note_text_strip_frontmatter_no_frontmatter(mock_mcp_read, mock_con
|
||||
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
assert result.output.strip() == "# Test Note\n\nhello world"
|
||||
mock_mcp_read.fn.assert_called_once()
|
||||
mock_mcp_read.assert_called_once()
|
||||
|
||||
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@@ -343,11 +341,11 @@ def test_recent_activity_json_output(mock_recent_json):
|
||||
|
||||
@patch(
|
||||
"basic_memory.cli.commands.tool.mcp_recent_activity",
|
||||
new_callable=AsyncMock,
|
||||
return_value="Recent activity:\n- Note A\n- Note B",
|
||||
)
|
||||
def test_recent_activity_text_output(mock_mcp_recent):
|
||||
"""recent-activity with default text format uses the MCP tool path."""
|
||||
mock_mcp_recent.fn = AsyncMock(return_value="Recent activity:\n- Note A\n- Note B")
|
||||
|
||||
result = runner.invoke(
|
||||
cli_app,
|
||||
["tool", "recent-activity"],
|
||||
@@ -355,7 +353,7 @@ def test_recent_activity_text_output(mock_mcp_recent):
|
||||
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
assert "Recent activity:" in result.output
|
||||
mock_mcp_recent.fn.assert_called_once()
|
||||
mock_mcp_recent.assert_called_once()
|
||||
|
||||
|
||||
# --- read-note title fallback ---
|
||||
@@ -413,21 +411,20 @@ def test_recent_activity_json_pagination(mock_recent_json):
|
||||
|
||||
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@patch("basic_memory.cli.commands.tool.mcp_build_context")
|
||||
@patch(
|
||||
"basic_memory.cli.commands.tool.mcp_build_context",
|
||||
new_callable=AsyncMock,
|
||||
return_value={
|
||||
"results": [],
|
||||
"metadata": {"uri": "test/topic", "depth": 1},
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
},
|
||||
)
|
||||
def test_build_context_format_json(mock_build_ctx, mock_config_cls):
|
||||
"""build-context --format json outputs valid JSON."""
|
||||
mock_config_cls.return_value = _mock_config_manager()
|
||||
|
||||
# build_context now returns a slimmed dict directly
|
||||
mock_build_ctx.fn = AsyncMock(
|
||||
return_value={
|
||||
"results": [],
|
||||
"metadata": {"uri": "test/topic", "depth": 1},
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
}
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
cli_app,
|
||||
["tool", "build-context", "memory://test/topic", "--format", "json"],
|
||||
@@ -436,25 +433,24 @@ def test_build_context_format_json(mock_build_ctx, mock_config_cls):
|
||||
assert result.exit_code == 0, f"CLI failed: {result.output}"
|
||||
data = json.loads(result.output)
|
||||
assert "results" in data
|
||||
mock_build_ctx.fn.assert_called_once()
|
||||
mock_build_ctx.assert_called_once()
|
||||
|
||||
|
||||
@patch("basic_memory.cli.commands.tool.ConfigManager")
|
||||
@patch("basic_memory.cli.commands.tool.mcp_build_context")
|
||||
@patch(
|
||||
"basic_memory.cli.commands.tool.mcp_build_context",
|
||||
new_callable=AsyncMock,
|
||||
return_value={
|
||||
"results": [],
|
||||
"metadata": {"uri": "test/topic", "depth": 1},
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
},
|
||||
)
|
||||
def test_build_context_default_format_is_json(mock_build_ctx, mock_config_cls):
|
||||
"""build-context defaults to JSON output (backward compatible)."""
|
||||
mock_config_cls.return_value = _mock_config_manager()
|
||||
|
||||
# build_context now returns a slimmed dict directly
|
||||
mock_build_ctx.fn = AsyncMock(
|
||||
return_value={
|
||||
"results": [],
|
||||
"metadata": {"uri": "test/topic", "depth": 1},
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
}
|
||||
)
|
||||
|
||||
result = runner.invoke(
|
||||
cli_app,
|
||||
["tool", "build-context", "memory://test/topic"],
|
||||
|
||||
@@ -9,7 +9,7 @@ from basic_memory.mcp.tools import write_note
|
||||
async def test_write_note_tags_yaml_format(app, project_config, test_project):
|
||||
"""Test that write_note creates files with proper YAML list format for tags."""
|
||||
# Create a note with tags using write_note
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="YAML Format Test",
|
||||
directory="test",
|
||||
@@ -41,7 +41,7 @@ async def test_write_note_tags_yaml_format(app, project_config, test_project):
|
||||
async def test_write_note_stringified_json_tags(app, project_config, test_project):
|
||||
"""Test that stringified JSON arrays are handled correctly."""
|
||||
# This simulates the issue where AI assistants pass tags as stringified JSON
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Stringified JSON Test",
|
||||
directory="test",
|
||||
@@ -71,7 +71,7 @@ async def test_write_note_stringified_json_tags(app, project_config, test_projec
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_single_tag_yaml_format(app, project_config, test_project):
|
||||
"""Test that single tags are still formatted as YAML lists."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Single Tag Test",
|
||||
directory="test",
|
||||
@@ -90,7 +90,7 @@ async def test_write_note_single_tag_yaml_format(app, project_config, test_proje
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_no_tags(app, project_config, test_project):
|
||||
"""Test that notes without tags work normally."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="No Tags Test",
|
||||
directory="test",
|
||||
@@ -109,7 +109,7 @@ async def test_write_note_no_tags(app, project_config, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_empty_tags_list(app, project_config, test_project):
|
||||
"""Test that empty tag lists are handled properly."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Empty Tags Test",
|
||||
directory="test",
|
||||
@@ -128,7 +128,7 @@ async def test_write_note_empty_tags_list(app, project_config, test_project):
|
||||
async def test_write_note_update_preserves_yaml_format(app, project_config, test_project):
|
||||
"""Test that updating a note preserves the YAML list format."""
|
||||
# First, create the note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Update Format Test",
|
||||
directory="test",
|
||||
@@ -137,7 +137,7 @@ async def test_write_note_update_preserves_yaml_format(app, project_config, test
|
||||
)
|
||||
|
||||
# Then update it with new tags
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Update Format Test",
|
||||
directory="test",
|
||||
@@ -170,7 +170,7 @@ async def test_write_note_update_preserves_yaml_format(app, project_config, test
|
||||
@pytest.mark.asyncio
|
||||
async def test_complex_tags_yaml_format(app, project_config, test_project):
|
||||
"""Test that complex tags with special characters format correctly."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Complex Tags Test",
|
||||
directory="test",
|
||||
|
||||
@@ -62,7 +62,7 @@ async def test_permalink_collision_should_not_overwrite_different_file(app, test
|
||||
- Data loss occurs without user warning
|
||||
"""
|
||||
# Step 1: Create first note "Node A"
|
||||
result_a = await write_note.fn(
|
||||
result_a = await write_note(
|
||||
project=test_project.name,
|
||||
title="Node A",
|
||||
directory="edge-cases",
|
||||
@@ -74,12 +74,12 @@ async def test_permalink_collision_should_not_overwrite_different_file(app, test
|
||||
assert f"permalink: {test_project.name}/edge-cases/node-a" in result_a
|
||||
|
||||
# Verify Node A content via read
|
||||
content_a = await read_note.fn("edge-cases/node-a", project=test_project.name)
|
||||
content_a = await read_note("edge-cases/node-a", project=test_project.name)
|
||||
assert "Node A" in content_a
|
||||
assert "Original content for Node A" in content_a
|
||||
|
||||
# Step 2: Create second note "Node B" (should be independent)
|
||||
result_b = await write_note.fn(
|
||||
result_b = await write_note(
|
||||
project=test_project.name,
|
||||
title="Node B",
|
||||
directory="edge-cases",
|
||||
@@ -91,7 +91,7 @@ async def test_permalink_collision_should_not_overwrite_different_file(app, test
|
||||
assert f"permalink: {test_project.name}/edge-cases/node-b" in result_b
|
||||
|
||||
# Step 3: Create third note "Node C" (this is where the bug occurs)
|
||||
result_c = await write_note.fn(
|
||||
result_c = await write_note(
|
||||
project=test_project.name,
|
||||
title="Node C",
|
||||
directory="edge-cases",
|
||||
@@ -104,7 +104,7 @@ async def test_permalink_collision_should_not_overwrite_different_file(app, test
|
||||
|
||||
# CRITICAL CHECK: Verify Node A still has its original content
|
||||
# This is where the bug manifests - Node A.md gets overwritten with Node C content
|
||||
content_a_after = await read_note.fn("edge-cases/node-a", project=test_project.name)
|
||||
content_a_after = await read_note("edge-cases/node-a", project=test_project.name)
|
||||
assert "Node A" in content_a_after, "Node A title should still be 'Node A'"
|
||||
assert "Original content for Node A" in content_a_after, (
|
||||
"Node A file should NOT be overwritten by Node C creation"
|
||||
@@ -112,7 +112,7 @@ async def test_permalink_collision_should_not_overwrite_different_file(app, test
|
||||
assert "Content for Node C" not in content_a_after, "Node A should NOT contain Node C's content"
|
||||
|
||||
# Verify Node C has its own content
|
||||
content_c = await read_note.fn("edge-cases/node-c", project=test_project.name)
|
||||
content_c = await read_note("edge-cases/node-c", project=test_project.name)
|
||||
assert "Node C" in content_c
|
||||
assert "Content for Node C" in content_c
|
||||
assert "Original content for Node A" not in content_c, (
|
||||
@@ -163,7 +163,7 @@ async def test_notes_with_similar_titles_maintain_separate_files(app, test_proje
|
||||
created_permalinks = []
|
||||
|
||||
for title, folder in titles_and_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title=title,
|
||||
directory=folder,
|
||||
@@ -179,7 +179,7 @@ async def test_notes_with_similar_titles_maintain_separate_files(app, test_proje
|
||||
break
|
||||
|
||||
# Verify each note can be read back with its own content
|
||||
content = await read_note.fn(permalink, project=test_project.name)
|
||||
content = await read_note(permalink, project=test_project.name)
|
||||
assert f"Unique content for {title}" in content, (
|
||||
f"Note with title '{title}' should maintain its unique content"
|
||||
)
|
||||
@@ -208,7 +208,7 @@ async def test_sequential_note_creation_preserves_all_files(app, test_project):
|
||||
|
||||
# Create all notes
|
||||
for title, content in notes_data:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title=title,
|
||||
directory="sequence-test",
|
||||
@@ -220,7 +220,7 @@ async def test_sequential_note_creation_preserves_all_files(app, test_project):
|
||||
for title, expected_content in notes_data:
|
||||
# Normalize title to permalink format
|
||||
permalink = f"sequence-test/{title.lower()}"
|
||||
content = await read_note.fn(permalink, project=test_project.name)
|
||||
content = await read_note(permalink, project=test_project.name)
|
||||
|
||||
assert title in content, f"Note '{title}' should still have its title"
|
||||
assert expected_content.split("\n\n")[1] in content, (
|
||||
|
||||
@@ -15,10 +15,10 @@ class _ContextState:
|
||||
def __init__(self):
|
||||
self._state: dict[str, object] = {}
|
||||
|
||||
def get_state(self, key: str):
|
||||
async def get_state(self, key: str):
|
||||
return self._state.get(key)
|
||||
|
||||
def set_state(self, key: str, value: object) -> None:
|
||||
async def set_state(self, key: str, value: object, **kwargs) -> None:
|
||||
self._state[key] = value
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ async def test_workspace_auto_selects_single_and_caches(monkeypatch):
|
||||
|
||||
resolved = await resolve_workspace_parameter(context=context)
|
||||
assert resolved.tenant_id == only_workspace.tenant_id
|
||||
assert context.get_state("active_workspace") == only_workspace
|
||||
assert await context.get_state("active_workspace") == only_workspace.model_dump()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -251,7 +251,7 @@ async def test_workspace_uses_cached_workspace_without_fetch(monkeypatch):
|
||||
role="owner",
|
||||
)
|
||||
context = _ContextState()
|
||||
context.set_state("active_workspace", cached_workspace)
|
||||
await context.set_state("active_workspace", cached_workspace.model_dump())
|
||||
|
||||
async def fail_if_called(context=None): # pragma: no cover
|
||||
raise AssertionError("Workspace fetch should not run when cache is available")
|
||||
|
||||
+10
-10
@@ -15,7 +15,7 @@ async def test_continue_conversation_with_topic(client, test_graph):
|
||||
# We can use the test_graph fixture which already has relevant content
|
||||
|
||||
# Call the function with a topic that should match existing content
|
||||
result = await continue_conversation.fn(topic="Root", timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await continue_conversation(topic="Root", timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check that the result contains expected content
|
||||
assert "Continuing conversation on: Root" in result # pyright: ignore [reportOperatorIssue]
|
||||
@@ -27,7 +27,7 @@ async def test_continue_conversation_with_topic(client, test_graph):
|
||||
async def test_continue_conversation_with_recent_activity(client, test_graph):
|
||||
"""Test continue_conversation with no topic, using recent activity."""
|
||||
# Call the function without a topic
|
||||
result = await continue_conversation.fn(timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await continue_conversation(timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check that the result contains expected content for recent activity
|
||||
assert "Continuing conversation on: Recent Activity" in result # pyright: ignore [reportOperatorIssue]
|
||||
@@ -40,7 +40,7 @@ async def test_continue_conversation_with_recent_activity(client, test_graph):
|
||||
async def test_continue_conversation_no_results(client):
|
||||
"""Test continue_conversation when no results are found."""
|
||||
# Call with a non-existent topic
|
||||
result = await continue_conversation.fn(topic="NonExistentTopic", timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await continue_conversation(topic="NonExistentTopic", timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check the response indicates no results found
|
||||
assert "Continuing conversation on: NonExistentTopic" in result # pyright: ignore [reportOperatorIssue]
|
||||
@@ -51,7 +51,7 @@ async def test_continue_conversation_no_results(client):
|
||||
async def test_continue_conversation_creates_structured_suggestions(client, test_graph):
|
||||
"""Test that continue_conversation generates structured tool usage suggestions."""
|
||||
# Call the function with a topic that should match existing content
|
||||
result = await continue_conversation.fn(topic="Root", timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await continue_conversation(topic="Root", timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Verify the response includes clear tool usage instructions
|
||||
assert "start by executing one of the suggested commands" in result.lower() # pyright: ignore [reportAttributeAccessIssue]
|
||||
@@ -69,7 +69,7 @@ async def test_continue_conversation_creates_structured_suggestions(client, test
|
||||
async def test_search_prompt_with_results(client, test_graph):
|
||||
"""Test search_prompt with a query that returns results."""
|
||||
# Call the function with a query that should match existing content
|
||||
result = await search_prompt.fn("Root") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await search_prompt("Root") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check the response contains expected content
|
||||
assert 'Search Results for: "Root"' in result # pyright: ignore [reportOperatorIssue]
|
||||
@@ -82,7 +82,7 @@ async def test_search_prompt_with_results(client, test_graph):
|
||||
async def test_search_prompt_with_timeframe(client, test_graph):
|
||||
"""Test search_prompt with a timeframe."""
|
||||
# Call the function with a query and timeframe
|
||||
result = await search_prompt.fn("Root", timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await search_prompt("Root", timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check the response includes timeframe information
|
||||
assert 'Search Results for: "Root" (after 7d)' in result # pyright: ignore [reportOperatorIssue]
|
||||
@@ -93,7 +93,7 @@ async def test_search_prompt_with_timeframe(client, test_graph):
|
||||
async def test_search_prompt_no_results(client):
|
||||
"""Test search_prompt when no results are found."""
|
||||
# Call with a query that won't match anything
|
||||
result = await search_prompt.fn("XYZ123NonExistentQuery") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await search_prompt("XYZ123NonExistentQuery") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check the response indicates no results found
|
||||
assert 'Search Results for: "XYZ123NonExistentQuery"' in result # pyright: ignore [reportOperatorIssue]
|
||||
@@ -151,7 +151,7 @@ def test_prompt_context_with_file_path_no_permalink():
|
||||
async def test_recent_activity_prompt_discovery_mode(client, test_project, test_graph):
|
||||
"""Test recent_activity_prompt in discovery mode (no project)."""
|
||||
# Call the function in discovery mode
|
||||
result = await recent_activity_prompt.fn(timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await recent_activity_prompt(timeframe="1w") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check the response contains expected discovery mode content
|
||||
assert "Recent Activity Context" in result # pyright: ignore [reportOperatorIssue]
|
||||
@@ -164,7 +164,7 @@ async def test_recent_activity_prompt_discovery_mode(client, test_project, test_
|
||||
async def test_recent_activity_prompt_project_specific(client, test_project, test_graph):
|
||||
"""Test recent_activity_prompt in project-specific mode."""
|
||||
# Call the function with a specific project
|
||||
result = await recent_activity_prompt.fn(timeframe="1w", project=test_project.name) # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await recent_activity_prompt(timeframe="1w", project=test_project.name) # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check the response contains expected project-specific content
|
||||
assert "Recent Activity Context" in result # pyright: ignore [reportOperatorIssue]
|
||||
@@ -177,7 +177,7 @@ async def test_recent_activity_prompt_project_specific(client, test_project, tes
|
||||
async def test_recent_activity_prompt_with_custom_timeframe(client, test_project, test_graph):
|
||||
"""Test recent_activity_prompt with custom timeframe."""
|
||||
# Call the function with a custom timeframe in discovery mode
|
||||
result = await recent_activity_prompt.fn(timeframe="1d") # pyright: ignore [reportGeneralTypeIssues]
|
||||
result = await recent_activity_prompt(timeframe="1d") # pyright: ignore [reportGeneralTypeIssues]
|
||||
|
||||
# Check the response includes the custom timeframe
|
||||
assert "1d" in result # pyright: ignore [reportOperatorIssue]
|
||||
|
||||
@@ -28,9 +28,9 @@ async def test_recent_activity_prompt_discovery_mode(monkeypatch):
|
||||
async def fake_fn(**_kwargs):
|
||||
return tool_output
|
||||
|
||||
monkeypatch.setattr("basic_memory.mcp.prompts.recent_activity.recent_activity.fn", fake_fn)
|
||||
monkeypatch.setattr("basic_memory.mcp.prompts.recent_activity.recent_activity", fake_fn)
|
||||
|
||||
out = await recent_activity_prompt.fn(timeframe="7d", project=None) # pyright: ignore[reportGeneralTypeIssues]
|
||||
out = await recent_activity_prompt(timeframe="7d", project=None) # pyright: ignore[reportGeneralTypeIssues]
|
||||
|
||||
# Should contain the tool output
|
||||
assert "Recent Activity Summary (7d)" in out
|
||||
@@ -59,9 +59,9 @@ async def test_recent_activity_prompt_project_mode(monkeypatch):
|
||||
async def fake_fn(**_kwargs):
|
||||
return tool_output
|
||||
|
||||
monkeypatch.setattr("basic_memory.mcp.prompts.recent_activity.recent_activity.fn", fake_fn)
|
||||
monkeypatch.setattr("basic_memory.mcp.prompts.recent_activity.recent_activity", fake_fn)
|
||||
|
||||
out = await recent_activity_prompt.fn(timeframe="1d", project="my-project") # pyright: ignore[reportGeneralTypeIssues]
|
||||
out = await recent_activity_prompt(timeframe="1d", project="my-project") # pyright: ignore[reportGeneralTypeIssues]
|
||||
|
||||
# Should contain the tool output
|
||||
assert "Recent Activity: my-project" in out
|
||||
@@ -82,9 +82,9 @@ async def test_recent_activity_prompt_passes_correct_params(monkeypatch):
|
||||
captured_kwargs.update(kwargs)
|
||||
return "## Recent Activity"
|
||||
|
||||
monkeypatch.setattr("basic_memory.mcp.prompts.recent_activity.recent_activity.fn", fake_fn)
|
||||
monkeypatch.setattr("basic_memory.mcp.prompts.recent_activity.recent_activity", fake_fn)
|
||||
|
||||
await recent_activity_prompt.fn(timeframe="2d", project="test-proj") # pyright: ignore[reportGeneralTypeIssues]
|
||||
await recent_activity_prompt(timeframe="2d", project="test-proj") # pyright: ignore[reportGeneralTypeIssues]
|
||||
|
||||
assert captured_kwargs["timeframe"] == "2d"
|
||||
assert captured_kwargs["project"] == "test-proj"
|
||||
@@ -100,9 +100,9 @@ async def test_recent_activity_prompt_defaults_timeframe(monkeypatch):
|
||||
captured_kwargs.update(kwargs)
|
||||
return "## Recent Activity"
|
||||
|
||||
monkeypatch.setattr("basic_memory.mcp.prompts.recent_activity.recent_activity.fn", fake_fn)
|
||||
monkeypatch.setattr("basic_memory.mcp.prompts.recent_activity.recent_activity", fake_fn)
|
||||
|
||||
await recent_activity_prompt.fn(timeframe=None, project=None) # pyright: ignore[reportGeneralTypeIssues]
|
||||
await recent_activity_prompt(timeframe=None, project=None) # pyright: ignore[reportGeneralTypeIssues]
|
||||
|
||||
assert captured_kwargs["timeframe"] == "7d"
|
||||
assert captured_kwargs["project"] is None
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest
|
||||
async def test_ai_assistant_guide_exists(app):
|
||||
"""Test that the canvas spec resource exists and returns content."""
|
||||
# Call the resource function
|
||||
guide = ai_assistant_guide.fn()
|
||||
guide = ai_assistant_guide()
|
||||
|
||||
# Verify basic characteristics of the content
|
||||
assert guide is not None
|
||||
|
||||
@@ -10,7 +10,7 @@ from basic_memory.mcp.tools import build_context
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_basic_discussion_context(client, test_graph, test_project):
|
||||
"""Test getting basic discussion context returns slimmed JSON dict."""
|
||||
result = await build_context.fn(project=test_project.name, url="memory://test/root")
|
||||
result = await build_context(project=test_project.name, url="memory://test/root")
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert len(result["results"]) == 1
|
||||
@@ -43,7 +43,7 @@ async def test_get_basic_discussion_context(client, test_graph, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_discussion_context_pattern(client, test_graph, test_project):
|
||||
"""Test getting context with pattern matching."""
|
||||
result = await build_context.fn(project=test_project.name, url="memory://test/*", depth=1)
|
||||
result = await build_context(project=test_project.name, url="memory://test/*", depth=1)
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert len(result["results"]) > 1 # Should match multiple test/* paths
|
||||
@@ -58,14 +58,14 @@ async def test_get_discussion_context_pattern(client, test_graph, test_project):
|
||||
async def test_get_discussion_context_timeframe(client, test_graph, test_project):
|
||||
"""Test timeframe parameter filtering."""
|
||||
# Get recent context
|
||||
recent = await build_context.fn(
|
||||
recent = await build_context(
|
||||
project=test_project.name,
|
||||
url="memory://test/root",
|
||||
timeframe="1d",
|
||||
)
|
||||
|
||||
# Get older context
|
||||
older = await build_context.fn(
|
||||
older = await build_context(
|
||||
project=test_project.name,
|
||||
url="memory://test/root",
|
||||
timeframe="30d",
|
||||
@@ -85,7 +85,7 @@ async def test_get_discussion_context_timeframe(client, test_graph, test_project
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_discussion_context_not_found(client, test_project):
|
||||
"""Test handling of non-existent URIs."""
|
||||
result = await build_context.fn(project=test_project.name, url="memory://test/does-not-exist")
|
||||
result = await build_context(project=test_project.name, url="memory://test/does-not-exist")
|
||||
|
||||
assert isinstance(result, dict)
|
||||
assert len(result["results"]) == 0
|
||||
@@ -114,7 +114,7 @@ async def test_build_context_timeframe_formats(client, test_graph, test_project)
|
||||
# Test each valid timeframe
|
||||
for timeframe in valid_timeframes:
|
||||
try:
|
||||
result = await build_context.fn(
|
||||
result = await build_context(
|
||||
project=test_project.name,
|
||||
url=test_url,
|
||||
timeframe=timeframe,
|
||||
@@ -129,7 +129,7 @@ async def test_build_context_timeframe_formats(client, test_graph, test_project)
|
||||
# Test invalid timeframes should raise ValidationError
|
||||
for timeframe in invalid_timeframes:
|
||||
with pytest.raises(ToolError):
|
||||
await build_context.fn(project=test_project.name, url=test_url, timeframe=timeframe)
|
||||
await build_context(project=test_project.name, url=test_url, timeframe=timeframe)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -139,7 +139,7 @@ async def test_build_context_string_depth_parameter(client, test_graph, test_pro
|
||||
|
||||
# Test valid string depth parameter — should convert to int
|
||||
try:
|
||||
result = await build_context.fn(url=test_url, depth="2", project=test_project.name)
|
||||
result = await build_context(url=test_url, depth="2", project=test_project.name)
|
||||
assert isinstance(result["metadata"]["depth"], int)
|
||||
assert result["metadata"]["depth"] == 2
|
||||
except ToolError:
|
||||
@@ -148,13 +148,13 @@ async def test_build_context_string_depth_parameter(client, test_graph, test_pro
|
||||
|
||||
# Test invalid string depth parameter - should raise ToolError
|
||||
with pytest.raises(ToolError):
|
||||
await build_context.fn(test_url, depth="invalid", project=test_project.name)
|
||||
await build_context(test_url, depth="invalid", project=test_project.name)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_text_format(client, test_graph, test_project):
|
||||
"""Test that output_format='text' returns compact text."""
|
||||
result = await build_context.fn(
|
||||
result = await build_context(
|
||||
project=test_project.name,
|
||||
url="memory://test/root",
|
||||
output_format="text",
|
||||
@@ -173,7 +173,7 @@ async def test_build_context_text_format(client, test_graph, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_markdown_pattern(client, test_graph, test_project):
|
||||
"""Test markdown format with pattern matching (multiple results)."""
|
||||
result = await build_context.fn(
|
||||
result = await build_context(
|
||||
project=test_project.name,
|
||||
url="memory://test/*",
|
||||
output_format="text",
|
||||
@@ -190,7 +190,7 @@ async def test_build_context_markdown_pattern(client, test_graph, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_markdown_not_found(client, test_project):
|
||||
"""Test markdown format for non-existent URIs."""
|
||||
result = await build_context.fn(
|
||||
result = await build_context(
|
||||
project=test_project.name,
|
||||
url="memory://test/does-not-exist",
|
||||
output_format="text",
|
||||
|
||||
@@ -34,7 +34,7 @@ async def test_create_canvas(app, project_config, test_project):
|
||||
folder = "visualizations"
|
||||
|
||||
# Execute
|
||||
result = await canvas.fn(
|
||||
result = await canvas(
|
||||
project=test_project.name, nodes=nodes, edges=edges, title=title, directory=folder
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ async def test_create_canvas_with_extension(app, project_config, test_project):
|
||||
folder = "visualizations"
|
||||
|
||||
# Execute
|
||||
result = await canvas.fn(
|
||||
result = await canvas(
|
||||
project=test_project.name, nodes=nodes, edges=edges, title=title, directory=folder
|
||||
)
|
||||
|
||||
@@ -109,9 +109,7 @@ async def test_update_existing_canvas(app, project_config, test_project):
|
||||
folder = "visualizations"
|
||||
|
||||
# Create initial canvas
|
||||
await canvas.fn(
|
||||
project=test_project.name, nodes=nodes, edges=edges, title=title, directory=folder
|
||||
)
|
||||
await canvas(project=test_project.name, nodes=nodes, edges=edges, title=title, directory=folder)
|
||||
|
||||
# Verify file exists
|
||||
file_path = Path(project_config.home) / folder / f"{title}.canvas"
|
||||
@@ -134,7 +132,7 @@ async def test_update_existing_canvas(app, project_config, test_project):
|
||||
]
|
||||
|
||||
# Execute update
|
||||
result = await canvas.fn(
|
||||
result = await canvas(
|
||||
project=test_project.name,
|
||||
nodes=updated_nodes,
|
||||
edges=updated_edges,
|
||||
@@ -171,7 +169,7 @@ async def test_create_canvas_with_nested_folders(app, project_config, test_proje
|
||||
folder = "visualizations/nested/folders" # Deep path
|
||||
|
||||
# Execute
|
||||
result = await canvas.fn(
|
||||
result = await canvas(
|
||||
project=test_project.name, nodes=nodes, edges=edges, title=title, directory=folder
|
||||
)
|
||||
|
||||
@@ -256,7 +254,7 @@ async def test_create_canvas_complex_content(app, project_config, test_project):
|
||||
test_file_path.write_text("# Test File\nThis is referenced by the canvas")
|
||||
|
||||
# Execute
|
||||
result = await canvas.fn(
|
||||
result = await canvas(
|
||||
project=test_project.name, nodes=nodes, edges=edges, title=title, directory=folder
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from basic_memory.mcp.tools import cloud_info, release_notes
|
||||
|
||||
|
||||
def test_cloud_info_tool_returns_expected_copy():
|
||||
result = cloud_info.fn()
|
||||
result = cloud_info()
|
||||
|
||||
assert "# Basic Memory Cloud (optional)" in result
|
||||
assert "{{OSS_DISCOUNT_CODE}}" in result
|
||||
@@ -12,7 +12,7 @@ def test_cloud_info_tool_returns_expected_copy():
|
||||
|
||||
|
||||
def test_release_notes_tool_returns_expected_copy():
|
||||
result = release_notes.fn()
|
||||
result = release_notes()
|
||||
|
||||
assert "# Release Notes" in result
|
||||
assert "2026-02-06" in result
|
||||
|
||||
@@ -134,9 +134,8 @@ TOOL_FUNCTIONS: dict[str, object] = {
|
||||
|
||||
|
||||
def _signature_params(tool_obj: object) -> list[str]:
|
||||
fn = tool_obj.fn
|
||||
params = []
|
||||
for param in inspect.signature(fn).parameters.values():
|
||||
for param in inspect.signature(tool_obj).parameters.values():
|
||||
if param.name == "context":
|
||||
continue
|
||||
params.append(param.name)
|
||||
|
||||
@@ -10,7 +10,7 @@ from basic_memory.mcp.tools.write_note import write_note
|
||||
async def test_edit_note_append_operation(client, test_project):
|
||||
"""Test appending content to an existing note."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -18,7 +18,7 @@ async def test_edit_note_append_operation(client, test_project):
|
||||
)
|
||||
|
||||
# Append content
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="test/test-note",
|
||||
operation="append",
|
||||
@@ -38,7 +38,7 @@ async def test_edit_note_append_operation(client, test_project):
|
||||
async def test_edit_note_prepend_operation(client, test_project):
|
||||
"""Test prepending content to an existing note."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Meeting Notes",
|
||||
directory="meetings",
|
||||
@@ -46,7 +46,7 @@ async def test_edit_note_prepend_operation(client, test_project):
|
||||
)
|
||||
|
||||
# Prepend content
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="meetings/meeting-notes",
|
||||
operation="prepend",
|
||||
@@ -66,7 +66,7 @@ async def test_edit_note_prepend_operation(client, test_project):
|
||||
async def test_edit_note_find_replace_operation(client, test_project):
|
||||
"""Test find and replace operation."""
|
||||
# Create initial note with version info
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Config Document",
|
||||
directory="config",
|
||||
@@ -74,7 +74,7 @@ async def test_edit_note_find_replace_operation(client, test_project):
|
||||
)
|
||||
|
||||
# Replace version - expecting 2 replacements
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="config/config-document",
|
||||
operation="find_replace",
|
||||
@@ -95,7 +95,7 @@ async def test_edit_note_find_replace_operation(client, test_project):
|
||||
async def test_edit_note_replace_section_operation(client, test_project):
|
||||
"""Test replacing content under a specific section."""
|
||||
# Create initial note with sections
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="API Specification",
|
||||
directory="specs",
|
||||
@@ -103,7 +103,7 @@ async def test_edit_note_replace_section_operation(client, test_project):
|
||||
)
|
||||
|
||||
# Replace implementation section
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="specs/api-specification",
|
||||
operation="replace_section",
|
||||
@@ -122,7 +122,7 @@ async def test_edit_note_replace_section_operation(client, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_note_nonexistent_note(client, test_project):
|
||||
"""Test editing a note that doesn't exist - should return helpful guidance."""
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="nonexistent/note",
|
||||
operation="append",
|
||||
@@ -139,7 +139,7 @@ async def test_edit_note_nonexistent_note(client, test_project):
|
||||
async def test_edit_note_invalid_operation(client, test_project):
|
||||
"""Test using an invalid operation."""
|
||||
# Create a note first
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -147,7 +147,7 @@ async def test_edit_note_invalid_operation(client, test_project):
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await edit_note.fn(
|
||||
await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="test/test-note",
|
||||
operation="invalid_op",
|
||||
@@ -161,7 +161,7 @@ async def test_edit_note_invalid_operation(client, test_project):
|
||||
async def test_edit_note_find_replace_missing_find_text(client, test_project):
|
||||
"""Test find_replace operation without find_text parameter."""
|
||||
# Create a note first
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -169,7 +169,7 @@ async def test_edit_note_find_replace_missing_find_text(client, test_project):
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await edit_note.fn(
|
||||
await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="test/test-note",
|
||||
operation="find_replace",
|
||||
@@ -183,7 +183,7 @@ async def test_edit_note_find_replace_missing_find_text(client, test_project):
|
||||
async def test_edit_note_replace_section_missing_section(client, test_project):
|
||||
"""Test replace_section operation without section parameter."""
|
||||
# Create a note first
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -191,7 +191,7 @@ async def test_edit_note_replace_section_missing_section(client, test_project):
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await edit_note.fn(
|
||||
await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="test/test-note",
|
||||
operation="replace_section",
|
||||
@@ -205,7 +205,7 @@ async def test_edit_note_replace_section_missing_section(client, test_project):
|
||||
async def test_edit_note_replace_section_nonexistent_section(client, test_project):
|
||||
"""Test replacing a section that doesn't exist - should append it."""
|
||||
# Create initial note without the target section
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Document",
|
||||
directory="docs",
|
||||
@@ -213,7 +213,7 @@ async def test_edit_note_replace_section_nonexistent_section(client, test_projec
|
||||
)
|
||||
|
||||
# Try to replace non-existent section
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="docs/document",
|
||||
operation="replace_section",
|
||||
@@ -233,7 +233,7 @@ async def test_edit_note_replace_section_nonexistent_section(client, test_projec
|
||||
async def test_edit_note_with_observations_and_relations(client, test_project):
|
||||
"""Test editing a note that contains observations and relations."""
|
||||
# Create note with semantic content
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Feature Spec",
|
||||
directory="features",
|
||||
@@ -241,7 +241,7 @@ async def test_edit_note_with_observations_and_relations(client, test_project):
|
||||
)
|
||||
|
||||
# Append more semantic content
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="features/feature-spec",
|
||||
operation="append",
|
||||
@@ -258,7 +258,7 @@ async def test_edit_note_with_observations_and_relations(client, test_project):
|
||||
async def test_edit_note_identifier_variations(client, test_project):
|
||||
"""Test that various identifier formats work."""
|
||||
# Create a note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Document",
|
||||
directory="docs",
|
||||
@@ -273,7 +273,7 @@ async def test_edit_note_identifier_variations(client, test_project):
|
||||
]
|
||||
|
||||
for identifier in identifiers_to_test:
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier=identifier,
|
||||
operation="append",
|
||||
@@ -290,7 +290,7 @@ async def test_edit_note_identifier_variations(client, test_project):
|
||||
async def test_edit_note_find_replace_no_matches(client, test_project):
|
||||
"""Test find_replace when the find_text doesn't exist - should return error."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -298,7 +298,7 @@ async def test_edit_note_find_replace_no_matches(client, test_project):
|
||||
)
|
||||
|
||||
# Try to replace text that doesn't exist - should fail with default expected_replacements=1
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="test/test-note",
|
||||
operation="find_replace",
|
||||
@@ -316,7 +316,7 @@ async def test_edit_note_find_replace_no_matches(client, test_project):
|
||||
async def test_edit_note_empty_content_operations(client, test_project):
|
||||
"""Test operations with empty content."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -324,7 +324,7 @@ async def test_edit_note_empty_content_operations(client, test_project):
|
||||
)
|
||||
|
||||
# Test append with empty content
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name, identifier="test/test-note", operation="append", content=""
|
||||
)
|
||||
|
||||
@@ -337,7 +337,7 @@ async def test_edit_note_empty_content_operations(client, test_project):
|
||||
async def test_edit_note_find_replace_wrong_count(client, test_project):
|
||||
"""Test find_replace when replacement count doesn't match expected."""
|
||||
# Create initial note with version info
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Config Document",
|
||||
directory="config",
|
||||
@@ -345,7 +345,7 @@ async def test_edit_note_find_replace_wrong_count(client, test_project):
|
||||
)
|
||||
|
||||
# Try to replace expecting 1 occurrence, but there are actually 2
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="config/config-document",
|
||||
operation="find_replace",
|
||||
@@ -366,7 +366,7 @@ async def test_edit_note_find_replace_wrong_count(client, test_project):
|
||||
async def test_edit_note_replace_section_multiple_sections(client, test_project):
|
||||
"""Test replace_section with multiple sections having same header - should return helpful error."""
|
||||
# Create note with duplicate section headers
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Sample Note",
|
||||
directory="docs",
|
||||
@@ -374,7 +374,7 @@ async def test_edit_note_replace_section_multiple_sections(client, test_project)
|
||||
)
|
||||
|
||||
# Try to replace section when multiple exist
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="docs/sample-note",
|
||||
operation="replace_section",
|
||||
@@ -393,7 +393,7 @@ async def test_edit_note_replace_section_multiple_sections(client, test_project)
|
||||
async def test_edit_note_find_replace_empty_find_text(client, test_project):
|
||||
"""Test find_replace with empty/whitespace find_text - should return helpful error."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -401,7 +401,7 @@ async def test_edit_note_find_replace_empty_find_text(client, test_project):
|
||||
)
|
||||
|
||||
# Try with whitespace-only find_text - this should be caught by service validation
|
||||
result = await edit_note.fn(
|
||||
result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="test/test-note",
|
||||
operation="find_replace",
|
||||
@@ -423,7 +423,7 @@ async def test_edit_note_preserves_permalink_when_frontmatter_missing(client, te
|
||||
in its frontmatter.
|
||||
"""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -431,7 +431,7 @@ async def test_edit_note_preserves_permalink_when_frontmatter_missing(client, te
|
||||
)
|
||||
|
||||
# Verify the note was created with a permalink
|
||||
first_result = await edit_note.fn(
|
||||
first_result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="test/test-note",
|
||||
operation="append",
|
||||
@@ -443,7 +443,7 @@ async def test_edit_note_preserves_permalink_when_frontmatter_missing(client, te
|
||||
|
||||
# Perform another edit - this should preserve the permalink even if the
|
||||
# file doesn't have a permalink in its frontmatter
|
||||
second_result = await edit_note.fn(
|
||||
second_result = await edit_note(
|
||||
project=test_project.name,
|
||||
identifier="test/test-note",
|
||||
operation="append",
|
||||
|
||||
@@ -22,7 +22,7 @@ from basic_memory.mcp.tools import (
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_text_and_json_modes(app, test_project):
|
||||
text_result = await write_note.fn(
|
||||
text_result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Write Note",
|
||||
directory="mode-tests",
|
||||
@@ -32,7 +32,7 @@ async def test_write_note_text_and_json_modes(app, test_project):
|
||||
assert isinstance(text_result, str)
|
||||
assert "note" in text_result.lower()
|
||||
|
||||
json_result = await write_note.fn(
|
||||
json_result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Write Note",
|
||||
directory="mode-tests",
|
||||
@@ -49,14 +49,14 @@ async def test_write_note_text_and_json_modes(app, test_project):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_note_text_and_json_modes(app, test_project):
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Read Note",
|
||||
directory="mode-tests",
|
||||
content="# Mode Read Note\n\nbody",
|
||||
)
|
||||
|
||||
text_result = await read_note.fn(
|
||||
text_result = await read_note(
|
||||
identifier="mode-tests/mode-read-note",
|
||||
project=test_project.name,
|
||||
output_format="text",
|
||||
@@ -64,7 +64,7 @@ async def test_read_note_text_and_json_modes(app, test_project):
|
||||
assert isinstance(text_result, str)
|
||||
assert "Mode Read Note" in text_result
|
||||
|
||||
json_result = await read_note.fn(
|
||||
json_result = await read_note(
|
||||
identifier="mode-tests/mode-read-note",
|
||||
project=test_project.name,
|
||||
output_format="json",
|
||||
@@ -76,7 +76,7 @@ async def test_read_note_text_and_json_modes(app, test_project):
|
||||
assert isinstance(json_result["content"], str)
|
||||
assert "frontmatter" in json_result
|
||||
|
||||
missing_json = await read_note.fn(
|
||||
missing_json = await read_note(
|
||||
identifier="mode-tests/missing-note",
|
||||
project=test_project.name,
|
||||
output_format="json",
|
||||
@@ -89,14 +89,14 @@ async def test_read_note_text_and_json_modes(app, test_project):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_note_text_and_json_modes(app, test_project):
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Edit Note",
|
||||
directory="mode-tests",
|
||||
content="# Mode Edit Note\n\nstart",
|
||||
)
|
||||
|
||||
text_result = await edit_note.fn(
|
||||
text_result = await edit_note(
|
||||
identifier="mode-tests/mode-edit-note",
|
||||
operation="append",
|
||||
content="\n\ntext-append",
|
||||
@@ -106,7 +106,7 @@ async def test_edit_note_text_and_json_modes(app, test_project):
|
||||
assert isinstance(text_result, str)
|
||||
assert "Edited note" in text_result
|
||||
|
||||
json_result = await edit_note.fn(
|
||||
json_result = await edit_note(
|
||||
identifier="mode-tests/mode-edit-note",
|
||||
operation="append",
|
||||
content="\n\njson-append",
|
||||
@@ -123,14 +123,14 @@ async def test_edit_note_text_and_json_modes(app, test_project):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recent_activity_text_and_json_modes(app, test_project):
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Activity Note",
|
||||
directory="mode-tests",
|
||||
content="# Mode Activity Note\n\nactivity",
|
||||
)
|
||||
|
||||
text_result = await recent_activity.fn(
|
||||
text_result = await recent_activity(
|
||||
project=test_project.name,
|
||||
timeframe="7d",
|
||||
output_format="text",
|
||||
@@ -138,7 +138,7 @@ async def test_recent_activity_text_and_json_modes(app, test_project):
|
||||
assert isinstance(text_result, str)
|
||||
assert "Recent Activity" in text_result
|
||||
|
||||
json_result = await recent_activity.fn(
|
||||
json_result = await recent_activity(
|
||||
project=test_project.name,
|
||||
timeframe="7d",
|
||||
output_format="json",
|
||||
@@ -151,7 +151,7 @@ async def test_recent_activity_text_and_json_modes(app, test_project):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recent_activity_json_preserves_relation_and_observation_types(app, test_project):
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Activity Type Source",
|
||||
directory="mode-tests",
|
||||
@@ -161,14 +161,14 @@ async def test_recent_activity_json_preserves_relation_and_observation_types(app
|
||||
"- links_to [[Activity Type Target]]"
|
||||
),
|
||||
)
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Activity Type Target",
|
||||
directory="mode-tests",
|
||||
content="# Activity Type Target",
|
||||
)
|
||||
|
||||
relation_json = await recent_activity.fn(
|
||||
relation_json = await recent_activity(
|
||||
project=test_project.name,
|
||||
type="relation",
|
||||
timeframe="7d",
|
||||
@@ -180,7 +180,7 @@ async def test_recent_activity_json_preserves_relation_and_observation_types(app
|
||||
for item in relation_json:
|
||||
assert set(["type", "title", "permalink", "file_path", "created_at"]).issubset(item.keys())
|
||||
|
||||
observation_json = await recent_activity.fn(
|
||||
observation_json = await recent_activity(
|
||||
project=test_project.name,
|
||||
type="observation",
|
||||
timeframe="7d",
|
||||
@@ -195,11 +195,11 @@ async def test_recent_activity_json_preserves_relation_and_observation_types(app
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_and_create_project_text_and_json_modes(app, test_project, tmp_path):
|
||||
list_text = await list_memory_projects.fn(output_format="text")
|
||||
list_text = await list_memory_projects(output_format="text")
|
||||
assert isinstance(list_text, str)
|
||||
assert test_project.name in list_text
|
||||
|
||||
list_json = await list_memory_projects.fn(output_format="json")
|
||||
list_json = await list_memory_projects(output_format="json")
|
||||
assert isinstance(list_json, dict)
|
||||
assert "projects" in list_json
|
||||
assert any(project["name"] == test_project.name for project in list_json["projects"])
|
||||
@@ -207,7 +207,7 @@ async def test_list_and_create_project_text_and_json_modes(app, test_project, tm
|
||||
project_name = "mode-create-project"
|
||||
project_path = str(tmp_path.parent / (tmp_path.name + "-projects") / "mode-create-project")
|
||||
|
||||
create_text = await create_memory_project.fn(
|
||||
create_text = await create_memory_project(
|
||||
project_name=project_name,
|
||||
project_path=project_path,
|
||||
output_format="text",
|
||||
@@ -215,7 +215,7 @@ async def test_list_and_create_project_text_and_json_modes(app, test_project, tm
|
||||
assert isinstance(create_text, str)
|
||||
assert "mode-create-project" in create_text
|
||||
|
||||
create_json_again = await create_memory_project.fn(
|
||||
create_json_again = await create_memory_project(
|
||||
project_name=project_name,
|
||||
project_path=project_path,
|
||||
output_format="json",
|
||||
@@ -231,14 +231,14 @@ async def test_list_and_create_project_text_and_json_modes(app, test_project, tm
|
||||
default_project_path = str(
|
||||
tmp_path.parent / (tmp_path.name + "-projects") / "mode-default-project"
|
||||
)
|
||||
await create_memory_project.fn(
|
||||
await create_memory_project(
|
||||
project_name=default_project_name,
|
||||
project_path=default_project_path,
|
||||
set_default=True,
|
||||
output_format="text",
|
||||
)
|
||||
|
||||
default_text_again = await create_memory_project.fn(
|
||||
default_text_again = await create_memory_project(
|
||||
project_name=default_project_name,
|
||||
project_path=default_project_path,
|
||||
output_format="text",
|
||||
@@ -249,28 +249,28 @@ async def test_list_and_create_project_text_and_json_modes(app, test_project, tm
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_note_text_and_json_modes(app, test_project):
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Delete Text",
|
||||
directory="mode-tests",
|
||||
content="# Mode Delete Text",
|
||||
)
|
||||
|
||||
text_delete = await delete_note.fn(
|
||||
text_delete = await delete_note(
|
||||
identifier="mode-tests/mode-delete-text",
|
||||
project=test_project.name,
|
||||
output_format="text",
|
||||
)
|
||||
assert text_delete is True
|
||||
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Delete Json",
|
||||
directory="mode-tests",
|
||||
content="# Mode Delete Json",
|
||||
)
|
||||
|
||||
json_delete = await delete_note.fn(
|
||||
json_delete = await delete_note(
|
||||
identifier="mode-tests/mode-delete-json",
|
||||
project=test_project.name,
|
||||
output_format="json",
|
||||
@@ -291,7 +291,7 @@ async def test_delete_directory_json_mode_returns_structured_error_on_failure(
|
||||
|
||||
monkeypatch.setattr(KnowledgeClient, "delete_directory", mock_delete_directory)
|
||||
|
||||
json_delete = await delete_note.fn(
|
||||
json_delete = await delete_note(
|
||||
identifier="mode-tests",
|
||||
is_directory=True,
|
||||
project=test_project.name,
|
||||
@@ -306,14 +306,14 @@ async def test_delete_directory_json_mode_returns_structured_error_on_failure(
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_text_and_json_modes(app, test_project):
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Move Text",
|
||||
directory="mode-tests",
|
||||
content="# Mode Move Text",
|
||||
)
|
||||
|
||||
text_move = await move_note.fn(
|
||||
text_move = await move_note(
|
||||
identifier="mode-tests/mode-move-text",
|
||||
destination_path="mode-tests/moved/mode-move-text.md",
|
||||
project=test_project.name,
|
||||
@@ -322,14 +322,14 @@ async def test_move_note_text_and_json_modes(app, test_project):
|
||||
assert isinstance(text_move, str)
|
||||
assert "moved" in text_move.lower()
|
||||
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Mode Move Json",
|
||||
directory="mode-tests",
|
||||
content="# Mode Move Json",
|
||||
)
|
||||
|
||||
json_move = await move_note.fn(
|
||||
json_move = await move_note(
|
||||
identifier="mode-tests/mode-move-json",
|
||||
destination_path="mode-tests/moved/mode-move-json.md",
|
||||
project=test_project.name,
|
||||
@@ -346,14 +346,14 @@ async def test_move_note_text_and_json_modes(app, test_project):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_json_default_and_text_mode(client, test_graph, test_project):
|
||||
json_result = await build_context.fn(
|
||||
json_result = await build_context(
|
||||
project=test_project.name,
|
||||
url="memory://test/root",
|
||||
)
|
||||
assert isinstance(json_result, dict)
|
||||
assert "results" in json_result
|
||||
|
||||
text_result = await build_context.fn(
|
||||
text_result = await build_context(
|
||||
project=test_project.name,
|
||||
url="memory://test/root",
|
||||
output_format="text",
|
||||
|
||||
@@ -9,7 +9,7 @@ from basic_memory.mcp.tools.write_note import write_note
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_directory_empty(client, test_project):
|
||||
"""Test listing directory when no entities exist."""
|
||||
result = await list_directory.fn(project=test_project.name)
|
||||
result = await list_directory(project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "No files found in directory '/'" in result
|
||||
@@ -26,7 +26,7 @@ async def test_list_directory_with_test_graph(client, test_graph, test_project):
|
||||
# /test/Root.md
|
||||
|
||||
# List root directory
|
||||
result = await list_directory.fn(project=test_project.name)
|
||||
result = await list_directory(project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "Contents of '/' (depth 1):" in result
|
||||
@@ -38,7 +38,7 @@ async def test_list_directory_with_test_graph(client, test_graph, test_project):
|
||||
async def test_list_directory_specific_path(client, test_graph, test_project):
|
||||
"""Test listing specific directory path."""
|
||||
# List the test directory
|
||||
result = await list_directory.fn(project=test_project.name, dir_name="/test")
|
||||
result = await list_directory(project=test_project.name, dir_name="/test")
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "Contents of '/test' (depth 1):" in result
|
||||
@@ -54,7 +54,7 @@ async def test_list_directory_specific_path(client, test_graph, test_project):
|
||||
async def test_list_directory_with_glob_filter(client, test_graph, test_project):
|
||||
"""Test listing directory with glob filtering."""
|
||||
# Filter for files containing "Connected"
|
||||
result = await list_directory.fn(
|
||||
result = await list_directory(
|
||||
project=test_project.name, dir_name="/test", file_name_glob="*Connected*"
|
||||
)
|
||||
|
||||
@@ -72,7 +72,7 @@ async def test_list_directory_with_glob_filter(client, test_graph, test_project)
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_directory_with_markdown_filter(client, test_graph, test_project):
|
||||
"""Test listing directory with markdown file filter."""
|
||||
result = await list_directory.fn(
|
||||
result = await list_directory(
|
||||
project=test_project.name, dir_name="/test", file_name_glob="*.md"
|
||||
)
|
||||
|
||||
@@ -91,7 +91,7 @@ async def test_list_directory_with_markdown_filter(client, test_graph, test_proj
|
||||
async def test_list_directory_with_depth_control(client, test_graph, test_project):
|
||||
"""Test listing directory with depth control."""
|
||||
# Depth 1: should return only the test directory
|
||||
result_depth_1 = await list_directory.fn(project=test_project.name, dir_name="/", depth=1)
|
||||
result_depth_1 = await list_directory(project=test_project.name, dir_name="/", depth=1)
|
||||
|
||||
assert isinstance(result_depth_1, str)
|
||||
assert "Contents of '/' (depth 1):" in result_depth_1
|
||||
@@ -99,7 +99,7 @@ async def test_list_directory_with_depth_control(client, test_graph, test_projec
|
||||
assert "Total: 1 items (1 directory)" in result_depth_1
|
||||
|
||||
# Depth 2: should return directory + its files
|
||||
result_depth_2 = await list_directory.fn(project=test_project.name, dir_name="/", depth=2)
|
||||
result_depth_2 = await list_directory(project=test_project.name, dir_name="/", depth=2)
|
||||
|
||||
assert isinstance(result_depth_2, str)
|
||||
assert "Contents of '/' (depth 2):" in result_depth_2
|
||||
@@ -115,7 +115,7 @@ async def test_list_directory_with_depth_control(client, test_graph, test_projec
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_directory_nonexistent_path(client, test_graph, test_project):
|
||||
"""Test listing nonexistent directory."""
|
||||
result = await list_directory.fn(project=test_project.name, dir_name="/nonexistent")
|
||||
result = await list_directory(project=test_project.name, dir_name="/nonexistent")
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "No files found in directory '/nonexistent'" in result
|
||||
@@ -124,7 +124,7 @@ async def test_list_directory_nonexistent_path(client, test_graph, test_project)
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_directory_glob_no_matches(client, test_graph, test_project):
|
||||
"""Test listing directory with glob that matches nothing."""
|
||||
result = await list_directory.fn(
|
||||
result = await list_directory(
|
||||
project=test_project.name, dir_name="/test", file_name_glob="*.xyz"
|
||||
)
|
||||
|
||||
@@ -136,7 +136,7 @@ async def test_list_directory_glob_no_matches(client, test_graph, test_project):
|
||||
async def test_list_directory_with_created_notes(client, test_project):
|
||||
"""Test listing directory with dynamically created notes."""
|
||||
# Create some test notes
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Project Planning",
|
||||
directory="projects",
|
||||
@@ -144,7 +144,7 @@ async def test_list_directory_with_created_notes(client, test_project):
|
||||
tags=["planning", "project"],
|
||||
)
|
||||
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Meeting Notes",
|
||||
directory="projects",
|
||||
@@ -152,7 +152,7 @@ async def test_list_directory_with_created_notes(client, test_project):
|
||||
tags=["meeting", "notes"],
|
||||
)
|
||||
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Research Document",
|
||||
directory="research",
|
||||
@@ -161,7 +161,7 @@ async def test_list_directory_with_created_notes(client, test_project):
|
||||
)
|
||||
|
||||
# List root directory
|
||||
result_root = await list_directory.fn(project=test_project.name)
|
||||
result_root = await list_directory(project=test_project.name)
|
||||
|
||||
assert isinstance(result_root, str)
|
||||
assert "Contents of '/' (depth 1):" in result_root
|
||||
@@ -170,7 +170,7 @@ async def test_list_directory_with_created_notes(client, test_project):
|
||||
assert "Total: 2 items (2 directories)" in result_root
|
||||
|
||||
# List projects directory
|
||||
result_projects = await list_directory.fn(project=test_project.name, dir_name="/projects")
|
||||
result_projects = await list_directory(project=test_project.name, dir_name="/projects")
|
||||
|
||||
assert isinstance(result_projects, str)
|
||||
assert "Contents of '/projects' (depth 1):" in result_projects
|
||||
@@ -179,7 +179,7 @@ async def test_list_directory_with_created_notes(client, test_project):
|
||||
assert "Total: 2 items (2 files)" in result_projects
|
||||
|
||||
# Test glob filter for "Meeting"
|
||||
result_meeting = await list_directory.fn(
|
||||
result_meeting = await list_directory(
|
||||
project=test_project.name, dir_name="/projects", file_name_glob="*Meeting*"
|
||||
)
|
||||
|
||||
@@ -197,7 +197,7 @@ async def test_list_directory_path_normalization(client, test_graph, test_projec
|
||||
paths_to_test = ["/test", "test", "/test/", "test/"]
|
||||
|
||||
for path in paths_to_test:
|
||||
result = await list_directory.fn(project=test_project.name, dir_name=path)
|
||||
result = await list_directory(project=test_project.name, dir_name=path)
|
||||
# All should return the same number of items
|
||||
assert "Total: 5 items (5 files)" in result
|
||||
assert "📄 Connected Entity 1.md" in result
|
||||
@@ -206,7 +206,7 @@ async def test_list_directory_path_normalization(client, test_graph, test_projec
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_directory_shows_file_metadata(client, test_graph, test_project):
|
||||
"""Test that file metadata is displayed correctly."""
|
||||
result = await list_directory.fn(project=test_project.name, dir_name="/test")
|
||||
result = await list_directory(project=test_project.name, dir_name="/test")
|
||||
|
||||
assert isinstance(result, str)
|
||||
# Should show file names
|
||||
|
||||
@@ -39,7 +39,7 @@ async def test_detect_cross_project_move_attempt_is_defensive_on_api_error(monke
|
||||
async def test_move_note_success(app, client, test_project):
|
||||
"""Test successfully moving a note to a new location."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -47,7 +47,7 @@ async def test_move_note_success(app, client, test_project):
|
||||
)
|
||||
|
||||
# Move note
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path="target/MovedNote.md",
|
||||
@@ -58,13 +58,13 @@ async def test_move_note_success(app, client, test_project):
|
||||
|
||||
# Verify original location no longer exists
|
||||
try:
|
||||
await read_note.fn(test_project.name, "source/test-note")
|
||||
await read_note(test_project.name, "source/test-note")
|
||||
assert False, "Original note should not exist after move"
|
||||
except Exception:
|
||||
pass # Expected - note should not exist at original location
|
||||
|
||||
# Verify note exists at new location with same content
|
||||
content = await read_note.fn("target/moved-note", project=test_project.name)
|
||||
content = await read_note("target/moved-note", project=test_project.name)
|
||||
assert "# Test Note" in content
|
||||
assert "Original content here" in content
|
||||
assert f"permalink: {test_project.name}/target/moved-note" in content
|
||||
@@ -74,7 +74,7 @@ async def test_move_note_success(app, client, test_project):
|
||||
async def test_move_note_with_folder_creation(client, test_project):
|
||||
"""Test moving note creates necessary folders."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Deep Note",
|
||||
directory="",
|
||||
@@ -82,7 +82,7 @@ async def test_move_note_with_folder_creation(client, test_project):
|
||||
)
|
||||
|
||||
# Move to deeply nested path
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="deep-note",
|
||||
destination_path="deeply/nested/folder/DeepNote.md",
|
||||
@@ -92,7 +92,7 @@ async def test_move_note_with_folder_creation(client, test_project):
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify note exists at new location
|
||||
content = await read_note.fn("deeply/nested/folder/deep-note", project=test_project.name)
|
||||
content = await read_note("deeply/nested/folder/deep-note", project=test_project.name)
|
||||
assert "# Deep Note" in content
|
||||
assert "Content in root folder" in content
|
||||
|
||||
@@ -101,7 +101,7 @@ async def test_move_note_with_folder_creation(client, test_project):
|
||||
async def test_move_note_with_observations_and_relations(app, client, test_project):
|
||||
"""Test moving note preserves observations and relations."""
|
||||
# Create note with complex semantic content
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Complex Entity",
|
||||
directory="source",
|
||||
@@ -120,7 +120,7 @@ Some additional content.
|
||||
)
|
||||
|
||||
# Move note
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/complex-entity",
|
||||
destination_path="target/MovedComplex.md",
|
||||
@@ -130,7 +130,7 @@ Some additional content.
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify moved note preserves all content
|
||||
content = await read_note.fn("target/moved-complex", project=test_project.name)
|
||||
content = await read_note("target/moved-complex", project=test_project.name)
|
||||
assert "Important observation #tag1" in content
|
||||
assert "Key feature #feature" in content
|
||||
assert "[[SomeOtherEntity]]" in content
|
||||
@@ -142,7 +142,7 @@ Some additional content.
|
||||
async def test_move_note_by_title(client, test_project):
|
||||
"""Test moving note using title as identifier."""
|
||||
# Create note with unique title
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="UniqueTestTitle",
|
||||
directory="source",
|
||||
@@ -150,7 +150,7 @@ async def test_move_note_by_title(client, test_project):
|
||||
)
|
||||
|
||||
# Move using title as identifier
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="UniqueTestTitle",
|
||||
destination_path="target/MovedByTitle.md",
|
||||
@@ -160,7 +160,7 @@ async def test_move_note_by_title(client, test_project):
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify note exists at new location
|
||||
content = await read_note.fn("target/moved-by-title", project=test_project.name)
|
||||
content = await read_note("target/moved-by-title", project=test_project.name)
|
||||
assert "# UniqueTestTitle" in content
|
||||
assert "Test content" in content
|
||||
|
||||
@@ -169,7 +169,7 @@ async def test_move_note_by_title(client, test_project):
|
||||
async def test_move_note_by_file_path(client, test_project):
|
||||
"""Test moving note using file path as identifier."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="PathTest",
|
||||
directory="source",
|
||||
@@ -177,7 +177,7 @@ async def test_move_note_by_file_path(client, test_project):
|
||||
)
|
||||
|
||||
# Move using file path as identifier
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/PathTest.md",
|
||||
destination_path="target/MovedByPath.md",
|
||||
@@ -187,7 +187,7 @@ async def test_move_note_by_file_path(client, test_project):
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify note exists at new location
|
||||
content = await read_note.fn("target/moved-by-path", project=test_project.name)
|
||||
content = await read_note("target/moved-by-path", project=test_project.name)
|
||||
assert "# PathTest" in content
|
||||
assert "Content for path test" in content
|
||||
|
||||
@@ -195,7 +195,7 @@ async def test_move_note_by_file_path(client, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_nonexistent_note(client, test_project):
|
||||
"""Test moving a note that doesn't exist."""
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="nonexistent/note",
|
||||
destination_path="target/SomeFile.md",
|
||||
@@ -212,7 +212,7 @@ async def test_move_note_nonexistent_note(client, test_project):
|
||||
async def test_move_note_invalid_destination_path(client, test_project):
|
||||
"""Test moving note with invalid destination path."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="TestNote",
|
||||
directory="source",
|
||||
@@ -220,7 +220,7 @@ async def test_move_note_invalid_destination_path(client, test_project):
|
||||
)
|
||||
|
||||
# Test absolute path (should be rejected by validation)
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path="/absolute/path.md",
|
||||
@@ -236,7 +236,7 @@ async def test_move_note_invalid_destination_path(client, test_project):
|
||||
async def test_move_note_missing_file_extension(client, test_project):
|
||||
"""Test moving note without file extension in destination path."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="ExtensionTest",
|
||||
directory="source",
|
||||
@@ -244,7 +244,7 @@ async def test_move_note_missing_file_extension(client, test_project):
|
||||
)
|
||||
|
||||
# Test path without extension
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/extension-test",
|
||||
destination_path="target/renamed-note",
|
||||
@@ -258,7 +258,7 @@ async def test_move_note_missing_file_extension(client, test_project):
|
||||
assert "renamed-note.md" in result # Should suggest adding .md
|
||||
|
||||
# Test path with empty extension (edge case)
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/extension-test",
|
||||
destination_path="target/renamed-note.",
|
||||
@@ -269,7 +269,7 @@ async def test_move_note_missing_file_extension(client, test_project):
|
||||
assert "must include a file extension" in result
|
||||
|
||||
# Test that note still exists at original location
|
||||
content = await read_note.fn("source/extension-test", project=test_project.name)
|
||||
content = await read_note("source/extension-test", project=test_project.name)
|
||||
assert "# Extension Test" in content
|
||||
assert "Testing extension validation" in content
|
||||
|
||||
@@ -278,7 +278,7 @@ async def test_move_note_missing_file_extension(client, test_project):
|
||||
async def test_move_note_file_extension_mismatch(client, test_project):
|
||||
"""Test that moving note with different extension is blocked."""
|
||||
# Create initial note with .md extension
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="MarkdownNote",
|
||||
directory="source",
|
||||
@@ -286,7 +286,7 @@ async def test_move_note_file_extension_mismatch(client, test_project):
|
||||
)
|
||||
|
||||
# Try to move with .txt extension
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/markdown-note",
|
||||
destination_path="target/renamed-note.txt",
|
||||
@@ -301,7 +301,7 @@ async def test_move_note_file_extension_mismatch(client, test_project):
|
||||
assert "renamed-note.md" in result # Should suggest correct extension
|
||||
|
||||
# Test that note still exists at original location with original extension
|
||||
content = await read_note.fn("source/markdown-note", project=test_project.name)
|
||||
content = await read_note("source/markdown-note", project=test_project.name)
|
||||
assert "# Markdown Note" in content
|
||||
assert "This is a markdown file" in content
|
||||
|
||||
@@ -310,7 +310,7 @@ async def test_move_note_file_extension_mismatch(client, test_project):
|
||||
async def test_move_note_preserves_file_extension(client, test_project):
|
||||
"""Test that moving note with matching extension succeeds."""
|
||||
# Create initial note with .md extension
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="PreserveExtension",
|
||||
directory="source",
|
||||
@@ -318,7 +318,7 @@ async def test_move_note_preserves_file_extension(client, test_project):
|
||||
)
|
||||
|
||||
# Move with same .md extension
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/preserve-extension",
|
||||
destination_path="target/preserved-note.md",
|
||||
@@ -329,13 +329,13 @@ async def test_move_note_preserves_file_extension(client, test_project):
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify note exists at new location with same extension
|
||||
content = await read_note.fn("target/preserved-note", project=test_project.name)
|
||||
content = await read_note("target/preserved-note", project=test_project.name)
|
||||
assert "# Preserve Extension" in content
|
||||
assert "Testing that extension is preserved" in content
|
||||
|
||||
# Verify old location no longer exists
|
||||
try:
|
||||
await read_note.fn("source/preserve-extension")
|
||||
await read_note("source/preserve-extension")
|
||||
assert False, "Original note should not exist after move"
|
||||
except Exception:
|
||||
pass # Expected
|
||||
@@ -345,7 +345,7 @@ async def test_move_note_preserves_file_extension(client, test_project):
|
||||
async def test_move_note_destination_exists(client, test_project):
|
||||
"""Test moving note to existing destination."""
|
||||
# Create source note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="SourceNote",
|
||||
directory="source",
|
||||
@@ -353,7 +353,7 @@ async def test_move_note_destination_exists(client, test_project):
|
||||
)
|
||||
|
||||
# Create destination note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="DestinationNote",
|
||||
directory="target",
|
||||
@@ -361,7 +361,7 @@ async def test_move_note_destination_exists(client, test_project):
|
||||
)
|
||||
|
||||
# Try to move source to existing destination
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/source-note",
|
||||
destination_path="target/DestinationNote.md",
|
||||
@@ -377,7 +377,7 @@ async def test_move_note_destination_exists(client, test_project):
|
||||
async def test_move_note_same_location(client, test_project):
|
||||
"""Test moving note to the same location."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="SameLocationTest",
|
||||
directory="test",
|
||||
@@ -385,7 +385,7 @@ async def test_move_note_same_location(client, test_project):
|
||||
)
|
||||
|
||||
# Try to move to same location
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="test/same-location-test",
|
||||
destination_path="test/SameLocationTest.md",
|
||||
@@ -401,7 +401,7 @@ async def test_move_note_same_location(client, test_project):
|
||||
async def test_move_note_rename_only(client, test_project):
|
||||
"""Test moving note within same folder (rename operation)."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="OriginalName",
|
||||
directory="test",
|
||||
@@ -409,7 +409,7 @@ async def test_move_note_rename_only(client, test_project):
|
||||
)
|
||||
|
||||
# Rename within same folder
|
||||
await move_note.fn(
|
||||
await move_note(
|
||||
project=test_project.name,
|
||||
identifier="test/original-name",
|
||||
destination_path="test/NewName.md",
|
||||
@@ -417,13 +417,13 @@ async def test_move_note_rename_only(client, test_project):
|
||||
|
||||
# Verify original is gone
|
||||
try:
|
||||
await read_note.fn("test/original-name", project=test_project.name)
|
||||
await read_note("test/original-name", project=test_project.name)
|
||||
assert False, "Original note should not exist after rename"
|
||||
except Exception:
|
||||
pass # Expected
|
||||
|
||||
# Verify new name exists with same content
|
||||
content = await read_note.fn("test/new-name", project=test_project.name)
|
||||
content = await read_note("test/new-name", project=test_project.name)
|
||||
assert "# OriginalName" in content # Title in content remains same
|
||||
assert "Content to rename" in content
|
||||
assert f"permalink: {test_project.name}/test/new-name" in content
|
||||
@@ -433,7 +433,7 @@ async def test_move_note_rename_only(client, test_project):
|
||||
async def test_move_note_complex_filename(client, test_project):
|
||||
"""Test moving note with spaces in filename."""
|
||||
# Create note with spaces in name
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Meeting Notes 2025",
|
||||
directory="meetings",
|
||||
@@ -441,7 +441,7 @@ async def test_move_note_complex_filename(client, test_project):
|
||||
)
|
||||
|
||||
# Move to new location
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="meetings/meeting-notes-2025",
|
||||
destination_path="archive/2025/meetings/Meeting Notes 2025.md",
|
||||
@@ -451,9 +451,7 @@ async def test_move_note_complex_filename(client, test_project):
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify note exists at new location with correct content
|
||||
content = await read_note.fn(
|
||||
"archive/2025/meetings/meeting-notes-2025", project=test_project.name
|
||||
)
|
||||
content = await read_note("archive/2025/meetings/meeting-notes-2025", project=test_project.name)
|
||||
assert "# Meeting Notes 2025" in content
|
||||
assert "Meeting content with dates" in content
|
||||
|
||||
@@ -462,7 +460,7 @@ async def test_move_note_complex_filename(client, test_project):
|
||||
async def test_move_note_with_tags(app, client, test_project):
|
||||
"""Test moving note with tags preserves tags."""
|
||||
# Create note with tags
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Tagged Note",
|
||||
directory="source",
|
||||
@@ -471,7 +469,7 @@ async def test_move_note_with_tags(app, client, test_project):
|
||||
)
|
||||
|
||||
# Move note
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/tagged-note",
|
||||
destination_path="target/MovedTaggedNote.md",
|
||||
@@ -481,7 +479,7 @@ async def test_move_note_with_tags(app, client, test_project):
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify tags are preserved in correct YAML format
|
||||
content = await read_note.fn("target/moved-tagged-note", project=test_project.name)
|
||||
content = await read_note("target/moved-tagged-note", project=test_project.name)
|
||||
assert "- important" in content
|
||||
assert "- work" in content
|
||||
assert "- project" in content
|
||||
@@ -491,7 +489,7 @@ async def test_move_note_with_tags(app, client, test_project):
|
||||
async def test_move_note_empty_string_destination(client, test_project):
|
||||
"""Test moving note with empty destination path."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="TestNote",
|
||||
directory="source",
|
||||
@@ -499,7 +497,7 @@ async def test_move_note_empty_string_destination(client, test_project):
|
||||
)
|
||||
|
||||
# Test empty destination path
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path="",
|
||||
@@ -515,7 +513,7 @@ async def test_move_note_empty_string_destination(client, test_project):
|
||||
async def test_move_note_parent_directory_path(client, test_project):
|
||||
"""Test moving note with parent directory in destination path."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="TestNote",
|
||||
directory="source",
|
||||
@@ -523,7 +521,7 @@ async def test_move_note_parent_directory_path(client, test_project):
|
||||
)
|
||||
|
||||
# Test parent directory path
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path="../parent/file.md",
|
||||
@@ -539,7 +537,7 @@ async def test_move_note_parent_directory_path(client, test_project):
|
||||
async def test_move_note_identifier_variations(client, test_project):
|
||||
"""Test that various identifier formats work for moving."""
|
||||
# Create a note to test different identifier formats
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Document",
|
||||
directory="docs",
|
||||
@@ -547,7 +545,7 @@ async def test_move_note_identifier_variations(client, test_project):
|
||||
)
|
||||
|
||||
# Test with permalink identifier
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="docs/test-document",
|
||||
destination_path="moved/TestDocument.md",
|
||||
@@ -557,7 +555,7 @@ async def test_move_note_identifier_variations(client, test_project):
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify it moved correctly
|
||||
content = await read_note.fn("moved/test-document", project=test_project.name)
|
||||
content = await read_note("moved/test-document", project=test_project.name)
|
||||
assert "# Test Document" in content
|
||||
assert "Content for testing identifiers" in content
|
||||
|
||||
@@ -566,7 +564,7 @@ async def test_move_note_identifier_variations(client, test_project):
|
||||
async def test_move_note_preserves_frontmatter(app, client, test_project):
|
||||
"""Test that moving preserves custom frontmatter."""
|
||||
# Create note with custom frontmatter by first creating it normally
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Custom Frontmatter Note",
|
||||
directory="source",
|
||||
@@ -574,7 +572,7 @@ async def test_move_note_preserves_frontmatter(app, client, test_project):
|
||||
)
|
||||
|
||||
# Move the note
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/custom-frontmatter-note",
|
||||
destination_path="target/MovedCustomNote.md",
|
||||
@@ -584,7 +582,7 @@ async def test_move_note_preserves_frontmatter(app, client, test_project):
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify the moved note has proper frontmatter structure
|
||||
content = await read_note.fn("target/moved-custom-note", project=test_project.name)
|
||||
content = await read_note("target/moved-custom-note", project=test_project.name)
|
||||
assert "title: Custom Frontmatter Note" in content
|
||||
assert "type: note" in content
|
||||
assert f"permalink: {test_project.name}/target/moved-custom-note" in content
|
||||
@@ -638,7 +636,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_blocks_path_traversal_unix(self, client, test_project):
|
||||
"""Test that Unix-style path traversal attacks are blocked."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -656,7 +654,7 @@ class TestMoveNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path=attack_path,
|
||||
@@ -672,7 +670,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_blocks_path_traversal_windows(self, client, test_project):
|
||||
"""Test that Windows-style path traversal attacks are blocked."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -690,7 +688,7 @@ class TestMoveNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path=attack_path,
|
||||
@@ -705,7 +703,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_blocks_absolute_paths(self, client, test_project):
|
||||
"""Test that absolute paths are blocked."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -725,7 +723,7 @@ class TestMoveNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path=attack_path,
|
||||
@@ -740,7 +738,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_blocks_home_directory_access(self, client, test_project):
|
||||
"""Test that home directory access patterns are blocked."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -758,7 +756,7 @@ class TestMoveNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path=attack_path,
|
||||
@@ -773,7 +771,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_blocks_mixed_attack_patterns(self, client, test_project):
|
||||
"""Test that mixed legitimate/attack patterns are blocked."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -790,7 +788,7 @@ class TestMoveNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path=attack_path,
|
||||
@@ -804,7 +802,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_allows_safe_paths(self, client, test_project):
|
||||
"""Test that legitimate paths are still allowed."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -822,7 +820,7 @@ class TestMoveNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for safe_path in safe_paths:
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path=safe_path,
|
||||
@@ -841,7 +839,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_security_logging(self, client, test_project, caplog):
|
||||
"""Test that security violations are properly logged."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -849,7 +847,7 @@ class TestMoveNoteSecurityValidation:
|
||||
)
|
||||
|
||||
# Attempt path traversal attack
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path="../../../etc/passwd",
|
||||
@@ -865,7 +863,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_empty_path_security(self, client, test_project):
|
||||
"""Test that empty destination path is handled securely."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -873,7 +871,7 @@ class TestMoveNoteSecurityValidation:
|
||||
)
|
||||
|
||||
# Test empty destination path (should be allowed as it resolves to project root)
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path="",
|
||||
@@ -887,7 +885,7 @@ class TestMoveNoteSecurityValidation:
|
||||
async def test_move_note_current_directory_references_security(self, client, test_project):
|
||||
"""Test that current directory references are handled securely."""
|
||||
# Create initial note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="source",
|
||||
@@ -902,7 +900,7 @@ class TestMoveNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for safe_path in safe_paths:
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/test-note",
|
||||
destination_path=safe_path,
|
||||
@@ -919,14 +917,14 @@ class TestMoveNoteDestinationFolder:
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_with_destination_folder(self, client, test_project):
|
||||
"""Test moving a note using destination_folder preserves the original filename."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Folder Move Test",
|
||||
directory="source",
|
||||
content="# Folder Move Test\nContent for folder move.",
|
||||
)
|
||||
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/folder-move-test",
|
||||
destination_folder="archive",
|
||||
@@ -936,21 +934,21 @@ class TestMoveNoteDestinationFolder:
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
# Verify note exists at archive/Folder Move Test.md (original filename preserved)
|
||||
content = await read_note.fn("archive/folder-move-test", project=test_project.name)
|
||||
content = await read_note("archive/folder-move-test", project=test_project.name)
|
||||
assert "# Folder Move Test" in content
|
||||
assert "Content for folder move" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_with_nested_destination_folder(self, client, test_project):
|
||||
"""Test moving a note into a nested folder structure."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Nested Folder Test",
|
||||
directory="source",
|
||||
content="# Nested Folder Test\nNested folder content.",
|
||||
)
|
||||
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/nested-folder-test",
|
||||
destination_folder="archive/2025/q1",
|
||||
@@ -959,22 +957,20 @@ class TestMoveNoteDestinationFolder:
|
||||
assert isinstance(result, str)
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
content = await read_note.fn(
|
||||
"archive/2025/q1/nested-folder-test", project=test_project.name
|
||||
)
|
||||
content = await read_note("archive/2025/q1/nested-folder-test", project=test_project.name)
|
||||
assert "# Nested Folder Test" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_destination_folder_strips_slashes(self, client, test_project):
|
||||
"""Test that leading/trailing slashes are stripped from destination_folder."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Slash Strip Test",
|
||||
directory="source",
|
||||
content="# Slash Strip Test\nSlash stripping content.",
|
||||
)
|
||||
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/slash-strip-test",
|
||||
destination_folder="/archive/notes/",
|
||||
@@ -983,13 +979,13 @@ class TestMoveNoteDestinationFolder:
|
||||
assert isinstance(result, str)
|
||||
assert "✅ Note moved successfully" in result
|
||||
|
||||
content = await read_note.fn("archive/notes/slash-strip-test", project=test_project.name)
|
||||
content = await read_note("archive/notes/slash-strip-test", project=test_project.name)
|
||||
assert "# Slash Strip Test" in content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_both_params_error(self, client, test_project):
|
||||
"""Test that providing both destination_path and destination_folder is an error."""
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="some-note",
|
||||
destination_path="target/note.md",
|
||||
@@ -1003,7 +999,7 @@ class TestMoveNoteDestinationFolder:
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_both_params_error_json(self, client, test_project):
|
||||
"""Test JSON output when both destination_path and destination_folder are provided."""
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="some-note",
|
||||
destination_path="target/note.md",
|
||||
@@ -1018,7 +1014,7 @@ class TestMoveNoteDestinationFolder:
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_neither_param_error(self, client, test_project):
|
||||
"""Test that providing neither destination_path nor destination_folder is an error."""
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="some-note",
|
||||
)
|
||||
@@ -1030,7 +1026,7 @@ class TestMoveNoteDestinationFolder:
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_neither_param_error_json(self, client, test_project):
|
||||
"""Test JSON output when neither param is provided."""
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="some-note",
|
||||
output_format="json",
|
||||
@@ -1043,7 +1039,7 @@ class TestMoveNoteDestinationFolder:
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_destination_folder_with_is_directory_error(self, client, test_project):
|
||||
"""Test that destination_folder is rejected for directory moves."""
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="some-dir",
|
||||
destination_folder="archive",
|
||||
@@ -1059,7 +1055,7 @@ class TestMoveNoteDestinationFolder:
|
||||
self, client, test_project
|
||||
):
|
||||
"""Test JSON output when destination_folder is used with is_directory."""
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="some-dir",
|
||||
destination_folder="archive",
|
||||
@@ -1074,7 +1070,7 @@ class TestMoveNoteDestinationFolder:
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_destination_folder_nonexistent_note(self, client, test_project):
|
||||
"""Test destination_folder with a note that doesn't exist."""
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="nonexistent/note",
|
||||
destination_folder="archive",
|
||||
@@ -1086,14 +1082,14 @@ class TestMoveNoteDestinationFolder:
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_destination_folder_json_output(self, client, test_project):
|
||||
"""Test JSON output for successful destination_folder move."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="JSON Folder Test",
|
||||
directory="source",
|
||||
content="# JSON Folder Test\nJSON folder content.",
|
||||
)
|
||||
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/json-folder-test",
|
||||
destination_folder="archive",
|
||||
@@ -1109,7 +1105,7 @@ class TestMoveNoteDestinationFolder:
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_destination_folder_path_traversal(self, client, test_project):
|
||||
"""Test that path traversal via destination_folder is blocked."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Traversal Test",
|
||||
directory="source",
|
||||
@@ -1123,7 +1119,7 @@ class TestMoveNoteDestinationFolder:
|
||||
]
|
||||
|
||||
for attack_folder in attack_folders:
|
||||
result = await move_note.fn(
|
||||
result = await move_note(
|
||||
project=test_project.name,
|
||||
identifier="source/traversal-test",
|
||||
destination_folder=attack_folder,
|
||||
|
||||
@@ -10,7 +10,7 @@ from basic_memory.models.project import Project
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_memory_projects_unconstrained(app, test_project):
|
||||
result = await list_memory_projects.fn()
|
||||
result = await list_memory_projects()
|
||||
assert "Available projects:" in result
|
||||
assert f"• {test_project.name}" in result
|
||||
|
||||
@@ -49,7 +49,7 @@ async def test_list_memory_projects_shows_display_name(app, client, test_project
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_list,
|
||||
):
|
||||
result = await list_memory_projects.fn()
|
||||
result = await list_memory_projects()
|
||||
|
||||
# Regular project shows just the name
|
||||
assert "• main\n" in result
|
||||
@@ -77,7 +77,7 @@ async def test_list_memory_projects_no_display_name_shows_name_only(app, client,
|
||||
new_callable=AsyncMock,
|
||||
return_value=mock_list,
|
||||
):
|
||||
result = await list_memory_projects.fn()
|
||||
result = await list_memory_projects()
|
||||
|
||||
assert "• my-project\n" in result
|
||||
# Should NOT have parenthetical format
|
||||
@@ -87,7 +87,7 @@ async def test_list_memory_projects_no_display_name_shows_name_only(app, client,
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_memory_projects_constrained_env(monkeypatch, app, test_project):
|
||||
monkeypatch.setenv("BASIC_MEMORY_MCP_PROJECT", test_project.name)
|
||||
result = await list_memory_projects.fn()
|
||||
result = await list_memory_projects()
|
||||
assert f"Project: {test_project.name}" in result
|
||||
assert "constrained to a single project" in result
|
||||
|
||||
@@ -98,7 +98,7 @@ async def test_create_and_delete_project_and_name_match_branch(
|
||||
):
|
||||
# Create a project through the tool (exercises POST + response formatting).
|
||||
project_root = tmp_path_factory.mktemp("extra-project-home")
|
||||
result = await create_memory_project.fn(
|
||||
result = await create_memory_project(
|
||||
project_name="My Project",
|
||||
project_path=str(project_root),
|
||||
set_default=False,
|
||||
@@ -114,5 +114,5 @@ async def test_create_and_delete_project_and_name_match_branch(
|
||||
project.permalink = "custom-permalink"
|
||||
await session.commit()
|
||||
|
||||
delete_result = await delete_project.fn("My Project")
|
||||
delete_result = await delete_project("My Project")
|
||||
assert delete_result.startswith("✓")
|
||||
|
||||
@@ -25,7 +25,7 @@ async def test_read_content_blocks_path_traversal_unix(client, test_project):
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await read_content.fn(project=test_project.name, path=attack_path)
|
||||
result = await read_content(project=test_project.name, path=attack_path)
|
||||
assert result["type"] == "error"
|
||||
assert "paths must stay within project boundaries" in result["error"]
|
||||
assert attack_path in result["error"]
|
||||
@@ -44,7 +44,7 @@ async def test_read_content_blocks_path_traversal_windows(client, test_project):
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await read_content.fn(project=test_project.name, path=attack_path)
|
||||
result = await read_content(project=test_project.name, path=attack_path)
|
||||
assert result["type"] == "error"
|
||||
assert "paths must stay within project boundaries" in result["error"]
|
||||
assert attack_path in result["error"]
|
||||
@@ -65,7 +65,7 @@ async def test_read_content_blocks_absolute_paths(client, test_project):
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await read_content.fn(project=test_project.name, path=attack_path)
|
||||
result = await read_content(project=test_project.name, path=attack_path)
|
||||
assert result["type"] == "error"
|
||||
assert "paths must stay within project boundaries" in result["error"]
|
||||
assert attack_path in result["error"]
|
||||
@@ -85,7 +85,7 @@ async def test_read_content_blocks_home_directory_access(client, test_project):
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await read_content.fn(project=test_project.name, path=attack_path)
|
||||
result = await read_content(project=test_project.name, path=attack_path)
|
||||
assert result["type"] == "error"
|
||||
assert "paths must stay within project boundaries" in result["error"]
|
||||
assert attack_path in result["error"]
|
||||
@@ -101,7 +101,7 @@ async def test_read_content_blocks_memory_url_attacks(client, test_project):
|
||||
]
|
||||
|
||||
for attack_path in attack_paths:
|
||||
result = await read_content.fn(project=test_project.name, path=attack_path)
|
||||
result = await read_content(project=test_project.name, path=attack_path)
|
||||
assert result["type"] == "error"
|
||||
assert "paths must stay within project boundaries" in result["error"]
|
||||
|
||||
@@ -115,7 +115,7 @@ async def test_read_content_unicode_path_attacks(client, test_project):
|
||||
]
|
||||
|
||||
for attack_path in unicode_attacks:
|
||||
result = await read_content.fn(project=test_project.name, path=attack_path)
|
||||
result = await read_content(project=test_project.name, path=attack_path)
|
||||
assert result["type"] == "error"
|
||||
assert "paths must stay within project boundaries" in result["error"]
|
||||
|
||||
@@ -123,7 +123,7 @@ async def test_read_content_unicode_path_attacks(client, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_content_very_long_attack_path(client, test_project):
|
||||
long_attack = "../" * 1000 + "etc/passwd"
|
||||
result = await read_content.fn(project=test_project.name, path=long_attack)
|
||||
result = await read_content(project=test_project.name, path=long_attack)
|
||||
assert result["type"] == "error"
|
||||
assert "paths must stay within project boundaries" in result["error"]
|
||||
|
||||
@@ -138,21 +138,21 @@ async def test_read_content_case_variations_attacks(client, test_project):
|
||||
]
|
||||
|
||||
for attack_path in case_attacks:
|
||||
result = await read_content.fn(project=test_project.name, path=attack_path)
|
||||
result = await read_content(project=test_project.name, path=attack_path)
|
||||
assert result["type"] == "error"
|
||||
assert "paths must stay within project boundaries" in result["error"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_content_allows_safe_path_integration(client, test_project):
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Meeting",
|
||||
directory="notes",
|
||||
content="This is a safe note for read_content()",
|
||||
)
|
||||
|
||||
result = await read_content.fn(project=test_project.name, path="notes/meeting")
|
||||
result = await read_content(project=test_project.name, path="notes/meeting")
|
||||
assert result["type"] == "text"
|
||||
assert "safe note" in result["text"]
|
||||
|
||||
@@ -160,7 +160,7 @@ async def test_read_content_allows_safe_path_integration(client, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_content_empty_path_does_not_trigger_security_error(client, test_project):
|
||||
try:
|
||||
result = await read_content.fn(project=test_project.name, path="")
|
||||
result = await read_content(project=test_project.name, path="")
|
||||
if isinstance(result, dict) and result.get("type") == "error":
|
||||
assert "paths must stay within project boundaries" not in result.get("error", "")
|
||||
except ToolError:
|
||||
|
||||
@@ -12,7 +12,7 @@ from basic_memory.utils import normalize_newlines
|
||||
async def test_read_note_by_title(app, test_project):
|
||||
"""Test reading a note by its title."""
|
||||
# First create a note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Special Note",
|
||||
directory="test",
|
||||
@@ -20,14 +20,14 @@ async def test_read_note_by_title(app, test_project):
|
||||
)
|
||||
|
||||
# Should be able to read it by title
|
||||
content = await read_note.fn("Special Note", project=test_project.name)
|
||||
content = await read_note("Special Note", project=test_project.name)
|
||||
assert "Note content here" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_note_title_search_fallback_fetches_by_permalink(monkeypatch, app, test_project):
|
||||
"""Force direct resolve to fail so we exercise the title-search + fetch fallback path."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Fallback Title Note",
|
||||
directory="test",
|
||||
@@ -50,7 +50,7 @@ async def test_read_note_title_search_fallback_fetches_by_permalink(monkeypatch,
|
||||
|
||||
monkeypatch.setattr(clients_mod, "KnowledgeClient", SelectiveKnowledgeClient)
|
||||
|
||||
content = await read_note.fn("Fallback Title Note", project=test_project.name)
|
||||
content = await read_note("Fallback Title Note", project=test_project.name)
|
||||
assert "fallback content" in content
|
||||
|
||||
|
||||
@@ -98,9 +98,9 @@ async def test_read_note_returns_related_results_when_text_search_finds_matches(
|
||||
raise RuntimeError("force fallback")
|
||||
|
||||
monkeypatch.setattr(clients_mod, "KnowledgeClient", FailingKnowledgeClient)
|
||||
monkeypatch.setattr(read_note_module.search_notes, "fn", fake_search_notes_fn)
|
||||
monkeypatch.setattr(read_note_module, "search_notes", fake_search_notes_fn)
|
||||
|
||||
result = await read_note.fn("missing-note", project=test_project.name)
|
||||
result = await read_note("missing-note", project=test_project.name)
|
||||
assert "I couldn't find an exact match" in result
|
||||
assert "## 1. Related One" in result
|
||||
assert "## 2. Related Two" in result
|
||||
@@ -109,7 +109,7 @@ async def test_read_note_returns_related_results_when_text_search_finds_matches(
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_note_title_fallback_requires_exact_title_match(monkeypatch, app, test_project):
|
||||
"""Do not fetch note content when title-search returns only fuzzy matches."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Existing Note",
|
||||
directory="test",
|
||||
@@ -147,9 +147,9 @@ async def test_read_note_title_fallback_requires_exact_title_match(monkeypatch,
|
||||
return {"results": [], "current_page": 1, "page_size": 10}
|
||||
|
||||
monkeypatch.setattr(clients_mod, "KnowledgeClient", StrictFailingKnowledgeClient)
|
||||
monkeypatch.setattr(read_note_module.search_notes, "fn", fake_search_notes_fn)
|
||||
monkeypatch.setattr(read_note_module, "search_notes", fake_search_notes_fn)
|
||||
|
||||
result = await read_note.fn("Missing Exact Title", project=test_project.name)
|
||||
result = await read_note("Missing Exact Title", project=test_project.name)
|
||||
assert "Note Not Found" in result
|
||||
assert "Missing Exact Title" in result
|
||||
assert "existing note content" not in result
|
||||
@@ -159,7 +159,7 @@ async def test_read_note_title_fallback_requires_exact_title_match(monkeypatch,
|
||||
async def test_note_unicode_content(app, test_project):
|
||||
"""Test handling of unicode content in"""
|
||||
content = "# Test 🚀\nThis note has emoji 🎉 and unicode ♠♣♥♦"
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name, title="Unicode Test", directory="test", content=content
|
||||
)
|
||||
|
||||
@@ -171,7 +171,7 @@ async def test_note_unicode_content(app, test_project):
|
||||
assert "checksum:" in result # Checksum exists but may be "unknown"
|
||||
|
||||
# Read back should preserve unicode
|
||||
result = await read_note.fn("test/unicode-test", project=test_project.name)
|
||||
result = await read_note("test/unicode-test", project=test_project.name)
|
||||
assert normalize_newlines(content) in result
|
||||
|
||||
|
||||
@@ -186,13 +186,13 @@ async def test_multiple_notes(app, test_project):
|
||||
]
|
||||
|
||||
for _, title, folder, content, tags in notes_data:
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name, title=title, directory=folder, content=content, tags=tags
|
||||
)
|
||||
|
||||
# Should be able to read each one individually
|
||||
for permalink, title, folder, content, _ in notes_data:
|
||||
note = await read_note.fn(permalink, project=test_project.name)
|
||||
note = await read_note(permalink, project=test_project.name)
|
||||
assert content in note
|
||||
|
||||
# Note: v2 API does not support glob patterns in read_note
|
||||
@@ -211,14 +211,14 @@ async def test_multiple_notes_pagination(app, test_project):
|
||||
]
|
||||
|
||||
for _, title, folder, content, tags in notes_data:
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name, title=title, directory=folder, content=content, tags=tags
|
||||
)
|
||||
|
||||
# Should be able to read each one individually with pagination
|
||||
# Note: pagination now applies to single note content, not multiple notes
|
||||
for permalink, title, folder, content, _ in notes_data:
|
||||
note = await read_note.fn(permalink, page=1, page_size=10, project=test_project.name)
|
||||
note = await read_note(permalink, page=1, page_size=10, project=test_project.name)
|
||||
assert content in note
|
||||
|
||||
# Note: v2 API does not support glob patterns in read_note
|
||||
@@ -235,7 +235,7 @@ async def test_read_note_memory_url(app, test_project):
|
||||
- Return the note content
|
||||
"""
|
||||
# First create a note
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Memory URL Test",
|
||||
directory="test",
|
||||
@@ -245,14 +245,14 @@ async def test_read_note_memory_url(app, test_project):
|
||||
|
||||
# Should be able to read it with a memory:// URL
|
||||
memory_url = "memory://test/memory-url-test"
|
||||
content = await read_note.fn(memory_url, project=test_project.name)
|
||||
content = await read_note(memory_url, project=test_project.name)
|
||||
assert "Testing memory:// URL handling" in content
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_note_memory_url_with_project_prefix(app, test_project):
|
||||
"""Test reading a note using a memory:// URL with explicit project prefix."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Project Prefixed Memory URL Test",
|
||||
directory="test",
|
||||
@@ -260,7 +260,7 @@ async def test_read_note_memory_url_with_project_prefix(app, test_project):
|
||||
)
|
||||
|
||||
memory_url = f"memory://{test_project.name}/test/project-prefixed-memory-url-test"
|
||||
content = await read_note.fn(memory_url)
|
||||
content = await read_note(memory_url)
|
||||
assert "Testing memory:// URL handling with project prefix" in content
|
||||
|
||||
|
||||
@@ -282,7 +282,7 @@ class TestReadNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_identifier in attack_identifiers:
|
||||
result = await read_note.fn(attack_identifier, project=test_project.name)
|
||||
result = await read_note(attack_identifier, project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -304,7 +304,7 @@ class TestReadNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_identifier in attack_identifiers:
|
||||
result = await read_note.fn(attack_identifier, project=test_project.name)
|
||||
result = await read_note(attack_identifier, project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -328,7 +328,7 @@ class TestReadNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_identifier in attack_identifiers:
|
||||
result = await read_note.fn(project=test_project.name, identifier=attack_identifier)
|
||||
result = await read_note(project=test_project.name, identifier=attack_identifier)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -351,7 +351,7 @@ class TestReadNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_identifier in attack_identifiers:
|
||||
result = await read_note.fn(project=test_project.name, identifier=attack_identifier)
|
||||
result = await read_note(project=test_project.name, identifier=attack_identifier)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -372,7 +372,7 @@ class TestReadNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_identifier in attack_identifiers:
|
||||
result = await read_note.fn(project=test_project.name, identifier=attack_identifier)
|
||||
result = await read_note(project=test_project.name, identifier=attack_identifier)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -392,7 +392,7 @@ class TestReadNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_identifier in attack_identifiers:
|
||||
result = await read_note.fn(project=test_project.name, identifier=attack_identifier)
|
||||
result = await read_note(project=test_project.name, identifier=attack_identifier)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -414,7 +414,7 @@ class TestReadNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for safe_identifier in safe_identifiers:
|
||||
result = await read_note.fn(project=test_project.name, identifier=safe_identifier)
|
||||
result = await read_note(project=test_project.name, identifier=safe_identifier)
|
||||
|
||||
assert isinstance(result, str)
|
||||
# Should not contain security error message
|
||||
@@ -428,7 +428,7 @@ class TestReadNoteSecurityValidation:
|
||||
async def test_read_note_allows_legitimate_titles(self, app, test_project):
|
||||
"""Test that legitimate note titles work normally."""
|
||||
# Create a test note first
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Security Test Note",
|
||||
directory="security-tests",
|
||||
@@ -436,7 +436,7 @@ class TestReadNoteSecurityValidation:
|
||||
)
|
||||
|
||||
# Test reading by title (should work)
|
||||
result = await read_note.fn("Security Test Note", project=test_project.name)
|
||||
result = await read_note("Security Test Note", project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
# Should not be a security error
|
||||
@@ -447,7 +447,7 @@ class TestReadNoteSecurityValidation:
|
||||
async def test_read_note_empty_identifier_security(self, app, test_project):
|
||||
"""Test that empty identifier is handled securely."""
|
||||
# Empty identifier should be allowed (may return search results or error, but not security error)
|
||||
result = await read_note.fn(identifier="", project=test_project.name)
|
||||
result = await read_note(identifier="", project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
# Empty identifier should not trigger security error
|
||||
@@ -457,7 +457,7 @@ class TestReadNoteSecurityValidation:
|
||||
async def test_read_note_security_with_all_parameters(self, app, test_project):
|
||||
"""Test security validation works with all read_note parameters."""
|
||||
# Test that security validation is applied even when all other parameters are provided
|
||||
result = await read_note.fn(
|
||||
result = await read_note(
|
||||
project=test_project.name,
|
||||
identifier="../../../etc/malicious",
|
||||
page=1,
|
||||
@@ -473,7 +473,7 @@ class TestReadNoteSecurityValidation:
|
||||
async def test_read_note_security_logging(self, app, caplog, test_project):
|
||||
"""Test that security violations are properly logged."""
|
||||
# Attempt path traversal attack
|
||||
result = await read_note.fn(identifier="../../../etc/passwd", project=test_project.name)
|
||||
result = await read_note(identifier="../../../etc/passwd", project=test_project.name)
|
||||
|
||||
assert "# Error" in result
|
||||
assert "paths must stay within project boundaries" in result
|
||||
@@ -486,7 +486,7 @@ class TestReadNoteSecurityValidation:
|
||||
async def test_read_note_preserves_functionality_with_security(self, app, test_project):
|
||||
"""Test that security validation doesn't break normal note reading functionality."""
|
||||
# Create a note with complex content to ensure security validation doesn't interfere
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Full Feature Security Test Note",
|
||||
directory="security-tests",
|
||||
@@ -510,7 +510,7 @@ class TestReadNoteSecurityValidation:
|
||||
)
|
||||
|
||||
# Test reading by permalink
|
||||
result = await read_note.fn(
|
||||
result = await read_note(
|
||||
"security-tests/full-feature-security-test-note", project=test_project.name
|
||||
)
|
||||
|
||||
@@ -534,7 +534,7 @@ class TestReadNoteSecurityEdgeCases:
|
||||
]
|
||||
|
||||
for attack_identifier in unicode_attack_identifiers:
|
||||
result = await read_note.fn(attack_identifier, project=test_project.name)
|
||||
result = await read_note(attack_identifier, project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -546,7 +546,7 @@ class TestReadNoteSecurityEdgeCases:
|
||||
# Create a very long path traversal attack
|
||||
long_attack_identifier = "../" * 1000 + "etc/malicious"
|
||||
|
||||
result = await read_note.fn(long_attack_identifier, project=test_project.name)
|
||||
result = await read_note(long_attack_identifier, project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -564,7 +564,7 @@ class TestReadNoteSecurityEdgeCases:
|
||||
]
|
||||
|
||||
for attack_identifier in case_attack_identifiers:
|
||||
result = await read_note.fn(attack_identifier, project=test_project.name)
|
||||
result = await read_note(attack_identifier, project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert "# Error" in result
|
||||
@@ -582,7 +582,7 @@ class TestReadNoteSecurityEdgeCases:
|
||||
]
|
||||
|
||||
for attack_identifier in whitespace_attack_identifiers:
|
||||
result = await read_note.fn(attack_identifier, project=test_project.name)
|
||||
result = await read_note(attack_identifier, project=test_project.name)
|
||||
|
||||
assert isinstance(result, str)
|
||||
# The attack should still be blocked even with whitespace
|
||||
|
||||
@@ -37,7 +37,7 @@ async def test_recent_activity_timeframe_formats(client, test_project, test_grap
|
||||
# Test each valid timeframe with project-specific mode
|
||||
for timeframe in valid_timeframes:
|
||||
try:
|
||||
result = await recent_activity.fn(
|
||||
result = await recent_activity(
|
||||
project=test_project.name,
|
||||
type=["entity"],
|
||||
timeframe=timeframe,
|
||||
@@ -52,7 +52,7 @@ async def test_recent_activity_timeframe_formats(client, test_project, test_grap
|
||||
# Test invalid timeframes should raise ValidationError
|
||||
for timeframe in invalid_timeframes:
|
||||
with pytest.raises(ToolError):
|
||||
await recent_activity.fn(project=test_project.name, timeframe=timeframe)
|
||||
await recent_activity(project=test_project.name, timeframe=timeframe)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -60,28 +60,28 @@ async def test_recent_activity_type_filters(client, test_project, test_graph):
|
||||
"""Test that recent_activity correctly filters by types."""
|
||||
|
||||
# Test single string type
|
||||
result = await recent_activity.fn(project=test_project.name, type=SearchItemType.ENTITY)
|
||||
result = await recent_activity(project=test_project.name, type=SearchItemType.ENTITY)
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert "Recent Activity:" in result
|
||||
assert "Recent Notes & Documents" in result
|
||||
|
||||
# Test single string type
|
||||
result = await recent_activity.fn(project=test_project.name, type="entity")
|
||||
result = await recent_activity(project=test_project.name, type="entity")
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert "Recent Activity:" in result
|
||||
assert "Recent Notes & Documents" in result
|
||||
|
||||
# Test single type
|
||||
result = await recent_activity.fn(project=test_project.name, type=["entity"])
|
||||
result = await recent_activity(project=test_project.name, type=["entity"])
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert "Recent Activity:" in result
|
||||
assert "Recent Notes & Documents" in result
|
||||
|
||||
# Test multiple types
|
||||
result = await recent_activity.fn(project=test_project.name, type=["entity", "observation"])
|
||||
result = await recent_activity(project=test_project.name, type=["entity", "observation"])
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
assert "Recent Activity:" in result
|
||||
@@ -89,7 +89,7 @@ async def test_recent_activity_type_filters(client, test_project, test_graph):
|
||||
assert "Recent Notes & Documents" in result or "Recent Observations" in result
|
||||
|
||||
# Test multiple types
|
||||
result = await recent_activity.fn(
|
||||
result = await recent_activity(
|
||||
project=test_project.name, type=[SearchItemType.ENTITY, SearchItemType.OBSERVATION]
|
||||
)
|
||||
assert result is not None
|
||||
@@ -99,7 +99,7 @@ async def test_recent_activity_type_filters(client, test_project, test_graph):
|
||||
assert "Recent Notes & Documents" in result or "Recent Observations" in result
|
||||
|
||||
# Test all types
|
||||
result = await recent_activity.fn(
|
||||
result = await recent_activity(
|
||||
project=test_project.name, type=["entity", "observation", "relation"]
|
||||
)
|
||||
assert result is not None
|
||||
@@ -114,14 +114,14 @@ async def test_recent_activity_type_invalid(client, test_project, test_graph):
|
||||
|
||||
# Test single invalid string type
|
||||
with pytest.raises(ValueError) as e:
|
||||
await recent_activity.fn(project=test_project.name, type="note")
|
||||
await recent_activity(project=test_project.name, type="note")
|
||||
assert (
|
||||
str(e.value) == "Invalid type: note. Valid types are: ['entity', 'observation', 'relation']"
|
||||
)
|
||||
|
||||
# Test invalid string array type
|
||||
with pytest.raises(ValueError) as e:
|
||||
await recent_activity.fn(project=test_project.name, type=["note"])
|
||||
await recent_activity(project=test_project.name, type=["note"])
|
||||
assert (
|
||||
str(e.value) == "Invalid type: note. Valid types are: ['entity', 'observation', 'relation']"
|
||||
)
|
||||
@@ -136,7 +136,7 @@ async def test_recent_activity_discovery_mode(client, test_project, test_graph,
|
||||
config_manager.save_config(cfg)
|
||||
|
||||
# Test discovery mode (no project parameter)
|
||||
result = await recent_activity.fn()
|
||||
result = await recent_activity()
|
||||
assert result is not None
|
||||
assert isinstance(result, str)
|
||||
|
||||
@@ -159,7 +159,7 @@ async def test_recent_activity_discovery_mode_no_activity(client, test_project,
|
||||
cfg.default_project = None
|
||||
config_manager.save_config(cfg)
|
||||
|
||||
result = await recent_activity.fn()
|
||||
result = await recent_activity()
|
||||
assert "Recent Activity Summary" in result
|
||||
assert "No recent activity found in any project." in result
|
||||
|
||||
@@ -178,17 +178,17 @@ async def test_recent_activity_discovery_mode_multiple_active_projects(
|
||||
|
||||
second_root = tmp_path_factory.mktemp("second-project-home")
|
||||
|
||||
result = await create_memory_project.fn(
|
||||
result = await create_memory_project(
|
||||
project_name="second-project",
|
||||
project_path=str(second_root),
|
||||
set_default=False,
|
||||
)
|
||||
assert result.startswith("✓")
|
||||
|
||||
await write_note.fn(project=test_project.name, title="One", directory="notes", content="one")
|
||||
await write_note.fn(project="second-project", title="Two", directory="notes", content="two")
|
||||
await write_note(project=test_project.name, title="One", directory="notes", content="one")
|
||||
await write_note(project="second-project", title="Two", directory="notes", content="two")
|
||||
|
||||
out = await recent_activity.fn()
|
||||
out = await recent_activity()
|
||||
assert "Recent Activity Summary" in out
|
||||
assert "or would you prefer a different project" in out
|
||||
|
||||
@@ -438,7 +438,7 @@ async def test_recent_activity_entity_only_default(client, test_project, test_gr
|
||||
test_graph creates entities with observations and relations, so if all types
|
||||
were returned we'd see observation/relation rows in the JSON output.
|
||||
"""
|
||||
json_result = await recent_activity.fn(project=test_project.name, output_format="json")
|
||||
json_result = await recent_activity(project=test_project.name, output_format="json")
|
||||
assert isinstance(json_result, list)
|
||||
assert len(json_result) > 0
|
||||
# Every item should be an entity — no observations or relations
|
||||
@@ -452,7 +452,7 @@ async def test_recent_activity_explicit_types_returns_requested_types(
|
||||
):
|
||||
"""Explicitly requesting observation/relation types should return those types."""
|
||||
# Request observations only
|
||||
obs_result = await recent_activity.fn(
|
||||
obs_result = await recent_activity(
|
||||
project=test_project.name, type=["observation"], output_format="json"
|
||||
)
|
||||
assert isinstance(obs_result, list)
|
||||
@@ -460,7 +460,7 @@ async def test_recent_activity_explicit_types_returns_requested_types(
|
||||
assert item["type"] == "observation", f"Expected observation type, got type={item['type']}"
|
||||
|
||||
# Request relations only
|
||||
rel_result = await recent_activity.fn(
|
||||
rel_result = await recent_activity(
|
||||
project=test_project.name, type=["relation"], output_format="json"
|
||||
)
|
||||
assert isinstance(rel_result, list)
|
||||
@@ -468,7 +468,7 @@ async def test_recent_activity_explicit_types_returns_requested_types(
|
||||
assert item["type"] == "relation", f"Expected relation type, got type={item['type']}"
|
||||
|
||||
# Request all types explicitly
|
||||
all_result = await recent_activity.fn(
|
||||
all_result = await recent_activity(
|
||||
project=test_project.name,
|
||||
type=["entity", "observation", "relation"],
|
||||
output_format="json",
|
||||
@@ -483,7 +483,7 @@ async def test_recent_activity_explicit_types_returns_requested_types(
|
||||
@pytest.mark.asyncio
|
||||
async def test_recent_activity_pagination_params(client, test_project, test_graph):
|
||||
"""Test that page and page_size params are forwarded correctly."""
|
||||
result = await recent_activity.fn(
|
||||
result = await recent_activity(
|
||||
project=test_project.name,
|
||||
type=["entity"],
|
||||
page=1,
|
||||
@@ -497,19 +497,19 @@ async def test_recent_activity_pagination_params(client, test_project, test_grap
|
||||
async def test_recent_activity_pagination_validation():
|
||||
"""Invalid page/page_size values should raise clear ValueError messages."""
|
||||
with pytest.raises(ValueError, match="page must be >= 1, got 0"):
|
||||
await recent_activity.fn(page=0)
|
||||
await recent_activity(page=0)
|
||||
|
||||
with pytest.raises(ValueError, match="page must be >= 1, got -1"):
|
||||
await recent_activity.fn(page=-1)
|
||||
await recent_activity(page=-1)
|
||||
|
||||
with pytest.raises(ValueError, match="page_size must be >= 1, got 0"):
|
||||
await recent_activity.fn(page_size=0)
|
||||
await recent_activity(page_size=0)
|
||||
|
||||
with pytest.raises(ValueError, match="page_size must be >= 1, got -5"):
|
||||
await recent_activity.fn(page_size=-5)
|
||||
await recent_activity(page_size=-5)
|
||||
|
||||
with pytest.raises(ValueError, match="page_size must be <= 100, got 999"):
|
||||
await recent_activity.fn(page_size=999)
|
||||
await recent_activity(page_size=999)
|
||||
|
||||
|
||||
def test_format_project_output_has_more_pagination_guidance():
|
||||
|
||||
@@ -25,7 +25,7 @@ async def test_read_file_text_file(app, synced_files, test_project):
|
||||
- Include correct metadata
|
||||
"""
|
||||
# First create a text file via notes
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Text Resource",
|
||||
directory="test",
|
||||
@@ -35,7 +35,7 @@ async def test_read_file_text_file(app, synced_files, test_project):
|
||||
assert result is not None
|
||||
|
||||
# Now read it as a resource
|
||||
response = await read_content.fn("test/text-resource", project=test_project.name)
|
||||
response = await read_content("test/text-resource", project=test_project.name)
|
||||
|
||||
assert response["type"] == "text"
|
||||
assert "This is a test text resource" in response["text"]
|
||||
@@ -53,7 +53,7 @@ async def test_read_content_file_path(app, synced_files, test_project):
|
||||
- Include correct metadata
|
||||
"""
|
||||
# First create a text file via notes
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Text Resource",
|
||||
directory="test",
|
||||
@@ -63,7 +63,7 @@ async def test_read_content_file_path(app, synced_files, test_project):
|
||||
assert result is not None
|
||||
|
||||
# Now read it as a resource
|
||||
response = await read_content.fn("test/Text Resource.md", project=test_project.name)
|
||||
response = await read_content("test/Text Resource.md", project=test_project.name)
|
||||
|
||||
assert response["type"] == "text"
|
||||
assert "This is a test text resource" in response["text"]
|
||||
@@ -84,7 +84,7 @@ async def test_read_file_image_file(app, synced_files, test_project):
|
||||
image_path = synced_files["image"].name
|
||||
|
||||
# Read it as a resource
|
||||
response = await read_content.fn(image_path, project=test_project.name)
|
||||
response = await read_content(image_path, project=test_project.name)
|
||||
|
||||
assert response["type"] == "image"
|
||||
assert response["source"]["type"] == "base64"
|
||||
@@ -112,7 +112,7 @@ async def test_read_file_pdf_file(app, synced_files, test_project):
|
||||
pdf_path = synced_files["pdf"].name
|
||||
|
||||
# Read it as a resource
|
||||
response = await read_content.fn(pdf_path, project=test_project.name)
|
||||
response = await read_content(pdf_path, project=test_project.name)
|
||||
|
||||
assert response["type"] == "document"
|
||||
assert response["source"]["type"] == "base64"
|
||||
@@ -128,14 +128,14 @@ async def test_read_file_pdf_file(app, synced_files, test_project):
|
||||
async def test_read_file_not_found(app, test_project):
|
||||
"""Test trying to read a non-existent"""
|
||||
with pytest.raises(ToolError, match="Resource not found"):
|
||||
await read_content.fn("does-not-exist", project=test_project.name)
|
||||
await read_content("does-not-exist", project=test_project.name)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_file_memory_url(app, synced_files, test_project):
|
||||
"""Test reading a resource using a memory:// URL."""
|
||||
# Create a text file via notes
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Memory URL Test",
|
||||
directory="test",
|
||||
@@ -144,7 +144,7 @@ async def test_read_file_memory_url(app, synced_files, test_project):
|
||||
|
||||
# Read it with a memory:// URL
|
||||
memory_url = "memory://test/memory-url-test"
|
||||
response = await read_content.fn(memory_url, project=test_project.name)
|
||||
response = await read_content(memory_url, project=test_project.name)
|
||||
|
||||
assert response["type"] == "text"
|
||||
assert "Testing memory:// URL handling for resources" in response["text"]
|
||||
@@ -153,7 +153,7 @@ async def test_read_file_memory_url(app, synced_files, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_file_memory_url_with_project_prefix(app, synced_files, test_project):
|
||||
"""Test reading a resource using a memory:// URL with explicit project prefix."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Project Prefixed Resource URL Test",
|
||||
directory="test",
|
||||
@@ -161,7 +161,7 @@ async def test_read_file_memory_url_with_project_prefix(app, synced_files, test_
|
||||
)
|
||||
|
||||
memory_url = f"memory://{test_project.name}/test/project-prefixed-resource-url-test"
|
||||
response = await read_content.fn(memory_url)
|
||||
response = await read_content(memory_url)
|
||||
|
||||
assert response["type"] == "text"
|
||||
assert "Testing memory:// URL handling for resources with project prefix" in response["text"]
|
||||
@@ -225,7 +225,7 @@ async def test_image_conversion(app, synced_files, test_project):
|
||||
image_path = synced_files["image"].name
|
||||
|
||||
# Test reading the resource
|
||||
response = await read_content.fn(image_path, project=test_project.name)
|
||||
response = await read_content(image_path, project=test_project.name)
|
||||
|
||||
assert response["type"] == "image"
|
||||
assert response["source"]["media_type"] == "image/jpeg"
|
||||
|
||||
@@ -77,7 +77,7 @@ async def test_schema_validate_by_type(app, test_project, sync_service):
|
||||
# Sync so the database picks up the files
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
result = await schema_validate.fn(
|
||||
result = await schema_validate(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -100,7 +100,7 @@ async def test_schema_validate_by_identifier(app, test_project, sync_service):
|
||||
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
result = await schema_validate.fn(
|
||||
result = await schema_validate(
|
||||
identifier="people/alice",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -123,7 +123,7 @@ async def test_schema_infer(app, test_project, sync_service):
|
||||
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
result = await schema_infer.fn(
|
||||
result = await schema_infer(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -162,7 +162,7 @@ permalink: people/dave
|
||||
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
result = await schema_diff.fn(
|
||||
result = await schema_diff(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -191,7 +191,7 @@ async def test_write_note_metadata_creates_schema_note(app, test_project, sync_s
|
||||
)
|
||||
|
||||
# 2. Create the schema note via write_note with metadata
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
title="Person",
|
||||
directory="schemas",
|
||||
note_type="schema",
|
||||
@@ -209,7 +209,7 @@ async def test_write_note_metadata_creates_schema_note(app, test_project, sync_s
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
# 4. Validate — schema_validate should find the schema and validate person notes
|
||||
result = await schema_validate.fn(
|
||||
result = await schema_validate(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -274,7 +274,7 @@ permalink: employees/{name.lower()}
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
# Validate — must find "Employee Schema" via entity_metadata['entity'] == "employee"
|
||||
result = await schema_validate.fn(
|
||||
result = await schema_validate(
|
||||
note_type="employee",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -323,7 +323,7 @@ permalink: things/{name}
|
||||
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
result = await schema_infer.fn(
|
||||
result = await schema_infer(
|
||||
note_type="widget",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -341,7 +341,7 @@ permalink: things/{name}
|
||||
@pytest.mark.asyncio
|
||||
async def test_schema_validate_no_notes_returns_guidance(app, test_project, sync_service):
|
||||
"""When no notes of the requested type exist, return guidance on creating notes."""
|
||||
result = await schema_validate.fn(
|
||||
result = await schema_validate(
|
||||
note_type="employee",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -369,7 +369,7 @@ async def test_schema_validate_no_schema_returns_guidance(app, test_project, syn
|
||||
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
result = await schema_validate.fn(
|
||||
result = await schema_validate(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -396,7 +396,7 @@ async def test_schema_diff_no_schema_returns_guidance(app, test_project, sync_se
|
||||
|
||||
await sync_service.sync(project_path)
|
||||
|
||||
result = await schema_diff.fn(
|
||||
result = await schema_diff(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -418,7 +418,7 @@ async def test_schema_validate_error_returns_guidance(app, test_project):
|
||||
mock_validate = AsyncMock(side_effect=RuntimeError("connection lost"))
|
||||
|
||||
with patch("basic_memory.mcp.clients.schema.SchemaClient.validate", mock_validate):
|
||||
result = await schema_validate.fn(
|
||||
result = await schema_validate(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -434,7 +434,7 @@ async def test_schema_infer_error_returns_guidance(app, test_project):
|
||||
mock_infer = AsyncMock(side_effect=RuntimeError("db unavailable"))
|
||||
|
||||
with patch("basic_memory.mcp.clients.schema.SchemaClient.infer", mock_infer):
|
||||
result = await schema_infer.fn(
|
||||
result = await schema_infer(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
@@ -450,7 +450,7 @@ async def test_schema_diff_error_returns_guidance(app, test_project):
|
||||
mock_diff = AsyncMock(side_effect=RuntimeError("network error"))
|
||||
|
||||
with patch("basic_memory.mcp.clients.schema.SchemaClient.diff", mock_diff):
|
||||
result = await schema_diff.fn(
|
||||
result = await schema_diff(
|
||||
note_type="person",
|
||||
project=test_project.name,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from basic_memory.schemas.search import SearchItemType, SearchResponse
|
||||
async def test_search_text(client, test_project):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Search Note",
|
||||
directory="test",
|
||||
@@ -23,7 +23,7 @@ async def test_search_text(client, test_project):
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
response = await search_notes.fn(project=test_project.name, query="searchable")
|
||||
response = await search_notes(project=test_project.name, query="searchable")
|
||||
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
@@ -41,7 +41,7 @@ async def test_search_text(client, test_project):
|
||||
async def test_search_title(client, test_project):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Search Note",
|
||||
directory="test",
|
||||
@@ -51,7 +51,7 @@ async def test_search_title(client, test_project):
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
response = await search_notes.fn(
|
||||
response = await search_notes(
|
||||
project=test_project.name, query="Search Note", search_type="title"
|
||||
)
|
||||
|
||||
@@ -71,7 +71,7 @@ async def test_search_title(client, test_project):
|
||||
async def test_search_permalink(client, test_project):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Search Note",
|
||||
directory="test",
|
||||
@@ -81,7 +81,7 @@ async def test_search_permalink(client, test_project):
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
response = await search_notes.fn(
|
||||
response = await search_notes(
|
||||
project=test_project.name,
|
||||
query=f"{test_project.name}/test/test-search-note",
|
||||
search_type="permalink",
|
||||
@@ -103,7 +103,7 @@ async def test_search_permalink(client, test_project):
|
||||
async def test_search_permalink_match(client, test_project):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Search Note",
|
||||
directory="test",
|
||||
@@ -113,7 +113,7 @@ async def test_search_permalink_match(client, test_project):
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
response = await search_notes.fn(
|
||||
response = await search_notes(
|
||||
project=test_project.name,
|
||||
query=f"{test_project.name}/test/test-search-*",
|
||||
search_type="permalink",
|
||||
@@ -134,7 +134,7 @@ async def test_search_permalink_match(client, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_memory_url_with_project_prefix(client, test_project):
|
||||
"""Test searching with a memory:// URL that includes the project prefix."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Memory URL Search Note",
|
||||
directory="test",
|
||||
@@ -142,9 +142,7 @@ async def test_search_memory_url_with_project_prefix(client, test_project):
|
||||
)
|
||||
assert result
|
||||
|
||||
response = await search_notes.fn(
|
||||
query=f"memory://{test_project.name}/test/memory-url-search-note"
|
||||
)
|
||||
response = await search_notes(query=f"memory://{test_project.name}/test/memory-url-search-note")
|
||||
|
||||
if isinstance(response, SearchResponse):
|
||||
assert len(response.results) > 0
|
||||
@@ -160,7 +158,7 @@ async def test_search_memory_url_with_project_prefix(client, test_project):
|
||||
async def test_search_pagination(client, test_project):
|
||||
"""Test basic search functionality."""
|
||||
# Create a test note
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Search Note",
|
||||
directory="test",
|
||||
@@ -170,7 +168,7 @@ async def test_search_pagination(client, test_project):
|
||||
assert result
|
||||
|
||||
# Search for it
|
||||
response = await search_notes.fn(
|
||||
response = await search_notes(
|
||||
project=test_project.name, query="searchable", page=1, page_size=1
|
||||
)
|
||||
|
||||
@@ -190,7 +188,7 @@ async def test_search_pagination(client, test_project):
|
||||
async def test_search_with_type_filter(client, test_project):
|
||||
"""Test search with entity type filter."""
|
||||
# Create test content
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Entity Type Test",
|
||||
directory="test",
|
||||
@@ -198,7 +196,7 @@ async def test_search_with_type_filter(client, test_project):
|
||||
)
|
||||
|
||||
# Search with type filter
|
||||
response = await search_notes.fn(project=test_project.name, query="type", types=["note"])
|
||||
response = await search_notes(project=test_project.name, query="type", types=["note"])
|
||||
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
@@ -213,7 +211,7 @@ async def test_search_with_type_filter(client, test_project):
|
||||
async def test_search_with_entity_type_filter(client, test_project):
|
||||
"""Test search with entity type filter."""
|
||||
# Create test content
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Entity Type Test",
|
||||
directory="test",
|
||||
@@ -221,9 +219,7 @@ async def test_search_with_entity_type_filter(client, test_project):
|
||||
)
|
||||
|
||||
# Search with entity type filter
|
||||
response = await search_notes.fn(
|
||||
project=test_project.name, query="type", entity_types=["entity"]
|
||||
)
|
||||
response = await search_notes(project=test_project.name, query="type", entity_types=["entity"])
|
||||
|
||||
# Verify results - handle both success and error cases
|
||||
if isinstance(response, SearchResponse):
|
||||
@@ -238,7 +234,7 @@ async def test_search_with_entity_type_filter(client, test_project):
|
||||
async def test_search_with_date_filter(client, test_project):
|
||||
"""Test search with date filter."""
|
||||
# Create test content
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Recent Note",
|
||||
directory="test",
|
||||
@@ -247,7 +243,7 @@ async def test_search_with_date_filter(client, test_project):
|
||||
|
||||
# Search with date filter
|
||||
one_hour_ago = datetime.now() - timedelta(hours=1)
|
||||
response = await search_notes.fn(
|
||||
response = await search_notes(
|
||||
project=test_project.name, query="recent", after_date=one_hour_ago.isoformat()
|
||||
)
|
||||
|
||||
@@ -385,7 +381,7 @@ class TestSearchToolErrorHandling:
|
||||
# Patch at the clients module level where the import happens
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
result = await search_mod.search_notes.fn(project="test-project", query="test query")
|
||||
result = await search_mod.search_notes(project="test-project", query="test query")
|
||||
assert isinstance(result, str)
|
||||
assert "# Search Failed - Invalid Syntax" in result
|
||||
|
||||
@@ -423,7 +419,7 @@ class TestSearchToolErrorHandling:
|
||||
# Patch at the clients module level where the import happens
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
result = await search_mod.search_notes.fn(project="test-project", query="test query")
|
||||
result = await search_mod.search_notes(project="test-project", query="test query")
|
||||
assert isinstance(result, str)
|
||||
assert "# Search Failed - Access Error" in result
|
||||
|
||||
@@ -466,7 +462,7 @@ async def test_search_notes_sets_retrieval_mode_for_semantic_types(monkeypatch,
|
||||
monkeypatch.setattr(search_mod, "resolve_project_and_path", fake_resolve_project_and_path)
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
result = await search_mod.search_notes.fn(
|
||||
result = await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="semantic lookup",
|
||||
search_type=search_type,
|
||||
@@ -517,7 +513,7 @@ async def test_search_notes_passes_metadata_filters(monkeypatch):
|
||||
monkeypatch.setattr(search_mod, "resolve_project_and_path", fake_resolve_project_and_path)
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
await search_mod.search_notes.fn(
|
||||
await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="test",
|
||||
metadata_filters={"status": "active"},
|
||||
@@ -564,7 +560,7 @@ async def test_search_by_metadata_basic(monkeypatch):
|
||||
monkeypatch.setattr(search_mod, "get_project_client", fake_get_project_client)
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
result = await search_by_metadata.fn(
|
||||
result = await search_by_metadata(
|
||||
filters={"status": "in-progress"},
|
||||
project="test-project",
|
||||
limit=10,
|
||||
@@ -581,7 +577,7 @@ async def test_search_by_metadata_limit_zero():
|
||||
"""search_by_metadata rejects limit <= 0 with error string."""
|
||||
from basic_memory.mcp.tools.search import search_by_metadata
|
||||
|
||||
result = await search_by_metadata.fn(
|
||||
result = await search_by_metadata(
|
||||
filters={"status": "active"},
|
||||
limit=0,
|
||||
)
|
||||
@@ -635,7 +631,7 @@ async def test_search_by_metadata_offset_within_page(monkeypatch):
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
# offset=2, limit=5 → page=1, offset_within_page=2
|
||||
result = await search_by_metadata.fn(
|
||||
result = await search_by_metadata(
|
||||
filters={"status": "active"},
|
||||
project="test-project",
|
||||
limit=5,
|
||||
@@ -675,7 +671,7 @@ async def test_search_by_metadata_error_handling(monkeypatch):
|
||||
monkeypatch.setattr(search_mod, "get_project_client", fake_get_project_client)
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
result = await search_by_metadata.fn(
|
||||
result = await search_by_metadata(
|
||||
filters={"status": "active"},
|
||||
project="test-project",
|
||||
)
|
||||
@@ -716,7 +712,7 @@ async def test_search_notes_invalid_search_type_returns_error(monkeypatch):
|
||||
monkeypatch.setattr(search_mod, "resolve_project_and_path", fake_resolve_project_and_path)
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
result = await search_mod.search_notes.fn(
|
||||
result = await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="test query",
|
||||
search_type="bogus",
|
||||
@@ -763,7 +759,7 @@ async def test_search_notes_passes_min_similarity(monkeypatch):
|
||||
monkeypatch.setattr(search_mod, "resolve_project_and_path", fake_resolve_project_and_path)
|
||||
monkeypatch.setattr(clients_mod, "SearchClient", MockSearchClient)
|
||||
|
||||
await search_mod.search_notes.fn(
|
||||
await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="test",
|
||||
search_type="vector",
|
||||
@@ -825,7 +821,7 @@ async def test_search_notes_defaults_to_hybrid_when_semantic_enabled(monkeypatch
|
||||
|
||||
monkeypatch.setattr(search_mod, "get_container", lambda: StubContainer())
|
||||
|
||||
await search_mod.search_notes.fn(
|
||||
await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="test query",
|
||||
)
|
||||
@@ -886,7 +882,7 @@ async def test_search_notes_defaults_to_fts_when_semantic_disabled(monkeypatch):
|
||||
|
||||
monkeypatch.setattr(search_mod, "get_container", lambda: StubContainer())
|
||||
|
||||
await search_mod.search_notes.fn(
|
||||
await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="test query",
|
||||
)
|
||||
@@ -946,7 +942,7 @@ async def test_search_notes_explicit_text_stays_fts_when_semantic_enabled(monkey
|
||||
|
||||
monkeypatch.setattr(search_mod, "get_container", lambda: StubContainer())
|
||||
|
||||
await search_mod.search_notes.fn(
|
||||
await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="test query",
|
||||
search_type="text",
|
||||
@@ -1006,7 +1002,7 @@ async def test_search_notes_defaults_to_hybrid_when_container_not_initialized(mo
|
||||
)(),
|
||||
)
|
||||
|
||||
await search_mod.search_notes.fn(
|
||||
await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="test query",
|
||||
)
|
||||
@@ -1067,7 +1063,7 @@ async def test_search_notes_defaults_to_fts_when_container_not_initialized_and_s
|
||||
)(),
|
||||
)
|
||||
|
||||
await search_mod.search_notes.fn(
|
||||
await search_mod.search_notes(
|
||||
project="test-project",
|
||||
query="test query",
|
||||
)
|
||||
|
||||
@@ -11,7 +11,7 @@ from basic_memory.mcp.tools import write_note, view_note
|
||||
async def test_view_note_basic_functionality(app, test_project):
|
||||
"""Test viewing a note creates an artifact."""
|
||||
# First create a note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test View Note",
|
||||
directory="test",
|
||||
@@ -19,7 +19,7 @@ async def test_view_note_basic_functionality(app, test_project):
|
||||
)
|
||||
|
||||
# View the note
|
||||
result = await view_note.fn("Test View Note", project=test_project.name)
|
||||
result = await view_note("Test View Note", project=test_project.name)
|
||||
|
||||
# Should contain note retrieval message
|
||||
assert 'Note retrieved: "Test View Note"' in result
|
||||
@@ -47,12 +47,12 @@ async def test_view_note_with_frontmatter_title(app, test_project):
|
||||
Content with frontmatter title.
|
||||
""").strip()
|
||||
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name, title="Frontmatter Title", directory="test", content=content
|
||||
)
|
||||
|
||||
# View the note
|
||||
result = await view_note.fn("Frontmatter Title", project=test_project.name)
|
||||
result = await view_note("Frontmatter Title", project=test_project.name)
|
||||
|
||||
# Should show title in retrieval message
|
||||
assert 'Note retrieved: "Frontmatter Title"' in result
|
||||
@@ -65,12 +65,12 @@ async def test_view_note_with_heading_title(app, test_project):
|
||||
# Create note with heading but no frontmatter title
|
||||
content = "# Heading Title\n\nContent with heading title."
|
||||
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name, title="Heading Title", directory="test", content=content
|
||||
)
|
||||
|
||||
# View the note
|
||||
result = await view_note.fn("Heading Title", project=test_project.name)
|
||||
result = await view_note("Heading Title", project=test_project.name)
|
||||
|
||||
# Should show title in retrieval message
|
||||
assert 'Note retrieved: "Heading Title"' in result
|
||||
@@ -82,12 +82,12 @@ async def test_view_note_unicode_content(app, test_project):
|
||||
"""Test viewing a note with Unicode content."""
|
||||
content = "# Unicode Test 🚀\n\nThis note has emoji 🎉 and unicode ♠♣♥♦"
|
||||
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name, title="Unicode Test 🚀", directory="test", content=content
|
||||
)
|
||||
|
||||
# View the note
|
||||
result = await view_note.fn("Unicode Test 🚀", project=test_project.name)
|
||||
result = await view_note("Unicode Test 🚀", project=test_project.name)
|
||||
|
||||
# Should handle Unicode properly
|
||||
assert "🚀" in result
|
||||
@@ -99,7 +99,7 @@ async def test_view_note_unicode_content(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_view_note_by_permalink(app, test_project):
|
||||
"""Test viewing a note by its permalink."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Permalink Test",
|
||||
directory="test",
|
||||
@@ -107,7 +107,7 @@ async def test_view_note_by_permalink(app, test_project):
|
||||
)
|
||||
|
||||
# View by permalink
|
||||
result = await view_note.fn("test/permalink-test", project=test_project.name)
|
||||
result = await view_note("test/permalink-test", project=test_project.name)
|
||||
|
||||
# Should work with permalink
|
||||
assert 'Note retrieved: "test/permalink-test"' in result
|
||||
@@ -118,7 +118,7 @@ async def test_view_note_by_permalink(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_view_note_with_memory_url(app, test_project):
|
||||
"""Test viewing a note using a memory:// URL."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Memory URL Test",
|
||||
directory="test",
|
||||
@@ -126,7 +126,7 @@ async def test_view_note_with_memory_url(app, test_project):
|
||||
)
|
||||
|
||||
# View with memory:// URL
|
||||
result = await view_note.fn("memory://test/memory-url-test", project=test_project.name)
|
||||
result = await view_note("memory://test/memory-url-test", project=test_project.name)
|
||||
|
||||
# Should work with memory:// URL
|
||||
assert 'Note retrieved: "memory://test/memory-url-test"' in result
|
||||
@@ -138,7 +138,7 @@ async def test_view_note_with_memory_url(app, test_project):
|
||||
async def test_view_note_not_found(app, test_project):
|
||||
"""Test viewing a non-existent note returns error without artifact."""
|
||||
# Try to view non-existent note
|
||||
result = await view_note.fn("NonExistent Note", project=test_project.name)
|
||||
result = await view_note("NonExistent Note", project=test_project.name)
|
||||
|
||||
# Should return error message without artifact instructions
|
||||
assert "# Note Not Found" in result
|
||||
@@ -151,7 +151,7 @@ async def test_view_note_not_found(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_view_note_pagination(app, test_project):
|
||||
"""Test viewing a note with pagination parameters."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Pagination Test",
|
||||
directory="test",
|
||||
@@ -159,7 +159,7 @@ async def test_view_note_pagination(app, test_project):
|
||||
)
|
||||
|
||||
# View with pagination
|
||||
result = await view_note.fn("Pagination Test", page=1, page_size=5, project=test_project.name)
|
||||
result = await view_note("Pagination Test", page=1, page_size=5, project=test_project.name)
|
||||
|
||||
# Should work with pagination
|
||||
assert 'Note retrieved: "Pagination Test"' in result
|
||||
@@ -170,7 +170,7 @@ async def test_view_note_pagination(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_view_note_project_parameter(app, test_project):
|
||||
"""Test viewing a note with project parameter."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Project Test",
|
||||
directory="test",
|
||||
@@ -178,7 +178,7 @@ async def test_view_note_project_parameter(app, test_project):
|
||||
)
|
||||
|
||||
# View with explicit project
|
||||
result = await view_note.fn("Project Test", project=test_project.name)
|
||||
result = await view_note("Project Test", project=test_project.name)
|
||||
|
||||
# Should work with project parameter
|
||||
assert 'Note retrieved: "Project Test"' in result
|
||||
@@ -190,16 +190,16 @@ async def test_view_note_project_parameter(app, test_project):
|
||||
async def test_view_note_artifact_identifier_unique(app, test_project):
|
||||
"""Test that different notes are retrieved correctly with unique identifiers."""
|
||||
# Create two notes
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name, title="Note One", directory="test", content="Content one"
|
||||
)
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name, title="Note Two", directory="test", content="Content two"
|
||||
)
|
||||
|
||||
# View both notes
|
||||
result1 = await view_note.fn("Note One", project=test_project.name)
|
||||
result2 = await view_note.fn("Note Two", project=test_project.name)
|
||||
result1 = await view_note("Note One", project=test_project.name)
|
||||
result2 = await view_note("Note Two", project=test_project.name)
|
||||
|
||||
# Should have different note identifiers in retrieval messages
|
||||
assert 'Note retrieved: "Note One"' in result1
|
||||
@@ -212,7 +212,7 @@ async def test_view_note_artifact_identifier_unique(app, test_project):
|
||||
async def test_view_note_fallback_identifier_as_title(app, test_project):
|
||||
"""Test that view_note uses identifier as title when no title is extractable."""
|
||||
# Create a note with no clear title structure
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Simple Note",
|
||||
directory="test",
|
||||
@@ -220,7 +220,7 @@ async def test_view_note_fallback_identifier_as_title(app, test_project):
|
||||
)
|
||||
|
||||
# View the note
|
||||
result = await view_note.fn("Simple Note", project=test_project.name)
|
||||
result = await view_note("Simple Note", project=test_project.name)
|
||||
|
||||
# Should use identifier as title in retrieval message
|
||||
assert 'Note retrieved: "Simple Note"' in result
|
||||
@@ -230,7 +230,7 @@ async def test_view_note_fallback_identifier_as_title(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_view_note_direct_success(app, test_project):
|
||||
"""Direct permalink lookup should succeed without mocks via the full integration path."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -238,6 +238,6 @@ async def test_view_note_direct_success(app, test_project):
|
||||
)
|
||||
|
||||
# This should take the direct permalink path (no title search needed).
|
||||
result = await view_note.fn("test/test-note", project=test_project.name)
|
||||
result = await view_note("test/test-note", project=test_project.name)
|
||||
assert 'Note retrieved: "test/test-note"' in result
|
||||
assert "This is a test note." in result
|
||||
|
||||
@@ -10,10 +10,10 @@ class _ContextState:
|
||||
def __init__(self):
|
||||
self._state: dict[str, object] = {}
|
||||
|
||||
def get_state(self, key: str):
|
||||
async def get_state(self, key: str):
|
||||
return self._state.get(key)
|
||||
|
||||
def set_state(self, key: str, value: object) -> None:
|
||||
async def set_state(self, key: str, value: object, **kwargs) -> None:
|
||||
self._state[key] = value
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ async def test_list_workspaces_formats_workspace_rows(monkeypatch):
|
||||
fake_get_available_workspaces,
|
||||
)
|
||||
|
||||
result = await list_workspaces.fn()
|
||||
result = await list_workspaces()
|
||||
assert "# Available Workspaces (2)" in result
|
||||
assert "Personal (type=personal, role=owner" in result
|
||||
assert "Team (type=organization, role=editor" in result
|
||||
@@ -56,7 +56,7 @@ async def test_list_workspaces_handles_empty_list(monkeypatch):
|
||||
fake_get_available_workspaces,
|
||||
)
|
||||
|
||||
result = await list_workspaces.fn()
|
||||
result = await list_workspaces()
|
||||
assert "# No Workspaces Available" in result
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ async def test_list_workspaces_oauth_error_bubbles_up(monkeypatch):
|
||||
)
|
||||
|
||||
with pytest.raises(RuntimeError, match="Workspace discovery requires OAuth login"):
|
||||
await list_workspaces.fn()
|
||||
await list_workspaces()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -87,11 +87,11 @@ async def test_list_workspaces_uses_context_cache_path(monkeypatch):
|
||||
|
||||
async def fake_get_available_workspaces(context=None):
|
||||
assert context is not None
|
||||
cached = context.get_state("available_workspaces")
|
||||
cached = await context.get_state("available_workspaces")
|
||||
if cached:
|
||||
return cached
|
||||
call_count["fetches"] += 1
|
||||
context.set_state("available_workspaces", [workspace])
|
||||
await context.set_state("available_workspaces", [workspace])
|
||||
return [workspace]
|
||||
|
||||
monkeypatch.setattr(
|
||||
@@ -99,8 +99,8 @@ async def test_list_workspaces_uses_context_cache_path(monkeypatch):
|
||||
fake_get_available_workspaces,
|
||||
)
|
||||
|
||||
first = await list_workspaces.fn(context=context)
|
||||
second = await list_workspaces.fn(context=context)
|
||||
first = await list_workspaces(context=context)
|
||||
second = await list_workspaces(context=context)
|
||||
|
||||
assert "# Available Workspaces (1)" in first
|
||||
assert "# Available Workspaces (1)" in second
|
||||
|
||||
@@ -17,7 +17,7 @@ async def test_write_note(app, test_project):
|
||||
- Handle tags correctly
|
||||
- Return valid permalink
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -35,7 +35,7 @@ async def test_write_note(app, test_project):
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Try reading it back via permalink
|
||||
content = await read_note.fn("test/test-note", project=test_project.name)
|
||||
content = await read_note("test/test-note", project=test_project.name)
|
||||
expected = normalize_newlines(
|
||||
dedent("""
|
||||
---
|
||||
@@ -59,7 +59,7 @@ async def test_write_note(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_no_tags(app, test_project):
|
||||
"""Test creating a note without tags."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name, title="Simple Note", directory="test", content="Just some text"
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ async def test_write_note_no_tags(app, test_project):
|
||||
assert f"permalink: {test_project.name}/test/simple-note" in result
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
# Should be able to read it back
|
||||
content = await read_note.fn("test/simple-note", project=test_project.name)
|
||||
content = await read_note("test/simple-note", project=test_project.name)
|
||||
expected = normalize_newlines(
|
||||
dedent("""
|
||||
---
|
||||
@@ -97,7 +97,7 @@ async def test_write_note_update_existing(app, test_project):
|
||||
- Handle tags correctly
|
||||
- Return valid permalink
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -114,7 +114,7 @@ async def test_write_note_update_existing(app, test_project):
|
||||
assert "- test, documentation" in result
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -130,7 +130,7 @@ async def test_write_note_update_existing(app, test_project):
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Try reading it back
|
||||
content = await read_note.fn("test/test-note", project=test_project.name)
|
||||
content = await read_note("test/test-note", project=test_project.name)
|
||||
assert (
|
||||
normalize_newlines(
|
||||
dedent(
|
||||
@@ -172,7 +172,7 @@ async def test_issue_93_write_note_respects_custom_permalink_new_note(app, test_
|
||||
- [note] Testing if custom permalink is respected
|
||||
""").strip()
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="My New Note",
|
||||
directory="notes",
|
||||
@@ -192,7 +192,7 @@ async def test_issue_93_write_note_respects_custom_permalink_existing_note(app,
|
||||
"""Test that write_note respects custom permalinks when updating existing notes (Issue #93)"""
|
||||
|
||||
# Step 1: Create initial note (auto-generated permalink)
|
||||
result1 = await write_note.fn(
|
||||
result1 = await write_note(
|
||||
project=test_project.name,
|
||||
title="Existing Note",
|
||||
directory="test",
|
||||
@@ -224,7 +224,7 @@ async def test_issue_93_write_note_respects_custom_permalink_existing_note(app,
|
||||
- [note] Custom permalink should be respected on update
|
||||
""").strip()
|
||||
|
||||
result2 = await write_note.fn(
|
||||
result2 = await write_note(
|
||||
project=test_project.name,
|
||||
title="Existing Note",
|
||||
directory="test",
|
||||
@@ -248,7 +248,7 @@ async def test_delete_note_existing(app, test_project):
|
||||
- Return valid permalink
|
||||
- Delete the note
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -259,7 +259,7 @@ async def test_delete_note_existing(app, test_project):
|
||||
assert result
|
||||
assert f"project: {test_project.name}" in result
|
||||
|
||||
deleted = await delete_note.fn("test/test-note", project=test_project.name)
|
||||
deleted = await delete_note("test/test-note", project=test_project.name)
|
||||
assert deleted is True
|
||||
|
||||
|
||||
@@ -271,7 +271,7 @@ async def test_delete_note_doesnt_exist(app, test_project):
|
||||
- Delete the note
|
||||
- verify returns false
|
||||
"""
|
||||
deleted = await delete_note.fn("doesnt-exist", project=test_project.name)
|
||||
deleted = await delete_note("doesnt-exist", project=test_project.name)
|
||||
assert deleted is False
|
||||
|
||||
|
||||
@@ -292,7 +292,7 @@ async def test_write_note_with_tag_array_from_bug_report(app, test_project):
|
||||
}
|
||||
|
||||
# Try to call the function with this data directly
|
||||
result = await write_note.fn(**bug_payload)
|
||||
result = await write_note(**bug_payload)
|
||||
|
||||
assert result
|
||||
assert f"project: {test_project.name}" in result
|
||||
@@ -312,7 +312,7 @@ async def test_write_note_verbose(app, test_project):
|
||||
- Handle tags correctly
|
||||
- Return valid permalink
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -351,7 +351,7 @@ async def test_write_note_preserves_custom_metadata(app, project_config, test_pr
|
||||
- Verify custom frontmatter is preserved
|
||||
"""
|
||||
# First, create a note with custom metadata using write_note
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Custom Metadata Note",
|
||||
directory="test",
|
||||
@@ -360,7 +360,7 @@ async def test_write_note_preserves_custom_metadata(app, project_config, test_pr
|
||||
)
|
||||
|
||||
# Read the note to get its permalink
|
||||
content = await read_note.fn("test/custom-metadata-note", project=test_project.name)
|
||||
content = await read_note("test/custom-metadata-note", project=test_project.name)
|
||||
|
||||
# Now directly update the file with custom frontmatter
|
||||
# We need to use a direct file update to add custom frontmatter
|
||||
@@ -379,7 +379,7 @@ async def test_write_note_preserves_custom_metadata(app, project_config, test_pr
|
||||
f.write(frontmatter.dumps(post))
|
||||
|
||||
# Now update the note using write_note
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Custom Metadata Note",
|
||||
directory="test",
|
||||
@@ -394,7 +394,7 @@ async def test_write_note_preserves_custom_metadata(app, project_config, test_pr
|
||||
assert f"project: {test_project.name}" in result
|
||||
|
||||
# Read the note back and check if custom frontmatter is preserved
|
||||
content = await read_note.fn("test/custom-metadata-note", project=test_project.name)
|
||||
content = await read_note("test/custom-metadata-note", project=test_project.name)
|
||||
|
||||
# Custom frontmatter should be preserved
|
||||
assert "Status: In Progress" in content
|
||||
@@ -414,7 +414,7 @@ async def test_write_note_preserves_custom_metadata(app, project_config, test_pr
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_preserves_content_frontmatter(app, test_project):
|
||||
"""Test creating a new note."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="test",
|
||||
@@ -435,7 +435,7 @@ async def test_write_note_preserves_content_frontmatter(app, test_project):
|
||||
)
|
||||
|
||||
# Try reading it back via permalink
|
||||
content = await read_note.fn("test/test-note", project=test_project.name)
|
||||
content = await read_note("test/test-note", project=test_project.name)
|
||||
assert (
|
||||
normalize_newlines(
|
||||
dedent(
|
||||
@@ -476,7 +476,7 @@ async def test_write_note_permalink_collision_fix_issue_139(app, test_project):
|
||||
After the fix, it should either update the existing note or create with unique permalink.
|
||||
"""
|
||||
# Step 1: Create first note
|
||||
result1 = await write_note.fn(
|
||||
result1 = await write_note(
|
||||
project=test_project.name,
|
||||
title="Note 1",
|
||||
directory="test",
|
||||
@@ -487,7 +487,7 @@ async def test_write_note_permalink_collision_fix_issue_139(app, test_project):
|
||||
assert f"permalink: {test_project.name}/test/note-1" in result1
|
||||
|
||||
# Step 2: Create second note with different title
|
||||
result2 = await write_note.fn(
|
||||
result2 = await write_note(
|
||||
project=test_project.name, title="Note 2", directory="test", content="Content for note 2"
|
||||
)
|
||||
assert "# Created note" in result2
|
||||
@@ -496,7 +496,7 @@ async def test_write_note_permalink_collision_fix_issue_139(app, test_project):
|
||||
|
||||
# Step 3: Try to create/replace first note again
|
||||
# This scenario would trigger the UNIQUE constraint failure before the fix
|
||||
result3 = await write_note.fn(
|
||||
result3 = await write_note(
|
||||
project=test_project.name,
|
||||
title="Note 1", # Same title as first note
|
||||
directory="test", # Same folder as first note
|
||||
@@ -521,14 +521,14 @@ async def test_write_note_permalink_collision_fix_issue_139(app, test_project):
|
||||
# Verify we can read back the content
|
||||
if f"permalink: {test_project.name}/test/note-1" in result3:
|
||||
# Updated existing note case
|
||||
content = await read_note.fn("test/note-1", project=test_project.name)
|
||||
content = await read_note("test/note-1", project=test_project.name)
|
||||
assert "Replacement content for note 1" in content
|
||||
else:
|
||||
# Created new note with unique permalink case
|
||||
content = await read_note.fn(test_project.name, "test/note-1-1")
|
||||
content = await read_note(test_project.name, "test/note-1-1")
|
||||
assert "Replacement content for note 1" in content
|
||||
# Original note should still exist
|
||||
original_content = await read_note.fn(test_project.name, "test/note-1")
|
||||
original_content = await read_note(test_project.name, "test/note-1")
|
||||
assert "Original content for note 1" in original_content
|
||||
|
||||
|
||||
@@ -539,7 +539,7 @@ async def test_write_note_with_custom_entity_type(app, test_project):
|
||||
This test verifies the fix for Issue #144 where entity_type parameter
|
||||
was hardcoded to "note" instead of allowing custom types.
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Guide",
|
||||
directory="guides",
|
||||
@@ -558,7 +558,7 @@ async def test_write_note_with_custom_entity_type(app, test_project):
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type is correctly set in the frontmatter
|
||||
content = await read_note.fn("guides/test-guide", project=test_project.name)
|
||||
content = await read_note("guides/test-guide", project=test_project.name)
|
||||
expected = normalize_newlines(
|
||||
dedent("""
|
||||
---
|
||||
@@ -582,7 +582,7 @@ async def test_write_note_with_custom_entity_type(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_with_report_entity_type(app, test_project):
|
||||
"""Test creating a note with note_type="report"."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Monthly Report",
|
||||
directory="reports",
|
||||
@@ -599,7 +599,7 @@ async def test_write_note_with_report_entity_type(app, test_project):
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type is correctly set in the frontmatter
|
||||
content = await read_note.fn("reports/monthly-report", project=test_project.name)
|
||||
content = await read_note("reports/monthly-report", project=test_project.name)
|
||||
assert "type: report" in content
|
||||
assert "# Monthly Report" in content
|
||||
|
||||
@@ -607,7 +607,7 @@ async def test_write_note_with_report_entity_type(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_with_config_entity_type(app, test_project):
|
||||
"""Test creating a note with note_type="config"."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="System Config",
|
||||
directory="config",
|
||||
@@ -623,7 +623,7 @@ async def test_write_note_with_config_entity_type(app, test_project):
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type is correctly set in the frontmatter
|
||||
content = await read_note.fn("config/system-config", project=test_project.name)
|
||||
content = await read_note("config/system-config", project=test_project.name)
|
||||
assert "type: config" in content
|
||||
assert "# System Configuration" in content
|
||||
|
||||
@@ -635,7 +635,7 @@ async def test_write_note_entity_type_default_behavior(app, test_project):
|
||||
This ensures backward compatibility - existing code that doesn't specify
|
||||
entity_type should continue to work as before.
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Default Type Test",
|
||||
directory="test",
|
||||
@@ -651,7 +651,7 @@ async def test_write_note_entity_type_default_behavior(app, test_project):
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type defaults to "note"
|
||||
content = await read_note.fn("test/default-type-test", project=test_project.name)
|
||||
content = await read_note("test/default-type-test", project=test_project.name)
|
||||
assert "type: note" in content
|
||||
assert "# Default Type Test" in content
|
||||
|
||||
@@ -660,7 +660,7 @@ async def test_write_note_entity_type_default_behavior(app, test_project):
|
||||
async def test_write_note_update_existing_with_different_entity_type(app, test_project):
|
||||
"""Test updating an existing note with a different entity_type."""
|
||||
# Create initial note as "note" type
|
||||
result1 = await write_note.fn(
|
||||
result1 = await write_note(
|
||||
project=test_project.name,
|
||||
title="Changeable Type",
|
||||
directory="test",
|
||||
@@ -674,7 +674,7 @@ async def test_write_note_update_existing_with_different_entity_type(app, test_p
|
||||
assert f"project: {test_project.name}" in result1
|
||||
|
||||
# Update the same note with a different note_type
|
||||
result2 = await write_note.fn(
|
||||
result2 = await write_note(
|
||||
project=test_project.name,
|
||||
title="Changeable Type",
|
||||
directory="test",
|
||||
@@ -688,7 +688,7 @@ async def test_write_note_update_existing_with_different_entity_type(app, test_p
|
||||
assert f"project: {test_project.name}" in result2
|
||||
|
||||
# Verify the entity type was updated
|
||||
content = await read_note.fn("test/changeable-type", project=test_project.name)
|
||||
content = await read_note("test/changeable-type", project=test_project.name)
|
||||
assert "type: guide" in content
|
||||
assert "# Updated Content" in content
|
||||
assert "- guide" in content
|
||||
@@ -717,7 +717,7 @@ async def test_write_note_respects_frontmatter_entity_type(app, test_project):
|
||||
""").strip()
|
||||
|
||||
# Call write_note without entity_type parameter - it should respect frontmatter type
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name, title="Test Guide", directory="guides", content=note
|
||||
)
|
||||
|
||||
@@ -729,7 +729,7 @@ async def test_write_note_respects_frontmatter_entity_type(app, test_project):
|
||||
assert f"[Session: Using project '{test_project.name}']" in result
|
||||
|
||||
# Verify the entity type from frontmatter is respected (should be "guide", not "note")
|
||||
content = await read_note.fn("guides/test-guide", project=test_project.name)
|
||||
content = await read_note("guides/test-guide", project=test_project.name)
|
||||
assert "type: guide" in content
|
||||
assert "# Guide Content" in content
|
||||
assert "- guide" in content
|
||||
@@ -756,7 +756,7 @@ class TestWriteNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_folder in attack_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory=attack_folder,
|
||||
@@ -785,7 +785,7 @@ class TestWriteNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_folder in attack_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory=attack_folder,
|
||||
@@ -814,7 +814,7 @@ class TestWriteNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_folder in attack_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory=attack_folder,
|
||||
@@ -842,7 +842,7 @@ class TestWriteNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_folder in attack_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory=attack_folder,
|
||||
@@ -868,7 +868,7 @@ class TestWriteNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for attack_folder in attack_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory=attack_folder,
|
||||
@@ -895,7 +895,7 @@ class TestWriteNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for safe_folder in safe_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title=f"Test Note in {safe_folder.replace('/', '-')}",
|
||||
directory=safe_folder,
|
||||
@@ -915,7 +915,7 @@ class TestWriteNoteSecurityValidation:
|
||||
async def test_write_note_empty_folder_security(self, app, test_project):
|
||||
"""Test that empty folder parameter is handled securely."""
|
||||
# Empty folder should be allowed (creates in root)
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Root Note",
|
||||
directory="",
|
||||
@@ -934,7 +934,7 @@ class TestWriteNoteSecurityValidation:
|
||||
"""Test that default folder behavior works securely when folder is omitted."""
|
||||
# The write_note function requires folder parameter, but we can test with empty string
|
||||
# which effectively creates in project root
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Root Folder Note",
|
||||
directory="", # Empty string instead of None since folder is required
|
||||
@@ -959,7 +959,7 @@ class TestWriteNoteSecurityValidation:
|
||||
]
|
||||
|
||||
for safe_folder in safe_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title=f"Current Dir Test {safe_folder.replace('/', '-').replace('.', 'dot')}",
|
||||
directory=safe_folder,
|
||||
@@ -977,7 +977,7 @@ class TestWriteNoteSecurityValidation:
|
||||
async def test_write_note_security_with_all_parameters(self, app, test_project):
|
||||
"""Test security validation works with all write_note parameters."""
|
||||
# Test that security validation is applied even when all other parameters are provided
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Security Test with All Params",
|
||||
directory="../../../etc/malicious",
|
||||
@@ -995,7 +995,7 @@ class TestWriteNoteSecurityValidation:
|
||||
async def test_write_note_security_logging(self, app, test_project, caplog):
|
||||
"""Test that security violations are properly logged."""
|
||||
# Attempt path traversal attack
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Security Logging Test",
|
||||
directory="../../../etc/passwd_folder",
|
||||
@@ -1013,7 +1013,7 @@ class TestWriteNoteSecurityValidation:
|
||||
async def test_write_note_preserves_functionality_with_security(self, app, test_project):
|
||||
"""Test that security validation doesn't break normal note creation functionality."""
|
||||
# Create a note with all features to ensure security validation doesn't interfere
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Full Feature Security Test",
|
||||
directory="security-tests",
|
||||
@@ -1068,7 +1068,7 @@ class TestWriteNoteSecurityEdgeCases:
|
||||
]
|
||||
|
||||
for attack_folder in unicode_attack_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Unicode Attack Test",
|
||||
directory=attack_folder,
|
||||
@@ -1085,7 +1085,7 @@ class TestWriteNoteSecurityEdgeCases:
|
||||
# Create a very long path traversal attack
|
||||
long_attack_folder = "../" * 1000 + "etc/malicious"
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Long Attack Test",
|
||||
directory=long_attack_folder,
|
||||
@@ -1108,7 +1108,7 @@ class TestWriteNoteSecurityEdgeCases:
|
||||
]
|
||||
|
||||
for attack_folder in case_attack_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Case Variation Attack Test",
|
||||
directory=attack_folder,
|
||||
@@ -1131,7 +1131,7 @@ class TestWriteNoteSecurityEdgeCases:
|
||||
]
|
||||
|
||||
for attack_folder in whitespace_attack_folders:
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Whitespace Attack Test",
|
||||
directory=attack_folder,
|
||||
|
||||
@@ -29,7 +29,7 @@ async def test_write_note_spaces_to_hyphens(app, test_project, app_config):
|
||||
"""Test that spaces are converted to hyphens when kebab_filenames=true."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="My Awesome Note",
|
||||
directory="test",
|
||||
@@ -45,7 +45,7 @@ async def test_write_note_underscores_to_hyphens(app, test_project, app_config):
|
||||
"""Test that underscores are converted to hyphens when kebab_filenames=true."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="my_note_with_underscores",
|
||||
directory="test",
|
||||
@@ -61,7 +61,7 @@ async def test_write_note_camelcase_to_kebab(app, test_project, app_config):
|
||||
"""Test that CamelCase is converted to kebab-case when kebab_filenames=true."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="MyAwesomeFeature",
|
||||
directory="test",
|
||||
@@ -77,7 +77,7 @@ async def test_write_note_mixed_case_to_lowercase(app, test_project, app_config)
|
||||
"""Test that mixed case is converted to lowercase when kebab_filenames=true."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="MIXED_Case_Example",
|
||||
directory="test",
|
||||
@@ -102,7 +102,7 @@ async def test_write_note_single_period_preserved(app, test_project, app_config)
|
||||
"""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test 3.0 Version",
|
||||
directory="test",
|
||||
@@ -118,7 +118,7 @@ async def test_write_note_multiple_periods_preserved(app, test_project, app_conf
|
||||
"""Test that multiple periods in version numbers are preserved when kebab_filenames=true."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Version 1.2.3 Release",
|
||||
directory="test",
|
||||
@@ -139,7 +139,7 @@ async def test_write_note_special_chars_to_hyphens(app, test_project, app_config
|
||||
"""Test that special characters are converted while preserving periods in version numbers."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test 2.0: New Feature",
|
||||
directory="test",
|
||||
@@ -155,7 +155,7 @@ async def test_write_note_parentheses_removed(app, test_project, app_config):
|
||||
"""Test that parentheses are handled while preserving periods in version numbers."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Feature (v2.0) Update",
|
||||
directory="test",
|
||||
@@ -171,7 +171,7 @@ async def test_write_note_apostrophes_removed(app, test_project, app_config):
|
||||
"""Test that apostrophes are removed when kebab_filenames=true."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="User's Guide",
|
||||
directory="test",
|
||||
@@ -192,7 +192,7 @@ async def test_write_note_all_transformations_combined(app, test_project, app_co
|
||||
"""Test multiple transformation types combined while preserving periods in version numbers."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="MyProject_v3.0: Feature Update (DRAFT)",
|
||||
directory="test",
|
||||
@@ -208,7 +208,7 @@ async def test_write_note_consecutive_special_chars_collapsed(app, test_project,
|
||||
"""Test that consecutive special characters collapse to single hyphen."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test___Multiple---Separators",
|
||||
directory="test",
|
||||
@@ -230,7 +230,7 @@ async def test_write_note_leading_trailing_hyphens_trimmed(app, test_project, ap
|
||||
"""Test that leading/trailing hyphens are trimmed when kebab_filenames=true."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="---Test Note---",
|
||||
directory="test",
|
||||
@@ -246,7 +246,7 @@ async def test_write_note_all_special_chars_becomes_valid_filename(app, test_pro
|
||||
"""Test that a title with mostly special characters becomes valid."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="!!!Test!!!",
|
||||
directory="test",
|
||||
@@ -267,7 +267,7 @@ async def test_write_note_folder_path_unaffected(app, test_project, app_config):
|
||||
"""Test that folder paths are NOT affected by kebab_filenames setting."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note",
|
||||
directory="My_Folder/Sub Folder", # Folder should remain as-is
|
||||
@@ -284,7 +284,7 @@ async def test_write_note_root_folder_with_kebab(app, test_project, app_config):
|
||||
"""Test kebab_filenames preserves periods in version numbers with root folder."""
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test 3.0 Note",
|
||||
directory="", # Root folder
|
||||
@@ -305,7 +305,7 @@ async def test_write_note_kebab_disabled_preserves_original(app, test_project, a
|
||||
"""Test that original formatting is preserved when kebab_filenames=false."""
|
||||
ConfigManager().config.kebab_filenames = False
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test 3.0 Version",
|
||||
directory="test",
|
||||
@@ -323,7 +323,7 @@ async def test_write_note_kebab_disabled_preserves_underscores(app, test_project
|
||||
"""Test that underscores are preserved when kebab_filenames=false."""
|
||||
ConfigManager().config.kebab_filenames = False
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="my_note_example",
|
||||
directory="test",
|
||||
@@ -339,7 +339,7 @@ async def test_write_note_kebab_disabled_preserves_case(app, test_project, app_c
|
||||
"""Test that case is preserved when kebab_filenames=false."""
|
||||
ConfigManager().config.kebab_filenames = False
|
||||
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="MyAwesomeNote",
|
||||
directory="test",
|
||||
@@ -366,7 +366,7 @@ async def test_permalinks_always_kebab_case(app, test_project, app_config):
|
||||
# Test with kebab disabled
|
||||
ConfigManager().config.kebab_filenames = False
|
||||
|
||||
result1 = await write_note.fn(
|
||||
result1 = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note 1",
|
||||
directory="test",
|
||||
@@ -380,7 +380,7 @@ async def test_permalinks_always_kebab_case(app, test_project, app_config):
|
||||
# Test with kebab enabled
|
||||
ConfigManager().config.kebab_filenames = True
|
||||
|
||||
result2 = await write_note.fn(
|
||||
result2 = await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Note 2",
|
||||
directory="test",
|
||||
|
||||
@@ -15,7 +15,7 @@ from basic_memory.mcp.tools import write_note, read_note
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_simple_keys(app, test_project):
|
||||
"""Simple key-value metadata appears as top-level YAML frontmatter."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Simple Metadata",
|
||||
directory="meta-tests",
|
||||
@@ -25,7 +25,7 @@ async def test_metadata_simple_keys(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/simple-metadata", project=test_project.name)
|
||||
content = await read_note("meta-tests/simple-metadata", project=test_project.name)
|
||||
assert "author: Alice" in content
|
||||
assert "status: draft" in content
|
||||
|
||||
@@ -33,7 +33,7 @@ async def test_metadata_simple_keys(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_nested_dict(app, test_project):
|
||||
"""Nested dict metadata renders as nested YAML."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Nested Metadata",
|
||||
directory="meta-tests",
|
||||
@@ -46,7 +46,7 @@ async def test_metadata_nested_dict(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/nested-metadata", project=test_project.name)
|
||||
content = await read_note("meta-tests/nested-metadata", project=test_project.name)
|
||||
assert "schema:" in content
|
||||
assert "name: string" in content
|
||||
assert "role?: string" in content
|
||||
@@ -57,7 +57,7 @@ async def test_metadata_nested_dict(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_with_tags(app, test_project):
|
||||
"""Metadata and tags coexist — both appear in frontmatter."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Tags And Metadata",
|
||||
directory="meta-tests",
|
||||
@@ -69,7 +69,7 @@ async def test_metadata_with_tags(app, test_project):
|
||||
assert "# Created note" in result
|
||||
assert "## Tags" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/tags-and-metadata", project=test_project.name)
|
||||
content = await read_note("meta-tests/tags-and-metadata", project=test_project.name)
|
||||
assert "priority: high" in content
|
||||
assert "- one" in content
|
||||
assert "- two" in content
|
||||
@@ -78,7 +78,7 @@ async def test_metadata_with_tags(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_various_value_types(app, test_project):
|
||||
"""Metadata values of int, bool, and list types survive round-trip."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Typed Values",
|
||||
directory="meta-tests",
|
||||
@@ -88,7 +88,7 @@ async def test_metadata_various_value_types(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/typed-values", project=test_project.name)
|
||||
content = await read_note("meta-tests/typed-values", project=test_project.name)
|
||||
# YAML normalizes values to strings during frontmatter round-trip
|
||||
assert "version:" in content
|
||||
assert "active:" in content
|
||||
@@ -99,7 +99,7 @@ async def test_metadata_various_value_types(app, test_project):
|
||||
async def test_metadata_survives_update(app, test_project):
|
||||
"""Metadata set at create time persists through an update cycle."""
|
||||
# Create with metadata
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Update Cycle",
|
||||
directory="meta-tests",
|
||||
@@ -108,7 +108,7 @@ async def test_metadata_survives_update(app, test_project):
|
||||
)
|
||||
|
||||
# Update same note with new content + metadata
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Update Cycle",
|
||||
directory="meta-tests",
|
||||
@@ -118,7 +118,7 @@ async def test_metadata_survives_update(app, test_project):
|
||||
|
||||
assert "# Updated note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/update-cycle", project=test_project.name)
|
||||
content = await read_note("meta-tests/update-cycle", project=test_project.name)
|
||||
assert "# Version 2" in content
|
||||
assert "author: Bob" in content
|
||||
assert "version:" in content
|
||||
@@ -127,7 +127,7 @@ async def test_metadata_survives_update(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_with_note_type(app, test_project):
|
||||
"""Metadata works together with a custom note_type."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Config Entry",
|
||||
directory="meta-tests",
|
||||
@@ -138,7 +138,7 @@ async def test_metadata_with_note_type(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/config-entry", project=test_project.name)
|
||||
content = await read_note("meta-tests/config-entry", project=test_project.name)
|
||||
assert "type: config" in content
|
||||
assert "env: production" in content
|
||||
|
||||
@@ -149,7 +149,7 @@ async def test_metadata_with_note_type(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_none_default(app, test_project):
|
||||
"""metadata=None (default) produces the same output as before the feature existed."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="No Metadata",
|
||||
directory="meta-tests",
|
||||
@@ -160,7 +160,7 @@ async def test_metadata_none_default(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/no-metadata", project=test_project.name)
|
||||
content = await read_note("meta-tests/no-metadata", project=test_project.name)
|
||||
# Only standard keys should be present
|
||||
assert "title: No Metadata" in content
|
||||
assert "type: note" in content
|
||||
@@ -170,7 +170,7 @@ async def test_metadata_none_default(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_empty_dict(app, test_project):
|
||||
"""metadata={} behaves identically to metadata=None."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Empty Dict",
|
||||
directory="meta-tests",
|
||||
@@ -180,7 +180,7 @@ async def test_metadata_empty_dict(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/empty-dict", project=test_project.name)
|
||||
content = await read_note("meta-tests/empty-dict", project=test_project.name)
|
||||
assert "title: Empty Dict" in content
|
||||
assert "type: note" in content
|
||||
|
||||
@@ -194,7 +194,7 @@ async def test_metadata_title_key_stripped(app, test_project):
|
||||
|
||||
schema_to_markdown pops 'title' from entity_metadata so the Entity.title wins.
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Real Title",
|
||||
directory="meta-tests",
|
||||
@@ -204,7 +204,7 @@ async def test_metadata_title_key_stripped(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/real-title", project=test_project.name)
|
||||
content = await read_note("meta-tests/real-title", project=test_project.name)
|
||||
assert "title: Real Title" in content
|
||||
assert "Fake Title" not in content
|
||||
|
||||
@@ -215,7 +215,7 @@ async def test_metadata_type_key_stripped(app, test_project):
|
||||
|
||||
schema_to_markdown pops 'type' from entity_metadata so Entity.entity_type wins.
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Type Conflict",
|
||||
directory="meta-tests",
|
||||
@@ -226,7 +226,7 @@ async def test_metadata_type_key_stripped(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/type-conflict", project=test_project.name)
|
||||
content = await read_note("meta-tests/type-conflict", project=test_project.name)
|
||||
assert "type: guide" in content
|
||||
assert "evil" not in content
|
||||
|
||||
@@ -237,7 +237,7 @@ async def test_metadata_permalink_key_stripped(app, test_project):
|
||||
|
||||
schema_to_markdown pops 'permalink' from entity_metadata.
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Permalink Conflict",
|
||||
directory="meta-tests",
|
||||
@@ -249,7 +249,7 @@ async def test_metadata_permalink_key_stripped(app, test_project):
|
||||
# The canonical permalink should be based on title/directory, not the metadata value
|
||||
assert "meta-tests/permalink-conflict" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/permalink-conflict", project=test_project.name)
|
||||
content = await read_note("meta-tests/permalink-conflict", project=test_project.name)
|
||||
assert "hacked/path" not in content
|
||||
|
||||
|
||||
@@ -260,7 +260,7 @@ async def test_tags_param_wins_over_metadata_tags(app, test_project):
|
||||
The explicit tags parameter is applied after metadata.update(), so it takes
|
||||
precedence. The summary and file contents stay consistent.
|
||||
"""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Tags Override",
|
||||
directory="meta-tests",
|
||||
@@ -273,7 +273,7 @@ async def test_tags_param_wins_over_metadata_tags(app, test_project):
|
||||
# Summary should reflect the winning tags
|
||||
assert "from-param" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/tags-override", project=test_project.name)
|
||||
content = await read_note("meta-tests/tags-override", project=test_project.name)
|
||||
# Explicit tags parameter wins over metadata tags key
|
||||
assert "- from-param" in content
|
||||
assert "from-metadata" not in content
|
||||
@@ -282,7 +282,7 @@ async def test_tags_param_wins_over_metadata_tags(app, test_project):
|
||||
@pytest.mark.asyncio
|
||||
async def test_metadata_tags_key_works_when_no_tags_param(app, test_project):
|
||||
"""When only metadata['tags'] is provided (no tags param), it is used."""
|
||||
result = await write_note.fn(
|
||||
result = await write_note(
|
||||
project=test_project.name,
|
||||
title="Metadata Tags Only",
|
||||
directory="meta-tests",
|
||||
@@ -292,6 +292,6 @@ async def test_metadata_tags_key_works_when_no_tags_param(app, test_project):
|
||||
|
||||
assert "# Created note" in result
|
||||
|
||||
content = await read_note.fn("meta-tests/metadata-tags-only", project=test_project.name)
|
||||
content = await read_note("meta-tests/metadata-tags-only", project=test_project.name)
|
||||
assert "- meta-tag-1" in content
|
||||
assert "- meta-tag-2" in content
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for MCP UI resource endpoints (resources/ui.py).
|
||||
|
||||
Each resource function is wrapped by @mcp.resource() into a FunctionResource.
|
||||
We call the underlying .fn() to exercise the template loading logic.
|
||||
We call the underlying () to exercise the template loading logic.
|
||||
|
||||
NOTE: UI resources are temporarily disabled (not registered with MCP server)
|
||||
while MCP client rendering is being sorted out. These tests are skipped
|
||||
@@ -30,14 +30,14 @@ class TestVariantResources:
|
||||
def test_search_results_ui(self, monkeypatch):
|
||||
"""search_results_ui loads the variant-specific template."""
|
||||
monkeypatch.setenv("BASIC_MEMORY_MCP_UI_VARIANT", "vanilla")
|
||||
html = search_results_ui.fn()
|
||||
html = search_results_ui()
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
|
||||
def test_note_preview_ui(self, monkeypatch):
|
||||
"""note_preview_ui loads the variant-specific template."""
|
||||
monkeypatch.setenv("BASIC_MEMORY_MCP_UI_VARIANT", "vanilla")
|
||||
html = note_preview_ui.fn()
|
||||
html = note_preview_ui()
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
|
||||
@@ -46,31 +46,31 @@ class TestExplicitVariantResources:
|
||||
"""Tests for variant-specific resource endpoints."""
|
||||
|
||||
def test_search_results_vanilla(self):
|
||||
html = search_results_ui_vanilla.fn()
|
||||
html = search_results_ui_vanilla()
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
|
||||
def test_search_results_tool_ui(self):
|
||||
html = search_results_ui_tool_ui.fn()
|
||||
html = search_results_ui_tool_ui()
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
|
||||
def test_search_results_mcp_ui(self):
|
||||
html = search_results_ui_mcp_ui.fn()
|
||||
html = search_results_ui_mcp_ui()
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
|
||||
def test_note_preview_vanilla(self):
|
||||
html = note_preview_ui_vanilla.fn()
|
||||
html = note_preview_ui_vanilla()
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
|
||||
def test_note_preview_tool_ui(self):
|
||||
html = note_preview_ui_tool_ui.fn()
|
||||
html = note_preview_ui_tool_ui()
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
|
||||
def test_note_preview_mcp_ui(self):
|
||||
html = note_preview_ui_mcp_ui.fn()
|
||||
html = note_preview_ui_mcp_ui()
|
||||
assert isinstance(html, str)
|
||||
assert len(html) > 0
|
||||
|
||||
@@ -10,13 +10,13 @@ from basic_memory.schemas.search import SearchResponse, SearchResult, SearchItem
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_successful_results(client, test_project):
|
||||
"""Test search with successful results returns proper MCP content array format."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Document 1",
|
||||
directory="docs",
|
||||
content="# Test Document 1\n\nThis is test content for document 1",
|
||||
)
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Document 2",
|
||||
directory="docs",
|
||||
@@ -25,7 +25,7 @@ async def test_search_successful_results(client, test_project):
|
||||
|
||||
from basic_memory.mcp.tools.chatgpt_tools import search
|
||||
|
||||
result = await search.fn("test content")
|
||||
result = await search("test content")
|
||||
|
||||
# Verify MCP content array format
|
||||
assert isinstance(result, list)
|
||||
@@ -52,9 +52,9 @@ async def test_search_with_error_response(monkeypatch, client, test_project):
|
||||
async def fake_search_notes_fn(*args, **kwargs):
|
||||
return error_message
|
||||
|
||||
monkeypatch.setattr(chatgpt_tools.search_notes, "fn", fake_search_notes_fn)
|
||||
monkeypatch.setattr(chatgpt_tools, "search_notes", fake_search_notes_fn)
|
||||
|
||||
result = await chatgpt_tools.search.fn("invalid query")
|
||||
result = await chatgpt_tools.search("invalid query")
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
@@ -77,9 +77,9 @@ async def test_search_uses_dynamic_default_search_type(monkeypatch, client, test
|
||||
captured_kwargs.update(kwargs)
|
||||
return {"results": []}
|
||||
|
||||
monkeypatch.setattr(chatgpt_tools.search_notes, "fn", fake_search_notes_fn)
|
||||
monkeypatch.setattr(chatgpt_tools, "search_notes", fake_search_notes_fn)
|
||||
|
||||
result = await chatgpt_tools.search.fn("default search mode query")
|
||||
result = await chatgpt_tools.search("default search mode query")
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert "search_type" not in captured_kwargs
|
||||
@@ -88,7 +88,7 @@ async def test_search_uses_dynamic_default_search_type(monkeypatch, client, test
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_successful_document(client, test_project):
|
||||
"""Test fetch with successful document retrieval."""
|
||||
await write_note.fn(
|
||||
await write_note(
|
||||
project=test_project.name,
|
||||
title="Test Document",
|
||||
directory="docs",
|
||||
@@ -97,7 +97,7 @@ async def test_fetch_successful_document(client, test_project):
|
||||
|
||||
from basic_memory.mcp.tools.chatgpt_tools import fetch
|
||||
|
||||
result = await fetch.fn("docs/test-document")
|
||||
result = await fetch("docs/test-document")
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
@@ -116,7 +116,7 @@ async def test_fetch_document_not_found(client, test_project):
|
||||
"""Test fetch when document is not found."""
|
||||
from basic_memory.mcp.tools.chatgpt_tools import fetch
|
||||
|
||||
result = await fetch.fn("nonexistent-doc")
|
||||
result = await fetch("nonexistent-doc")
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
@@ -208,9 +208,9 @@ async def test_search_internal_exception_returns_error_payload(monkeypatch, clie
|
||||
async def boom(*args, **kwargs):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(chatgpt_tools.search_notes, "fn", boom)
|
||||
monkeypatch.setattr(chatgpt_tools, "search_notes", boom)
|
||||
|
||||
result = await chatgpt_tools.search.fn("anything")
|
||||
result = await chatgpt_tools.search("anything")
|
||||
assert isinstance(result, list)
|
||||
content = json.loads(result[0]["text"])
|
||||
assert content["error"] == "Internal search error"
|
||||
@@ -225,9 +225,9 @@ async def test_fetch_internal_exception_returns_error_payload(monkeypatch, clien
|
||||
async def boom(*args, **kwargs):
|
||||
raise RuntimeError("boom")
|
||||
|
||||
monkeypatch.setattr(chatgpt_tools.read_note, "fn", boom)
|
||||
monkeypatch.setattr(chatgpt_tools, "read_note", boom)
|
||||
|
||||
result = await chatgpt_tools.fetch.fn("docs/test")
|
||||
result = await chatgpt_tools.fetch("docs/test")
|
||||
assert isinstance(result, list)
|
||||
content = json.loads(result[0]["text"])
|
||||
assert content["id"] == "docs/test"
|
||||
|
||||
@@ -7,6 +7,18 @@ resolution-markers = [
|
||||
"python_full_version < '3.13'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiofile"
|
||||
version = "3.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "caio" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "25.1.0"
|
||||
@@ -212,7 +224,7 @@ requires-dist = [
|
||||
{ name = "dateparser", specifier = ">=1.2.0" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.8" },
|
||||
{ name = "fastembed", specifier = ">=0.7.4" },
|
||||
{ name = "fastmcp", specifier = "==2.12.3" },
|
||||
{ name = "fastmcp", specifier = ">=3.0.1,<4" },
|
||||
{ name = "greenlet", specifier = ">=3.1.1" },
|
||||
{ name = "httpx", specifier = ">=0.28.0" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
@@ -262,6 +274,39 @@ dev = [
|
||||
{ name = "ty", specifier = ">=0.0.18" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "beartype"
|
||||
version = "0.22.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "7.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/07/56595285564e90777d758ebd383d6b0b971b87729bbe2184a849932a3736/cachetools-7.0.1.tar.gz", hash = "sha256:e31e579d2c5b6e2944177a0397150d312888ddf4e16e12f1016068f0c03b8341", size = 36126, upload-time = "2026-02-10T22:24:05.03Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/9e/5faefbf9db1db466d633735faceda1f94aa99ce506ac450d232536266b32/cachetools-7.0.1-py3-none-any.whl", hash = "sha256:8f086515c254d5664ae2146d14fc7f65c9a4bce75152eb247e5a9c5e6d7b2ecf", size = 13484, upload-time = "2026-02-10T22:24:03.741Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "caio"
|
||||
version = "0.9.25"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.1.4"
|
||||
@@ -816,24 +861,33 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "fastmcp"
|
||||
version = "2.12.3"
|
||||
version = "3.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "authlib" },
|
||||
{ name = "cyclopts" },
|
||||
{ name = "exceptiongroup" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jsonref" },
|
||||
{ name = "jsonschema-path" },
|
||||
{ name = "mcp" },
|
||||
{ name = "openapi-core" },
|
||||
{ name = "openapi-pydantic" },
|
||||
{ name = "opentelemetry-api" },
|
||||
{ name = "packaging" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
|
||||
{ name = "pydantic", extra = ["email"] },
|
||||
{ name = "pyperclip" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "rich" },
|
||||
{ name = "uvicorn" },
|
||||
{ name = "watchfiles" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/5e/035fdfa23646de8811776cd62d93440e334e8a4557b35c63c1bff125c08c/fastmcp-2.12.3.tar.gz", hash = "sha256:541dd569d5b6c083140b04d997ba3dc47f7c10695cee700d0a733ce63b20bb65", size = 5246812, upload-time = "2025-09-12T12:28:07.136Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/39/0847a868a8681f0d9bf42ad4b6856ef675f799eb464bd10dbcfe9ae87323/fastmcp-3.0.1.tar.gz", hash = "sha256:ba463ae51e357fba2bafe513cc97f0a06c9f31220e6584990b7d8bcbf69f0516", size = 17236395, upload-time = "2026-02-21T01:35:25.696Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/96/79/0fd386e61819e205563d4eb15da76564b80dc2edd3c64b46f2706235daec/fastmcp-2.12.3-py3-none-any.whl", hash = "sha256:aee50872923a9cba731861fc0120e7dbe4642a2685ba251b2b202b82fb6c25a9", size = 314031, upload-time = "2025-09-12T12:28:05.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/f9/94f2531d0c519e1e394c264941848caf2309284c5318c070f7b0e95ac496/fastmcp-3.0.1-py3-none-any.whl", hash = "sha256:71de15ffa4e54baebb78d7031c4c9a042a1ab8d1c0b44a9961b75d65809b67e8", size = 605494, upload-time = "2026-02-21T01:35:22.857Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1100,6 +1154,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "zipp" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
@@ -1110,12 +1176,45 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isodate"
|
||||
version = "0.7.2"
|
||||
name = "jaraco-classes"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" }
|
||||
dependencies = [
|
||||
{ name = "more-itertools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-context"
|
||||
version = "6.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cb/9c/a788f5bb29c61e456b8ee52ce76dbdd32fd72cd73dd67bc95f42c7a8d13c/jaraco_context-6.1.0.tar.gz", hash = "sha256:129a341b0a85a7db7879e22acd66902fda67882db771754574338898b2d5d86f", size = 15850, upload-time = "2026-01-13T02:53:53.847Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/48/aa685dbf1024c7bd82bede569e3a85f82c32fd3d79ba5fea578f0159571a/jaraco_context-6.1.0-py3-none-any.whl", hash = "sha256:a43b5ed85815223d0d3cfdb6d7ca0d2bc8946f28f30b6f3216bda070f68badda", size = 7065, upload-time = "2026-01-13T02:53:53.031Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-functools"
|
||||
version = "4.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "more-itertools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jeepney"
|
||||
version = "0.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1198,6 +1297,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonref"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "4.26.0"
|
||||
@@ -1241,35 +1349,20 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-object-proxy"
|
||||
version = "1.12.0"
|
||||
name = "keyring"
|
||||
version = "25.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" }
|
||||
dependencies = [
|
||||
{ name = "jaraco-classes" },
|
||||
{ name = "jaraco-context" },
|
||||
{ name = "jaraco-functools" },
|
||||
{ name = "jeepney", marker = "sys_platform == 'linux'" },
|
||||
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1685,25 +1778,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/44/97/284535aa75e6e84ab388248b5a323fc296b1f70530130dee37f7f4fbe856/openai-2.17.0-py3-none-any.whl", hash = "sha256:4f393fd886ca35e113aac7ff239bcd578b81d8f104f5aedc7d3693eb2af1d338", size = 1069524, upload-time = "2026-02-05T16:27:38.941Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-core"
|
||||
version = "0.22.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "isodate" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "jsonschema-path" },
|
||||
{ name = "more-itertools" },
|
||||
{ name = "openapi-schema-validator" },
|
||||
{ name = "openapi-spec-validator" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "werkzeug" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fd/65/ee75f25b9459a02df6f713f8ffde5dacb57b8b4e45145cde4cab28b5abba/openapi_core-0.22.0.tar.gz", hash = "sha256:b30490dfa74e3aac2276105525590135212352f5dd7e5acf8f62f6a89ed6f2d0", size = 109242, upload-time = "2025-12-22T19:19:49.608Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/8e/a1bf9e7d1b170122aa33b6cf2c788b68824712427deb19795eb7db1b8dd5/openapi_core-0.22.0-py3-none-any.whl", hash = "sha256:8fb7c325f2db4ef6c60584b1870f90eeb3183aa47e30643715c5003b7677a149", size = 108384, upload-time = "2025-12-22T19:19:47.904Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-pydantic"
|
||||
version = "0.5.1"
|
||||
@@ -1717,32 +1791,16 @@ wheels = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-schema-validator"
|
||||
version = "0.6.3"
|
||||
name = "opentelemetry-api"
|
||||
version = "1.39.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonschema" },
|
||||
{ name = "jsonschema-specifications" },
|
||||
{ name = "rfc3339-validator" },
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openapi-spec-validator"
|
||||
version = "0.7.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonschema" },
|
||||
{ name = "jsonschema-path" },
|
||||
{ name = "lazy-object-proxy" },
|
||||
{ name = "openapi-schema-validator" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1829,6 +1887,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
@@ -1866,6 +1933,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/f3/0b4a4c25a47c2d907afa97674287dab61bc9941c9ac3972a67100e33894d/psycopg-3.3.1-py3-none-any.whl", hash = "sha256:e44d8eae209752efe46318f36dd0fdf5863e928009338d736843bb1084f6435c", size = 212760, upload-time = "2025-12-02T21:02:36.029Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-key-value-aio"
|
||||
version = "0.4.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "beartype" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
filetree = [
|
||||
{ name = "aiofile" },
|
||||
{ name = "anyio" },
|
||||
]
|
||||
keyring = [
|
||||
{ name = "keyring" },
|
||||
]
|
||||
memory = [
|
||||
{ name = "cachetools" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "py-rust-stemmers"
|
||||
version = "0.1.5"
|
||||
@@ -2244,6 +2336,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
@@ -2397,18 +2498,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc3339-validator"
|
||||
version = "0.1.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "six" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "14.2.0"
|
||||
@@ -2633,6 +2722,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secretstorage"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "jeepney" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.49.0"
|
||||
@@ -3104,18 +3206,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "werkzeug"
|
||||
version = "3.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markupsafe" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "win32-setctime"
|
||||
version = "1.2.0"
|
||||
@@ -3194,6 +3284,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.23.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zope-event"
|
||||
version = "6.1"
|
||||
|
||||
Reference in New Issue
Block a user