This repository has been archived on 2026-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
orchestrator/agent-team/tests/test_confluence_mermaid.py
Adam Moussa c7f9c1bac2 feat(agent-team): Confluence-writer node (draft -> approve gate -> write)
Add a Confluence documentation lane to the Plane-2 pipeline, flag-gated behind
AGENT_TEAM_CONFLUENCE_ENABLED (default off; daemon behavior unchanged when off).

- confluence/client.py: OAuth 2LO + Basic REST client, dry-run-default writes
- confluence/mermaid.py: vendored ADF-only Mermaid editor (macro-count +
  revert-diff guards, dry-run default)
- nodes/confluence_writer.py(+_llm): conf_draft -> conf_gate -> conf_write,
  both direct (task_kind=confluence) and post-build documentation flows
- task_model/graph/coordinator: new phases, state channels, route_after_intake,
  CONFLUENCE_APPROVAL_KIND gate delivery, task_kind forwarding
- db schema v5: widen pending_questions kind CHECK (atomic rebuild)
- tests for client, mermaid, writer node, ledger v5, coordinator gate, e2e
2026-06-25 10:56:48 -04:00

182 lines
5.6 KiB
Python

"""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