mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
feat: add memory-json importer, tweak observation content
This commit is contained in:
@@ -1,9 +1,5 @@
|
||||
"""CLI commands package."""
|
||||
"""Command module exports."""
|
||||
|
||||
from . import init, status
|
||||
from . import init, status, sync, import_memory_json
|
||||
|
||||
|
||||
__all__ = [
|
||||
"init",
|
||||
"status",
|
||||
]
|
||||
__all__ = ["init", "status", "sync", "import_memory_json.py"]
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""Import command for basic-memory CLI to import from JSON memory format."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
import typer
|
||||
from loguru import logger
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
||||
|
||||
from basic_memory.cli.app import app
|
||||
from basic_memory.config import config
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import EntityMarkdown, EntityFrontmatter, Observation, Relation
|
||||
|
||||
|
||||
console = Console()
|
||||
|
||||
async def process_memory_json(json_path: Path, base_path: Path,markdown_processor: MarkdownProcessor):
|
||||
"""Import entities from memory.json using markdown processor."""
|
||||
|
||||
# First pass - collect all relations by source entity
|
||||
entity_relations: Dict[str, List[Relation]] = {}
|
||||
entities: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
||||
console=console,
|
||||
) as progress:
|
||||
read_task = progress.add_task("Reading memory.json...", total=None)
|
||||
|
||||
# First pass - collect entities and relations
|
||||
with open(json_path) as f:
|
||||
lines = f.readlines()
|
||||
progress.update(read_task, total=len(lines))
|
||||
|
||||
for line in lines:
|
||||
data = json.loads(line)
|
||||
if data["type"] == "entity":
|
||||
entities[data["name"]] = data
|
||||
elif data["type"] == "relation":
|
||||
# Store relation with its source entity
|
||||
source = data["from"]
|
||||
if source not in entity_relations:
|
||||
entity_relations[source] = []
|
||||
entity_relations[source].append(
|
||||
Relation(
|
||||
type=data["relationType"],
|
||||
target=data["to"]
|
||||
)
|
||||
)
|
||||
progress.update(read_task, advance=1)
|
||||
|
||||
# Second pass - create and write entities
|
||||
write_task = progress.add_task("Creating entities...", total=len(entities))
|
||||
|
||||
entities_created = 0
|
||||
for name, entity_data in entities.items():
|
||||
entity = EntityMarkdown(
|
||||
frontmatter=EntityFrontmatter(
|
||||
metadata={
|
||||
"type": entity_data["entityType"],
|
||||
"title": name,
|
||||
"permalink": f"{entity_data['entityType']}/{name}"
|
||||
}
|
||||
),
|
||||
content=f"# {name}\n",
|
||||
observations=[
|
||||
Observation(content=obs)
|
||||
for obs in entity_data["observations"]
|
||||
],
|
||||
relations=entity_relations.get(name, []) # Add any relations where this entity is the source
|
||||
)
|
||||
|
||||
# Let markdown processor handle writing
|
||||
file_path = base_path / f"{entity_data['entityType']}/{name}.md"
|
||||
await markdown_processor.write_file(file_path, entity)
|
||||
entities_created += 1
|
||||
progress.update(write_task, advance=1)
|
||||
|
||||
return {
|
||||
"entities": entities_created,
|
||||
"relations": sum(len(rels) for rels in entity_relations.values())
|
||||
}
|
||||
|
||||
|
||||
async def get_markdown_processor() -> MarkdownProcessor:
|
||||
"""Get MarkdownProcessor instance."""
|
||||
entity_parser = EntityParser(config.home)
|
||||
return MarkdownProcessor(entity_parser)
|
||||
|
||||
|
||||
@app.command()
|
||||
def import_memory_json(
|
||||
json_path: Path = typer.Argument(..., help="Path to memory.json file to import"),
|
||||
):
|
||||
"""Import entities and relations from a memory.json file.
|
||||
|
||||
This command will:
|
||||
1. Read entities and relations from the JSON file
|
||||
2. Create markdown files for each entity
|
||||
3. Include outgoing relations in each entity's markdown
|
||||
|
||||
After importing, run 'basic-memory sync' to index the new files.
|
||||
"""
|
||||
|
||||
if not json_path.exists():
|
||||
typer.echo(f"Error: File not found: {json_path}", err=True)
|
||||
raise typer.Exit(1)
|
||||
|
||||
try:
|
||||
# Get markdown processor
|
||||
markdown_processor = asyncio.run(get_markdown_processor())
|
||||
|
||||
# Process the file
|
||||
base_path = config.home
|
||||
console.print(f"\nImporting from {json_path}...writing to {base_path}")
|
||||
results = asyncio.run(process_memory_json(json_path, base_path, markdown_processor))
|
||||
|
||||
# Show results
|
||||
console.print(Panel(
|
||||
f"[green]Import complete![/green]\n\n"
|
||||
f"Created {results['entities']} entities\n"
|
||||
f"Added {results['relations']} relations",
|
||||
expand=False
|
||||
))
|
||||
|
||||
console.print("\nRun 'basic-memory sync' to index the new files.")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Import failed")
|
||||
typer.echo(f"Error during import: {e}", err=True)
|
||||
raise typer.Exit(1)
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from basic_memory.file_utils import ParseError
|
||||
from basic_memory.markdown.entity_parser import EntityParser
|
||||
from basic_memory.markdown.markdown_processor import MarkdownProcessor
|
||||
from basic_memory.markdown.schemas import (
|
||||
EntityMarkdown,
|
||||
EntityFrontmatter,
|
||||
@@ -13,6 +14,7 @@ __all__ = [
|
||||
"EntityMarkdown",
|
||||
"EntityFrontmatter",
|
||||
"EntityParser",
|
||||
"MarkdownProcessor",
|
||||
"Observation",
|
||||
"Relation",
|
||||
"ParseError",
|
||||
|
||||
@@ -68,7 +68,7 @@ def parse_observation(token: Token) -> Dict[str, Any]:
|
||||
|
||||
return {
|
||||
'category': category,
|
||||
'content': ' '.join(content_parts).strip(),
|
||||
'content': content,
|
||||
'tags': list(tags) if tags else None,
|
||||
'context': context
|
||||
}
|
||||
|
||||
@@ -9,15 +9,13 @@ from pydantic import BaseModel
|
||||
class Observation(BaseModel):
|
||||
"""An observation about an entity."""
|
||||
|
||||
category: Optional[str] = None
|
||||
category: Optional[str] = "Note"
|
||||
content: str
|
||||
tags: Optional[List[str]] = None
|
||||
context: Optional[str] = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
obs_string = f"- [{self.category}] {self.content}"
|
||||
if self.tags:
|
||||
obs_string += " " + " ".join(f"#{tag}" for tag in sorted(self.tags))
|
||||
if self.context:
|
||||
obs_string += f" ({self.context})"
|
||||
return obs_string
|
||||
|
||||
@@ -68,7 +68,7 @@ async def test_create_entity_observations_relations(client: AsyncClient, file_se
|
||||
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0].category == "note"
|
||||
assert entity.observations[0].content == "This is notable"
|
||||
assert entity.observations[0].content == "This is notable #tag1"
|
||||
assert entity.observations[0].tags == ["tag1"]
|
||||
assert entity.observations[0].context == "testing"
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
"""Test import-json command functionality."""
|
||||
|
||||
import json
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from rich.console import Console
|
||||
|
||||
from basic_memory.cli.commands.import_memory_json import process_memory_json
|
||||
from basic_memory.markdown import EntityParser, MarkdownProcessor
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def console():
|
||||
"""Create test console that captures output."""
|
||||
output = StringIO()
|
||||
return Console(file=output), output
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_memory_json(tmp_path) -> Path:
|
||||
"""Create a sample memory.json file with test data."""
|
||||
json_path = tmp_path / "memory.json"
|
||||
|
||||
# Create test data modeling the real format
|
||||
test_data = [
|
||||
{
|
||||
"type": "entity",
|
||||
"name": "Basic_Memory",
|
||||
"entityType": "software_system",
|
||||
"observations": [
|
||||
"A core component of Basic Machines",
|
||||
"Local-first knowledge management system",
|
||||
"Combines filesystem persistence with graph-based knowledge representation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "entity",
|
||||
"name": "Basic_Machines",
|
||||
"entityType": "project",
|
||||
"observations": [
|
||||
"Local-first knowledge management system",
|
||||
"Focuses on enhancing human agency and understanding",
|
||||
"Current focus includes basic-memory system"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "relation",
|
||||
"from": "Basic_Memory",
|
||||
"to": "Basic_Machines",
|
||||
"relationType": "is_component_of"
|
||||
}
|
||||
]
|
||||
|
||||
# Write each item as a JSON line
|
||||
with open(json_path, 'w') as f:
|
||||
for item in test_data:
|
||||
f.write(json.dumps(item) + '\n')
|
||||
|
||||
return json_path
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_memory_json(
|
||||
sample_memory_json: Path,
|
||||
markdown_processor: MarkdownProcessor,
|
||||
test_config,
|
||||
):
|
||||
"""Test importing from memory.json format."""
|
||||
# Process the import
|
||||
results = await process_memory_json(sample_memory_json, test_config.home, markdown_processor)
|
||||
|
||||
# Check results
|
||||
assert results["entities"] == 2
|
||||
assert results["relations"] == 1
|
||||
|
||||
# Verify Basic_Memory entity file was created correctly
|
||||
basic_memory_path = test_config.home / "software_system/Basic_Memory.md"
|
||||
entity = await markdown_processor.read_file(basic_memory_path)
|
||||
|
||||
assert entity.frontmatter.title == "Basic_Memory"
|
||||
assert entity.frontmatter.type == "software_system"
|
||||
assert len(entity.observations) == 3
|
||||
assert len(entity.relations) == 1 # Should have the outgoing relation
|
||||
assert entity.relations[0].type == "is_component_of"
|
||||
assert entity.relations[0].target == "Basic_Machines"
|
||||
|
||||
# Verify Basic_Machines entity file
|
||||
basic_machines_path = test_config.home / "project/Basic_Machines.md"
|
||||
entity = await markdown_processor.read_file(basic_machines_path)
|
||||
|
||||
assert entity.frontmatter.title == "Basic_Machines"
|
||||
assert entity.frontmatter.type == "project"
|
||||
assert len(entity.observations) == 3
|
||||
assert len(entity.relations) == 0 # No outgoing relations
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_memory_json_empty_observations(
|
||||
tmp_path: Path,
|
||||
markdown_processor: MarkdownProcessor,
|
||||
test_config,
|
||||
):
|
||||
"""Test handling entities with no observations."""
|
||||
# Create test data
|
||||
json_path = tmp_path / "memory.json"
|
||||
test_data = [
|
||||
{
|
||||
"type": "entity",
|
||||
"name": "Empty_Entity",
|
||||
"entityType": "test",
|
||||
"observations": [] # Empty observations
|
||||
}
|
||||
]
|
||||
|
||||
with open(json_path, 'w') as f:
|
||||
for item in test_data:
|
||||
f.write(json.dumps(item) + '\n')
|
||||
|
||||
# Process import
|
||||
results = await process_memory_json(json_path, test_config.home, markdown_processor)
|
||||
|
||||
# Check results
|
||||
assert results["entities"] == 1
|
||||
assert results["relations"] == 0
|
||||
|
||||
# Verify file was created
|
||||
entity_path = test_config.home / "test/Empty_Entity.md"
|
||||
entity = await markdown_processor.read_file(entity_path)
|
||||
|
||||
assert entity.frontmatter.title == "Empty_Entity"
|
||||
assert entity.observations == []
|
||||
assert entity.relations == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_process_memory_json_special_characters(
|
||||
tmp_path: Path,
|
||||
markdown_processor: MarkdownProcessor,
|
||||
test_config,
|
||||
):
|
||||
"""Test handling entities with special characters in text."""
|
||||
# Create test data
|
||||
json_path = tmp_path / "memory.json"
|
||||
test_data = [
|
||||
{
|
||||
"type": "entity",
|
||||
"name": "Special_Entity",
|
||||
"entityType": "test",
|
||||
"observations": [
|
||||
"Contains *markdown* formatting",
|
||||
"Has #hashtags and @mentions",
|
||||
"Uses [square brackets] and {curly braces}"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
with open(json_path, 'w') as f:
|
||||
for item in test_data:
|
||||
f.write(json.dumps(item) + '\n')
|
||||
|
||||
# Process import
|
||||
results = await process_memory_json(json_path, test_config.home, markdown_processor)
|
||||
assert results["entities"] == 1
|
||||
|
||||
# Verify file was created and content preserved
|
||||
entity_path = test_config.home / "test/Special_Entity.md"
|
||||
entity = await markdown_processor.read_file(entity_path)
|
||||
|
||||
assert len(entity.observations) == 3
|
||||
assert entity.observations[0].content == "Contains *markdown* formatting"
|
||||
assert entity.observations[1].content == "Has #hashtags and @mentions"
|
||||
assert entity.observations[2].content == "Uses [square brackets] and {curly braces}"
|
||||
@@ -1,170 +0,0 @@
|
||||
# """Test status command functionality."""
|
||||
#
|
||||
# from io import StringIO
|
||||
#
|
||||
# import pytest
|
||||
# from rich.console import Console
|
||||
#
|
||||
# from basic_memory.cli.commands.status import display_changes, run_status
|
||||
# from basic_memory.sync.file_change_scanner import FileState
|
||||
# from basic_memory.sync.utils import SyncReport
|
||||
# from basic_memory.file_utils import compute_checksum
|
||||
#
|
||||
#
|
||||
# @pytest.fixture
|
||||
# def console():
|
||||
# """Create test console that captures output."""
|
||||
# output = StringIO()
|
||||
# return Console(file=output), output
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_display_no_changes(console):
|
||||
# """Test display with no changes."""
|
||||
# test_console, output = console
|
||||
# changes = SyncReport()
|
||||
# display_changes("Test Files", changes, verbose=False)
|
||||
# assert "No changes" in output.getvalue()
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_display_compact_changes(console):
|
||||
# """Test compact display of changes."""
|
||||
# test_console, output = console
|
||||
# changes = SyncReport(
|
||||
# new={"docs/new.md"},
|
||||
# modified={"docs/mod.md"},
|
||||
# deleted={"old/deleted.md"},
|
||||
# moved={
|
||||
# "new/location.md": FileState(
|
||||
# permalink="new/location.md", checksum="abc123", moved_from="old/location.md"
|
||||
# )
|
||||
# },
|
||||
# )
|
||||
# display_changes("Test Files", changes, verbose=False)
|
||||
# output_text = output.getvalue()
|
||||
#
|
||||
# # Check directory summaries
|
||||
# assert "docs/ +1 new" in output_text.replace(" ", " ")
|
||||
# assert "docs/ ~1 modified" in output_text.replace(" ", " ")
|
||||
# assert "old/ -1 deleted" in output_text.replace(" ", " ")
|
||||
# assert "new/ ->1 moved" in output_text.replace(" ", " ")
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_display_verbose_changes(console):
|
||||
# """Test verbose display of changes."""
|
||||
# test_console, output = console
|
||||
# changes = SyncReport(
|
||||
# new={"docs/new.md"},
|
||||
# modified={"docs/mod.md"},
|
||||
# deleted={"old/deleted.md"},
|
||||
# moved={
|
||||
# "new/location.md": FileState(
|
||||
# permalink="new/location.md",
|
||||
# checksum="abc123def", # 8 chars for display
|
||||
# moved_from="old/location.md",
|
||||
# )
|
||||
# },
|
||||
# checksums={
|
||||
# "docs/new.md": "def456789abcdef",
|
||||
# "docs/mod.md": "ghi789abcdef123",
|
||||
# },
|
||||
# )
|
||||
# display_changes("Test Files", changes, verbose=True)
|
||||
# output_text = output.getvalue()
|
||||
#
|
||||
# # Verify sections
|
||||
# assert "New Files" in output_text
|
||||
# assert "Modified" in output_text
|
||||
# assert "Deleted" in output_text
|
||||
# assert "Moved" in output_text
|
||||
#
|
||||
# # Check file listings with checksums
|
||||
# assert "new.md (def45678)" in output_text
|
||||
# assert "mod.md (ghi78789)" in output_text
|
||||
# assert "location.md (abc123de)" in output_text
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_end_to_end_status(file_change_scanner, test_config, entity_repository):
|
||||
# """Test complete status command with real files."""
|
||||
# # Create test files in both knowledge and documents directories
|
||||
# project_dir = test_config.home
|
||||
#
|
||||
# # Create knowledge files
|
||||
# component_dir = project_dir / "component"
|
||||
# component_dir.mkdir(exist_ok=True)
|
||||
# component_path = component_dir / "test.md"
|
||||
# component_path.write_text("test component")
|
||||
#
|
||||
# # Add some files to DB with different paths to test moves
|
||||
# await entity_repository.create(
|
||||
# {"permalink": "old/doc.md", "file_path": "old/doc.md", "checksum": "abc123"}
|
||||
# )
|
||||
#
|
||||
# # Run status check
|
||||
# await run_status(file_change_scanner, verbose=True)
|
||||
#
|
||||
# # Verify changes through sync service directly
|
||||
# doc_changes = await file_change_scanner.find_knowledge_changes(project_dir)
|
||||
# assert len(doc_changes.new) == 2 # test.md and nested.md
|
||||
# assert "test.md" in doc_changes.new
|
||||
# assert "subdir/nested.md" in doc_changes.new
|
||||
# assert len(doc_changes.deleted) == 1 # old/doc.md
|
||||
#
|
||||
# knowledge_changes = await file_change_scanner.find_knowledge_changes(project_dir)
|
||||
# assert len(knowledge_changes.new) == 1
|
||||
# assert "component/test.md" in knowledge_changes.new
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_status_with_case_changes(file_change_scanner, test_config, entity_repository):
|
||||
# """Test status detection with case-sensitive path changes."""
|
||||
# docs_dir = test_config.documents_dir
|
||||
# docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
#
|
||||
# # Create file with initial case
|
||||
# content = "test content"
|
||||
# original_path = "Test.md"
|
||||
# orig_file = docs_dir / original_path
|
||||
# orig_file.write_text(content)
|
||||
# checksum = await compute_checksum(content)
|
||||
#
|
||||
# # Add to DB
|
||||
# await entity_repository.create(
|
||||
# {"permalink": original_path, "file_path": original_path, "checksum": checksum}
|
||||
# )
|
||||
#
|
||||
# # Simulate case change in filesystem
|
||||
# orig_file.rename(docs_dir / "test.md")
|
||||
#
|
||||
# # Check changes
|
||||
# changes = await file_change_scanner.find_knowledge_changes(docs_dir)
|
||||
# assert len(changes.moved) == 1
|
||||
# assert "test.md" in changes.moved
|
||||
# assert changes.moved["test.md"].moved_from == original_path
|
||||
#
|
||||
#
|
||||
# @pytest.mark.asyncio
|
||||
# async def test_status_with_spaces(file_change_scanner, test_config, entity_repository):
|
||||
# """Test status handling files with spaces and special characters."""
|
||||
# docs_dir = test_config.documents_dir
|
||||
# docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
#
|
||||
# # Create file with spaces
|
||||
# content = "test content"
|
||||
# path = "My Document.md"
|
||||
# file_path = docs_dir / path
|
||||
# file_path.write_text(content)
|
||||
# checksum = await compute_checksum(content)
|
||||
#
|
||||
# # Add to DB with same path
|
||||
# await entity_repository.create({"permalink": path, "file_path": path, "checksum": checksum})
|
||||
#
|
||||
# # Check changes
|
||||
# changes = await file_change_scanner.find_knowledge_changes(docs_dir)
|
||||
# assert not changes.modified # File unchanged
|
||||
# assert not changes.moved # Path matches exactly
|
||||
# assert not changes.deleted
|
||||
# assert not changes.new
|
||||
@@ -65,7 +65,7 @@ async def test_parse_complete_file(test_config, entity_parser, valid_entity_cont
|
||||
assert len(entity.observations) == 3
|
||||
obs = entity.observations[0]
|
||||
assert obs.category == "design"
|
||||
assert obs.content == "Stateless authentication"
|
||||
assert obs.content == "Stateless authentication #security #architecture"
|
||||
assert set(obs.tags or []) == {"security", "architecture"}
|
||||
assert obs.context == "JWT based"
|
||||
|
||||
@@ -174,7 +174,7 @@ async def test_parse_file_without_section_headers(test_config, entity_parser):
|
||||
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0].category == "note"
|
||||
assert entity.observations[0].content == "Basic observation"
|
||||
assert entity.observations[0].content == "Basic observation #test"
|
||||
assert entity.observations[0].tags == ["test"]
|
||||
|
||||
assert len(entity.relations) == 2
|
||||
|
||||
@@ -16,7 +16,7 @@ def test_observation_plugin():
|
||||
obs = obs_token.meta['observation']
|
||||
|
||||
assert obs['category'] == 'design'
|
||||
assert obs['content'] == 'Core feature'
|
||||
assert obs['content'] == 'Core feature #important #mvp'
|
||||
assert set(obs['tags']) == {'important', 'mvp'}
|
||||
assert obs['context'] is None
|
||||
|
||||
@@ -26,7 +26,7 @@ def test_observation_plugin():
|
||||
obs = obs_token.meta['observation']
|
||||
|
||||
assert obs['category'] == 'feature'
|
||||
assert obs['content'] == 'Authentication system'
|
||||
assert obs['content'] == 'Authentication system #security'
|
||||
assert set(obs['tags']) == {'security'}
|
||||
assert obs['context'] == 'Required for MVP'
|
||||
|
||||
@@ -36,7 +36,7 @@ def test_observation_plugin():
|
||||
obs = obs_token.meta['observation']
|
||||
|
||||
assert obs['category'] is None
|
||||
assert obs['content'] == 'Authentication system'
|
||||
assert obs['content'] == 'Authentication system #security'
|
||||
assert set(obs['tags']) == {'security'}
|
||||
assert obs['context'] == 'Required for MVP'
|
||||
|
||||
@@ -61,7 +61,7 @@ def test_observation_edge_cases():
|
||||
tokens = md.parse("- [code] Function (x) returns y #function")
|
||||
obs_token = next(t for t in tokens if t.meta and 'observation' in t.meta)
|
||||
obs = obs_token.meta['observation']
|
||||
assert obs['content'] == 'Function (x) returns y'
|
||||
assert obs['content'] == 'Function (x) returns y #function'
|
||||
assert obs['context'] is None
|
||||
|
||||
# Multiple hashtags together
|
||||
|
||||
@@ -71,7 +71,7 @@ async def test_write_new_file_with_content(markdown_processor: MarkdownProcessor
|
||||
content="# Custom Title\n\nMy content here.\nMultiple lines.",
|
||||
observations=[
|
||||
Observation(
|
||||
content="Test observation",
|
||||
content="Test observation #test",
|
||||
category="tech",
|
||||
tags=["test"],
|
||||
context="test context",
|
||||
|
||||
@@ -50,13 +50,13 @@ def test_complex_format():
|
||||
obs = parse_observation(token)
|
||||
assert obs["category"] == "complex test"
|
||||
assert set(obs["tags"]) == {"tag1", "tag2", "tag3"}
|
||||
assert obs["content"] == "This is with content"
|
||||
assert obs["content"] == "This is #tag1#tag2 with #tag3 content"
|
||||
|
||||
# Pydantic model validation
|
||||
observation = Observation.model_validate(obs)
|
||||
assert observation.category == "complex test"
|
||||
assert set(observation.tags) == {"tag1", "tag2", "tag3"}
|
||||
assert observation.content == "This is with content"
|
||||
assert observation.content == "This is #tag1#tag2 with #tag3 content"
|
||||
|
||||
|
||||
def test_malformed_category():
|
||||
@@ -91,20 +91,6 @@ def test_no_category():
|
||||
assert observation.content == "No category"
|
||||
|
||||
|
||||
def test_whitespace_handling():
|
||||
"""Test handling of various whitespace in content."""
|
||||
md = MarkdownIt().use(observation_plugin)
|
||||
|
||||
# Various whitespace in content
|
||||
test_chars = {" ": "space", "\t": "tab", "\r": "return"}
|
||||
for char, name in test_chars.items():
|
||||
content = f"- [test] Content{char}with{char}{name}"
|
||||
tokens = md.parse(content)
|
||||
token = next(t for t in tokens if t.type == "inline")
|
||||
obs = parse_observation(token)
|
||||
observation = Observation.model_validate(obs)
|
||||
assert observation.content == f"Content with {name}"
|
||||
|
||||
|
||||
def test_unicode_content():
|
||||
"""Test handling of Unicode content."""
|
||||
|
||||
@@ -44,7 +44,7 @@ async def test_unicode_content(tmp_path):
|
||||
assert "🧪" in entity.content
|
||||
|
||||
# Verify Unicode in observations
|
||||
assert any(o.content == "Emoji test 👍" for o in entity.observations)
|
||||
assert any(o.content == "Emoji test 👍 #emoji #test" for o in entity.observations)
|
||||
assert any(o.category == "中文" for o in entity.observations)
|
||||
assert any(o.category == "русский" for o in entity.observations)
|
||||
|
||||
|
||||
@@ -326,7 +326,7 @@ See the [[Git Cheat Sheet]] for reference.
|
||||
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0].category == "design"
|
||||
assert entity.observations[0].content == "Keep feature branches short-lived"
|
||||
assert entity.observations[0].content == "Keep feature branches short-lived #git #workflow"
|
||||
assert set(entity.observations[0].tags) == {"git", "workflow"}
|
||||
assert entity.observations[0].context == "Reduces merge conflicts"
|
||||
|
||||
@@ -416,7 +416,7 @@ See the [[Git Cheat Sheet]] for reference.
|
||||
|
||||
assert len(entity.observations) == 1
|
||||
assert entity.observations[0].category == "design"
|
||||
assert entity.observations[0].content == "Keep feature branches short-lived"
|
||||
assert entity.observations[0].content == "Keep feature branches short-lived #git #workflow"
|
||||
assert set(entity.observations[0].tags) == {"git", "workflow"}
|
||||
assert entity.observations[0].context == "Reduces merge conflicts"
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ async def test_handle_file_moved(test_config, watch_service, sync_service, sampl
|
||||
assert len(watch_service.state.recent_events) == 2
|
||||
assert watch_service.state.synced_files == 1
|
||||
event = watch_service.state.recent_events[0]
|
||||
assert event.path == "test.md"
|
||||
assert event.path == "test.md -> moved.md"
|
||||
assert event.action == "moved"
|
||||
assert event.status == "success"
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@ from basic_memory import __version__
|
||||
|
||||
|
||||
def test_version():
|
||||
"""Test version is set correctly"""
|
||||
assert __version__ == "0.1.0"
|
||||
"""Test version is set"""
|
||||
assert __version__ is not None
|
||||
Reference in New Issue
Block a user