"""Unit tests for agent_team.confluence.mermaid (ADF-only Mermaid editor, D7). Pure in-memory module: no seams, no network, no Claude. Fixtures are small inline ADF dicts. The load-bearing assertions are the page-1540098 safety invariants documented in the module: * a NO-OP edit set yields an EMPTY revert-diff and mutates nothing; * a real edit yields a non-empty revert-diff and an edited ``new_adf`` while leaving the caller's inputs untouched (dry-run default); * a zero-macro document signals ``skip_mermaid``. """ from __future__ import annotations import copy from agent_team.confluence.mermaid import ( MermaidEdit, count_mermaid_macros, plan_mermaid_edits, ) def _mermaid_macro(local_id: str, source: str) -> dict: """A weweave mermaid-cloud extension node carrying ``source``.""" return { "type": "extension", "attrs": { "extensionType": "com.weweave.mermaid", "extensionKey": "mermaid-cloud", "localId": local_id, "parameters": {"macroParams": {"code": {"value": source}}}, }, } def _doc(*macros: dict) -> dict: """An ADF doc wrapping ``macros`` with some non-mermaid filler content.""" return { "version": 1, "type": "doc", "content": [ { "type": "paragraph", "content": [{"type": "text", "text": "intro"}], }, *macros, ], } # --- count_mermaid_macros ------------------------------------------------- def test_count_returns_n_for_n_macros() -> None: doc = _doc( _mermaid_macro("a", "graph TD; A-->B"), _mermaid_macro("b", "graph TD; C-->D"), _mermaid_macro("c", "graph TD; E-->F"), ) assert count_mermaid_macros(doc) == 3 def test_count_returns_zero_when_no_macros() -> None: doc = { "version": 1, "type": "doc", "content": [ {"type": "paragraph", "content": [{"type": "text", "text": "no diagrams"}]}, { "type": "extension", "attrs": { "extensionType": "com.atlassian.confluence.macro.core", "extensionKey": "info", "parameters": {"macroParams": {}}, }, }, ], } assert count_mermaid_macros(doc) == 0 # --- NO-OP safety invariant (page 1540098) -------------------------------- def test_noop_empty_edits_yields_empty_revert_and_no_mutation() -> None: doc = _doc(_mermaid_macro("a", "graph TD; A-->B")) before = copy.deepcopy(doc) result = plan_mermaid_edits(doc, []) # EMPTY revert-diff is the safety invariant. assert result.revert_diff == [] assert result.changed is False assert result.skip_mermaid is False assert result.macro_count == 1 # Writes nothing: the caller's input is byte-for-byte unchanged. assert doc == before def test_noop_identical_source_edit_yields_empty_revert() -> None: doc = _doc(_mermaid_macro("a", "graph TD; A-->B")) before = copy.deepcopy(doc) # An edit whose new_source equals the current source is a no-op. result = plan_mermaid_edits( doc, [MermaidEdit(macro_key="a", new_source="graph TD; A-->B")] ) assert result.revert_diff == [] assert result.changed is False assert result.unmatched_keys == [] assert doc == before # --- real edit -> non-empty revert-diff, edited new_adf, inputs untouched -- def test_real_edit_produces_revert_diff_and_edited_new_adf() -> None: doc = _doc(_mermaid_macro("a", "graph TD; A-->B")) doc_before = copy.deepcopy(doc) edits = [MermaidEdit(macro_key="a", new_source="graph TD; X-->Y")] edits_before = copy.deepcopy(edits) result = plan_mermaid_edits(doc, edits, apply=False) # Non-empty, reversible revert-diff capturing old + new. assert result.changed is True assert len(result.revert_diff) == 1 op = result.revert_diff[0] assert op.macro_key == "a" assert op.old_source == "graph TD; A-->B" assert op.new_source == "graph TD; X-->Y" assert result.unmatched_keys == [] # new_adf reflects the change; macro count is preserved. new_macro = result.new_adf["content"][1] assert ( new_macro["attrs"]["parameters"]["macroParams"]["code"]["value"] == "graph TD; X-->Y" ) assert count_mermaid_macros(result.new_adf) == 1 # DRY-RUN DEFAULT: apply=False leaves both inputs unmutated. assert doc == doc_before assert edits == edits_before assert result.new_adf is not doc def test_unmatched_key_is_surfaced_not_raised() -> None: doc = _doc(_mermaid_macro("a", "graph TD; A-->B")) before = copy.deepcopy(doc) result = plan_mermaid_edits(doc, [MermaidEdit(macro_key="nope", new_source="x")]) assert result.unmatched_keys == ["nope"] assert result.revert_diff == [] assert result.changed is False assert doc == before # --- zero-macro contract: skip_mermaid ------------------------------------ def test_zero_macro_doc_signals_skip_mermaid() -> None: doc = { "version": 1, "type": "doc", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "hi"}]}], } before = copy.deepcopy(doc) result = plan_mermaid_edits(doc, [MermaidEdit(macro_key="a", new_source="x")]) # Contract: zero macros -> skip_mermaid True so the caller falls back to a # storage-format body update (D7) rather than an empty ADF edit. assert result.skip_mermaid is True assert result.macro_count == 0 assert result.revert_diff == [] # Unmatched edit keys are still surfaced. assert result.unmatched_keys == ["a"] assert doc == before