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

645 lines
27 KiB
Python

"""Tests for parse_wave BFS + inflate uses (kind, canonical_name)
tuple-keyed visited tracking. A flat per-name visited set would silently
drop the second of two same-named different-kind nodes (Flow Foo + Apex Foo),
and would infinite-loop / depth-cap on self-cycles without annotation.
"""
from __future__ import annotations
import unittest
from . import _bootstrap # noqa: F401
import parse_wave # type: ignore
class BfsStepTupleKeyingTests(unittest.TestCase):
"""Same-name, different-kind refs must not collide."""
def test_flow_and_apex_same_name_are_distinct(self):
pending = {k: set() for k in parse_wave._BFS_KINDS}
visited = {k: set() for k in parse_wave._BFS_KINDS}
new_refs = {"FLOW": {"Foo"}, "APEX": {"Foo"}}
merged, cycles = parse_wave.bfs_step(pending, visited, new_refs)
self.assertIn("Foo", merged["FLOW"])
self.assertIn("Foo", merged["APEX"])
self.assertEqual(cycles, [])
def test_already_visited_ref_skipped_and_recorded_as_cycle(self):
pending = {k: set() for k in parse_wave._BFS_KINDS}
visited = {k: set() for k in parse_wave._BFS_KINDS}
visited["FLOW"] = {"Foo"} # simulate prior wave visit
new_refs = {"FLOW": {"Foo", "Bar"}}
merged, cycles = parse_wave.bfs_step(pending, visited, new_refs)
self.assertNotIn("Foo", merged["FLOW"]) # filtered out
self.assertIn("Bar", merged["FLOW"])
self.assertIn(("FLOW", "Foo"), cycles)
def test_cross_type_cycle_is_distinct(self):
"""Flow Foo visited does not mask Apex Foo on the same wave."""
pending = {k: set() for k in parse_wave._BFS_KINDS}
visited = {k: set() for k in parse_wave._BFS_KINDS}
visited["FLOW"] = {"Foo"}
new_refs = {"FLOW": {"Foo"}, "APEX": {"Foo"}}
merged, cycles = parse_wave.bfs_step(pending, visited, new_refs)
self.assertEqual(merged["FLOW"], set()) # Foo is visited
self.assertEqual(merged["APEX"], {"Foo"}) # distinct kind → keeps
self.assertIn(("FLOW", "Foo"), cycles)
self.assertNotIn(("APEX", "Foo"), cycles)
def test_empty_initial_pending_terminates(self):
pending = {k: set() for k in parse_wave._BFS_KINDS}
visited = {k: set() for k in parse_wave._BFS_KINDS}
merged, cycles = parse_wave.bfs_step(pending, visited, {})
self.assertEqual(cycles, [])
for k in parse_wave._BFS_KINDS:
self.assertEqual(merged[k], set())
def test_unknown_kind_raises(self):
"""unknown BFS kinds raise ValueError — bfs_step is an
internal API and a typo in a caller is a programming error, not a
runtime condition we can silently paper over. Silent-drop produced
false confidence: the ref was never fetched and no log mentioned it.
"""
pending = {k: set() for k in parse_wave._BFS_KINDS}
visited = {k: set() for k in parse_wave._BFS_KINDS}
new_refs = {"NOT_A_KIND": {"X"}}
with self.assertRaises(ValueError) as ctx:
parse_wave.bfs_step(pending, visited, new_refs)
self.assertIn("unknown BFS kind", ctx.exception.args[0])
class InflateCycleDetectionTests(unittest.TestCase):
"""`inflate_flow_leaf` must terminate on self-cycles and annotate
`_cycle_back_to` on the repeat node."""
def test_self_cycle_flow_a_calls_subflow_a(self):
"""Flow A → subflow A — one real node + a stub child with cycle tag."""
flow_children = {
"A": [
{"kind": "FLOW", "element_name": "recurse", "api_name": "A"},
],
}
leaf = {"kind": "FLOW", "api_name": "A", "children": []}
parse_wave.inflate_flow_leaf(leaf, flow_children)
self.assertEqual(len(leaf["children"]), 1)
child = leaf["children"][0]
self.assertEqual(child["kind"], "FLOW")
self.assertEqual(child["api_name"], "A")
self.assertEqual(child.get("_cycle_back_to"), "FLOW:A")
# Must NOT have expanded children on the cycle stub.
self.assertEqual(child.get("children", []), [])
def test_cross_type_cycle_flow_a_to_apex_b_to_flow_a(self):
"""Flow A → Apex B (leaf) → nothing (Apex doesn't recurse in inflate).
The inflate layer only descends through FLOW→FLOW. A cycle through
Apex would be caught at the BFS layer (bfs_step), not here. This
test confirms the inflate layer doesn't misclassify a FLOW→APEX
edge as a cycle and doesn't over-recurse on it.
"""
flow_children = {
"A": [
{"kind": "APEX", "element_name": "callApex", "api_name": "B"},
],
}
leaf = {"kind": "FLOW", "api_name": "A", "children": []}
parse_wave.inflate_flow_leaf(leaf, flow_children)
self.assertEqual(len(leaf["children"]), 1)
self.assertEqual(leaf["children"][0]["kind"], "APEX")
self.assertNotIn("_cycle_back_to", leaf["children"][0])
def test_three_layer_linear_graph_fully_expanded(self):
"""A → B → C — each Flow visited once, no false cycle tags."""
flow_children = {
"A": [{"kind": "FLOW", "element_name": "callB", "api_name": "B"}],
"B": [{"kind": "FLOW", "element_name": "callC", "api_name": "C"}],
"C": [],
}
leaf = {"kind": "FLOW", "api_name": "A", "children": []}
parse_wave.inflate_flow_leaf(leaf, flow_children)
# A has one child: B
self.assertEqual(len(leaf["children"]), 1)
b = leaf["children"][0]
self.assertEqual(b["api_name"], "B")
self.assertNotIn("_cycle_back_to", b)
# B has one child: C
self.assertEqual(len(b["children"]), 1)
c = b["children"][0]
self.assertEqual(c["api_name"], "C")
self.assertNotIn("_cycle_back_to", c)
# C has no children (empty list in flow_children)
self.assertEqual(c.get("children", []), [])
def test_sibling_flows_sharing_target_not_cross_contaminated(self):
"""If Flow X is called by two different siblings, both sibling
branches must fully expand X — not prune the second as a cycle.
Frozenset path-visited semantics protect this: siblings don't see
each other's descent. A mutable shared set would cause the second
sibling to treat X as a cycle.
"""
flow_children = {
"Root": [
{"kind": "FLOW", "element_name": "a", "api_name": "Sib1"},
{"kind": "FLOW", "element_name": "b", "api_name": "Sib2"},
],
"Sib1": [{"kind": "FLOW", "element_name": "shared", "api_name": "Shared"}],
"Sib2": [{"kind": "FLOW", "element_name": "shared", "api_name": "Shared"}],
"Shared": [],
}
leaf = {"kind": "FLOW", "api_name": "Root", "children": []}
parse_wave.inflate_flow_leaf(leaf, flow_children)
self.assertEqual(len(leaf["children"]), 2)
for sib in leaf["children"]:
# Each sibling must have Shared as a fully-expanded child —
# NOT a cycle stub.
self.assertEqual(len(sib["children"]), 1)
shared = sib["children"][0]
self.assertEqual(shared["api_name"], "Shared")
self.assertNotIn("_cycle_back_to", shared)
def test_empty_flow_children_returns_immediately(self):
"""No data for this leaf → preserve existing children (no-op)."""
leaf = {"kind": "FLOW", "api_name": "X", "children": [{"kind": "APEX", "api_name": "Y"}]}
parse_wave.inflate_flow_leaf(leaf, {}) # empty map
# Preserved unchanged
self.assertEqual(leaf["children"], [{"kind": "APEX", "api_name": "Y"}])
def test_non_flow_leaf_ignored(self):
"""Only FLOW kind is inflated."""
leaf = {"kind": "APEX", "api_name": "X"}
parse_wave.inflate_flow_leaf(leaf, {"X": [{"kind": "FLOW", "api_name": "Z"}]})
self.assertNotIn("children", leaf)
class CycleKeyTests(unittest.TestCase):
"""Unit test for the tuple-key helper — safety net on schema drift."""
def test_tuple_shape(self):
self.assertEqual(
parse_wave._cycle_key({"kind": "FLOW", "api_name": "Foo"}),
("FLOW", "Foo"),
)
def test_missing_kind_or_name_safe(self):
self.assertEqual(parse_wave._cycle_key({}), ("", ""))
def _linear_flow_chain(names: list[str]) -> dict:
"""Build a flow_children graph: FLOW[i] → FLOW[i+1], last flow has no kids.
Returns the `flow_children` mapping that inflate_flow_leaf consumes.
"""
graph: dict = {}
for i, n in enumerate(names):
if i + 1 < len(names):
graph[n] = [
{"kind": "FLOW", "element_name": f"call_{names[i+1]}",
"api_name": names[i+1]},
]
else:
graph[n] = []
return graph
class DepthCapPartialTests(unittest.TestCase):
"""`MAX_BFS_DEPTH` is a defensive guard,
not a functional chain-depth limit. Fixtures here push past the cap
to prove it still terminates and still populates `pending_out` when
it trips. Cycle semantics live in per-branch ancestor tracking and
are covered by `test_per_branch_visited.py`.
"""
def _deep_linear_names(self, n: int) -> list[str]:
return [f"F{i}" for i in range(1, n + 1)]
def test_chain_under_cap_fully_expanded(self):
"""A linear chain shorter than `MAX_BFS_DEPTH` fully expands with
no pending. Prior to the revision this used a 5-flow chain; with
a defensive cap of 20, 5 is trivially under-cap but the shape of
the assertion is unchanged.
"""
names = ["F1", "F2", "F3", "F4", "F5"]
graph = _linear_flow_chain(names)
leaf = {"kind": "FLOW", "api_name": "F1", "children": []}
pending: dict[str, set[str]] = {k: set() for k in parse_wave._BFS_KINDS}
parse_wave.inflate_flow_leaf(leaf, graph, pending_out=pending)
node = leaf
for expected in names[1:]:
kids = node.get("children", [])
self.assertEqual(len(kids), 1, f"expected one child under {node['api_name']}")
self.assertEqual(kids[0]["api_name"], expected)
node = kids[0]
self.assertEqual(node.get("children", []), [])
for k in parse_wave._BFS_KINDS:
self.assertEqual(pending[k], set())
def test_chain_at_exact_cap_trips_and_records_pending(self):
"""A linear chain of MAX_BFS_DEPTH+1 unique flows trips the
defensive cap: the last flow must NOT be expanded AND must land
in `pending_out["FLOW"]`. This is the only functional invariant
of the cap after the revision.
"""
n = parse_wave.MAX_BFS_DEPTH + 1
names = self._deep_linear_names(n)
graph = _linear_flow_chain(names)
leaf = {"kind": "FLOW", "api_name": names[0], "children": []}
pending: dict[str, set[str]] = {k: set() for k in parse_wave._BFS_KINDS}
parse_wave.inflate_flow_leaf(leaf, graph, pending_out=pending)
# Walk down to the flow at `MAX_BFS_DEPTH - 1` index (last one
# before the cap trips on its child). All prior flows must have
# been fully expanded.
node = leaf
for expected in names[1:parse_wave.MAX_BFS_DEPTH]:
kids = node.get("children", [])
self.assertEqual(len(kids), 1)
self.assertEqual(kids[0]["api_name"], expected)
node = kids[0]
# The last flow in the chain (the (MAX_BFS_DEPTH+1)-th) must be
# in pending_out.
self.assertIn(names[-1], pending["FLOW"])
def test_deep_chain_with_tail_self_loop_terminates_via_cycle_detection(self):
"""Long chain with a self-loop at the tail terminates via
per-branch cycle detection (the revised primitive), NOT the
defensive cap. The self-loop flow appears once with children
expanded, its self-reference annotated `_cycle_back_to`.
"""
# Chain of 7 unique flows, each pointing to the next, then the
# last one points at itself — 7 is well under the cap of 20 so
# this proves cycle detection, not depth, terminates the walk.
names = ["F1", "F2", "F3", "F4", "F5", "F6", "F7"]
graph = _linear_flow_chain(names)
# Rewrite F7 to self-loop instead of empty.
graph["F7"] = [{"kind": "FLOW", "api_name": "F7", "element_name": "self"}]
leaf = {"kind": "FLOW", "api_name": "F1", "children": []}
pending: dict[str, set[str]] = {k: set() for k in parse_wave._BFS_KINDS}
# Must not hang / recurse infinitely.
parse_wave.inflate_flow_leaf(leaf, graph, pending_out=pending)
# Walk to F7.
node = leaf
for expected in names[1:]:
node = node["children"][0]
self.assertEqual(node["api_name"], expected)
# F7 has ONE child — the cycle-annotated self-reference.
f7_kids = node.get("children", [])
self.assertEqual(len(f7_kids), 1)
self.assertEqual(f7_kids[0]["api_name"], "F7")
self.assertEqual(f7_kids[0]["_cycle_back_to"], "FLOW:F7")
self.assertEqual(
f7_kids[0]["_truncated"],
{"reason": "cycle", "target": "FLOW:F7"},
)
# Nothing in pending — cycle is annotated, not pended.
self.assertEqual(pending["FLOW"], set())
def test_chain_under_cap_with_legacy_pending_none(self):
"""Callers pre-dating pass no `pending_out`. An under-cap
chain still expands fully — no crash on `None`, no spurious
truncation. The pre-revision form of this test asserted cap=5
behavior; now it asserts that a short chain expands cleanly when
the cap plays no role.
"""
names = ["F1", "F2", "F3", "F4", "F5", "F6"]
graph = _linear_flow_chain(names)
leaf = {"kind": "FLOW", "api_name": "F1", "children": []}
# No pending_out — legacy call shape. Must not crash.
parse_wave.inflate_flow_leaf(leaf, graph)
# All flows expanded to the terminal leaf.
node = leaf
for expected in names[1:]:
kids = node.get("children", [])
self.assertEqual(len(kids), 1, f"{node['api_name']} should have one child")
self.assertEqual(kids[0]["api_name"], expected)
node = kids[0]
self.assertEqual(node.get("children", []), [])
def test_partial_and_unresolved_coexist(self):
"""_unresolved + _partial-reason must both be reported.
Simulate: finalize_cap drains some pending into _unresolved, but
depth-cap already set _partial_reason=max-depth-cap. Finalize must
NOT clobber the depth-cap reason.
"""
tree = {
"_partial": True,
"_partial_reason": "max-depth-cap",
"_pending_fetches": {
"FLOW": ["LateFlow"],
"APEX": [],
"PROMPT_TEMPLATE": [],
"STANDARD_ACTION": [],
},
"_unresolved": [{"kind": "APEX", "api_name": "PriorUnresolved",
"reason": "resolve_invocation_target failed"}],
}
parse_wave.finalize_cap(tree)
# Pending drained into unresolved.
self.assertEqual(tree["_pending_fetches"]["FLOW"], [])
self.assertEqual(len(tree["_unresolved"]), 2)
# Depth-cap reason preserved (NOT overwritten by max-wave-depth).
self.assertEqual(tree["_partial_reason"], "max-depth-cap")
self.assertTrue(tree["_partial"])
class MaxBfsDepthConstantTests(unittest.TestCase):
"""`MAX_BFS_DEPTH` is a defensive guard,
not a functional chain-depth limit. The constant must match
`scripts/config.py::MAX_BFS_DEPTH` exactly (duplicated intentionally
— see comment there on the "no intra-skill imports" convention).
"""
def test_max_bfs_depth_matches_config(self):
"""Cross-module consistency. If someone bumps one literal they
must bump the other."""
import config # type: ignore
self.assertEqual(parse_wave.MAX_BFS_DEPTH, config.MAX_BFS_DEPTH)
def test_max_bfs_depth_is_defensive_not_functional(self):
"""The cap should be comfortably larger than any realistic
production flow-chain depth, so that per-branch cycle detection
— not the cap — terminates the walk in practice. `>= 15` is a
loose sanity bound; if someone quietly tightens it below 15
they're probably reintroducing the shared-utility-flow bug."""
self.assertGreaterEqual(parse_wave.MAX_BFS_DEPTH, 15)
def test_legacy_alias_tracks_new_constant(self):
"""MAX_INFLATE_DEPTH is retained as an alias."""
self.assertEqual(parse_wave.MAX_INFLATE_DEPTH, parse_wave.MAX_BFS_DEPTH)
class PublicSymbolPromotionTests(unittest.TestCase):
"""`_BFS_KINDS` and `_empty_kind_sets` were
promoted to public names (`BFS_KINDS`, `empty_kind_sets`) so the
in-process `main.py` orchestrator doesn't have to reach across the
leading-underscore boundary that closed for `redact_text`.
The underscore forms remain as deprecated aliases for one more minor
version.
"""
def test_public_bfs_kinds_importable(self):
"""`parse_wave.BFS_KINDS` exists, is a tuple, and carries the
expected four tokens in the same order as before promotion.
"""
self.assertTrue(hasattr(parse_wave, "BFS_KINDS"))
self.assertIsInstance(parse_wave.BFS_KINDS, tuple)
self.assertEqual(
parse_wave.BFS_KINDS,
("FLOW", "APEX", "PROMPT_TEMPLATE", "STANDARD_ACTION"),
)
def test_public_empty_kind_sets_importable_and_callable(self):
"""`parse_wave.empty_kind_sets()` returns a fresh {kind: set()}
mapping keyed by every BFS_KINDS entry. Each call yields an
independent dict — callers must not share buckets across waves.
"""
self.assertTrue(hasattr(parse_wave, "empty_kind_sets"))
d1 = parse_wave.empty_kind_sets()
d2 = parse_wave.empty_kind_sets()
self.assertEqual(set(d1.keys()), set(parse_wave.BFS_KINDS))
for kind in parse_wave.BFS_KINDS:
self.assertEqual(d1[kind], set())
# Independence: mutating one must not affect the other.
d1["FLOW"].add("X")
self.assertEqual(d2["FLOW"], set())
def test_underscore_aliases_still_work_for_backcompat(self):
"""Legacy `_BFS_KINDS` / `_empty_kind_sets` imports continue to
resolve. They are deprecated but not yet removed — existing
tests and any external callers must keep working through the
migration window.
"""
self.assertTrue(hasattr(parse_wave, "_BFS_KINDS"))
self.assertTrue(hasattr(parse_wave, "_empty_kind_sets"))
# Same object (alias, not a copy) so equality holds structurally.
self.assertIs(parse_wave._BFS_KINDS, parse_wave.BFS_KINDS)
self.assertIs(parse_wave._empty_kind_sets, parse_wave.empty_kind_sets)
class FetchableKindsTests(unittest.TestCase):
"""STANDARD_ACTION is declared-only, never fetched.
Must stay out of `_pending_fetches` even though it remains a BFS kind
for tree counts + visited dedup."""
def test_fetchable_kinds_excludes_standard_action(self):
"""FETCHABLE_KINDS = (FLOW, APEX, PROMPT_TEMPLATE). STANDARD_ACTION
must be in BFS_KINDS (tree bookkeeping) but NOT in
FETCHABLE_KINDS (body-fetch eligibility)."""
self.assertIn("STANDARD_ACTION", parse_wave.BFS_KINDS)
self.assertNotIn("STANDARD_ACTION", parse_wave.FETCHABLE_KINDS)
self.assertEqual(
parse_wave.FETCHABLE_KINDS,
("FLOW", "APEX", "PROMPT_TEMPLATE"),
)
def test_build_root_children_skips_standard_action_refs(self):
"""A GenAiFunction whose unwrap is STANDARD_ACTION must NOT land
in new_refs. Before this fix, `streamKnowledgeSearch` landed in
`new_refs["STANDARD_ACTION"]` → merged into pending → never
visited → surfaced in _pending_fetches (pollution)."""
bundle = {
"topics": [{
"name": "T",
"actions": [{
"name": "A1",
"invocationTarget": "streamKnowledgeSearch",
"invocationTargetType": "standardInvocableAction",
}],
}],
"plannerActions": [],
}
visited = parse_wave.empty_kind_sets()
aux_visited: set = set()
children, new_refs = parse_wave.build_root_children(
bundle, visited, aux_visited,
)
# Children list is built normally — tree still has the leaf.
self.assertEqual(len(children), 1)
# But new_refs["STANDARD_ACTION"] is empty → nothing to pend on.
self.assertEqual(new_refs["STANDARD_ACTION"], set())
self.assertEqual(new_refs["FLOW"], set())
self.assertEqual(new_refs["APEX"], set())
self.assertEqual(new_refs["PROMPT_TEMPLATE"], set())
def test_build_root_children_still_captures_flow_apex_prompt_refs(self):
"""Positive control — fetchable kinds still accumulate into
new_refs. The FOLLOWUP-2 gate is on STANDARD_ACTION only."""
bundle = {
"topics": [{
"name": "T",
"actions": [
{"name": "F1", "invocationTarget": "FlowA",
"invocationTargetType": "flow"},
{"name": "A1", "invocationTarget": "ApexA",
"invocationTargetType": "apex"},
{"name": "P1", "invocationTarget": "PromptA",
"invocationTargetType": "generatePromptResponse"},
],
}],
"plannerActions": [],
}
visited = parse_wave.empty_kind_sets()
aux_visited: set = set()
_, new_refs = parse_wave.build_root_children(
bundle, visited, aux_visited,
)
self.assertIn("FlowA", new_refs["FLOW"])
self.assertIn("ApexA", new_refs["APEX"])
self.assertIn("PromptA", new_refs["PROMPT_TEMPLATE"])
self.assertEqual(new_refs["STANDARD_ACTION"], set())
class UnifiedTruncationAnnotationTests(unittest.TestCase):
"""both cycle and max-depth code paths annotate truncated
nodes with the unified `_truncated = {reason, target}` sub-object.
`_cycle_back_to` is preserved as a deprecated alias (backcompat)."""
def test_truncation_constants_are_exposed(self):
"""Public constants let consumers match on strings without
duplicating the literal values."""
self.assertEqual(parse_wave.TRUNCATION_CYCLE, "cycle")
self.assertEqual(parse_wave.TRUNCATION_MAX_DEPTH, "max-depth")
self.assertIn(
parse_wave.TRUNCATION_CYCLE, parse_wave.TRUNCATION_REASONS,
)
self.assertIn(
parse_wave.TRUNCATION_MAX_DEPTH, parse_wave.TRUNCATION_REASONS,
)
def test_cycle_emits_unified_truncated_and_legacy_alias(self):
"""Cycle annotations carry BOTH the unified `_truncated` and the
legacy `_cycle_back_to` string (same target). Consumers that
haven't migrated still work; new consumers read one field."""
flow_children = {
# A simple A → A self-cycle inside one expansion.
"A": [{"kind": "FLOW", "api_name": "A", "element_name": "loop"}],
}
leaf = {"kind": "FLOW", "api_name": "A", "children": []}
parse_wave.inflate_flow_leaf(leaf, flow_children)
# Find the cycle-annotated child (second encounter of A).
kids = leaf.get("children") or []
self.assertEqual(len(kids), 1)
cycle_node = kids[0]
# Unified annotation present.
self.assertIn("_truncated", cycle_node)
self.assertEqual(
cycle_node["_truncated"],
{"reason": "cycle", "target": "FLOW:A"},
)
# Legacy annotation still present for backcompat.
self.assertEqual(cycle_node["_cycle_back_to"], "FLOW:A")
def test_depth_cap_emits_unified_truncated_on_leaf(self):
"""When MAX_BFS_DEPTH trips, the truncated FLOW leaf picks up
`_truncated = {reason: 'max-depth', target: 'FLOW:<name>'}`.
Before the leaf carried no per-node signal at all — only
the tree-level `_partial_reason` and the `pending_out`
accumulator identified it.
2026-05-03: since the cap was bumped from 5 to 20 (defensive
guard, not functional limit), the chain here is built
programmatically at `MAX_BFS_DEPTH + 1` unique flows so the
test tracks the constant instead of hard-coding chain length.
"""
n = parse_wave.MAX_BFS_DEPTH + 1 # one past the cap
names = [f"F{i}" for i in range(n)]
flow_children: dict[str, list[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_out = parse_wave.empty_kind_sets()
parse_wave.inflate_flow_leaf(root, flow_children, pending_out=pending_out)
# Walk down the chain to the terminal (unreached) flow.
def walk(n, name):
if n.get("api_name") == name:
return n
for c in n.get("children") or []:
found = walk(c, name)
if found:
return found
return None
tail = walk(root, names[-1])
self.assertIsNotNone(tail, f"{names[-1]} should appear as a leaf in the tree")
# Unified annotation on the depth-capped leaf.
self.assertIn("_truncated", tail)
self.assertEqual(
tail["_truncated"],
{"reason": "max-depth", "target": f"FLOW:{names[-1]}"},
)
# The capped leaf should NOT also carry _cycle_back_to — this
# is a depth-cap, not a cycle.
self.assertNotIn("_cycle_back_to", tail)
# pending_out still picks up the unreached name — existing
# contract preserved.
self.assertIn(names[-1], pending_out["FLOW"])
def test_non_flow_leaf_depth_cap_no_annotation(self):
"""The cap is scoped to FLOW leaves. Non-FLOW leaves at
cap depth are just no-op returns — no spurious `_truncated`."""
flow_children = {
"X": [{"kind": "APEX", "api_name": "SomeApex"}],
}
# Build an APEX leaf that we try to "inflate" at depth = MAX.
# inflate_flow_leaf with a non-FLOW leaf at depth >= MAX does
# nothing and emits no annotation.
leaf = {"kind": "APEX", "api_name": "SomeApex"}
pending_out = parse_wave.empty_kind_sets()
parse_wave.inflate_flow_leaf(
leaf, flow_children,
depth=parse_wave.MAX_BFS_DEPTH,
pending_out=pending_out,
)
self.assertNotIn("_truncated", leaf)
self.assertEqual(pending_out["FLOW"], set())
if __name__ == "__main__":
unittest.main()