sudden Code review
This commit is contained in:
@@ -213,9 +213,7 @@ class ForeachNode(BaseModel):
|
|||||||
)
|
)
|
||||||
as_: str = Field(alias="as", description="Context key for the current item.")
|
as_: str = Field(alias="as", description="Context key for the current item.")
|
||||||
mode: Literal["serial", "concurrent"] = "serial"
|
mode: Literal["serial", "concurrent"] = "serial"
|
||||||
item_error: ForeachItemErrorPolicy = Field(
|
item_error: ForeachItemErrorPolicy = Field(default_factory=ForeachItemErrorPolicy)
|
||||||
default_factory=ForeachItemErrorPolicy
|
|
||||||
)
|
|
||||||
concurrent: ForeachConcurrentPolicy | None = None
|
concurrent: ForeachConcurrentPolicy | None = None
|
||||||
on_item_error: Literal["fail", "collect", "skip"] | None = Field(
|
on_item_error: Literal["fail", "collect", "skip"] | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
@@ -252,9 +250,7 @@ class ForeachNode(BaseModel):
|
|||||||
if self.mode == "concurrent" and self.concurrent is None:
|
if self.mode == "concurrent" and self.concurrent is None:
|
||||||
raise ValueError("concurrent foreach requires concurrent policy")
|
raise ValueError("concurrent foreach requires concurrent policy")
|
||||||
if self.mode == "serial" and self.concurrent is not None:
|
if self.mode == "serial" and self.concurrent is not None:
|
||||||
raise ValueError(
|
raise ValueError("concurrent policy is only valid when mode='concurrent'")
|
||||||
"concurrent policy is only valid when mode='concurrent'"
|
|
||||||
)
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,11 +35,10 @@ class ItemErrorRecord:
|
|||||||
raise WorkflowExecutionError(
|
raise WorkflowExecutionError(
|
||||||
f"malformed foreach item error record missing {exc.args[0]!r}"
|
f"malformed foreach item error record missing {exc.args[0]!r}"
|
||||||
) from exc
|
) from exc
|
||||||
if not isinstance(index, int):
|
if not isinstance(index, int) or index < 0:
|
||||||
raise WorkflowExecutionError("malformed foreach item error index")
|
raise WorkflowExecutionError("malformed foreach item error index")
|
||||||
if not all(
|
if not all(
|
||||||
isinstance(value, str)
|
isinstance(value, str) for value in (frame_id, node_id, error_type, message)
|
||||||
for value in (frame_id, node_id, error_type, message)
|
|
||||||
):
|
):
|
||||||
raise WorkflowExecutionError("malformed foreach item error text fields")
|
raise WorkflowExecutionError("malformed foreach item error text fields")
|
||||||
return cls(
|
return cls(
|
||||||
@@ -76,11 +75,16 @@ class PendingItemResult:
|
|||||||
def from_metadata(cls, raw: object) -> PendingItemResult:
|
def from_metadata(cls, raw: object) -> PendingItemResult:
|
||||||
if not isinstance(raw, dict):
|
if not isinstance(raw, dict):
|
||||||
raise WorkflowExecutionError("malformed pending foreach result")
|
raise WorkflowExecutionError("malformed pending foreach result")
|
||||||
index = raw.get("index")
|
try:
|
||||||
frame_id = raw.get("frame_id")
|
index = raw["index"]
|
||||||
status = raw.get("status")
|
frame_id = raw["frame_id"]
|
||||||
|
status = raw["status"]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"malformed pending foreach result missing {exc.args[0]!r}"
|
||||||
|
) from exc
|
||||||
patch_changes = raw.get("patch_changes", {})
|
patch_changes = raw.get("patch_changes", {})
|
||||||
if not isinstance(index, int):
|
if not isinstance(index, int) or index < 0:
|
||||||
raise WorkflowExecutionError("malformed pending foreach result index")
|
raise WorkflowExecutionError("malformed pending foreach result index")
|
||||||
if not isinstance(frame_id, str):
|
if not isinstance(frame_id, str):
|
||||||
raise WorkflowExecutionError("malformed pending foreach result frame id")
|
raise WorkflowExecutionError("malformed pending foreach result frame id")
|
||||||
@@ -93,10 +97,12 @@ class PendingItemResult:
|
|||||||
index=index,
|
index=index,
|
||||||
frame_id=frame_id,
|
frame_id=frame_id,
|
||||||
status=status,
|
status=status,
|
||||||
patch=StatePatch(changes=dict(patch_changes)),
|
patch=StatePatch(changes=patch_changes),
|
||||||
error=ItemErrorRecord.from_metadata(raw_error)
|
error=(
|
||||||
|
ItemErrorRecord.from_metadata(raw_error)
|
||||||
if raw_error is not None
|
if raw_error is not None
|
||||||
else None,
|
else None
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_metadata(self) -> dict[str, Any]:
|
def to_metadata(self) -> dict[str, Any]:
|
||||||
@@ -175,12 +181,17 @@ class ForeachBarrierState:
|
|||||||
|
|
||||||
def save_to_frame(self, frame: ExecutionFrame, foreach_node_id: str) -> None:
|
def save_to_frame(self, frame: ExecutionFrame, foreach_node_id: str) -> None:
|
||||||
"""Store this barrier state in frame metadata under its foreach node id."""
|
"""Store this barrier state in frame metadata under its foreach node id."""
|
||||||
raw = frame.metadata.setdefault(_BARRIER_METADATA_KEY, {})
|
existing = frame.metadata.get(_BARRIER_METADATA_KEY)
|
||||||
if not isinstance(raw, dict):
|
if existing is None:
|
||||||
|
frame.metadata[_BARRIER_METADATA_KEY] = {
|
||||||
|
foreach_node_id: self.to_metadata()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
if not isinstance(existing, dict):
|
||||||
raise WorkflowExecutionError(
|
raise WorkflowExecutionError(
|
||||||
f"malformed foreach barrier table for frame {frame.id!r}"
|
f"malformed foreach barrier table for frame {frame.id!r}"
|
||||||
)
|
)
|
||||||
raw[foreach_node_id] = self.to_metadata()
|
existing[foreach_node_id] = self.to_metadata()
|
||||||
|
|
||||||
def to_metadata(self) -> dict[str, Any]:
|
def to_metadata(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -136,6 +136,10 @@ def block_frame_on_children(
|
|||||||
run: RunState, frame_id: str, child_frame_ids: Sequence[str]
|
run: RunState, frame_id: str, child_frame_ids: Sequence[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Mark a frame blocked on child completion and remove it from readiness."""
|
"""Mark a frame blocked on child completion and remove it from readiness."""
|
||||||
|
if not child_frame_ids:
|
||||||
|
raise WorkflowExecutionError(
|
||||||
|
f"cannot block frame {frame_id!r} on an empty child set"
|
||||||
|
)
|
||||||
frame = _frame(run, frame_id)
|
frame = _frame(run, frame_id)
|
||||||
run.ready_frame_ids = [item for item in run.ready_frame_ids if item != frame_id]
|
run.ready_frame_ids = [item for item in run.ready_frame_ids if item != frame_id]
|
||||||
frame.status = FrameStatus.BLOCKED
|
frame.status = FrameStatus.BLOCKED
|
||||||
|
|||||||
@@ -64,3 +64,43 @@ def test_foreach_barrier_state_rejects_malformed_metadata() -> None:
|
|||||||
|
|
||||||
with pytest.raises(WorkflowExecutionError, match="next_index"):
|
with pytest.raises(WorkflowExecutionError, match="next_index"):
|
||||||
ForeachBarrierState.from_frame(frame, "each")
|
ForeachBarrierState.from_frame(frame, "each")
|
||||||
|
|
||||||
|
|
||||||
|
def test_item_error_record_rejects_negative_index() -> None:
|
||||||
|
with pytest.raises(WorkflowExecutionError, match="index"):
|
||||||
|
ItemErrorRecord.from_metadata(
|
||||||
|
{
|
||||||
|
"index": -1,
|
||||||
|
"frame_id": "child",
|
||||||
|
"node_id": "work",
|
||||||
|
"error_type": "ValueError",
|
||||||
|
"message": "bad",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pending_item_result_reports_missing_required_field() -> None:
|
||||||
|
with pytest.raises(WorkflowExecutionError, match="missing 'frame_id'"):
|
||||||
|
PendingItemResult.from_metadata({"index": 0, "status": "succeeded"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_pending_item_result_rejects_negative_index() -> None:
|
||||||
|
with pytest.raises(WorkflowExecutionError, match="index"):
|
||||||
|
PendingItemResult.from_metadata(
|
||||||
|
{"index": -1, "frame_id": "child", "status": "succeeded"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_to_frame_rejects_corrupt_table_without_mutating() -> None:
|
||||||
|
frame = ExecutionFrame(
|
||||||
|
id="root",
|
||||||
|
kind="root",
|
||||||
|
node_id="each",
|
||||||
|
metadata={"foreach_barriers": "corrupt"},
|
||||||
|
)
|
||||||
|
barrier = ForeachBarrierState(next_index=1)
|
||||||
|
|
||||||
|
with pytest.raises(WorkflowExecutionError, match="barrier table"):
|
||||||
|
barrier.save_to_frame(frame, "each")
|
||||||
|
|
||||||
|
assert frame.metadata["foreach_barriers"] == "corrupt"
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ def test_blocked_frame_is_not_selectable_until_woken() -> None:
|
|||||||
assert selected.id == "parent"
|
assert selected.id == "parent"
|
||||||
|
|
||||||
|
|
||||||
|
def test_block_frame_on_children_rejects_empty_child_set() -> None:
|
||||||
|
run = _run()
|
||||||
|
add_frame(run, ExecutionFrame(id="parent", kind="root", node_id="foreach"))
|
||||||
|
|
||||||
|
with pytest.raises(WorkflowExecutionError, match="empty child set"):
|
||||||
|
block_frame_on_children(run, "parent", ())
|
||||||
|
|
||||||
|
|
||||||
def test_create_run_state_queues_root_frame() -> None:
|
def test_create_run_state_queues_root_frame() -> None:
|
||||||
workflow = Workflow(
|
workflow = Workflow(
|
||||||
name="demo",
|
name="demo",
|
||||||
|
|||||||
Reference in New Issue
Block a user