mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix unresolved links on sync
This commit is contained in:
@@ -70,6 +70,7 @@ async def get_sync_service(db_type=DatabaseType.FILESYSTEM):
|
||||
entity_sync_service=knowledge_sync_service,
|
||||
entity_parser=entity_parser,
|
||||
entity_repository=entity_repository,
|
||||
relation_repository=relation_repository,
|
||||
search_service=search_service,
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from sqlalchemy import (
|
||||
Text,
|
||||
ForeignKey,
|
||||
UniqueConstraint,
|
||||
text,
|
||||
DateTime,
|
||||
Index,
|
||||
JSON,
|
||||
@@ -185,7 +184,10 @@ class Relation(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
from_id: Mapped[int] = mapped_column(Integer, ForeignKey("entity.id", ondelete="CASCADE"))
|
||||
to_id: Mapped[int] = mapped_column(Integer, ForeignKey("entity.id", ondelete="CASCADE"))
|
||||
to_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("entity.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
to_name: Mapped[str] = mapped_column(String)
|
||||
relation_type: Mapped[str] = mapped_column(String)
|
||||
context: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime)
|
||||
@@ -206,7 +208,9 @@ class Relation(Base):
|
||||
|
||||
return generate_permalink(
|
||||
f"{self.from_entity.permalink}/{self.relation_type}/{self.to_entity.permalink}"
|
||||
if self.to_entity
|
||||
else f"{self.from_entity.permalink}/{self.relation_type}"
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Relation(id={self.id}, from_id={self.from_id}, to_id={self.to_id}, type='{self.relation_type}')"
|
||||
return f"Relation(id={self.id}, from_id={self.from_id}, to_id={self.to_id}, type='{self.relation_type}')"
|
||||
|
||||
@@ -67,5 +67,12 @@ class RelationRepository(Repository[Relation]):
|
||||
async with db.scoped_session(self.session_maker) as session:
|
||||
await session.execute(delete(Relation).where(Relation.from_id == entity_id))
|
||||
|
||||
async def find_unresolved_relations(self) -> Sequence[Relation]:
|
||||
"""Find all unresolved relations, where to_id is null."""
|
||||
query = select(Relation).filter(Relation.to_id.is_(None))
|
||||
result = await self.execute_query(query)
|
||||
return result.scalars().all()
|
||||
|
||||
|
||||
def get_load_options(self) -> List[LoaderOption]:
|
||||
return [selectinload(Relation.from_entity), selectinload(Relation.to_entity)]
|
||||
|
||||
@@ -47,6 +47,7 @@ class RelationService(BaseService[RelationRepository]):
|
||||
relation = RelationModel(
|
||||
from_id=from_entity.id,
|
||||
to_id=to_entity.id,
|
||||
to_name=to_entity.title,
|
||||
relation_type=rs.relation_type,
|
||||
context=rs.context,
|
||||
)
|
||||
|
||||
@@ -177,7 +177,7 @@ class SearchService:
|
||||
# Only index outgoing relations (ones defined in this file)
|
||||
for rel in entity.outgoing_relations:
|
||||
# Create descriptive title showing the relationship
|
||||
relation_title = f"{rel.from_entity.title} → {rel.to_entity.title}"
|
||||
relation_title = f"{rel.from_entity.title} → {rel.to_entity.title}" if rel.to_entity else f"{rel.from_entity.title}"
|
||||
|
||||
await self._do_index(
|
||||
SearchIndexRow(
|
||||
|
||||
@@ -159,15 +159,16 @@ class EntitySyncService:
|
||||
rel.target,
|
||||
)
|
||||
|
||||
# Look up target entity
|
||||
if not target_entity:
|
||||
logger.warning(f"Skipping relation in {file_path}: target not found: {rel.target}")
|
||||
continue
|
||||
# if the target is found, store the id
|
||||
target_id = target_entity.id if target_entity else None
|
||||
# if the target is found, store the title, otherwise add the target for a "forward link"
|
||||
target_name = target_entity.title if target_entity else rel.target
|
||||
|
||||
# Create the relation
|
||||
relation = Relation(
|
||||
from_id=db_entity.id,
|
||||
to_id=target_entity.id,
|
||||
to_id=target_id,
|
||||
to_name=target_name,
|
||||
relation_type=rel.type,
|
||||
context=rel.context,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Dict
|
||||
from loguru import logger
|
||||
|
||||
from basic_memory.markdown import EntityParser, EntityMarkdown
|
||||
from basic_memory.repository import EntityRepository
|
||||
from basic_memory.repository import EntityRepository, RelationRepository
|
||||
from basic_memory.services.search_service import SearchService
|
||||
from basic_memory.sync import FileChangeScanner
|
||||
from basic_memory.sync.entity_sync_service import EntitySyncService
|
||||
@@ -27,12 +27,14 @@ class SyncService:
|
||||
entity_sync_service: EntitySyncService,
|
||||
entity_parser: EntityParser,
|
||||
entity_repository: EntityRepository,
|
||||
relation_repository: RelationRepository,
|
||||
search_service: SearchService,
|
||||
):
|
||||
self.scanner = scanner
|
||||
self.entity_sync_service = entity_sync_service
|
||||
self.entity_parser = entity_parser
|
||||
self.entity_repository = entity_repository
|
||||
self.relation_repository = relation_repository
|
||||
self.search_service = search_service
|
||||
|
||||
async def handle_entity_deletion(self, file_path: str):
|
||||
@@ -119,4 +121,20 @@ class SyncService:
|
||||
# Set final checksum to mark sync complete
|
||||
await self.entity_repository.update(entity.id, {"checksum": checksum})
|
||||
|
||||
# Third pass: Try to resolve any forward references
|
||||
logger.debug("Attempting to resolve forward references")
|
||||
for relation in await self.relation_repository.find_unresolved_relations():
|
||||
target_entity = await self.entity_sync_service.link_resolver.resolve_link(relation.to_name)
|
||||
# check we found a link that is not the source
|
||||
if target_entity and target_entity.id != relation.from_id:
|
||||
logger.debug(f"Resolved forward reference: {relation.to_name} -> {target_entity.permalink}")
|
||||
await self.relation_repository.update(relation.id, {
|
||||
"to_id": target_entity.id,
|
||||
"to_name": target_entity.title # Update to actual title
|
||||
})
|
||||
|
||||
# update search index
|
||||
await self.search_service.index_entity(target_entity)
|
||||
|
||||
|
||||
return changes
|
||||
|
||||
@@ -182,6 +182,7 @@ async def sync_service(
|
||||
file_change_scanner: FileChangeScanner,
|
||||
entity_parser: EntityParser,
|
||||
entity_repository: EntityRepository,
|
||||
relation_repository: RelationRepository,
|
||||
search_service: SearchService,
|
||||
) -> SyncService:
|
||||
"""Create sync service for testing."""
|
||||
@@ -189,6 +190,7 @@ async def sync_service(
|
||||
scanner=file_change_scanner,
|
||||
entity_sync_service=entity_sync_service,
|
||||
entity_repository=entity_repository,
|
||||
relation_repository=relation_repository,
|
||||
entity_parser=entity_parser,
|
||||
search_service=search_service,
|
||||
)
|
||||
@@ -263,6 +265,7 @@ async def full_entity(sample_entity, entity_repository):
|
||||
Relation(
|
||||
from_id=search_entity.id,
|
||||
to_id=sample_entity.id,
|
||||
to_name=sample_entity.title,
|
||||
relation_type="out1",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
@@ -270,6 +273,7 @@ async def full_entity(sample_entity, entity_repository):
|
||||
Relation(
|
||||
from_id=search_entity.id,
|
||||
to_id=sample_entity.id,
|
||||
to_name=sample_entity.title,
|
||||
relation_type="out2",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
@@ -359,6 +363,7 @@ async def test_graph(
|
||||
Relation(
|
||||
from_id=root.id,
|
||||
to_id=conn1.id,
|
||||
to_name = conn1.title,
|
||||
relation_type="connects_to",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
@@ -366,6 +371,7 @@ async def test_graph(
|
||||
Relation(
|
||||
from_id=conn1.id,
|
||||
to_id=conn2.id,
|
||||
to_name=conn2.title,
|
||||
relation_type="connected_to",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
@@ -374,6 +380,7 @@ async def test_graph(
|
||||
Relation(
|
||||
from_id=conn2.id,
|
||||
to_id=deep.id,
|
||||
to_name=deep.title,
|
||||
relation_type="deep_connection",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
@@ -382,6 +389,7 @@ async def test_graph(
|
||||
Relation(
|
||||
from_id=deep.id,
|
||||
to_id=deeper.id,
|
||||
to_name=deeper.title,
|
||||
relation_type="deeper_connection",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
|
||||
@@ -65,6 +65,7 @@ async def related_results(session_maker):
|
||||
relation = Relation(
|
||||
from_id=source.id,
|
||||
to_id=target.id,
|
||||
to_name=target.title,
|
||||
relation_type="connects_to",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
@@ -465,11 +466,23 @@ async def test_list_entities_with_related(entity_repository: EntityRepository, s
|
||||
# Create relations in both directions
|
||||
relations = [
|
||||
# core -> db (depends_on)
|
||||
Relation(from_id=core.id, to_id=dbe.id, relation_type="depends_on", created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),),
|
||||
Relation(
|
||||
from_id=core.id,
|
||||
to_id=dbe.id,
|
||||
to_name="db_service",
|
||||
relation_type="depends_on",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
),
|
||||
# config -> core (configures)
|
||||
Relation(from_id=config.id, to_id=core.id, relation_type="configures", created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),),
|
||||
Relation(
|
||||
from_id=config.id,
|
||||
to_id=core.id,
|
||||
to_name="core_service",
|
||||
relation_type="configures",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
),
|
||||
]
|
||||
session.add_all(relations)
|
||||
|
||||
|
||||
@@ -52,9 +52,9 @@ async def target_entity(session_maker):
|
||||
async def test_relations(session_maker, source_entity, target_entity):
|
||||
"""Create test relations."""
|
||||
relations = [
|
||||
Relation(from_id=source_entity.id, to_id=target_entity.id, relation_type="connects_to", created_at=datetime.now(timezone.utc),
|
||||
Relation(from_id=source_entity.id, to_id=target_entity.id, to_name=target_entity.title, relation_type="connects_to", created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),),
|
||||
Relation(from_id=source_entity.id, to_id=target_entity.id, relation_type="depends_on", created_at=datetime.now(timezone.utc),
|
||||
Relation(from_id=source_entity.id, to_id=target_entity.id, to_name=target_entity.title, relation_type="depends_on", created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),),
|
||||
]
|
||||
async with db.scoped_session(session_maker) as session:
|
||||
@@ -86,6 +86,7 @@ async def sample_relation(
|
||||
relation_data = {
|
||||
"from_id": sample_entity.id,
|
||||
"to_id": related_entity.id,
|
||||
"to_name": related_entity.title,
|
||||
"relation_type": "test_relation",
|
||||
"context": "test-context",
|
||||
}
|
||||
@@ -101,18 +102,21 @@ async def multiple_relations(
|
||||
{
|
||||
"from_id": sample_entity.id,
|
||||
"to_id": related_entity.id,
|
||||
"to_name": related_entity.title,
|
||||
"relation_type": "relation_one",
|
||||
"context": "context_one",
|
||||
},
|
||||
{
|
||||
"from_id": sample_entity.id,
|
||||
"to_id": related_entity.id,
|
||||
"to_name": related_entity.title,
|
||||
"relation_type": "relation_two",
|
||||
"context": "context_two",
|
||||
},
|
||||
{
|
||||
"from_id": related_entity.id,
|
||||
"to_id": sample_entity.id,
|
||||
"to_name": related_entity.title,
|
||||
"relation_type": "relation_one",
|
||||
"context": "context_three",
|
||||
},
|
||||
@@ -128,6 +132,7 @@ async def test_create_relation(
|
||||
relation_data = {
|
||||
"from_id": sample_entity.id,
|
||||
"to_id": related_entity.id,
|
||||
"to_name": related_entity.title,
|
||||
"relation_type": "test_relation",
|
||||
"context": "test-context",
|
||||
}
|
||||
@@ -147,6 +152,7 @@ async def test_create_relation_entity_does_not_exist(
|
||||
relation_data = {
|
||||
"from_id": "not_exist",
|
||||
"to_id": related_entity.id,
|
||||
"to_name": related_entity.title,
|
||||
"relation_type": "test_relation",
|
||||
"context": "test-context",
|
||||
}
|
||||
@@ -187,6 +193,28 @@ async def test_find_by_type(relation_repository: RelationRepository, sample_rela
|
||||
assert relations[0].id == sample_relation.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_find_unresolved_relations(
|
||||
relation_repository: RelationRepository, sample_entity: Entity, related_entity: Entity
|
||||
):
|
||||
"""Test creating a new relation"""
|
||||
relation_data = {
|
||||
"from_id": sample_entity.id,
|
||||
"to_id": None,
|
||||
"to_name": related_entity.title,
|
||||
"relation_type": "test_relation",
|
||||
"context": "test-context",
|
||||
}
|
||||
relation = await relation_repository.create(relation_data)
|
||||
|
||||
assert relation.from_id == sample_entity.id
|
||||
assert relation.to_id is None
|
||||
|
||||
unresolved = await relation_repository.find_unresolved_relations()
|
||||
assert len(unresolved) == 1
|
||||
assert unresolved[0].id == relation.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_by_fields_single_field(
|
||||
relation_repository: RelationRepository, multiple_relations: list[Relation]
|
||||
@@ -311,3 +339,4 @@ async def test_delete_nonexistent_relation(relation_repository):
|
||||
"""Test deleting a relation that doesn't exist."""
|
||||
result = await relation_repository.delete_by_fields(relation_type="nonexistent")
|
||||
assert result is False
|
||||
|
||||
|
||||
@@ -110,6 +110,10 @@ async def test_update_entity_relations(
|
||||
entity_sync_service: EntitySyncService, test_markdown: EntityMarkdown
|
||||
):
|
||||
"""Test second pass relation updates."""
|
||||
|
||||
# add a forward link to the markdown (entity does not exist)
|
||||
test_markdown.content.relations.append(MarkdownRelation(type="depends_on", target="concept/doesnt-exist"))
|
||||
|
||||
# Create main entity first
|
||||
entity = await entity_sync_service.create_entity_from_markdown("test.md", test_markdown)
|
||||
|
||||
@@ -136,10 +140,10 @@ async def test_update_entity_relations(
|
||||
|
||||
# Check relations
|
||||
assert updated is not None, "Entity should be updated"
|
||||
assert len(updated.relations) == 2
|
||||
assert len(updated.relations) == 3
|
||||
|
||||
# Check relation details
|
||||
relations = sorted(updated.relations, key=lambda r: r.relation_type)
|
||||
relations = sorted(updated.relations, key=lambda r: r.id)
|
||||
|
||||
assert relations[0].relation_type == "depends_on"
|
||||
assert relations[0].from_id == entity.id
|
||||
@@ -149,6 +153,11 @@ async def test_update_entity_relations(
|
||||
assert relations[1].from_id == entity.id
|
||||
assert relations[1].to_id == another_entity.id
|
||||
|
||||
assert relations[2].relation_type == "depends_on"
|
||||
assert relations[2].from_id == entity.id
|
||||
assert relations[2].to_id is None
|
||||
assert relations[2].to_name == "concept/doesnt-exist"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_two_pass_sync_flow(
|
||||
|
||||
@@ -20,6 +20,57 @@ async def create_test_file(path: Path, content: str = "test content") -> None:
|
||||
path.write_text(content)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_knowledge(
|
||||
sync_service: SyncService, test_config: ProjectConfig, entity_service: EntityService
|
||||
@@ -71,11 +122,63 @@ A test concept.
|
||||
test_concept = next(e for e in entities if e.permalink == "concept/test-concept")
|
||||
assert test_concept.entity_type == "knowledge"
|
||||
|
||||
# Verify relation was not created
|
||||
# because file for related entity was not found
|
||||
# Verify relation was created
|
||||
# with forward link
|
||||
entity = await entity_service.get_by_permalink(test_concept.permalink)
|
||||
relations = entity.relations
|
||||
assert len(relations) == 0
|
||||
assert len(relations) == 1
|
||||
assert relations[0].to_name == "concept/other"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -112,7 +215,60 @@ modified: 2024-01-01
|
||||
"concept/depends-on-future"
|
||||
)
|
||||
assert entity is not None
|
||||
assert len(entity.relations) == 0 # Relations to nonexistent entities should be skipped
|
||||
assert len(entity.relations) == 2
|
||||
assert entity.relations[0].to_name == "concept/not_created_yet"
|
||||
assert entity.relations[1].to_name == "concept/also_future"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -189,6 +345,57 @@ modified: 2024-01-01
|
||||
assert b_relation.to_id == entity_a.id
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_entity_duplicate_relations(
|
||||
sync_service: SyncService, test_config: ProjectConfig
|
||||
@@ -251,6 +458,57 @@ modified: 2024-01-01
|
||||
assert relation_counts["uses"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_entity_with_invalid_category(
|
||||
sync_service: SyncService, test_config: ProjectConfig
|
||||
@@ -292,6 +550,57 @@ modified: 2024-01-01
|
||||
assert "design" in categories
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_entity_with_order_dependent_relations(
|
||||
sync_service: SyncService, test_config: ProjectConfig
|
||||
@@ -378,6 +687,57 @@ modified: 2024-01-01
|
||||
assert len(entity_c.incoming_relations) == 2 # A and B depend on C
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_empty_directories(sync_service: SyncService, test_config: ProjectConfig):
|
||||
"""Test syncing empty directories."""
|
||||
@@ -387,6 +747,57 @@ async def test_sync_empty_directories(sync_service: SyncService, test_config: Pr
|
||||
assert (test_config.home).exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_file_modified_during_sync(
|
||||
sync_service: SyncService, test_config: ProjectConfig
|
||||
@@ -425,6 +836,57 @@ modified: 2024-01-01
|
||||
assert doc.checksum is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_permalink_formatting(
|
||||
sync_service: SyncService, test_config: ProjectConfig, entity_service: EntityService
|
||||
@@ -468,6 +930,57 @@ Testing permalink generation.
|
||||
), f"File {filename} should have permalink {expected_permalink}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_entity_deletion(
|
||||
test_graph,
|
||||
@@ -498,6 +1011,57 @@ async def test_handle_entity_deletion(
|
||||
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_preserves_timestamps(
|
||||
sync_service: SyncService,
|
||||
@@ -544,6 +1108,57 @@ Testing file timestamps
|
||||
assert abs((file_entity.created_at.timestamp() - file_stats.st_ctime)) < 1 # Allow 1s difference
|
||||
assert abs((file_entity.updated_at.timestamp() - file_stats.st_mtime)) < 1 # Allow 1s difference
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_forward_reference_resolution(
|
||||
sync_service: SyncService,
|
||||
test_config: ProjectConfig,
|
||||
entity_service: EntityService,
|
||||
):
|
||||
"""Test that forward references get resolved when target file is created."""
|
||||
project_dir = test_config.home
|
||||
|
||||
# First create a file with a forward reference
|
||||
source_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Source Document
|
||||
|
||||
## Relations
|
||||
- depends_on [[target-doc]]
|
||||
"""
|
||||
await create_test_file(project_dir / "source.md", source_content)
|
||||
|
||||
# Initial sync - should create forward reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify forward reference
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id is None
|
||||
assert source.relations[0].to_name == "target-doc"
|
||||
|
||||
# Now create the target file
|
||||
target_content = """
|
||||
---
|
||||
type: knowledge
|
||||
---
|
||||
# Target Doc
|
||||
Target content
|
||||
"""
|
||||
await create_test_file(project_dir / "target_doc.md", target_content)
|
||||
|
||||
# Sync again - should resolve the reference
|
||||
await sync_service.sync(test_config.home)
|
||||
|
||||
# Verify reference is now resolved
|
||||
source = await entity_service.get_by_permalink("source")
|
||||
target = await entity_service.get_by_permalink("target-doc")
|
||||
assert len(source.relations) == 1
|
||||
assert source.relations[0].to_id == target.id
|
||||
assert source.relations[0].to_name == target.title
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_file_move_updates_search_index(
|
||||
sync_service: SyncService,
|
||||
@@ -581,7 +1196,6 @@ Content for move test
|
||||
assert len(results) == 1
|
||||
assert results[0].file_path == str(new_path.relative_to(project_dir))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_null_checksum_cleanup(
|
||||
sync_service: SyncService, test_config: ProjectConfig, entity_service: EntityService
|
||||
|
||||
Reference in New Issue
Block a user