mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 11:43:26 +08:00
242 lines
8.9 KiB
Python
242 lines
8.9 KiB
Python
"""Tests for ``summarize_tree`` — declared-action-tree → markdown summary.
|
|
|
|
Covers:
|
|
- ``render_tree`` recursive box-drawing renderer
|
|
- ``main`` CLI orchestration via argv (tree_json + out_md + built_at)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import unittest
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from unittest import mock
|
|
|
|
from . import _bootstrap # noqa: F401 — sys.path setup
|
|
|
|
import summarize_tree # type: ignore
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# render_tree — pure dict → list[str]
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class RenderTreeTests(unittest.TestCase):
|
|
|
|
def test_single_leaf_uses_last_connector(self):
|
|
# is_last=True (default) → "└── "; no children → no recursion.
|
|
out = summarize_tree.render_tree({"kind": "TOPIC", "api_name": "T1"})
|
|
self.assertEqual(out, ["└── [TOPIC] T1"])
|
|
|
|
def test_first_of_two_siblings_uses_branch_connector(self):
|
|
# render at top-level isn't the natural use, but the function
|
|
# accepts is_last=False → "├── ".
|
|
out = summarize_tree.render_tree(
|
|
{"kind": "TOPIC", "api_name": "T1"}, is_last=False
|
|
)
|
|
self.assertEqual(out, ["├── [TOPIC] T1"])
|
|
|
|
def test_bot_definition_with_version_includes_version_in_label(self):
|
|
agent = {"version": "v3"}
|
|
out = summarize_tree.render_tree(
|
|
{"kind": "BOT_DEFINITION", "api_name": "MyAgent"},
|
|
agent_=agent,
|
|
)
|
|
self.assertEqual(out, ["└── [BOT_DEFINITION] MyAgent (v3)"])
|
|
|
|
def test_bot_definition_without_version_omits_version_suffix(self):
|
|
out = summarize_tree.render_tree(
|
|
{"kind": "BOT_DEFINITION", "api_name": "MyAgent"},
|
|
agent_={},
|
|
)
|
|
self.assertEqual(out, ["└── [BOT_DEFINITION] MyAgent"])
|
|
|
|
def test_gen_ai_function_includes_unwraps_arrow(self):
|
|
out = summarize_tree.render_tree({
|
|
"kind": "GEN_AI_FUNCTION",
|
|
"api_name": "GAF1",
|
|
"unwraps_to": {"kind": "FLOW", "api_name": "FlowA"},
|
|
})
|
|
self.assertEqual(out, ["└── [GEN_AI_FUNCTION] GAF1 → FLOW:FlowA"])
|
|
|
|
def test_gen_ai_function_without_unwraps_omits_arrow(self):
|
|
out = summarize_tree.render_tree({
|
|
"kind": "GEN_AI_FUNCTION",
|
|
"api_name": "GAF1",
|
|
})
|
|
self.assertEqual(out, ["└── [GEN_AI_FUNCTION] GAF1"])
|
|
|
|
def test_standard_action_uses_invocation_type_when_present(self):
|
|
out = summarize_tree.render_tree({
|
|
"kind": "STANDARD_ACTION",
|
|
"api_name": "SA1",
|
|
"invocation_type": "standardinvocableaction",
|
|
})
|
|
self.assertEqual(
|
|
out, ["└── [STANDARD_ACTION] SA1 (standardinvocableaction)"]
|
|
)
|
|
|
|
def test_standard_action_falls_back_to_legacy_invocation_keys(self):
|
|
# raw_invocation_type wins when invocation_type missing.
|
|
with self.subTest("raw_invocation_type"):
|
|
out = summarize_tree.render_tree({
|
|
"kind": "STANDARD_ACTION",
|
|
"api_name": "SA1",
|
|
"raw_invocation_type": "legacyA",
|
|
})
|
|
self.assertIn("legacyA", out[0])
|
|
with self.subTest("raw_action_type"):
|
|
out = summarize_tree.render_tree({
|
|
"kind": "UNKNOWN",
|
|
"api_name": "U1",
|
|
"raw_action_type": "legacyB",
|
|
})
|
|
self.assertIn("legacyB", out[0])
|
|
|
|
def test_element_name_appended_for_non_bot_kinds(self):
|
|
out = summarize_tree.render_tree({
|
|
"kind": "FLOW",
|
|
"api_name": "FlowA",
|
|
"element_name": "callX",
|
|
})
|
|
self.assertEqual(out, ["└── [FLOW] FlowA — element:callX"])
|
|
|
|
def test_element_name_skipped_on_bot_definition(self):
|
|
out = summarize_tree.render_tree({
|
|
"kind": "BOT_DEFINITION",
|
|
"api_name": "MyAgent",
|
|
"element_name": "should-not-appear",
|
|
}, agent_={"version": "v1"})
|
|
self.assertNotIn("should-not-appear", out[0])
|
|
|
|
def test_nested_children_recurse_with_correct_prefix(self):
|
|
out = summarize_tree.render_tree({
|
|
"kind": "TOPIC",
|
|
"api_name": "T1",
|
|
"children": [
|
|
{"kind": "STANDARD_ACTION", "api_name": "A1"},
|
|
{"kind": "STANDARD_ACTION", "api_name": "A2"},
|
|
],
|
|
})
|
|
# Top-level (is_last=True) gets " " continuation prefix.
|
|
self.assertEqual(out, [
|
|
"└── [TOPIC] T1",
|
|
" ├── [STANDARD_ACTION] A1",
|
|
" └── [STANDARD_ACTION] A2",
|
|
])
|
|
|
|
def test_branch_node_uses_pipe_continuation_for_children(self):
|
|
# is_last=False at parent → "│ " continuation prefix.
|
|
out = summarize_tree.render_tree({
|
|
"kind": "TOPIC",
|
|
"api_name": "T1",
|
|
"children": [{"kind": "APEX", "api_name": "A1"}],
|
|
}, is_last=False)
|
|
self.assertEqual(out, [
|
|
"├── [TOPIC] T1",
|
|
"│ └── [APEX] A1",
|
|
])
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# main — argv-based CLI
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
def _minimal_tree() -> dict:
|
|
return {
|
|
"agent": {
|
|
"api_name": "MyAgent", "version": "v1", "bot_id": "0Xx000000000",
|
|
"master_label": "My Agent", "generation": "nga",
|
|
"planner_name": "MyPlanner", "planner_type": "Atlas__Reasoning",
|
|
"_version_auto_picked": False,
|
|
},
|
|
"node_count": 2,
|
|
"depth": 1,
|
|
"_kind_counts": {"TOPIC": 1, "STANDARD_ACTION": 1},
|
|
"_partial": False,
|
|
"root": {
|
|
"kind": "BOT_DEFINITION", "api_name": "MyAgent",
|
|
"children": [{"kind": "TOPIC", "api_name": "Greetings"}],
|
|
},
|
|
}
|
|
|
|
|
|
class MainTests(unittest.TestCase):
|
|
|
|
def test_main_returns_one_when_argv_count_wrong(self):
|
|
with mock.patch.object(summarize_tree, "sys") as fake_sys:
|
|
fake_sys.argv = ["summarize_tree.py"] # missing args
|
|
fake_sys.stderr.write = lambda *_a, **_kw: None
|
|
rc = summarize_tree.main()
|
|
self.assertEqual(rc, 1)
|
|
|
|
def test_main_returns_one_when_tree_json_unreadable(self):
|
|
with TemporaryDirectory() as t:
|
|
tmp = Path(t)
|
|
with mock.patch.object(summarize_tree, "sys") as fake_sys:
|
|
fake_sys.argv = [
|
|
"summarize_tree.py",
|
|
str(tmp / "missing.json"),
|
|
str(tmp / "out.md"),
|
|
"2026-05-09T00:00:00Z",
|
|
]
|
|
fake_sys.stderr.write = lambda *_a, **_kw: None
|
|
rc = summarize_tree.main()
|
|
self.assertEqual(rc, 1)
|
|
|
|
def test_main_writes_summary_md_with_expected_sections(self):
|
|
with TemporaryDirectory() as t:
|
|
tmp = Path(t)
|
|
tree_path = tmp / "tree.json"
|
|
tree_path.write_text(json.dumps(_minimal_tree()))
|
|
out_path = tmp / "out.summary.md"
|
|
|
|
with mock.patch.object(summarize_tree, "sys") as fake_sys:
|
|
fake_sys.argv = [
|
|
"summarize_tree.py",
|
|
str(tree_path), str(out_path),
|
|
"2026-05-09T00:00:00Z",
|
|
]
|
|
fake_sys.stderr.write = lambda *_a, **_kw: None
|
|
rc = summarize_tree.main()
|
|
|
|
self.assertEqual(rc, 0)
|
|
content = out_path.read_text()
|
|
self.assertIn("# MyAgent v1 — declared action tree", content)
|
|
self.assertIn("## Kind counts", content)
|
|
self.assertIn("## Declared action tree", content)
|
|
# tree section is wrapped in ``` fence
|
|
self.assertIn("```\n└── [BOT_DEFINITION] MyAgent (v1)", content)
|
|
# 2026-05-09 timestamp surfaced
|
|
self.assertIn("2026-05-09T00:00:00Z", content)
|
|
|
|
def test_main_appends_unresolved_section_when_present(self):
|
|
with TemporaryDirectory() as t:
|
|
tmp = Path(t)
|
|
tree = _minimal_tree()
|
|
tree["_unresolved"] = [
|
|
{"kind": "FLOW", "api_name": "Missing", "reason": "404"},
|
|
]
|
|
tree_path = tmp / "tree.json"
|
|
tree_path.write_text(json.dumps(tree))
|
|
out_path = tmp / "out.summary.md"
|
|
|
|
with mock.patch.object(summarize_tree, "sys") as fake_sys:
|
|
fake_sys.argv = [
|
|
"summarize_tree.py",
|
|
str(tree_path), str(out_path),
|
|
"2026-05-09T00:00:00Z",
|
|
]
|
|
fake_sys.stderr.write = lambda *_a, **_kw: None
|
|
summarize_tree.main()
|
|
|
|
content = out_path.read_text()
|
|
self.assertIn("## Unresolved", content)
|
|
self.assertIn("`FLOW`/`Missing` — 404", content)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|