209 lines
6.0 KiB
Python
209 lines
6.0 KiB
Python
from __future__ import annotations
|
|
|
|
from wf_core import (
|
|
ReducerRef,
|
|
ReducerSpec,
|
|
SchemaRef,
|
|
StateField,
|
|
StateSchema,
|
|
Workflow,
|
|
)
|
|
from wf_core.runtime.ops.merges import ReducerDefinition, apply_reducer
|
|
from wf_core.runtime.ops.state import write_state_value
|
|
|
|
|
|
def test_exact_nested_state_path_uses_declared_reducer() -> None:
|
|
workflow = _workflow(
|
|
fields={
|
|
"person.tags": StateField(
|
|
reducer=ReducerRef(name="wf.std.append"), type="array"
|
|
)
|
|
}
|
|
)
|
|
state = {"person": {"tags": ["seed"]}}
|
|
|
|
write_state_value(workflow, state, "state.person.tags", ["next"])
|
|
|
|
assert state["person"]["tags"] == ["seed", "next"]
|
|
|
|
|
|
def test_parent_state_declaration_does_not_apply_to_nested_write() -> None:
|
|
workflow = _workflow(
|
|
fields={
|
|
"person": StateField(
|
|
reducer=ReducerRef(name="wf.std.merge_object"), type="object"
|
|
)
|
|
}
|
|
)
|
|
state = {"person": {"tags": ["seed"]}}
|
|
|
|
write_state_value(workflow, state, "state.person.tags", ["next"])
|
|
|
|
assert state["person"]["tags"] == ["next"]
|
|
|
|
|
|
def test_undeclared_nested_state_path_defaults_to_replace() -> None:
|
|
workflow = _workflow(fields={})
|
|
state = {"person": {"tags": ["seed"]}}
|
|
|
|
write_state_value(workflow, state, "state.person.tags", ["next"])
|
|
|
|
assert state["person"]["tags"] == ["next"]
|
|
|
|
|
|
def test_state_field_defaults_to_replace_reducer() -> None:
|
|
assert StateField(type="string").reducer == ReducerRef(name="wf.std.replace")
|
|
|
|
|
|
def test_state_field_accepts_string_reducer_shorthand() -> None:
|
|
field = StateField.model_validate({"type": "array", "reducer": "wf.std.set_union"})
|
|
|
|
assert field.reducer == ReducerRef(name="wf.std.set_union")
|
|
|
|
|
|
def test_state_field_accepts_configured_reducer_reference() -> None:
|
|
field = StateField(
|
|
type="integer",
|
|
reducer=ReducerRef(name="wf.std.max", config={"sample": True}),
|
|
)
|
|
|
|
# PYLINT!!!! what is u on ts is so clear
|
|
assert field.reducer.name == "wf.std.max" # pylint: disable=no-member
|
|
assert field.reducer.config == {"sample": True} # pylint: disable=no-member
|
|
|
|
|
|
def test_unknown_state_reducer_fails_clearly() -> None:
|
|
workflow = _workflow(
|
|
fields={
|
|
"person.tags": StateField(reducer=ReducerRef(name="x.nope"), type="array")
|
|
}
|
|
)
|
|
state = {"person": {"tags": ["seed"]}}
|
|
|
|
try:
|
|
write_state_value(workflow, state, "state.person.tags", ["next"])
|
|
except Exception as exc:
|
|
assert "unknown reducer 'x.nope'" in str(exc)
|
|
else:
|
|
raise AssertionError("expected unknown reducer to fail")
|
|
|
|
|
|
def test_set_union_reducer_preserves_first_seen_order() -> None:
|
|
workflow = _workflow(
|
|
fields={
|
|
"person.tags": StateField(
|
|
reducer=ReducerRef(name="wf.std.set_union"), type="array"
|
|
)
|
|
}
|
|
)
|
|
state = {"person": {"tags": ["alpha", "beta"]}}
|
|
|
|
write_state_value(workflow, state, "state.person.tags", ["beta", "gamma"])
|
|
|
|
assert state["person"]["tags"] == ["alpha", "beta", "gamma"]
|
|
|
|
|
|
def test_unexpected_reducer_config_fails_before_state_mutation() -> None:
|
|
workflow = _workflow(
|
|
fields={
|
|
"person.tags": StateField(
|
|
type="array",
|
|
reducer=ReducerRef(name="wf.std.set_union", config={"bad": True}),
|
|
)
|
|
}
|
|
)
|
|
state = {"person": {"tags": ["alpha"]}}
|
|
|
|
try:
|
|
write_state_value(workflow, state, "state.person.tags", ["beta"])
|
|
except Exception as exc:
|
|
assert "reducer config for 'wf.std.set_union'" in str(exc)
|
|
else:
|
|
raise AssertionError("expected invalid reducer config to fail")
|
|
assert state["person"]["tags"] == ["alpha"]
|
|
|
|
|
|
def test_max_reducer_keeps_larger_value() -> None:
|
|
workflow = _workflow(
|
|
fields={
|
|
"best_score": StateField(
|
|
reducer=ReducerRef(name="wf.std.max"), type="integer"
|
|
)
|
|
}
|
|
)
|
|
state = {"best_score": 7}
|
|
|
|
write_state_value(workflow, state, "state.best_score", 9)
|
|
|
|
assert state["best_score"] == 9
|
|
|
|
|
|
def test_add_reducer_sums_numeric_values() -> None:
|
|
workflow = _workflow(
|
|
fields={
|
|
"count": StateField(reducer=ReducerRef(name="wf.std.add"), type="integer")
|
|
}
|
|
)
|
|
state = {"count": 7}
|
|
|
|
write_state_value(workflow, state, "state.count", 5)
|
|
|
|
assert state["count"] == 12
|
|
|
|
|
|
def test_reducer_definition_can_wrap_plain_two_arg_callable() -> None:
|
|
definition = ReducerDefinition(
|
|
spec=ReducerSpec(name="test.add"),
|
|
fn=lambda current, incoming: (current or 0) + incoming,
|
|
)
|
|
|
|
result = apply_reducer(
|
|
reducer=ReducerRef(name="test.add"),
|
|
current_value=2,
|
|
incoming_value=3,
|
|
destination_path="state.total",
|
|
reducers={"test.add": definition},
|
|
)
|
|
|
|
assert result == 5
|
|
|
|
|
|
def test_reducer_definition_can_wrap_config_aware_callable() -> None:
|
|
definition = ReducerDefinition(
|
|
spec=ReducerSpec(
|
|
name="test.modulo_add",
|
|
config_schema={
|
|
"type": "object",
|
|
"properties": {"modulus": {"type": "integer"}},
|
|
"required": ["modulus"],
|
|
"additionalProperties": False,
|
|
},
|
|
),
|
|
fn=lambda current, incoming, config: ((current or 0) + incoming)
|
|
% config["modulus"],
|
|
accepts_config=True,
|
|
)
|
|
|
|
result = apply_reducer(
|
|
reducer=ReducerRef(name="test.modulo_add", config={"modulus": 10}),
|
|
current_value=8,
|
|
incoming_value=5,
|
|
destination_path="state.total",
|
|
reducers={"test.modulo_add": definition},
|
|
)
|
|
|
|
assert result == 3
|
|
|
|
|
|
def _workflow(*, fields: dict[str, StateField]) -> Workflow:
|
|
return Workflow(
|
|
name="nested_state_paths",
|
|
input_schema=SchemaRef(type="object", properties={}),
|
|
state_schema=StateSchema(fields=fields),
|
|
output_schema=SchemaRef(type="object", properties={}),
|
|
node_defs=[],
|
|
start="unused",
|
|
nodes=[],
|
|
edges=[],
|
|
)
|