diff --git a/surfsense_backend/tests/unit/services/okf/test_serializer.py b/surfsense_backend/tests/unit/services/okf/test_serializer.py new file mode 100644 index 000000000..157ed44c8 --- /dev/null +++ b/surfsense_backend/tests/unit/services/okf/test_serializer.py @@ -0,0 +1,130 @@ +"""OKF serializer/validator self-checks: emitted concepts stay conformant and the +frontmatter fields consumers rely on (type/title/timestamp) round-trip. +""" + +from datetime import UTC, datetime + +from app.db import Document, DocumentType +from app.services.okf import ( + ConceptRef, + LogEntry, + SubdirRef, + document_to_concept, + folder_to_index, + folder_to_log, + is_conformant_concept, + parse_frontmatter, + validate_concept, +) + + +def _make_document() -> Document: + return Document( + title="Weekly Sync Notes", + document_type=DocumentType.NOTE, + document_metadata={"tags": ["team", "meeting"], "url": "https://example.com/n"}, + updated_at=datetime(2026, 5, 28, 22, 49, 59, tzinfo=UTC), + ) + + +def test_concept_is_conformant_and_roundtrips() -> None: + concept = document_to_concept(_make_document(), body="# Agenda\n\nShip OKF.") + + assert is_conformant_concept(concept) + + frontmatter, error = parse_frontmatter(concept) + assert error is None + assert frontmatter["type"] == "Note" + assert frontmatter["title"] == "Weekly Sync Notes" + assert frontmatter["tags"] == ["team", "meeting"] + assert frontmatter["resource"] == "https://example.com/n" + # timestamp must survive as an ISO-8601 string, not a parsed datetime. + assert frontmatter["timestamp"] == "2026-05-28T22:49:59+00:00" + assert "# Agenda" in concept + + +def test_type_is_always_present_even_without_metadata() -> None: + doc = Document(title="Raw", document_type=DocumentType.CRAWLED_URL) + concept = document_to_concept(doc, body="body") + frontmatter, error = parse_frontmatter(concept) + assert error is None + assert frontmatter["type"] == "Web Page" + # No source URL / tags available -> those recommended keys are omitted. + assert "resource" not in frontmatter + assert "tags" not in frontmatter + + +def test_validator_rejects_non_conformant_documents() -> None: + assert validate_concept("no frontmatter here") + assert validate_concept("---\ntitle: Missing type\n---\nbody") + + +def test_folder_index_groups_by_type_and_lists_subdirs() -> None: + index = folder_to_index( + concepts=[ + ConceptRef(title="Orders", filename="orders.md", type="Note", description="x"), + ], + subdirectories=[SubdirRef(name="tables", description="Table docs")], + ) + assert "# Subdirectories" in index + assert "* [tables](tables/index.md) - Table docs" in index + assert "# Note" in index + assert "* [Orders](orders.md) - x" in index + + +def test_folder_log_lists_concepts_newest_first() -> None: + log = folder_to_log( + [ + LogEntry(title="Older", timestamp="2026-01-01T00:00:00+00:00"), + LogEntry(title="Newer", timestamp="2026-06-01T00:00:00+00:00"), + LogEntry(title="Undated", timestamp=None), + ] + ) + assert "# Change Log" in log + # Newest dated entry precedes the older one; undated sorts last. + assert log.index("Newer") < log.index("Older") < log.index("Undated") + assert "* Newer - 2026-06-01T00:00:00+00:00" in log + + +def test_folder_log_is_empty_when_no_entries() -> None: + assert folder_to_log([]) == "" + + +def test_export_log_files_synthesized_only_where_docs_live() -> None: + from app.services.export_service import _build_log_files + + files = dict( + _build_log_files( + { + "": [LogEntry(title="Root Doc", timestamp="2026-05-01T00:00:00+00:00")], + "Research/AI": [LogEntry(title="Nested", timestamp=None)], + } + ) + ) + assert "# Change Log" in files["log.md"] + assert "Root Doc" in files["log.md"] + assert "Nested" in files["Research/AI/log.md"] + # No empty intermediate log: "Research" holds no concepts of its own. + assert "Research/log.md" not in files + + +def test_export_index_files_include_root_version_and_ancestors() -> None: + from app.services.export_service import _build_index_files + + # A concept nested two levels deep, with no direct docs in the middle dir. + files = dict( + _build_index_files( + { + "Research/AI": [ + ConceptRef(title="Note", filename="note.md", type="Note") + ] + } + ) + ) + + # Root, the empty intermediate dir, and the leaf all get an index.md so the + # hierarchy is fully navigable. + assert files["index.md"].startswith('---\nokf_version: "0.1"\n---') + assert "* [Research](Research/index.md)" in files["index.md"] + assert "* [AI](AI/index.md)" in files["Research/index.md"] + assert "* [Note](note.md)" in files["Research/AI/index.md"]