Compare commits

..

7 Commits

Author SHA1 Message Date
phernandez 8e69c8b533 chore: update version to 0.14.0 for v0.14.0 release 2025-06-26 16:18:10 -05:00
phernandez 627a5c3c22 docs: add comprehensive v0.14.0 changelog entry
Add detailed changelog for v0.14.0 release including:
- Docker Container Registry migration to GitHub Container Registry
- Enhanced search documentation with comprehensive syntax examples
- Cross-project file management with intelligent boundary detection
- 8 major bug fixes with issue numbers and commit links
- Technical improvements and infrastructure enhancements
- Migration guide and installation instructions

Covers all changes since v0.13.7 with proper categorization and
user-facing descriptions for better release communication.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-06-26 16:15:11 -05:00
phernandez cd88945b22 remove v0.13.0 from changelog
Signed-off-by: phernandez <paul@basicmachines.co>
2025-06-26 16:11:05 -05:00
phernandez cd8e372f0a fix: add test coverage for optional permalink in EntityResponse schema
- Add comprehensive test for None permalink validation in EntityResponse
- Ensures schema properly handles markdown files without explicit permalinks
- Addresses GitHub issue #170 validation errors during edit operations
- Test validates that permalink=None doesn't cause ValidationError

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2025-06-26 15:58:49 -05:00
Paul Hernandez a589f8b894 feat: enhance search_notes tool documentation with comprehensive syntax examples (#186)
Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-06-26 15:51:58 -05:00
Paul Hernandez c2f4b632cf fix: preserve permalink when editing notes without frontmatter permalink (#184)
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: Paul Hernandez <phernandez@users.noreply.github.com>
2025-06-26 15:35:31 -05:00
phernandez 46d102cef1 update tests for search_repository
Signed-off-by: phernandez <paul@basicmachines.co>
2025-06-26 14:30:10 -05:00
9 changed files with 378 additions and 105 deletions
+119 -37
View File
@@ -1,56 +1,138 @@
# CHANGELOG
## v0.13.8 (2025-06-20)
## v0.14.0 (2025-06-26)
### Features
- **Docker Container Support** - Complete Docker integration with volume mounting for Obsidian directories
([`3269a2f`](https://github.com/basicmachines-co/basic-memory/commit/3269a2f33a7595f6d9e5207924062e2542f46759))
- Docker Compose configuration for easy deployment
- Volume mounting for persistent data and Obsidian integration
- Comprehensive Docker documentation and setup guides
- Streamlined container-based workflow
- **Docker Container Registry Migration** - Switch from Docker Hub to GitHub Container Registry for better security and integration
([`616c1f0`](https://github.com/basicmachines-co/basic-memory/commit/616c1f0710da59c7098a5f4843d4f017877ff7b2))
- Automated Docker image publishing via GitHub Actions CI/CD pipeline
- Enhanced container security with GitHub's integrated vulnerability scanning
- Streamlined container deployment workflow for production environments
- **Enhanced Search Documentation** - Comprehensive search syntax examples for improved user experience
([`a589f8b`](https://github.com/basicmachines-co/basic-memory/commit/a589f8b894e78cce01eb25656856cfea8785fbbf))
- Detailed examples for Boolean search operators (AND, OR, NOT)
- Advanced search patterns including phrase matching and field-specific queries
- User-friendly documentation for complex search scenarios
- **Cross-Project File Management** - Intelligent move operations with project boundary detection
([`db5ef7d`](https://github.com/basicmachines-co/basic-memory/commit/db5ef7d35cc0894309c7a57b5741c9dd978526d4))
- Automatic detection of cross-project move attempts with helpful guidance
- Clear error messages when attempting unsupported cross-project operations
### Bug Fixes
- **#151**: Fix reset command project configuration persistence issue
([`af44941`](https://github.com/basicmachines-co/basic-memory/commit/af44941d5aa57b5ad7fcc6af4ed700f49bdb6d4d))
- Reset command now properly clears project configuration from `~/.basic-memory/config.json`
- Eliminates issue where projects would be recreated after database reset
- Ensures clean slate when resetting Basic Memory installation
- **#184**: Preserve permalinks when editing notes without frontmatter permalinks
([`c2f4b63`](https://github.com/basicmachines-co/basic-memory/commit/c2f4b632cf04921b1a3c2f0d43831b80c519cb31))
- Fix permalink preservation during note editing operations
- Ensure consistent permalink handling across different note formats
- Maintain note identity and searchability during incremental edits
- **#148**: Resolve project state inconsistency between MCP and CLI
([`35e4f73`](https://github.com/basicmachines-co/basic-memory/commit/35e4f73ae8a65501da4d48258ed702f957184c92))
- Fix "Project not found" errors when switching default projects
- MCP session now automatically refreshes when project configuration changes
- Eliminates need to restart MCP server after project operations
- Ensures consistent project state across CLI and MCP interfaces
- **#183**: Implement project-specific sync status checks for MCP tools
([`12b5152`](https://github.com/basicmachines-co/basic-memory/commit/12b51522bc953fca117fc5bc01fcb29c6ca7e13c))
- Fix sync status reporting to correctly reflect current project state
- Resolve inconsistencies where sync status showed global instead of project-specific information
- Improve project isolation for sync operations and status reporting
- **FastMCP Compatibility** - Resolve deprecation warnings for FastMCP integration
([`7be001c`](https://github.com/basicmachines-co/basic-memory/commit/7be001ca6834b3344bb6160cbe537b36bcbaa579))
- Update FastMCP usage patterns to eliminate deprecation warnings
- Improve future compatibility with FastMCP library updates
- Clean up entity repository and service layer code
- **#180**: Handle Boolean search syntax with hyphenated terms
([`546e3cd`](https://github.com/basicmachines-co/basic-memory/commit/546e3cd8db98b74f746749d41887f8a213cd0b11))
- Fix search parsing issues with hyphenated terms in Boolean queries
- Improve search query tokenization for complex term structures
- Enhanced search reliability for technical documentation and multi-word concepts
- **#174**: Respect BASIC_MEMORY_HOME environment variable in Docker containers
([`9f1db23`](https://github.com/basicmachines-co/basic-memory/commit/9f1db23c78d4648e2c242ad1ee27eed85e3f3b5d))
- Fix Docker container configuration to properly honor custom home directory settings
- Improve containerized deployment flexibility with environment variable support
- Ensure consistent behavior between local and containerized installations
- **#168**: Scope entity queries by project_id in upsert_entity method
([`2a3adc1`](https://github.com/basicmachines-co/basic-memory/commit/2a3adc109a3e4d7ccd65cae4abf63d9bb2338326))
- Fix entity isolation issues in multi-project setups
- Prevent cross-project entity conflicts during database operations
- Strengthen project boundary enforcement at the database level
- **#166**: Handle None from_entity in Context API RelationSummary
([`8a065c3`](https://github.com/basicmachines-co/basic-memory/commit/8a065c32f4e41613207d29aafc952a56e3a52241))
- Fix null pointer exceptions in relation processing
- Improve error handling for incomplete relation data
- Enhanced stability for knowledge graph traversal operations
- **#164**: Remove log level configuration from mcp_server.run()
([`224e4bf`](https://github.com/basicmachines-co/basic-memory/commit/224e4bf9e4438c44a82ffc21bd1a282fe9087690))
- Simplify MCP server startup by removing redundant log level settings
- Fix potential logging configuration conflicts
- Streamline server initialization process
- **#162**: Ensure permalinks are generated for entities with null permalinks during move operations
([`f506507`](https://github.com/basicmachines-co/basic-memory/commit/f50650763dbd4322c132e4bdc959ce4bf074374b))
- Fix move operations for entities without existing permalinks
- Automatic permalink generation during file move operations
- Maintain database consistency during file reorganization
### Technical Improvements
- **Comprehensive Integration Testing** - New test suites for critical user workflows
- Full integration tests for database reset functionality
- End-to-end project state synchronization testing
- Real MCP client-server communication validation
- Direct config file validation without complex mocking
- **Comprehensive Test Coverage** - Extensive test suites for new features and edge cases
- Enhanced test coverage for project-specific sync status functionality
- Additional test scenarios for search syntax validation and edge cases
- Integration tests for Docker CI workflow and container publishing
- Comprehensive move operations testing with project boundary validation
- **Code Quality** - Enhanced error handling and validation
- Improved project state management across system components
- Better session refresh patterns for configuration changes
- Streamlined Docker setup with reduced image size
- **Docker CI/CD Pipeline** - Production-ready automated container publishing
([`74847cc`](https://github.com/basicmachines-co/basic-memory/commit/74847cc3807b0c6ed511e0d83e0d560e9f07ec44))
- Automated Docker image building and publishing on release
- Multi-architecture container support for AMD64 and ARM64 platforms
- Integrated security scanning and vulnerability assessments
- Streamlined deployment pipeline for production environments
### Documentation
- **Release Process Improvements** - Enhanced automation and quality gates
([`a52ce1c`](https://github.com/basicmachines-co/basic-memory/commit/a52ce1c8605ec2cd450d1f909154172cbc30faa2))
- Homebrew formula updates limited to stable releases only
- Improved release automation with better quality control
- Enhanced CI/CD pipeline reliability and error handling
- **Docker Integration Guide** - Complete documentation for container deployment
- Step-by-step Docker Compose setup instructions
- Volume mounting configuration for Obsidian workflows
- Container-based development environment setup
- **Code Quality Enhancements** - Improved error handling and validation
- Better null safety in entity and relation processing
- Enhanced project isolation validation throughout the codebase
- Improved error messages and user guidance for edge cases
- Strengthened database consistency guarantees across operations
### Infrastructure
- **GitHub Container Registry Integration** - Modern container infrastructure
- Migration from Docker Hub to GitHub Container Registry (ghcr.io)
- Improved security with integrated vulnerability scanning
- Better integration with GitHub-based development workflow
- Enhanced container versioning and artifact management
- **Enhanced CI/CD Workflows** - Robust automated testing and deployment
- Automated Docker image publishing on releases
- Comprehensive test coverage validation before deployment
- Multi-platform container building and publishing
- Integration with GitHub's security and monitoring tools
### Migration Guide
This release includes several behind-the-scenes improvements and fixes. All changes are backward compatible:
- **Docker Users**: Container images now served from `ghcr.io/basicmachines-co/basic-memory` instead of Docker Hub
- **Search Users**: Enhanced search syntax handling - existing queries continue to work unchanged
- **Multi-Project Users**: Improved project isolation - all existing projects remain fully functional
- **All Users**: Enhanced stability and error handling - no breaking changes to existing workflows
### Installation
```bash
# Latest stable release
uv tool install basic-memory
# Update existing installation
uv tool upgrade basic-memory
# Docker (new registry)
docker pull ghcr.io/basicmachines-co/basic-memory:latest
```
## v0.13.7 (2025-06-19)
+1 -1
View File
@@ -1,7 +1,7 @@
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
# Package version - updated by release automation
__version__ = "0.14.0b1"
__version__ = "0.14.0"
# API version for FastAPI - independent of package version
__api_version__ = "v0"
+3 -1
View File
@@ -38,7 +38,9 @@ def entity_model_from_markdown(
# Update basic fields
model.title = markdown.frontmatter.title
model.entity_type = markdown.frontmatter.type
model.permalink = markdown.frontmatter.permalink
# Only update permalink if it exists in frontmatter, otherwise preserve existing
if markdown.frontmatter.permalink is not None:
model.permalink = markdown.frontmatter.permalink
model.file_path = str(file_path)
model.content_type = "text/markdown"
model.created_at = markdown.created
+111 -34
View File
@@ -45,13 +45,18 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
- Boolean OR: `meeting OR discussion`
- Boolean NOT: `project NOT archived`
- Grouped: `(project OR planning) AND notes`
- Exact phrases: `"weekly standup meeting"`
- Content-specific: `tag:example` or `category:observation`
## Try again with:
```
search_notes("INSERT_CLEAN_QUERY_HERE")
search_notes("{clean_query}")
```
Replace INSERT_CLEAN_QUERY_HERE with your simplified search terms.
## Alternative search strategies:
- Break into simpler terms: `search_notes("{' '.join(clean_query.split()[:2])}")`
- Try different search types: `search_notes("{clean_query}", search_type="title")`
- Use filtering: `search_notes("{clean_query}", types=["entity"])`
""").strip()
# Project not found errors (check before general "not found")
@@ -85,24 +90,39 @@ def _format_search_error_response(error_message: str, query: str, search_type: s
No content found matching '{query}' in the current project.
## Suggestions to try:
## Search strategy suggestions:
1. **Broaden your search**: Try fewer or more general terms
- Instead of: `{query}`
- Try: `{simplified_query}`
2. **Check spelling**: Verify terms are spelled correctly
3. **Try different search types**:
- Text search: `search_notes("{query}", search_type="text")`
- Title search: `search_notes("{query}", search_type="title")`
- Permalink search: `search_notes("{query}", search_type="permalink")`
2. **Check spelling and try variations**:
- Verify terms are spelled correctly
- Try synonyms or related terms
4. **Use boolean operators**:
- Try OR search for broader results
3. **Use different search approaches**:
- **Text search**: `search_notes("{query}", search_type="text")` (searches full content)
- **Title search**: `search_notes("{query}", search_type="title")` (searches only titles)
- **Permalink search**: `search_notes("{query}", search_type="permalink")` (searches file paths)
## Check what content exists:
- Recent activity: `recent_activity(timeframe="7d")`
- List files: `list_directory("/")`
- Browse by folder: `list_directory("/notes")` or `list_directory("/docs")`
4. **Try boolean operators for broader results**:
- OR search: `search_notes("{' OR '.join(query.split()[:3])}")`
- Remove restrictive terms: Focus on the most important keywords
5. **Use filtering to narrow scope**:
- By content type: `search_notes("{query}", types=["entity"])`
- By recent content: `search_notes("{query}", after_date="1 week")`
- By entity type: `search_notes("{query}", entity_types=["observation"])`
6. **Try advanced search patterns**:
- Tag search: `search_notes("tag:your-tag")`
- Category search: `search_notes("category:observation")`
- Pattern matching: `search_notes("*{query}*", search_type="permalink")`
## Explore what content exists:
- **Recent activity**: `recent_activity(timeframe="7d")` - See what's been updated recently
- **List directories**: `list_directory("/")` - Browse all content
- **Browse by folder**: `list_directory("/notes")` or `list_directory("/docs")`
- **Check project**: `get_current_project()` - Verify you're in the right project
""").strip()
# Server/API errors
@@ -151,25 +171,36 @@ You don't have permission to search in the current project: {error_message}
Error searching for '{query}': {error_message}
## General troubleshooting:
1. **Check your query**: Ensure it uses valid search syntax
2. **Try simpler terms**: Use basic words without special characters
## Troubleshooting steps:
1. **Simplify your query**: Try basic words without special characters
2. **Check search syntax**: Ensure boolean operators are correctly formatted
3. **Verify project access**: Make sure you can access the current project
4. **Check recent activity**: `recent_activity(timeframe="7d")` to see if content exists
4. **Test with simple search**: Try `search_notes("test")` to verify search is working
## Alternative approaches:
- Browse files: `list_directory("/")`
- Try different search type: `search_notes("{query}", search_type="title")`
- Search with filters: `search_notes("{query}", types=["entity"])`
## Alternative search approaches:
- **Different search types**:
- Title only: `search_notes("{query}", search_type="title")`
- Permalink patterns: `search_notes("{query}*", search_type="permalink")`
- **With filters**: `search_notes("{query}", types=["entity"])`
- **Recent content**: `search_notes("{query}", after_date="1 week")`
- **Boolean variations**: `search_notes("{' OR '.join(query.split()[:2])}")`
## Need help?
- View recent changes: `recent_activity()`
- List projects: `list_projects()`
- Check current project: `get_current_project()`"""
## Explore your content:
- **Browse files**: `list_directory("/")` - See all available content
- **Recent activity**: `recent_activity(timeframe="7d")` - Check what's been updated
- **Project info**: `get_current_project()` - Verify current project
- **All projects**: `list_projects()` - Switch to different project if needed
## Search syntax reference:
- **Basic**: `keyword` or `multiple words`
- **Boolean**: `term1 AND term2`, `term1 OR term2`, `term1 NOT term2`
- **Phrases**: `"exact phrase"`
- **Grouping**: `(term1 OR term2) AND term3`
- **Patterns**: `tag:example`, `category:observation`"""
@mcp.tool(
description="Search across all content in the knowledge base.",
description="Search across all content in the knowledge base with advanced syntax support.",
)
async def search_notes(
query: str,
@@ -181,24 +212,60 @@ async def search_notes(
after_date: Optional[str] = None,
project: Optional[str] = None,
) -> SearchResponse | str:
"""Search across all content in the knowledge base.
"""Search across all content in the knowledge base with comprehensive syntax support.
This tool searches the knowledge base using full-text search, pattern matching,
or exact permalink lookup. It supports filtering by content type, entity type,
and date.
and date, with advanced boolean and phrase search capabilities.
## Search Syntax Examples
### Basic Searches
- `search_notes("keyword")` - Find any content containing "keyword"
- `search_notes("exact phrase")` - Search for exact phrase match
### Advanced Boolean Searches
- `search_notes("term1 term2")` - Find content with both terms (implicit AND)
- `search_notes("term1 AND term2")` - Explicit AND search (both terms required)
- `search_notes("term1 OR term2")` - Either term can be present
- `search_notes("term1 NOT term2")` - Include term1 but exclude term2
- `search_notes("(project OR planning) AND notes")` - Grouped boolean logic
### Content-Specific Searches
- `search_notes("tag:example")` - Search within specific tags (if supported by content)
- `search_notes("category:observation")` - Filter by observation categories
- `search_notes("author:username")` - Find content by author (if metadata available)
### Search Type Examples
- `search_notes("Meeting", search_type="title")` - Search only in titles
- `search_notes("docs/meeting-*", search_type="permalink")` - Pattern match permalinks
- `search_notes("keyword", search_type="text")` - Full-text search (default)
### Filtering Options
- `search_notes("query", types=["entity"])` - Search only entities
- `search_notes("query", types=["note", "person"])` - Multiple content types
- `search_notes("query", entity_types=["observation"])` - Filter by entity type
- `search_notes("query", after_date="2024-01-01")` - Recent content only
- `search_notes("query", after_date="1 week")` - Relative date filtering
### Advanced Pattern Examples
- `search_notes("project AND (meeting OR discussion)")` - Complex boolean logic
- `search_notes("\"exact phrase\" AND keyword")` - Combine phrase and keyword search
- `search_notes("bug NOT fixed")` - Exclude resolved issues
- `search_notes("docs/2024-*", search_type="permalink")` - Year-based permalink search
Args:
query: The search query string
query: The search query string (supports boolean operators, phrases, patterns)
page: The page number of results to return (default 1)
page_size: The number of results to return per page (default 10)
search_type: Type of search to perform, one of: "text", "title", "permalink" (default: "text")
types: Optional list of note types to search (e.g., ["note", "person"])
entity_types: Optional list of entity types to filter by (e.g., ["entity", "observation"])
after_date: Optional date filter for recent content (e.g., "1 week", "2d")
after_date: Optional date filter for recent content (e.g., "1 week", "2d", "2024-01-01")
project: Optional project name to search in. If not provided, uses current active project.
Returns:
SearchResponse with results and pagination info
SearchResponse with results and pagination info, or helpful error guidance if search fails
Examples:
# Basic text search
@@ -216,16 +283,19 @@ async def search_notes(
# Boolean search with grouping
results = await search_notes("(project OR planning) AND notes")
# Exact phrase search
results = await search_notes("\"weekly standup meeting\"")
# Search with type filter
results = await search_notes(
query="meeting notes",
types=["entity"],
)
# Search with entity type filter, e.g., note vs
# Search with entity type filter
results = await search_notes(
query="meeting notes",
types=["entity"],
entity_types=["observation"],
)
# Search for recent content
@@ -242,6 +312,13 @@ async def search_notes(
# Search in specific project
results = await search_notes("meeting notes", project="work-project")
# Complex search with multiple filters
results = await search_notes(
query="(bug OR issue) AND NOT resolved",
types=["entity"],
after_date="2024-01-01"
)
"""
# Create a SearchQuery object based on the parameters
search_query = SearchQuery()
+1 -1
View File
@@ -131,7 +131,7 @@ class EntityResponse(SQLAlchemyModel):
}
"""
permalink: Permalink
permalink: Optional[Permalink]
title: str
file_path: str
entity_type: EntityType
+39
View File
@@ -359,3 +359,42 @@ async def test_edit_note_find_replace_empty_find_text(client):
assert isinstance(result, str)
assert "# Edit Failed" in result
# Should contain helpful guidance about the error
@pytest.mark.asyncio
async def test_edit_note_preserves_permalink_when_frontmatter_missing(client):
"""Test that editing a note preserves the permalink when frontmatter doesn't contain one.
This is a regression test for issue #170 where edit_note would fail with a validation error
because the permalink was being set to None when the markdown file didn't have a permalink
in its frontmatter.
"""
# Create initial note
await write_note.fn(
title="Test Note",
folder="test",
content="# Test Note\nOriginal content here.",
)
# Verify the note was created with a permalink
first_result = await edit_note.fn(
identifier="test/test-note",
operation="append",
content="\nFirst edit.",
)
assert isinstance(first_result, str)
assert "permalink: test/test-note" in first_result
# 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(
identifier="test/test-note",
operation="append",
content="\nSecond edit.",
)
assert isinstance(second_result, str)
assert "Edited note (append)" in second_result
assert "permalink: test/test-note" in second_result
# The edit should succeed without validation errors
+63 -22
View File
@@ -6,6 +6,7 @@ from unittest.mock import patch
from basic_memory.mcp.tools import write_note
from basic_memory.mcp.tools.search import search_notes, _format_search_error_response
from basic_memory.schemas.search import SearchResponse
@pytest.mark.asyncio
@@ -23,9 +24,14 @@ async def test_search_text(client):
# Search for it
response = await search_notes.fn(query="searchable")
# Verify results
assert len(response.results) > 0
assert any(r.permalink == "test/test-search-note" for r in response.results)
# Verify results - handle both success and error cases
if isinstance(response, SearchResponse):
# Success case - verify SearchResponse
assert len(response.results) > 0
assert any(r.permalink == "test/test-search-note" for r in response.results)
else:
# If search failed and returned error message, test should fail with informative message
pytest.fail(f"Search failed with error: {response}")
@pytest.mark.asyncio
@@ -43,9 +49,14 @@ async def test_search_title(client):
# Search for it
response = await search_notes.fn(query="Search Note", search_type="title")
# Verify results
assert len(response.results) > 0
assert any(r.permalink == "test/test-search-note" for r in response.results)
# Verify results - handle both success and error cases
if isinstance(response, str):
# If search failed and returned error message, test should fail with informative message
pytest.fail(f"Search failed with error: {response}")
else:
# Success case - verify SearchResponse
assert len(response.results) > 0
assert any(r.permalink == "test/test-search-note" for r in response.results)
@pytest.mark.asyncio
@@ -63,9 +74,14 @@ async def test_search_permalink(client):
# Search for it
response = await search_notes.fn(query="test/test-search-note", search_type="permalink")
# Verify results
assert len(response.results) > 0
assert any(r.permalink == "test/test-search-note" for r in response.results)
# Verify results - handle both success and error cases
if isinstance(response, SearchResponse):
# Success case - verify SearchResponse
assert len(response.results) > 0
assert any(r.permalink == "test/test-search-note" for r in response.results)
else:
# If search failed and returned error message, test should fail with informative message
pytest.fail(f"Search failed with error: {response}")
@pytest.mark.asyncio
@@ -83,9 +99,14 @@ async def test_search_permalink_match(client):
# Search for it
response = await search_notes.fn(query="test/test-search-*", search_type="permalink")
# Verify results
assert len(response.results) > 0
assert any(r.permalink == "test/test-search-note" for r in response.results)
# Verify results - handle both success and error cases
if isinstance(response, SearchResponse):
# Success case - verify SearchResponse
assert len(response.results) > 0
assert any(r.permalink == "test/test-search-note" for r in response.results)
else:
# If search failed and returned error message, test should fail with informative message
pytest.fail(f"Search failed with error: {response}")
@pytest.mark.asyncio
@@ -103,9 +124,14 @@ async def test_search_pagination(client):
# Search for it
response = await search_notes.fn(query="searchable", page=1, page_size=1)
# Verify results
assert len(response.results) == 1
assert any(r.permalink == "test/test-search-note" for r in response.results)
# Verify results - handle both success and error cases
if isinstance(response, SearchResponse):
# Success case - verify SearchResponse
assert len(response.results) == 1
assert any(r.permalink == "test/test-search-note" for r in response.results)
else:
# If search failed and returned error message, test should fail with informative message
pytest.fail(f"Search failed with error: {response}")
@pytest.mark.asyncio
@@ -121,8 +147,13 @@ async def test_search_with_type_filter(client):
# Search with type filter
response = await search_notes.fn(query="type", types=["note"])
# Verify all results are entities
assert all(r.type == "entity" for r in response.results)
# Verify results - handle both success and error cases
if isinstance(response, SearchResponse):
# Success case - verify all results are entities
assert all(r.type == "entity" for r in response.results)
else:
# If search failed and returned error message, test should fail with informative message
pytest.fail(f"Search failed with error: {response}")
@pytest.mark.asyncio
@@ -138,8 +169,13 @@ async def test_search_with_entity_type_filter(client):
# Search with entity type filter
response = await search_notes.fn(query="type", entity_types=["entity"])
# Verify all results are entities
assert all(r.type == "entity" for r in response.results)
# Verify results - handle both success and error cases
if isinstance(response, SearchResponse):
# Success case - verify all results are entities
assert all(r.type == "entity" for r in response.results)
else:
# If search failed and returned error message, test should fail with informative message
pytest.fail(f"Search failed with error: {response}")
@pytest.mark.asyncio
@@ -156,8 +192,13 @@ async def test_search_with_date_filter(client):
one_hour_ago = datetime.now() - timedelta(hours=1)
response = await search_notes.fn(query="recent", after_date=one_hour_ago.isoformat())
# Verify we get results within timeframe
assert len(response.results) > 0
# Verify results - handle both success and error cases
if isinstance(response, SearchResponse):
# Success case - verify we get results within timeframe
assert len(response.results) > 0
else:
# If search failed and returned error message, test should fail with informative message
pytest.fail(f"Search failed with error: {response}")
class TestSearchErrorFormatting:
@@ -212,7 +253,7 @@ class TestSearchErrorFormatting:
assert "# Search Failed" in result
assert "Error searching for 'test query': unknown error" in result
assert "General troubleshooting" in result
assert "## Troubleshooting steps:" in result
class TestSearchToolErrorHandling:
+11 -9
View File
@@ -551,12 +551,14 @@ class TestSearchTermPreparation:
def test_boolean_query_empty_parts_coverage(self, search_repository):
"""Test Boolean query parsing with empty parts (line 143 coverage)."""
# Create queries that will result in empty parts after splitting
result1 = search_repository._prepare_boolean_query("hello AND AND world") # Double operator
result1 = search_repository._prepare_boolean_query(
"hello AND AND world"
) # Double operator
assert "hello" in result1 and "world" in result1
result2 = search_repository._prepare_boolean_query(" OR test") # Leading operator
assert "test" in result2
result3 = search_repository._prepare_boolean_query("test OR ") # Trailing operator
assert "test" in result3
@@ -566,19 +568,19 @@ class TestSearchTermPreparation:
result = search_repository._prepare_parenthetical_term('(say "hello" world)')
# Should escape quotes by doubling them
assert '""hello""' in result
# Test term with single quotes
result2 = search_repository._prepare_parenthetical_term('(it\'s working)')
result2 = search_repository._prepare_parenthetical_term("(it's working)")
assert "it's working" in result2
def test_needs_quoting_empty_input(self, search_repository):
"""Test _needs_quoting with empty inputs (line 207 coverage)."""
# Test empty string
assert not search_repository._needs_quoting("")
# Test whitespace-only string
assert not search_repository._needs_quoting(" ")
# Test None-like cases
assert not search_repository._needs_quoting("\t")
@@ -587,11 +589,11 @@ class TestSearchTermPreparation:
# Test empty string
result1 = search_repository._prepare_single_term("")
assert result1 == ""
# Test whitespace-only string
result2 = search_repository._prepare_single_term(" ")
assert result2 == " " # Should return as-is
# Test string that becomes empty after strip
result3 = search_repository._prepare_single_term("\t\n")
assert result3 == "\t\n" # Should return original
+30
View File
@@ -127,6 +127,36 @@ def test_entity_out_from_attributes():
assert len(entity.relations) == 1
def test_entity_response_with_none_permalink():
"""Test EntityResponse can handle None permalink (fixes issue #170).
This test ensures that EntityResponse properly validates when the permalink
field is None, which can occur when markdown files don't have explicit
permalinks in their frontmatter during edit operations.
"""
# Simulate database model attributes with None permalink
db_data = {
"title": "Test Entity",
"permalink": None, # This should not cause validation errors
"file_path": "test/test-entity.md",
"entity_type": "note",
"content_type": "text/markdown",
"observations": [],
"relations": [],
"created_at": "2023-01-01T00:00:00",
"updated_at": "2023-01-01T00:00:00",
}
# This should not raise a ValidationError
entity = EntityResponse.model_validate(db_data)
assert entity.permalink is None
assert entity.title == "Test Entity"
assert entity.file_path == "test/test-entity.md"
assert entity.entity_type == "note"
assert len(entity.observations) == 0
assert len(entity.relations) == 0
def test_search_nodes_input():
"""Test SearchNodesInput validation."""
search = SearchNodesRequest.model_validate({"query": "test query"})