feat: identify dynamic foreach activations

This commit is contained in:
lda
2026-09-04 07:25:55 +07:00 Verified
parent 4bbd9f9650
commit f69c4502cf
12 changed files with 499 additions and 76 deletions
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
import pytest
from wf_core.errors import WorkflowExecutionError
from wf_core.run_state import ExecutionFrame
from wf_core.runtime.foreach_state import (
close_foreach_activation,
item_frame_owner,
load_or_begin_foreach_activation,
save_foreach_activation,
)
from wf_core.runtime.scheduler import ForeachIterationMetadata
def _frame() -> ExecutionFrame:
return ExecutionFrame(id="root", kind="workflow", node_id="each")
def test_activation_lifecycle_reuses_active_then_fresh_after_close() -> None:
frame = _frame()
first = load_or_begin_foreach_activation(frame, "each", mode="serial")
save_foreach_activation(frame, first)
restored = load_or_begin_foreach_activation(frame, "each", mode="serial")
assert restored.id == first.id
close_foreach_activation(frame, restored)
second = load_or_begin_foreach_activation(frame, "each", mode="serial")
assert second.id != first.id
assert second.barrier.next_index == 0
def test_activation_rejects_malformed_metadata() -> None:
frame = ExecutionFrame(
id="root",
kind="workflow",
node_id="each",
metadata={"foreach_activations": "corrupt"},
)
with pytest.raises(WorkflowExecutionError, match="activation"):
load_or_begin_foreach_activation(frame, "each", mode="serial")
def test_activation_rejects_mode_mismatch() -> None:
frame = _frame()
activation = load_or_begin_foreach_activation(frame, "each", mode="serial")
save_foreach_activation(frame, activation)
with pytest.raises(WorkflowExecutionError, match="mode"):
load_or_begin_foreach_activation(frame, "each", mode="concurrent")
def test_closing_stale_activation_fails_closed() -> None:
frame = _frame()
first = load_or_begin_foreach_activation(frame, "each", mode="serial")
save_foreach_activation(frame, first)
close_foreach_activation(frame, first)
second = load_or_begin_foreach_activation(frame, "each", mode="serial")
save_foreach_activation(frame, second)
with pytest.raises(WorkflowExecutionError, match="stale|closed|active"):
close_foreach_activation(frame, first)
def test_activation_json_round_trip_through_frame_metadata() -> None:
frame = _frame()
activation = load_or_begin_foreach_activation(frame, "each", mode="serial")
activation.barrier.next_index = 2
save_foreach_activation(frame, activation)
dumped = dict(frame.metadata)
restored_frame = ExecutionFrame(
id="root", kind="workflow", node_id="each", metadata=dumped
)
restored = load_or_begin_foreach_activation(restored_frame, "each", mode="serial")
assert restored.id == activation.id
assert restored.barrier.next_index == 2
def test_item_metadata_requires_activation_identity() -> None:
frame = ExecutionFrame(
id="root:each#0:0",
kind="foreach_iteration",
node_id="work",
parent_frame_id="root",
metadata={
"foreach_node_id": "each",
"loop_index": 0,
"loop_item": "a",
"loop_alias": "item",
},
)
with pytest.raises(WorkflowExecutionError, match="activation"):
ForeachIterationMetadata.from_frame(frame)
with pytest.raises(WorkflowExecutionError, match="activation"):
item_frame_owner(frame)