Files
2026-04-17 07:08:21 -05:00

400 lines
13 KiB
Python

"""Focused tests for the one-file markdown sync primitive."""
from pathlib import Path
from textwrap import dedent
from unittest.mock import AsyncMock
import pytest
from basic_memory.file_utils import compute_checksum, remove_frontmatter
from basic_memory.schemas import Entity as EntitySchema
def _write_markdown(project_root: Path, relative_path: str, content: str) -> Path:
"""Create one markdown file under the test project."""
file_path = project_root / relative_path
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(content, encoding="utf-8")
return file_path
@pytest.mark.asyncio
async def test_sync_one_markdown_file_writes_missing_frontmatter_and_returns_canonical_content(
sync_service,
test_project,
app_config,
monkeypatch,
):
"""Missing frontmatter is written once and returned exactly as stored on disk."""
app_config.ensure_frontmatter_on_sync = True
file_path = _write_markdown(
Path(test_project.path),
"notes/frontmatterless.md",
"# Frontmatterless\n\nBody content.\n",
)
index_entity_data = AsyncMock()
monkeypatch.setattr(sync_service.search_service, "index_entity_data", index_entity_data)
result = await sync_service.sync_one_markdown_file("notes/frontmatterless.md")
final_content = file_path.read_text(encoding="utf-8")
assert result.markdown_content == final_content
assert result.entity.permalink == f"{test_project.name}/notes/frontmatterless"
assert f"permalink: {result.entity.permalink}" in final_content
assert result.checksum == await sync_service.file_service.compute_checksum(
"notes/frontmatterless.md"
)
assert result.size == file_path.stat().st_size
index_entity_data.assert_awaited_once_with(
result.entity,
content=remove_frontmatter(final_content),
)
@pytest.mark.asyncio
async def test_sync_one_markdown_file_rewrites_permalink_once_after_repository_conflict(
sync_service,
entity_service,
test_project,
monkeypatch,
):
"""Late DB conflict resolution updates the file exactly once with the accepted permalink."""
existing = await entity_service.create_entity_with_content(
EntitySchema(
title="Existing Note",
directory="notes",
content="# Existing Note\n\nOriginal content.\n",
)
)
conflicting_permalink = existing.entity.permalink
assert conflicting_permalink is not None
file_path = _write_markdown(
Path(test_project.path),
"notes/race.md",
dedent(
f"""\
---
title: Race Note
type: note
permalink: {conflicting_permalink}
---
# Race Note
Body content.
"""
),
)
async def stale_permalink(*args, **kwargs) -> str:
return conflicting_permalink
original_writer = sync_service.file_service.update_frontmatter_with_result
frontmatter_writer = AsyncMock(side_effect=original_writer)
monkeypatch.setattr(sync_service.entity_service, "resolve_permalink", stale_permalink)
monkeypatch.setattr(
sync_service.file_service,
"update_frontmatter_with_result",
frontmatter_writer,
)
result = await sync_service.sync_one_markdown_file("notes/race.md", index_search=False)
final_content = file_path.read_text(encoding="utf-8")
assert frontmatter_writer.await_count == 1
assert result.entity.permalink == f"{conflicting_permalink}-1"
assert result.markdown_content == final_content
assert f"permalink: {result.entity.permalink}" in final_content
assert result.checksum == await sync_service.file_service.compute_checksum("notes/race.md")
@pytest.mark.asyncio
async def test_sync_one_markdown_file_returns_original_content_when_no_rewrite_needed(
sync_service,
test_project,
monkeypatch,
):
"""Canonical markdown is returned as-read when frontmatter already matches."""
original_content = dedent(
f"""\
---
title: No Rewrite
type: note
permalink: {test_project.name}/notes/no-rewrite
---
# No Rewrite
Body content.
"""
)
file_path = _write_markdown(
Path(test_project.path),
"notes/no-rewrite.md",
original_content,
)
original_writer = sync_service.file_service.update_frontmatter_with_result
frontmatter_writer = AsyncMock(side_effect=original_writer)
monkeypatch.setattr(
sync_service.file_service,
"update_frontmatter_with_result",
frontmatter_writer,
)
result = await sync_service.sync_one_markdown_file("notes/no-rewrite.md", index_search=False)
# Trigger: Windows persists CRLF for text files even when the test literal uses LF.
# Why: this assertion cares about "no rewrite happened", not about pinning one newline style.
# Outcome: compare against the exact markdown bytes stored on disk.
persisted_content = file_path.read_bytes().decode("utf-8")
assert frontmatter_writer.await_count == 0
assert result.markdown_content == persisted_content
assert file_path.read_bytes().decode("utf-8") == persisted_content
assert result.checksum == await sync_service.file_service.compute_checksum(
"notes/no-rewrite.md"
)
@pytest.mark.asyncio
async def test_sync_one_markdown_file_does_not_reread_for_initial_checksum_when_no_rewrite(
sync_service,
test_project,
monkeypatch,
):
"""Initial checksum comes from the loaded file bytes, not a second storage read."""
original_content = dedent(
f"""\
---
title: No Rewrite
type: note
permalink: {test_project.name}/notes/no-rewrite
---
# No Rewrite
Body content.
"""
)
file_path = _write_markdown(
Path(test_project.path),
"notes/no-rewrite.md",
original_content,
)
checksum_spy = AsyncMock()
monkeypatch.setattr(sync_service.file_service, "compute_checksum", checksum_spy)
result = await sync_service.sync_one_markdown_file("notes/no-rewrite.md", index_search=False)
checksum_spy.assert_not_awaited()
assert result.checksum == await compute_checksum(file_path.read_bytes())
@pytest.mark.asyncio
async def test_sync_one_markdown_file_skips_indexing_when_checksum_matches(
sync_service,
test_project,
monkeypatch,
):
"""A matching DB checksum is the consistency boundary for derived indexes."""
original_content = dedent(
f"""\
---
title: Already Current
type: note
permalink: {test_project.name}/notes/already-current
---
# Already Current
- [note] Derived indexes are assumed current when the file checksum matches.
"""
)
file_path = _write_markdown(
Path(test_project.path),
"notes/already-current.md",
original_content,
)
initial = await sync_service.sync_one_markdown_file(
"notes/already-current.md",
index_search=False,
)
assert initial.checksum == await compute_checksum(file_path.read_bytes())
index_markdown_file = AsyncMock(side_effect=AssertionError("indexer should not run"))
index_entity_data = AsyncMock(side_effect=AssertionError("search should not refresh"))
monkeypatch.setattr(sync_service.batch_indexer, "index_markdown_file", index_markdown_file)
monkeypatch.setattr(sync_service.search_service, "index_entity_data", index_entity_data)
result = await sync_service.sync_one_markdown_file("notes/already-current.md")
index_markdown_file.assert_not_awaited()
index_entity_data.assert_not_awaited()
assert result.entity.id == initial.entity.id
assert len(result.entity.observations) == 1
assert result.markdown_content == file_path.read_bytes().decode("utf-8")
assert result.checksum == initial.checksum
@pytest.mark.asyncio
async def test_sync_one_markdown_file_indexes_when_checksum_differs(
sync_service,
test_project,
monkeypatch,
):
"""A DB checksum mismatch still takes the full indexing path."""
initial_content = dedent(
f"""\
---
title: Changed
type: note
permalink: {test_project.name}/notes/changed
---
# Changed
Original body.
"""
)
file_path = _write_markdown(
Path(test_project.path),
"notes/changed.md",
initial_content,
)
initial = await sync_service.sync_one_markdown_file("notes/changed.md", index_search=False)
updated_content = initial_content.replace("Original body.", "Updated body.")
file_path.write_text(updated_content, encoding="utf-8")
original_index_markdown_file = sync_service.batch_indexer.index_markdown_file
async def index_markdown_file_spy(*args, **kwargs):
return await original_index_markdown_file(*args, **kwargs)
index_markdown_file = AsyncMock(side_effect=index_markdown_file_spy)
index_entity_data = AsyncMock()
monkeypatch.setattr(sync_service.batch_indexer, "index_markdown_file", index_markdown_file)
monkeypatch.setattr(sync_service.search_service, "index_entity_data", index_entity_data)
result = await sync_service.sync_one_markdown_file("notes/changed.md")
index_markdown_file.assert_awaited_once()
index_entity_data.assert_awaited_once()
assert result.entity.id == initial.entity.id
assert result.markdown_content == file_path.read_bytes().decode("utf-8")
assert result.checksum == await compute_checksum(file_path.read_bytes())
assert result.checksum != initial.checksum
@pytest.mark.asyncio
async def test_sync_one_markdown_file_can_defer_relation_resolution(
sync_service,
entity_service,
test_project,
monkeypatch,
):
"""Cloud callers can keep one-file sync cheap and repair relations later."""
await entity_service.create_entity_with_content(
EntitySchema(
title="Deferred Target",
directory="notes",
content="# Deferred Target\n",
)
)
_write_markdown(
Path(test_project.path),
"notes/deferred-source.md",
dedent(
"""
---
title: Deferred Source
type: note
---
# Deferred Source
- links_to [[Deferred Target]]
"""
),
)
resolve_link = AsyncMock(side_effect=AssertionError("relation lookup should be deferred"))
monkeypatch.setattr(sync_service.entity_service.link_resolver, "resolve_link", resolve_link)
result = await sync_service.sync_one_markdown_file(
"notes/deferred-source.md",
index_search=False,
resolve_relations=False,
)
resolve_link.assert_not_awaited()
assert len(result.entity.outgoing_relations) == 1
assert result.entity.outgoing_relations[0].to_id is None
assert result.entity.outgoing_relations[0].to_name == "Deferred Target"
@pytest.mark.asyncio
async def test_sync_markdown_file_remains_tuple_compatible(sync_service, test_project):
"""The legacy tuple-returning API still works for existing callers."""
_write_markdown(
Path(test_project.path),
"notes/compat.md",
dedent(
f"""\
---
title: Compat Note
type: note
permalink: {test_project.name}/notes/compat
---
# Compat Note
Body content.
"""
),
)
entity, checksum = await sync_service.sync_markdown_file("notes/compat.md")
assert entity is not None
assert entity.file_path == "notes/compat.md"
assert entity.permalink == f"{test_project.name}/notes/compat"
assert checksum == await sync_service.file_service.compute_checksum("notes/compat.md")
@pytest.mark.asyncio
async def test_sync_one_markdown_file_indexes_thematic_break_content_without_frontmatter(
sync_service,
test_project,
app_config,
monkeypatch,
):
"""Leading thematic-break markdown should index as raw content when frontmatter is absent."""
app_config.ensure_frontmatter_on_sync = False
original_content = "---\nBody content after a thematic break.\n"
file_path = _write_markdown(
Path(test_project.path),
"notes/thematic-break.md",
original_content,
)
index_entity_data = AsyncMock()
monkeypatch.setattr(sync_service.search_service, "index_entity_data", index_entity_data)
result = await sync_service.sync_one_markdown_file("notes/thematic-break.md")
persisted_content = file_path.read_bytes().decode("utf-8")
assert result.markdown_content == persisted_content
assert file_path.read_bytes().decode("utf-8") == persisted_content
index_entity_data.assert_awaited_once_with(
result.entity,
content=persisted_content,
)