state write

This commit is contained in:
lda
2026-05-24 22:08:00 +07:00 Verified
parent 2dc00518b9
commit 39b473ea25
9 changed files with 1889 additions and 17 deletions
+66
View File
@@ -16,6 +16,8 @@ from wf_core import (
Workflow,
WorkflowExecutionError,
)
from wf_core.paths import StatePath
from wf_core.run_state import StateWrite
from wf_core.models.steps import OutputBinding
from wf_core.runtime.engine import resume_workflow
from wf_core.runtime.ops.merges import ReducerDefinition
@@ -200,6 +202,70 @@ def test_build_output_patch_does_not_mutate_until_commit() -> None:
assert state["person"]["name"] == "Ada"
def test_output_patch_records_incoming_and_visible_values() -> None:
workflow = _workflow(
fields={
"count": StateField(
type="integer",
reducer=ReducerRef(name="wf.std.add"),
)
}
)
state = {"count": 2}
patch = build_output_patch(
workflow,
[_binding("delta", "state.count")],
{"delta": 3},
state,
)
assert patch.changes["state.count"] == 3
assert patch.visible_values["state.count"] == 5
assert patch.writes[0].incoming_value == 3
assert patch.writes[0].visible_value == 5
def test_barrier_replays_incoming_values_not_lineage_visible_values() -> None:
workflow = _workflow(
fields={
"number": StateField(
type="integer",
reducer=ReducerRef(name="wf.std.add"),
)
}
)
patch = build_barrier_patch(
workflow,
[
StatePatch(
writes=[
StateWrite(
path=StatePath(("number",)),
incoming_value=3,
visible_value=5,
reducer=ReducerRef(name="wf.std.add"),
)
]
),
StatePatch(
writes=[
StateWrite(
path=StatePath(("number",)),
incoming_value=1,
visible_value=3,
reducer=ReducerRef(name="wf.std.add"),
)
]
),
],
{"number": 2},
)
assert patch.changes["state.number"] == 6
assert patch.visible_values["state.number"] == 6
def test_build_and_commit_patch_matches_apply_output_bindings() -> None:
workflow = _workflow(fields={"person.name": StateField(type="string")})
state_from_apply = {"person": {"name": "old"}}
+237
View File
@@ -170,6 +170,63 @@ def test_sync_concurrent_foreach_sibling_overlays_do_not_leak() -> None:
assert run.state["seen"] == ["a", "b"]
def test_sync_concurrent_foreach_barrier_replays_add_reducer_inputs() -> None:
workflow = _sum_items_workflow()
run = execute_workflow(
workflow,
{"items": [3, 1]},
{
"add_item": lambda payload, _ctx: {
"outcome": "ok",
"output": {"number": payload["value"]},
}
},
)
assert run.state["number"] == 6
assert run.output["number"] == 6
foreach_entries = [entry for entry in run.trace if entry.step_type == "foreach"]
assert foreach_entries[-1].state_changes["state.number"] == 6
@pytest.mark.xfail(
reason=(
"Current foreach overlays use StatePatch.changes, which stores incoming "
"reducer values; lineage StateWrite.visible_value should make this pass."
),
strict=True,
)
def test_sync_concurrent_foreach_same_item_reads_add_reducer_visible_value() -> None:
workflow = _same_item_reducer_visibility_workflow()
run = execute_workflow(
workflow,
{"items": [3]},
{
"add_item": lambda payload, _ctx: {
"outcome": "ok",
"output": {"number": payload["value"]},
},
"read_number": lambda payload, _ctx: {
"outcome": "ok",
"output": {"seen_number": payload["number"]},
},
},
)
stage_entry = next(entry for entry in run.trace if entry.node_id == "add_item")
assert stage_entry.resolved_input["value"] == 3
assert stage_entry.resolved_input["current_number"] == 2
assert stage_entry.state_changes == {}
read_entry = next(entry for entry in run.trace if entry.node_id == "read_number")
# Current limitation: foreach overlays use StatePatch.changes, which stores
# the incoming reducer value. The future lineage StateWrite model should let
# this same item read the reducer-visible value 5 instead.
assert read_entry.resolved_input["number"] == 5
assert run.state["seen_number"] == [5]
def test_sync_concurrent_foreach_rejects_sibling_replace_writes() -> None:
workflow = _same_path_replace_workflow()
@@ -186,6 +243,186 @@ def test_sync_concurrent_foreach_rejects_sibling_replace_writes() -> None:
)
def _sum_items_workflow() -> Workflow:
foreach = ForeachNode.model_validate(
{
"id": "each",
"type": "foreach",
"over": "state.items",
"as": "item",
"mode": "concurrent",
"concurrent": {"max_active": 2, "max_outstanding": 2},
}
)
return Workflow(
name="concurrent_foreach_sum",
input_schema=SchemaRef(
type="object",
properties={"items": {"type": "array"}},
),
state_schema=StateSchema.from_field_map(
{
"items": StateField(type="array"),
"number": StateField(
type="integer",
default=2,
reducer=ReducerRef(name="wf.std.add"),
),
}
),
output_schema=SchemaRef(
type="object",
properties={"number": {"type": "integer"}},
),
node_defs=[
NodeDef(
name="add_item",
input_schema=SchemaRef(
type="object",
properties={
"value": {"type": "integer"},
"current_number": {"type": "integer"},
},
required=["value", "current_number"],
),
output_schema=SchemaRef(
type="object",
properties={"number": {"type": "integer"}},
required=["number"],
),
outcomes=["ok"],
)
],
start="each",
nodes=[
foreach,
NodeUse.model_validate(
{
"id": "add_item",
"type": "node",
"node": "add_item",
"input": [
{"target": "value", "path": "context.item"},
{"target": "current_number", "path": "state.number"},
],
"output": [{"source": "number", "target": "state.number"}],
}
),
],
edges=[
Edge.model_validate({"from": "each", "outcome": "loop", "to": "add_item"}),
Edge.model_validate({"from": "add_item", "outcome": "ok", "to": END}),
Edge.model_validate({"from": "each", "outcome": "done", "to": END}),
],
)
def _same_item_reducer_visibility_workflow() -> Workflow:
foreach = ForeachNode.model_validate(
{
"id": "each",
"type": "foreach",
"over": "state.items",
"as": "item",
"mode": "concurrent",
"concurrent": {"max_active": 1, "max_outstanding": 1},
}
)
return Workflow(
name="concurrent_foreach_same_item_reducer_visibility",
input_schema=SchemaRef(
type="object",
properties={"items": {"type": "array"}},
),
state_schema=StateSchema.from_field_map(
{
"items": StateField(type="array"),
"number": StateField(
type="integer",
default=2,
reducer=ReducerRef(name="wf.std.add"),
),
"seen_number": StateField(
type="array",
reducer=ReducerRef(name="wf.std.append"),
),
}
),
output_schema=SchemaRef(
type="object",
properties={"seen_number": {"type": "array"}},
),
node_defs=[
NodeDef(
name="add_item",
input_schema=SchemaRef(
type="object",
properties={
"value": {"type": "integer"},
"current_number": {"type": "integer"},
},
required=["value", "current_number"],
),
output_schema=SchemaRef(
type="object",
properties={"number": {"type": "integer"}},
required=["number"],
),
outcomes=["ok"],
),
NodeDef(
name="read_number",
input_schema=SchemaRef(
type="object",
properties={"number": {"type": "integer"}},
required=["number"],
),
output_schema=SchemaRef(
type="object",
properties={"seen_number": {"type": "integer"}},
required=["seen_number"],
),
outcomes=["ok"],
),
],
start="each",
nodes=[
foreach,
NodeUse.model_validate(
{
"id": "add_item",
"type": "node",
"node": "add_item",
"input": [
{"target": "value", "path": "context.item"},
{"target": "current_number", "path": "state.number"},
],
"output": [{"source": "number", "target": "state.number"}],
}
),
NodeUse.model_validate(
{
"id": "read_number",
"type": "node",
"node": "read_number",
"input": [{"target": "number", "path": "state.number"}],
"output": [
{"source": "seen_number", "target": "state.seen_number"}
],
}
),
],
edges=[
Edge.model_validate({"from": "each", "outcome": "loop", "to": "add_item"}),
Edge.model_validate(
{"from": "add_item", "outcome": "ok", "to": "read_number"}
),
Edge.model_validate({"from": "read_number", "outcome": "ok", "to": END}),
Edge.model_validate({"from": "each", "outcome": "done", "to": END}),
],
)
def _workflow(
*,
state_schema: StateSchema,