test: validate multica workflow reachability

This commit is contained in:
2026-06-13 14:46:15 +08:00
parent 1bc89446bc
commit 5d399a09e8
@@ -46,6 +46,59 @@ def build_agent_payload(agent_fixture: Dict[str, Any], workflow_id: str | None)
return payload return payload
def _find_parallel_merge(
node_by_id: Dict[str, Dict[str, Any]],
outgoing_edges: Dict[str, list[str]],
branch_starts: list[str],
) -> str | None:
branch_merges: list[str] = []
for start in branch_starts:
current = start
seen: set[str] = set()
while current and current not in seen:
seen.add(current)
next_nodes = outgoing_edges.get(current, [])
if not next_nodes or len(next_nodes) > 1:
break
current = next_nodes[0]
if (node_by_id.get(current) or {}).get("type") == "merge":
branch_merges.append(current)
break
unique_merges = set(branch_merges)
return branch_merges[0] if len(unique_merges) == 1 and len(branch_merges) == len(branch_starts) else None
def _walk_workflow_nodes(
node_by_id: Dict[str, Dict[str, Any]],
outgoing_edges: Dict[str, list[str]],
start_id: str,
) -> set[str]:
visited: set[str] = set()
current = start_id
while current and current not in visited:
visited.add(current)
node_type = (node_by_id.get(current) or {}).get("type")
if node_type == "end":
break
if node_type == "parallel":
branch_starts = outgoing_edges.get(current, [])
for branch_start in branch_starts:
branch_current = branch_start
branch_seen: set[str] = set()
while branch_current and branch_current not in branch_seen:
if (node_by_id.get(branch_current) or {}).get("type") == "merge":
break
visited.add(branch_current)
branch_seen.add(branch_current)
branch_next_nodes = outgoing_edges.get(branch_current, [])
branch_current = branch_next_nodes[0] if branch_next_nodes else None
current = _find_parallel_merge(node_by_id, outgoing_edges, branch_starts)
continue
next_nodes = outgoing_edges.get(current, [])
current = next_nodes[0] if next_nodes else None
return visited
def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]: def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]:
agents = fixture.get("agents", []) agents = fixture.get("agents", [])
definition = fixture.get("workflow", {}).get("definition", {}) definition = fixture.get("workflow", {}).get("definition", {})
@@ -77,6 +130,7 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]:
} }
required_node_types = {"start", "end", "template", "parallel", "merge"} required_node_types = {"start", "end", "template", "parallel", "merge"}
multi_outgoing_node_types = {"parallel", "condition", "intent", "choice"} multi_outgoing_node_types = {"parallel", "condition", "intent", "choice"}
start_node = next((node for node in nodes if node.get("type") == "start"), None)
missing_workflow_agents = sorted(workflow_agent_codes - agent_codes) missing_workflow_agents = sorted(workflow_agent_codes - agent_codes)
missing_persona_fields = { missing_persona_fields = {
@@ -91,6 +145,14 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]:
if len(targets) > 1 if len(targets) > 1
and (node_by_id.get(source) or {}).get("type") not in multi_outgoing_node_types and (node_by_id.get(source) or {}).get("type") not in multi_outgoing_node_types
} }
reachable_nodes = _walk_workflow_nodes(node_by_id, outgoing_edges, start_node.get("id")) if start_node else set()
unreachable_nodes = sorted(set(node_by_id) - reachable_nodes)
invalid_parallel_merges = {
source: targets
for source, targets in outgoing_edges.items()
if (node_by_id.get(source) or {}).get("type") == "parallel"
and not _find_parallel_merge(node_by_id, outgoing_edges, targets)
}
project_manager = next( project_manager = next(
(agent for agent in agents if agent.get("code") == "project_manager"), (agent for agent in agents if agent.get("code") == "project_manager"),
None, None,
@@ -101,6 +163,8 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]:
or missing_persona_fields or missing_persona_fields
or missing_node_types or missing_node_types
or invalid_multi_outgoing or invalid_multi_outgoing
or unreachable_nodes
or invalid_parallel_merges
or project_manager_workflow != "multica_org_collaboration_flow" or project_manager_workflow != "multica_org_collaboration_flow"
): ):
raise ValueError( raise ValueError(
@@ -108,6 +172,8 @@ def validate_fixture(fixture: Dict[str, Any]) -> Dict[str, int]:
f"missing_persona_fields={missing_persona_fields}, " f"missing_persona_fields={missing_persona_fields}, "
f"missing_node_types={missing_node_types}, " f"missing_node_types={missing_node_types}, "
f"invalid_multi_outgoing={invalid_multi_outgoing}, " f"invalid_multi_outgoing={invalid_multi_outgoing}, "
f"unreachable_nodes={unreachable_nodes}, "
f"invalid_parallel_merges={invalid_parallel_merges}, "
f"project_manager_workflow={project_manager_workflow}" f"project_manager_workflow={project_manager_workflow}"
) )