feat: derive structured foreach runtime context

This commit is contained in:
lda
2026-09-05 00:16:24 +07:00 Verified
parent e583b961f3
commit a519f5e2e4
6 changed files with 466 additions and 2 deletions
@@ -0,0 +1,301 @@
from __future__ import annotations
import pytest
from wf_core.errors import WorkflowExecutionError
from wf_core.run_state import ExecutionFrame, ForeachContext, RunState, RunStatus
from wf_core.runtime.ops.frames import frame_context_view
def _run_with_frames(frames: list[ExecutionFrame]) -> RunState:
run = RunState(
workflow_name="demo",
status=RunStatus.RUNNING,
workflow_input={},
state={},
)
for frame in frames:
run.frames[frame.id] = frame
return run
def _item_frame(
*,
frame_id: str,
parent_id: str | None,
node_id: str,
activation_id: str,
index: int,
item: object,
alias: str,
scope_id: str = "root",
lineage_id: str = "lineage",
) -> ExecutionFrame:
return ExecutionFrame(
id=frame_id,
kind="foreach_iteration",
node_id=node_id,
parent_frame_id=parent_id,
scope_id=scope_id,
lineage_id=lineage_id,
metadata={
"foreach_node_id": node_id,
"activation_id": activation_id,
"loop_index": index,
"loop_item": item,
"loop_alias": alias,
},
)
def test_root_frame_has_empty_structured_foreach_context() -> None:
run = _run_with_frames(
[ExecutionFrame(id="root", kind="root", node_id="start", scope_id="root")]
)
view = frame_context_view(run, run.frames["root"])
assert dict(view.foreach) == {}
assert view.graph["foreach"] == {}
assert "loop_item" not in view.graph
assert "loop_index" not in view.graph
def test_nested_same_scope_frames_expose_outermost_to_innermost_context() -> None:
run = _run_with_frames(
[
ExecutionFrame(id="root", kind="root", node_id="customers", scope_id="root"),
_item_frame(
frame_id="outer-item",
parent_id="root",
node_id="customers",
activation_id="customers:activation:1",
index=0,
item={"name": "Ada"},
alias="customer",
lineage_id="customers:lineage:0",
),
ExecutionFrame(
id="inner-controller",
kind="foreach",
node_id="orders",
parent_frame_id="outer-item",
scope_id="root",
lineage_id="customers:lineage:0",
),
_item_frame(
frame_id="inner-item",
parent_id="inner-controller",
node_id="orders",
activation_id="orders:activation:1",
index=2,
item={"sku": "A-17"},
alias="order",
lineage_id="orders:lineage:2",
),
]
)
view = frame_context_view(run, run.frames["inner-item"])
contexts = view.foreach
assert tuple(contexts) == ("customers", "orders")
assert contexts["customers"] == ForeachContext(
node_id="customers",
activation_id="customers:activation:1",
frame_id="outer-item",
scope_id="root",
lineage_id="customers:lineage:0",
index=0,
item={"name": "Ada"},
)
assert contexts["orders"].index == 2
assert contexts["orders"].item == {"sku": "A-17"}
def test_graph_context_values_keep_all_aliases_and_innermost_loop_keys() -> None:
run = _run_with_frames(
[
ExecutionFrame(id="root", kind="root", node_id="customers", scope_id="root"),
_item_frame(
frame_id="outer-item",
parent_id="root",
node_id="customers",
activation_id="customers:activation:1",
index=0,
item={"name": "Ada"},
alias="customer",
lineage_id="customers:lineage:0",
),
_item_frame(
frame_id="inner-item",
parent_id="outer-item",
node_id="orders",
activation_id="orders:activation:1",
index=2,
item={"sku": "A-17"},
alias="order",
lineage_id="orders:lineage:2",
),
]
)
view = frame_context_view(run, run.frames["inner-item"])
graph = dict(view.graph)
assert graph["customer"] == {"name": "Ada"}
assert graph["order"] == {"sku": "A-17"}
assert graph["loop_item"] == {"sku": "A-17"}
assert graph["loop_index"] == 2
foreach_map = graph["foreach"]
assert isinstance(foreach_map, dict)
assert set(foreach_map) == {"customers", "orders"}
assert foreach_map["orders"]["index"] == 2
assert foreach_map["orders"]["item"] == {"sku": "A-17"}
def test_context_ancestry_stops_at_runtime_scope_boundary() -> None:
run = _run_with_frames(
[
ExecutionFrame(id="root", kind="root", node_id="customers", scope_id="root"),
_item_frame(
frame_id="outer-item",
parent_id="root",
node_id="customers",
activation_id="customers:activation:1",
index=0,
item={"name": "Ada"},
alias="customer",
lineage_id="customers:lineage:0",
),
ExecutionFrame(
id="child-root",
kind="subgraph_root",
node_id="start",
parent_frame_id="outer-item",
scope_id="child",
lineage_id="child:root",
),
]
)
view = frame_context_view(run, run.frames["child-root"])
assert dict(view.foreach) == {}
assert view.graph["foreach"] == {}
def test_structured_context_rejects_malformed_foreach_metadata() -> None:
run = _run_with_frames(
[
ExecutionFrame(
id="bad",
kind="foreach_iteration",
node_id="body",
scope_id="root",
metadata={"foreach_node_id": "", "activation_id": "a"},
)
]
)
with pytest.raises(WorkflowExecutionError, match="malformed|missing"):
frame_context_view(run, run.frames["bad"])
def test_structured_context_rejects_missing_parent_frame() -> None:
run = _run_with_frames(
[
ExecutionFrame(
id="child",
kind="node",
node_id="body",
parent_frame_id="missing",
scope_id="root",
)
]
)
with pytest.raises(WorkflowExecutionError, match="missing parent frame"):
frame_context_view(run, run.frames["child"])
def test_structured_context_rejects_parent_cycle() -> None:
run = _run_with_frames(
[
ExecutionFrame(
id="a", kind="node", node_id="x", parent_frame_id="b", scope_id="root"
),
ExecutionFrame(
id="b", kind="node", node_id="y", parent_frame_id="a", scope_id="root"
),
]
)
with pytest.raises(WorkflowExecutionError, match="cyclic"):
frame_context_view(run, run.frames["a"])
def test_structured_context_rejects_duplicate_active_foreach_id() -> None:
run = _run_with_frames(
[
ExecutionFrame(id="root", kind="root", node_id="each", scope_id="root"),
_item_frame(
frame_id="outer-item",
parent_id="root",
node_id="each",
activation_id="act-1",
index=0,
item="a",
alias="first",
),
_item_frame(
frame_id="inner-item",
parent_id="outer-item",
node_id="each",
activation_id="act-2",
index=1,
item="b",
alias="second",
),
]
)
with pytest.raises(WorkflowExecutionError, match="duplicate active foreach id"):
frame_context_view(run, run.frames["inner-item"])
def test_structured_context_rejects_duplicate_active_alias() -> None:
run = _run_with_frames(
[
ExecutionFrame(id="root", kind="root", node_id="a", scope_id="root"),
_item_frame(
frame_id="outer-item",
parent_id="root",
node_id="customers",
activation_id="act-1",
index=0,
item="a",
alias="same",
),
_item_frame(
frame_id="inner-item",
parent_id="outer-item",
node_id="orders",
activation_id="act-2",
index=0,
item="b",
alias="same",
),
]
)
with pytest.raises(WorkflowExecutionError, match="duplicate active foreach alias"):
frame_context_view(run, run.frames["inner-item"])
def test_context_read_does_not_mutate_run_state() -> None:
run = _run_with_frames(
[
ExecutionFrame(id="root", kind="root", node_id="customers", scope_id="root"),
_item_frame(
frame_id="outer-item",
parent_id="root",
node_id="customers",
activation_id="act-1",
index=0,
item={"name": "Ada"},
alias="customer",
),
]
)
before = run.to_dict()
frame_context_view(run, run.frames["outer-item"])
assert run.to_dict() == before