feat(notion): migrate 31+32 to API 2025-09-03 + add property writes, upsert, anchor-link fix
Phase 1 — docs alignment: - Fix path drift in 32 CLAUDE.md (02-notion-writer → 32-notion-writer) - Align env var to NOTION_API_KEY in 31 docs (NOTION_TOKEN still accepted) - Document Phase 3 roadmap in 31 (metadata-aware migration, Notion-as-RAG) Phase 2 — multi-source database support: - New 32/scripts/_notion_compat.py: client factory, data_source resolution with cache, property coercion, find_existing_page (for upsert), explain_api_error for friendlier failure messages - 32 notion_writer.py: drop the 2022-06-28 pin, route schema introspection through data_sources.retrieve, build data_source_id parent shape, accept arbitrary properties via --properties JSON-or-file flag, add --upsert-by for idempotent re-runs - 32 markdown parser: anchor-link fix — Notion's URL validator rejects fragment URLs and relative paths; fragments now render as bold to preserve TOC nav intent, relatives drop the link, absolute URLs preserved - 31 async_organizer.py + schema_migrator.py: same data_sources migration with cached resolution; pages.create now uses data_source_id parent - 16/16 parser tests pass (was 13; +3 link-handling regression guards) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -165,6 +165,31 @@ def test_blocks_table():
|
||||
"link inline formatting preserved inside table cell")
|
||||
|
||||
|
||||
def test_rich_text_anchor_link_becomes_bold():
|
||||
"""TOC-style fragment links must not be sent as Notion link annotations."""
|
||||
spans = parse_rich_text("see [Section A](#section-a) below")
|
||||
bold_spans = [s for s in spans if s.get("annotations", {}).get("bold")]
|
||||
_assert(len(bold_spans) == 1, "anchor-only link converted to bold span")
|
||||
_assert(bold_spans[0]["text"]["content"] == "Section A", "anchor link text preserved")
|
||||
_assert("link" not in bold_spans[0]["text"], "no link annotation on fragment URL")
|
||||
|
||||
|
||||
def test_rich_text_relative_link_becomes_plain():
|
||||
"""Relative paths can't be Notion links either; render plain text."""
|
||||
spans = parse_rich_text("see [the spec](../spec.md) please")
|
||||
spec_span = next(s for s in spans if s["text"]["content"] == "the spec")
|
||||
_assert("link" not in spec_span["text"], "relative-path link annotation dropped")
|
||||
_assert("annotations" not in spec_span, "no spurious bold/italic on plain text")
|
||||
|
||||
|
||||
def test_rich_text_absolute_link_preserved():
|
||||
"""Regression: absolute URLs must still produce a real Notion link."""
|
||||
spans = parse_rich_text("[docs](https://example.com/x) and [mail](mailto:a@b.co)")
|
||||
urls = [s["text"].get("link", {}).get("url") for s in spans if s["text"].get("link")]
|
||||
_assert("https://example.com/x" in urls, "https URL preserved as link")
|
||||
_assert("mailto:a@b.co" in urls, "mailto URL preserved as link")
|
||||
|
||||
|
||||
def test_no_literal_markers_leak():
|
||||
"""Regression: before the rich-text rewrite, ** and [ ] leaked as literal text."""
|
||||
blocks = markdown_to_notion_blocks("A **bold** word and a [link](https://x.com).")
|
||||
@@ -189,6 +214,9 @@ def run_all():
|
||||
test_blocks_todo,
|
||||
test_blocks_quote_code_divider,
|
||||
test_blocks_table,
|
||||
test_rich_text_anchor_link_becomes_bold,
|
||||
test_rich_text_relative_link_becomes_plain,
|
||||
test_rich_text_absolute_link_preserved,
|
||||
test_no_literal_markers_leak,
|
||||
]
|
||||
for t in tests:
|
||||
|
||||
Reference in New Issue
Block a user