afv-library/skills/agentforce-architecture-analyze/scripts/tests/test_per_branch_visited.py

245 lines
9.5 KiB
Python

"""2026-05-03 revision: `inflate_flow_leaf` uses a per-branch ancestor
path set (`visited_in_path`) for cycle detection. The defensive
`MAX_BFS_DEPTH` cap is preserved as a last-resort termination guard
but is no longer the primitive that prevents runaway recursion.
The bug this regression suite prevents: shared utility flows such as
`handleFlowFault` appear on every real Agentforce flow's fault path.
Under the old `MAX_BFS_DEPTH = 5` behaviour the utility showed up in
`_pending_fetches["FLOW"]` with `PARTIAL_REASON=max-depth-cap` on any
moderately nested tree (e.g. `AGNT_Baz_Qux →
handleFlowFault`), even though `handleFlowFault` was trivially
expandable. Per-branch path-set semantics fix this: the same flow on
two sibling branches is NOT a cycle, only an ancestor-chain recurrence
is.
"""
from __future__ import annotations
import unittest
from . import _bootstrap # noqa: F401
import parse_wave # type: ignore
def _find_all(node: dict, api_name: str) -> list[dict]:
"""Collect every node in `node`'s subtree whose `api_name` matches."""
found: list[dict] = []
if node.get("api_name") == api_name:
found.append(node)
for c in node.get("children", []) or []:
found.extend(_find_all(c, api_name))
return found
class SharedUtilityFlowExpansionTests(unittest.TestCase):
"""Direct repro of the real-org `handleFlowFault` bug.
Parent flow has two sibling children, each of which invokes the
same utility flow. Both utility instances must expand fully.
"""
def test_shared_utility_flow_expands_on_every_branch(self):
# parent -> [childA, childB]; childA -> utility; childB -> utility
# utility itself calls a leaf apex (so we can assert `utility`
# got fully inflated with a child, not just a stub).
flow_children = {
"parent": [
{"kind": "FLOW", "api_name": "childA", "element_name": "call_A"},
{"kind": "FLOW", "api_name": "childB", "element_name": "call_B"},
],
"childA": [
{"kind": "FLOW", "api_name": "utility", "element_name": "call_util"},
],
"childB": [
{"kind": "FLOW", "api_name": "utility", "element_name": "call_util"},
],
"utility": [
{"kind": "APEX", "api_name": "XCSF_FlowFaultMessage", "element_name": "log"},
],
}
root = {"kind": "FLOW", "api_name": "parent", "children": []}
pending = parse_wave.empty_kind_sets()
parse_wave.inflate_flow_leaf(root, flow_children, pending_out=pending)
utilities = _find_all(root, "utility")
self.assertEqual(
len(utilities), 2,
"utility should appear once on each sibling branch",
)
# Both utility instances must have expanded children (the apex
# leaf). The regression shape was: first branch expanded, second
# either in `_pending_fetches` or emitted as an empty-children
# stub due to depth-cap or false-cycle pruning.
for i, u in enumerate(utilities):
kids = u.get("children", [])
self.assertEqual(
len(kids), 1,
f"utility instance {i} should have 1 expanded child, got {kids}",
)
self.assertEqual(kids[0]["api_name"], "XCSF_FlowFaultMessage")
self.assertNotIn("_truncated", u)
self.assertNotIn("_cycle_back_to", u)
# Critically: nothing pending. Shared-utility expansion must not
# leak into `_pending_fetches`.
self.assertEqual(pending["FLOW"], set())
class TrueCycleDetectionTests(unittest.TestCase):
"""Per-branch path-set must still catch genuine cycles."""
def test_true_cycle_detected(self):
"""A -> B -> A: second A is annotated, not recursed into, and
not surfaced in `_pending_fetches`."""
flow_children = {
"A": [{"kind": "FLOW", "api_name": "B", "element_name": "call_B"}],
"B": [{"kind": "FLOW", "api_name": "A", "element_name": "call_A"}],
}
root = {"kind": "FLOW", "api_name": "A", "children": []}
pending = parse_wave.empty_kind_sets()
parse_wave.inflate_flow_leaf(root, flow_children, pending_out=pending)
# A -> B -> A(cycle)
b = root["children"][0]
self.assertEqual(b["api_name"], "B")
a_cycle = b["children"][0]
self.assertEqual(a_cycle["api_name"], "A")
self.assertEqual(
a_cycle["_truncated"],
{"reason": "cycle", "target": "FLOW:A"},
)
self.assertEqual(a_cycle["_cycle_back_to"], "FLOW:A")
# Cycles live in the tree, not the pending accumulator.
self.assertEqual(pending["FLOW"], set())
def test_self_recursive_flow(self):
"""A -> A: direct self-cycle is annotated on first recurrence."""
flow_children = {
"A": [{"kind": "FLOW", "api_name": "A", "element_name": "self_call"}],
}
root = {"kind": "FLOW", "api_name": "A", "children": []}
pending = parse_wave.empty_kind_sets()
parse_wave.inflate_flow_leaf(root, flow_children, pending_out=pending)
kids = root.get("children", [])
self.assertEqual(len(kids), 1)
self.assertEqual(kids[0]["api_name"], "A")
self.assertEqual(
kids[0]["_truncated"],
{"reason": "cycle", "target": "FLOW:A"},
)
self.assertEqual(pending["FLOW"], set())
class DeepNonCyclicChainTests(unittest.TestCase):
"""Linear chains longer than the old `MAX_BFS_DEPTH = 5` must
expand fully under the revised semantics. Previously they tripped
`PARTIAL_REASON=max-depth-cap`; now they expand cleanly.
"""
def test_deep_non_cyclic_chain_fully_expands(self):
# A -> B -> C -> D -> E -> F -> G (7 unique flows, depth 6
# from the root). Old cap of 5 would have truncated at F;
# new cap of 20 doesn't care.
names = list("ABCDEFG")
flow_children: dict = {}
for i, n in enumerate(names):
if i + 1 < len(names):
flow_children[n] = [{
"kind": "FLOW",
"api_name": names[i + 1],
"element_name": f"call_{names[i + 1]}",
}]
else:
flow_children[n] = []
root = {"kind": "FLOW", "api_name": "A", "children": []}
pending = parse_wave.empty_kind_sets()
parse_wave.inflate_flow_leaf(root, flow_children, pending_out=pending)
node = root
for expected in names[1:]:
kids = node.get("children", [])
self.assertEqual(
len(kids), 1,
f"{node['api_name']} should have expanded child {expected}",
)
self.assertEqual(kids[0]["api_name"], expected)
self.assertNotIn("_truncated", kids[0])
node = kids[0]
# Terminal leaf has no children and carries no truncation
# annotation — the cap was never relevant.
self.assertEqual(node.get("children", []), [])
self.assertNotIn("_truncated", node)
self.assertEqual(pending["FLOW"], set())
class DefensiveCapStillTerminatesTests(unittest.TestCase):
"""Pathological chain longer than the defensive cap must still
terminate cleanly. This proves the safety net still works even
though per-branch cycle detection does the real work in practice.
"""
def test_defensive_cap_still_terminates(self):
# 25 unique flows, linear — longer than the defensive cap of 20.
# Either the tree fully expands (if someone lifts the cap
# further) OR it caps at MAX_BFS_DEPTH and annotates the
# unreached flow. The test asserts termination and one of
# those two well-defined shapes.
n = 25
names = [f"F{i:02d}" for i in range(n)]
flow_children: dict = {}
for i, name in enumerate(names):
if i + 1 < n:
flow_children[name] = [{
"kind": "FLOW",
"api_name": names[i + 1],
"element_name": "sub",
}]
else:
flow_children[name] = []
root = {"kind": "FLOW", "api_name": names[0], "children": []}
pending = parse_wave.empty_kind_sets()
# Primary assertion: this call terminates and does not recurse
# past Python's default recursion limit. If the defensive cap
# regresses, this test will hang or raise RecursionError.
parse_wave.inflate_flow_leaf(root, flow_children, pending_out=pending)
# Collect every flow that appears in the tree.
def all_api_names(node: dict) -> set[str]:
out = {node.get("api_name")}
for c in node.get("children", []) or []:
out.update(all_api_names(c))
return out - {None}
present = all_api_names(root)
# At least the first MAX_BFS_DEPTH flows must be present. The
# tail may or may not appear depending on where the cap trips.
for i in range(parse_wave.MAX_BFS_DEPTH):
self.assertIn(
names[i], present,
f"{names[i]} (index {i}) should be expanded — "
f"it's below MAX_BFS_DEPTH={parse_wave.MAX_BFS_DEPTH}",
)
# If any flow is pending, it must be a real descendant (not a
# fabricated name) and must carry the max-depth annotation
# somewhere in the tree.
if pending["FLOW"]:
for pending_name in pending["FLOW"]:
self.assertIn(pending_name, names)
if __name__ == "__main__":
unittest.main()