afv-library/skills/investigating-agentforce-architecture/scripts/tests/test_parse_wave_main.py
2026-06-12 10:55:46 +00:00

398 lines
16 KiB
Python

"""Tests for ``parse_wave.main`` orchestrator + tree-walking helpers.
Covers:
- ``walk_and_inflate`` recursive Flow inflation
- ``inflate_flow_leaf`` cycle + depth-cap behavior
- ``build_root_children`` bundle → root children + new_refs
- ``main`` (full path) env-driven pipeline against a tmp WORK_DIR
- ``main --finalize-cap`` drain pending → unresolved
"""
from __future__ import annotations
import json
import os
import unittest
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest import mock
from . import _bootstrap # noqa: F401 — sys.path setup
import parse_wave # type: ignore
_FLOW_NS = "http://soap.sforce.com/2006/04/metadata"
# -----------------------------------------------------------------------------
# walk_and_inflate
# -----------------------------------------------------------------------------
class WalkAndInflateTests(unittest.TestCase):
def test_no_op_for_non_FLOW_non_GEN_AI_FUNCTION_node(self):
node = {"kind": "TOPIC", "api_name": "T1", "children": []}
flow_children: dict = {}
# Should not raise; should not mutate.
parse_wave.walk_and_inflate(node, flow_children)
self.assertEqual(node["children"], [])
def test_inflates_flow_leaf_under_gen_ai_function(self):
# GEN_AI_FUNCTION wraps a FLOW leaf — walker recurses into it.
node = {
"kind": "GEN_AI_FUNCTION",
"api_name": "GAF1",
"children": [{"kind": "FLOW", "api_name": "MyFlow", "children": []}],
}
flow_children = {"MyFlow": [
{"kind": "APEX", "element_name": "callA", "api_name": "MyClass"},
]}
parse_wave.walk_and_inflate(node, flow_children)
# FLOW leaf got inflated with the APEX child
flow_leaf = node["children"][0]
self.assertEqual(len(flow_leaf["children"]), 1)
self.assertEqual(flow_leaf["children"][0]["kind"], "APEX")
def test_recursively_walks_through_topic_to_inflate_flow(self):
# Walker should descend through TOPIC/non-special nodes too.
node = {
"kind": "BOT_DEFINITION", "api_name": "Agent",
"children": [{
"kind": "TOPIC", "api_name": "T1",
"children": [{
"kind": "GEN_AI_FUNCTION", "api_name": "GAF1",
"children": [{"kind": "FLOW", "api_name": "DeepFlow",
"children": []}],
}],
}],
}
flow_children = {"DeepFlow": [
{"kind": "APEX", "element_name": "callA", "api_name": "DeepClass"},
]}
parse_wave.walk_and_inflate(node, flow_children)
# DeepFlow got the APEX child
deep_flow = node["children"][0]["children"][0]["children"][0]
self.assertEqual(deep_flow["children"][0]["api_name"], "DeepClass")
def test_pending_out_collects_depth_cap_truncations(self):
# Build a chain longer than MAX_BFS_DEPTH so the cap trips.
# Generate 25 flows that each subflow into the next.
flow_children: dict = {}
for i in range(25):
next_name = f"F{i+1}" if i < 24 else None
kids = []
if next_name:
kids.append({"kind": "FLOW", "element_name": f"sub_{i}",
"api_name": next_name})
flow_children[f"F{i}"] = kids
node = {"kind": "FLOW", "api_name": "F0", "children": []}
pending: dict = parse_wave.empty_kind_sets()
parse_wave.walk_and_inflate(node, flow_children, pending_out=pending)
# Some subflow ended up in pending due to the depth cap (>= MAX_BFS_DEPTH).
self.assertTrue(any(pending[k] for k in parse_wave.BFS_KINDS))
# -----------------------------------------------------------------------------
# inflate_flow_leaf — cycle detection
# -----------------------------------------------------------------------------
class InflateFlowLeafCycleTests(unittest.TestCase):
def test_self_recursion_marked_as_cycle(self):
# Flow A subflows back to A itself → cycle.
flow_children = {"A": [
{"kind": "FLOW", "element_name": "self", "api_name": "A"},
]}
leaf = {"kind": "FLOW", "api_name": "A", "children": []}
parse_wave.inflate_flow_leaf(leaf, flow_children)
# The recursive child got annotated with _truncated.
kid = leaf["children"][0]
self.assertIn("_truncated", kid)
self.assertEqual(kid["_truncated"]["reason"],
parse_wave.TRUNCATION_CYCLE)
def test_unknown_flow_no_inflation(self):
# leaf.api_name not in flow_children → nothing to inflate.
leaf = {"kind": "FLOW", "api_name": "Missing", "children": []}
parse_wave.inflate_flow_leaf(leaf, {})
self.assertEqual(leaf["children"], [])
# -----------------------------------------------------------------------------
# build_root_children
# -----------------------------------------------------------------------------
class BuildRootChildrenTests(unittest.TestCase):
def test_topics_become_root_children(self):
bundle = {
"topics": [
{"name": "Greetings", "actions": [
{"name": "Hello", "invocationTarget": "MyFlow",
"invocationTargetType": "flow"},
]},
],
}
visited = parse_wave.empty_kind_sets()
aux: set = set()
children, new_refs = parse_wave.build_root_children(bundle, visited, aux)
self.assertEqual(len(children), 1)
self.assertEqual(children[0]["kind"], "TOPIC")
self.assertEqual(children[0]["api_name"], "Greetings")
# GEN_AI_FUNCTION wrapping the action
self.assertEqual(children[0]["children"][0]["kind"], "GEN_AI_FUNCTION")
# FLOW ref harvested into new_refs
self.assertIn("MyFlow", new_refs["FLOW"])
# Topic + action recorded in aux_visited
self.assertIn(("TOPIC", "Greetings"), aux)
self.assertIn(("GEN_AI_FUNCTION", "Hello"), aux)
def test_skips_already_visited_refs(self):
bundle = {
"topics": [{"name": "T", "actions": [
{"name": "A1", "invocationTarget": "FlowA",
"invocationTargetType": "flow"},
]}],
}
visited = parse_wave.empty_kind_sets()
visited["FLOW"].add("FlowA")
aux: set = set()
_, new_refs = parse_wave.build_root_children(bundle, visited, aux)
# FlowA already visited → not added to new_refs
self.assertNotIn("FlowA", new_refs["FLOW"])
def test_standard_action_does_not_pollute_new_refs(self):
# STANDARD_ACTION is declared-only, must NOT
# land in new_refs (would pollute _pending_fetches).
bundle = {
"topics": [{"name": "T", "actions": [
{"name": "A1", "invocationTarget": "createRecord",
"invocationTargetType": "standardinvocableaction"},
]}],
}
visited = parse_wave.empty_kind_sets()
aux: set = set()
_, new_refs = parse_wave.build_root_children(bundle, visited, aux)
self.assertNotIn("createRecord", new_refs["STANDARD_ACTION"])
def test_empty_bundle_returns_empty_children(self):
children, new_refs = parse_wave.build_root_children(
{}, parse_wave.empty_kind_sets(), set(),
)
self.assertEqual(children, [])
for v in new_refs.values():
self.assertEqual(v, set())
# -----------------------------------------------------------------------------
# main() — happy path + finalize-cap branch
# -----------------------------------------------------------------------------
def _setup_workdir(tmp: Path, *, bundle: dict | None = None) -> Path:
"""Plant a WORK_DIR with _bundle_parsed.json + minimal sf_meta layout."""
work_dir = tmp / "work"
work_dir.mkdir()
bundle = bundle or {
"plannerName": "MyPlanner",
"plannerType": "Atlas__Reasoning",
"generation": "nga",
"topics": [{"name": "Greetings", "actions": [
{"name": "SayHi", "invocationTarget": "GreetingsFlow",
"invocationTargetType": "flow"},
]}],
}
(work_dir / "_bundle_parsed.json").write_text(json.dumps(bundle))
# Empty sf_meta is fine — harvest_waves returns empty when missing.
return work_dir
def _run_main(*, work_dir: Path, argv_extra: list[str] | None = None,
env_extra: dict | None = None) -> int:
"""Invoke parse_wave.main with controlled env + argv."""
saved_env = dict(os.environ)
saved_argv = list(parse_wave.sys.argv)
try:
os.environ.update({
"WORK_DIR": str(work_dir),
"AGENT_API_NAME": "DemoAgent",
"AGENT_VERSION": "v3",
"BOT_ID": "0Xx000000000ABC",
"BOT_MASTER_LABEL": "Demo Agent",
"VERSION_AUTO_PICKED": "false",
})
if env_extra:
os.environ.update(env_extra)
parse_wave.sys.argv = ["parse_wave.py", *(argv_extra or [])]
return parse_wave.main()
finally:
os.environ.clear()
os.environ.update(saved_env)
parse_wave.sys.argv = saved_argv
class MainHappyPathTests(unittest.TestCase):
def test_main_writes_declared_action_tree_json(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = _setup_workdir(tmp)
with mock.patch.object(parse_wave.sys, "stderr"):
rc = _run_main(work_dir=work_dir)
self.assertEqual(rc, 0)
tree_path = work_dir / "declared_action_tree.json"
self.assertTrue(tree_path.is_file())
tree = json.loads(tree_path.read_text())
# Tree has the expected shape
self.assertEqual(tree["_schema_version"], "3.1")
self.assertEqual(tree["agent"]["api_name"], "DemoAgent")
self.assertEqual(tree["root"]["kind"], "BOT_DEFINITION")
# One topic became a root child
kinds = [c["kind"] for c in tree["root"]["children"]]
self.assertIn("TOPIC", kinds)
def test_main_records_pending_for_unfetched_flow(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = _setup_workdir(tmp)
with mock.patch.object(parse_wave.sys, "stderr"):
_run_main(work_dir=work_dir)
tree = json.loads(
(work_dir / "declared_action_tree.json").read_text()
)
# GreetingsFlow not on disk → in _pending_fetches.FLOW
self.assertIn("GreetingsFlow", tree["_pending_fetches"]["FLOW"])
self.assertTrue(tree["_partial"])
# _partial_reason populated on first run when pending refs exist
# but no depth cap was tripped.
self.assertEqual(tree["_partial_reason"], "pending-refs")
def test_main_writes_pending_refs_reason_when_initial_reason_is_none(self):
# init_tree seeds `_partial_reason=None`. The writer's elif branch
# must populate it with "pending-refs" when any pending bucket is
# non-empty and the depth cap did not trip — a setdefault against
# `None` is a no-op, which is the bug this guards.
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = _setup_workdir(tmp)
with mock.patch.object(parse_wave.sys, "stderr"):
_run_main(work_dir=work_dir)
tree = json.loads(
(work_dir / "declared_action_tree.json").read_text()
)
# Sanity: pending exists, depth cap was NOT tripped.
self.assertIn("GreetingsFlow", tree["_pending_fetches"]["FLOW"])
# The fix: a None-valued reason gets promoted to "pending-refs".
self.assertEqual(tree["_partial_reason"], "pending-refs")
def test_main_preserves_existing_partial_reason_when_pending_refs(self):
# init_tree primes _partial_reason=None; setdefault used to be a
# no-op against that None and left the reason blank. Today the
# writer treats None as "unset" and fills in "pending-refs".
# When a prior reason exists (e.g. "max-depth-cap" from a previous
# wave), the writer must NOT overwrite it.
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = _setup_workdir(tmp)
# Plant a tree on disk that mimics a previously-suspended run:
# _partial_reason="max-depth-cap" and empty root children, so
# main() enters the fresh-tree branch and re-collects bundle
# refs (GreetingsFlow pending) yet sees the prior reason.
tree_path = work_dir / "declared_action_tree.json"
tree_path.write_text(json.dumps({
"_schema_version": "3.1",
"agent": {"api_name": "DemoAgent", "version": "v3"},
"root": {"kind": "BOT_DEFINITION", "api_name": "DemoAgent",
"children": []},
"_partial": True,
"_partial_reason": "max-depth-cap",
"_pending_fetches": {k: [] for k in parse_wave.BFS_KINDS},
"_unresolved": [],
"_visited": [],
}))
with mock.patch.object(parse_wave.sys, "stderr"):
_run_main(work_dir=work_dir)
tree2 = json.loads(tree_path.read_text())
# GreetingsFlow is pending → any_pending=True → elif branch runs.
self.assertIn("GreetingsFlow", tree2["_pending_fetches"]["FLOW"])
# Existing more-specific reason is preserved.
self.assertEqual(tree2["_partial_reason"], "max-depth-cap")
def test_main_returns_one_when_work_dir_env_missing(self):
saved_env = dict(os.environ)
try:
for k in ("WORK_DIR", "AGENT_API_NAME", "AGENT_VERSION"):
os.environ.pop(k, None)
with mock.patch.object(parse_wave.sys, "argv", ["parse_wave.py"]):
with mock.patch.object(parse_wave.sys, "stderr"):
rc = parse_wave.main()
finally:
os.environ.clear()
os.environ.update(saved_env)
self.assertEqual(rc, 1)
def test_main_returns_one_when_bundle_missing(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = tmp / "work"
work_dir.mkdir()
# No _bundle_parsed.json
with mock.patch.object(parse_wave.sys, "stderr"):
rc = _run_main(work_dir=work_dir)
self.assertEqual(rc, 1)
def test_main_reparses_existing_tree(self):
# Two-pass: first run creates the tree, second re-uses it.
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = _setup_workdir(tmp)
with mock.patch.object(parse_wave.sys, "stderr"):
_run_main(work_dir=work_dir)
# Second invocation should load + augment without crashing
rc = _run_main(work_dir=work_dir)
self.assertEqual(rc, 0)
class MainFinalizeCapTests(unittest.TestCase):
def test_finalize_cap_drains_pending_into_unresolved(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = _setup_workdir(tmp)
# First run produces a tree with pending FLOW=GreetingsFlow.
with mock.patch.object(parse_wave.sys, "stderr"):
_run_main(work_dir=work_dir)
# --finalize-cap drains pending → unresolved
rc = _run_main(
work_dir=work_dir, argv_extra=["--finalize-cap"],
)
self.assertEqual(rc, 0)
tree = json.loads(
(work_dir / "declared_action_tree.json").read_text()
)
# All buckets emptied
for kind in parse_wave.BFS_KINDS:
self.assertEqual(tree["_pending_fetches"][kind], [])
# GreetingsFlow now in _unresolved
api_names = [u["api_name"] for u in tree["_unresolved"]]
self.assertIn("GreetingsFlow", api_names)
def test_finalize_cap_with_no_existing_tree_is_noop(self):
with TemporaryDirectory() as t:
tmp = Path(t)
work_dir = tmp / "work"
work_dir.mkdir()
with mock.patch.object(parse_wave.sys, "stderr"):
rc = _run_main(
work_dir=work_dir, argv_extra=["--finalize-cap"],
)
# No tree file → noop, exit 0.
self.assertEqual(rc, 0)
if __name__ == "__main__":
unittest.main()