feat: analyze foreach control regions

This commit is contained in:
lda
2026-09-04 07:15:24 +07:00 Verified
parent 79f3054426
commit 6a5d886962
3 changed files with 614 additions and 0 deletions
+347
View File
@@ -0,0 +1,347 @@
from __future__ import annotations
from wf_core import END, Workflow
from wf_core.analysis.control_regions import (
ControlRegionAnalysis,
ControlRegionIssueKind,
analyze_control_regions,
)
def _workflow(
*,
start: str,
nodes: list[dict[str, object]],
edges: list[dict[str, str]],
) -> Workflow:
return Workflow.model_validate(
{
"name": "control-regions",
"input_schema": {"type": "object", "properties": {}},
"state_schema": {
"type": "object",
"properties": {
"items": {"type": "array", "items": {"type": "string"}},
"inner_items": {"type": "array", "items": {"type": "integer"}},
},
},
"output_schema": {"type": "object", "properties": {}},
"start": start,
"nodes": nodes,
"edges": edges,
"node_defs": [],
}
)
def _node(node_id: str) -> dict[str, object]:
return {"id": node_id, "type": "node", "node": "noop"}
def _foreach(node_id: str, *, alias: str = "item") -> dict[str, object]:
return {
"id": node_id,
"type": "foreach",
"over": "state.items",
"as": alias,
"mode": "serial",
}
def _condition(node_id: str) -> dict[str, object]:
return {
"id": node_id,
"type": "condition",
"check": {"op": "exists", "path": "state.items"},
}
def test_closed_root_cycle_has_one_empty_control_region() -> None:
workflow = _workflow(
start="a",
nodes=[_node("a"), _node("b")],
edges=[
{"from": "a", "outcome": "ok", "to": "b"},
{"from": "b", "outcome": "ok", "to": "a"},
],
)
analysis = analyze_control_regions(workflow)
assert analysis.issues == ()
assert analysis.owner_stack_by_node == {"a": (), "b": ()}
def test_foreach_cycle_with_possible_return_is_valid() -> None:
workflow = _workflow(
start="f",
nodes=[_foreach("f"), _node("a")],
edges=[
{"from": "f", "outcome": "loop", "to": "a"},
{"from": "a", "outcome": "again", "to": "a"},
{"from": "a", "outcome": "done", "to": "f"},
{"from": "f", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert analysis.issues == ()
assert analysis.owner_stack_by_node["a"] == ("f",)
assert analysis.owner_stack_by_node["f"] == ()
def test_conditional_foreach_paths_can_both_return() -> None:
workflow = _workflow(
start="f",
nodes=[_foreach("f"), _condition("condition"), _node("work")],
edges=[
{"from": "f", "outcome": "loop", "to": "condition"},
{"from": "condition", "outcome": "true", "to": "work"},
{"from": "condition", "outcome": "false", "to": "f"},
{"from": "work", "outcome": "ok", "to": "f"},
{"from": "f", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert analysis.issues == ()
assert analysis.owner_stack_by_node["condition"] == ("f",)
assert analysis.owner_stack_by_node["work"] == ("f",)
def test_nested_foreach_assigns_static_owner_stacks() -> None:
workflow = _workflow(
start="f1",
nodes=[
_foreach("f1"),
_foreach("f2"),
_node("work"),
_node("tail"),
_node("after"),
],
edges=[
{"from": "f1", "outcome": "loop", "to": "f2"},
{"from": "f2", "outcome": "loop", "to": "work"},
{"from": "work", "outcome": "ok", "to": "f2"},
{"from": "f2", "outcome": "done", "to": "tail"},
{"from": "tail", "outcome": "ok", "to": "f1"},
{"from": "f1", "outcome": "done", "to": "after"},
{"from": "after", "outcome": "ok", "to": END},
],
)
analysis: ControlRegionAnalysis = analyze_control_regions(workflow)
assert analysis.owner_stack_by_node == {
"f1": (),
"f2": ("f1",),
"work": ("f1", "f2"),
"tail": ("f1",),
"after": (),
}
assert analysis.issues == ()
def test_reentering_completed_foreach_keeps_one_static_region() -> None:
workflow = _workflow(
start="again",
nodes=[_condition("again"), _foreach("f"), _node("work")],
edges=[
{"from": "again", "outcome": "true", "to": "f"},
{"from": "f", "outcome": "loop", "to": "work"},
{"from": "work", "outcome": "ok", "to": "f"},
{"from": "f", "outcome": "done", "to": "again"},
{"from": "again", "outcome": "false", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert analysis.issues == ()
assert analysis.owner_stack_by_node["f"] == ()
assert analysis.owner_stack_by_node["work"] == ("f",)
assert analysis.owner_stack_by_node["again"] == ()
def test_external_entry_into_foreach_body_is_region_conflict() -> None:
workflow = _workflow(
start="start",
nodes=[_condition("start"), _foreach("f"), _node("b")],
edges=[
{"from": "start", "outcome": "true", "to": "f"},
{"from": "start", "outcome": "false", "to": "b"},
{"from": "f", "outcome": "loop", "to": "b"},
{"from": "b", "outcome": "ok", "to": "f"},
{"from": "f", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert (ControlRegionIssueKind.FOREACH_REGION_CONFLICT, "nodes[b]") in [
(issue.kind, issue.path) for issue in analysis.issues
]
assert "b" not in analysis.owner_stack_by_node
def test_foreach_body_escape_is_region_conflict() -> None:
workflow = _workflow(
start="f",
nodes=[_foreach("f"), _node("b"), _node("after")],
edges=[
{"from": "f", "outcome": "loop", "to": "b"},
{"from": "b", "outcome": "ok", "to": "after"},
{"from": "f", "outcome": "done", "to": "after"},
{"from": "after", "outcome": "ok", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert (ControlRegionIssueKind.FOREACH_REGION_CONFLICT, "nodes[after]") in [
(issue.kind, issue.path) for issue in analysis.issues
]
assert "after" not in analysis.owner_stack_by_node
def test_skipping_inner_foreach_owner_is_invalid_return() -> None:
workflow = _workflow(
start="f1",
nodes=[_foreach("f1"), _foreach("f2"), _node("work")],
edges=[
{"from": "f1", "outcome": "loop", "to": "f2"},
{"from": "f2", "outcome": "loop", "to": "work"},
{"from": "work", "outcome": "ok", "to": "f1"},
{"from": "f1", "outcome": "done", "to": END},
{"from": "f2", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert (ControlRegionIssueKind.INVALID_FOREACH_RETURN, "edges[2]") in [
(issue.kind, issue.path) for issue in analysis.issues
]
def test_entering_sibling_foreach_body_is_region_conflict() -> None:
workflow = _workflow(
start="f1",
nodes=[_foreach("f1"), _foreach("f2"), _node("b1"), _node("b2")],
edges=[
{"from": "f1", "outcome": "loop", "to": "b1"},
{"from": "b1", "outcome": "ok", "to": "b2"},
{"from": "f2", "outcome": "loop", "to": "b2"},
{"from": "b2", "outcome": "ok", "to": "f1"},
{"from": "f1", "outcome": "done", "to": "f2"},
{"from": "f2", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert (ControlRegionIssueKind.FOREACH_REGION_CONFLICT, "nodes[b2]") in [
(issue.kind, issue.path) for issue in analysis.issues
]
def test_empty_foreach_body_is_rejected() -> None:
workflow = _workflow(
start="f",
nodes=[_foreach("f")],
edges=[
{"from": "f", "outcome": "loop", "to": "f"},
{"from": "f", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert (ControlRegionIssueKind.EMPTY_FOREACH_BODY, "edges[0]") in [
(issue.kind, issue.path) for issue in analysis.issues
]
def test_closed_foreach_body_cycle_has_no_return() -> None:
workflow = _workflow(
start="f",
nodes=[_foreach("f"), _node("a"), _node("b")],
edges=[
{"from": "f", "outcome": "loop", "to": "a"},
{"from": "a", "outcome": "ok", "to": "b"},
{"from": "b", "outcome": "ok", "to": "a"},
{"from": "f", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert (ControlRegionIssueKind.FOREACH_BODY_NO_RETURN, "nodes[f]") in [
(issue.kind, issue.path) for issue in analysis.issues
]
def test_foreach_body_cannot_target_end_token() -> None:
workflow = _workflow(
start="f",
nodes=[_foreach("f"), _node("body")],
edges=[
{"from": "f", "outcome": "loop", "to": "body"},
{"from": "body", "outcome": "ok", "to": END},
{"from": "f", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert (ControlRegionIssueKind.INVALID_FOREACH_TERMINAL, "edges[1]") in [
(issue.kind, issue.path) for issue in analysis.issues
]
def test_foreach_body_cannot_target_explicit_end_node() -> None:
workflow = _workflow(
start="f",
nodes=[
_foreach("f"),
_node("body"),
{"id": "stop", "type": "end", "outcome": "ok"},
],
edges=[
{"from": "f", "outcome": "loop", "to": "body"},
{"from": "body", "outcome": "ok", "to": "stop"},
{"from": "f", "outcome": "done", "to": END},
],
)
analysis = analyze_control_regions(workflow)
assert (ControlRegionIssueKind.INVALID_FOREACH_TERMINAL, "edges[1]") in [
(issue.kind, issue.path) for issue in analysis.issues
]
def test_every_unreachable_node_is_reported() -> None:
workflow = _workflow(
start="work",
nodes=[_node("work"), _node("detached_a"), _node("detached_b")],
edges=[
{"from": "work", "outcome": "ok", "to": END},
{"from": "detached_a", "outcome": "ok", "to": "detached_b"},
{"from": "detached_b", "outcome": "ok", "to": "detached_a"},
],
)
analysis = analyze_control_regions(workflow)
by_kind_path = [(issue.kind, issue.path) for issue in analysis.issues]
assert (
ControlRegionIssueKind.UNREACHABLE_NODE,
"nodes[detached_a]",
) in by_kind_path
assert (
ControlRegionIssueKind.UNREACHABLE_NODE,
"nodes[detached_b]",
) in by_kind_path