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
182 lines
5.6 KiB
Python
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
|