test: validate multica workflow reachability
This commit is contained in:
@@ -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}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user