Three correctness bugs in the Notion action set caused the visible rendering issues we saw in the design doc earlier:
NOTION_UPDATE_BLOCK did not parse markdownapps/notion/actions/update_block.py:create_rich_text_array wrapped the entire content in one rich_text run with every annotation forced False. Markdown like **bold** and `code` passed through UPDATE_BLOCK rendered literally. The sibling NOTION_ADD_MULTIPLE_PAGE_CONTENT already routed through parse_markdown_to_rich_text — update_block now reuses it, so updates and inserts behave identically.
apps/notion/utils/utils.py:82 used (?<!_)_(?!_)…(?<!_)_(?!_) which only blocked adjacent underscores. Snake_case identifiers with 2+ underscores (e.g. tool._auth_schemes, _auth_schemes drives the gate, _x_y_z) had their middle segment italicized. Tightened both endpoints to require a non-word boundary so identifiers no longer get chewed into; standard _italic_ between word boundaries still matches.
block_property was overridden by structural promotionapps/notion/utils/utils.py:create_notion_blocks_from_markdown ran parse_markdown_to_blocks on every text-based block. If the content began with a structural marker (1. Foo, - foo, # foo), the parser silently rewrote the block type and stripped the marker. Calling with block_property="heading_2" and content="3. Data model" came back as a numbered_list_item with text "Data model" (style + prefix lost). Now skip structural promotion when the caller explicitly picked a non-paragraph block type — trust them.
Drove Mercury action code locally via the same import + actcls().execute(request, metadata) path that run.py uses, against a real Notion workspace. Read each created block back via Notion's REST API to inspect persisted rich_text.annotations and block type. 27 scenarios across 6 groups.
8 cases: **bold**, _italic_, *italic*, `code`, ~~strike~~, [link](url), all-combined, ***bold-italic***. Identical 5- and 7-run output pre and post — no regression.
| Input | Pre-fix | Post-fix |
|---|---|---|
Use tool._auth_schemes for compat | [Use tool.][_I_:auth][_schemes for compat] | single plain run |
field _auth_schemes is new | [field ][_I_:auth][_schemes is new] | single plain run |
_auth_schemes drives the gate | [_I_:auth][_schemes drives the gate] | single plain run |
see _x_y_z var | [see ][_I_:x][y_z var] | single plain run |
send bearer_token in headers (1 underscore) | single plain run | single plain run (preserved) |
this is _italic_ text (legit italic) | italic italicized | italic italicized (preserved) |
wrap in \_auth_schemes` to be safe` | inline-code wins | inline-code wins (preserved) |
UPDATE_BLOCK markdown rendering (Bug A)| Input | Pre-fix | Post-fix |
|---|---|---|
plain then **bold** and \code` end` | 1 literal run | 5 runs with B and C |
_italic_ **bold** ~~strike~~ \code` end` | 1 literal run | 8 runs with I, B, S, C |
just plain text no markdown | 1 plain run | 1 plain run (preserved) |
see [docs](https://composio.dev) for more | 1 literal run | 3 runs with link annotation |
| Input | Pre-fix actual_type | Post-fix actual_type |
|---|---|---|
heading_1 + 1. Top Section | numbered_list_item (text "Top Section") | heading_1 (text "1. Top Section") |
heading_2 + 3. Data model | numbered_list_item (text "Data model") | heading_2 (text "3. Data model") |
heading_3 + 2. Subsection | numbered_list_item (text "Subsection") | heading_3 (text "2. Subsection") |
heading_2 + Plain Heading No Number | heading_2 | heading_2 (preserved) |
callout + - this is a callout note | bulleted_list_item | callout (text preserved) |
| explicit numbered_list_item | numbered_list_item | numbered_list_item (preserved) |
Default block_property=paragraph with multi-line content # Big Heading\n\n- item one\n- item two\n\nfinal paragraph still promotes correctly to [heading_1, bulleted_list_item, bulleted_list_item, paragraph] both pre and post. No regression.
TypeScript with some_snake_case_var, '_italic_ not parsed', '**not bold**' — preserved verbatim with no spurious annotations both pre and post. No regression.
pytest tests/test_apps/test_notion/ — 106 passed, 0 failures. Existing markdown-parser, structural-block, code-block-split, and rich-text tests all green.
Test driver: /tmp/test_mercury_notion_scenarios.py (27 scenarios, full JSON output saved to /tmp/pre_fix_full.json and /tmp/post_fix_full.json).
🤖 Generated with Claude Code