return value cleanup + raise on empty
This commit is contained in:
@@ -27,3 +27,6 @@ addopts = "-p no:cacheprovider"
|
|||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
package = true
|
package = true
|
||||||
|
|
||||||
|
[tool.basedpyright]
|
||||||
|
typeCheckingMode = "basic"
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
from .core import WorkflowBuilder
|
from .core import WorkflowBuilder
|
||||||
from .refs import BranchRef, RouteRef, StepRef
|
from .refs import BranchRef, BranchResult, DecisionResult, HandleResult, StepRef
|
||||||
|
|
||||||
__all__ = ["BranchRef", "RouteRef", "StepRef", "WorkflowBuilder"]
|
__all__ = [
|
||||||
|
"BranchRef",
|
||||||
|
"BranchResult",
|
||||||
|
"DecisionResult",
|
||||||
|
"HandleResult",
|
||||||
|
"StepRef",
|
||||||
|
"WorkflowBuilder",
|
||||||
|
]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from wf_core import (
|
|||||||
from wf_core.errors import WorkflowExecutionError
|
from wf_core.errors import WorkflowExecutionError
|
||||||
from wf_core.models.conditions import Condition as CoreCondition
|
from wf_core.models.conditions import Condition as CoreCondition
|
||||||
from wf_core.models.conditions import BinaryCondition, ExistsCondition, PathOperand
|
from wf_core.models.conditions import BinaryCondition, ExistsCondition, PathOperand
|
||||||
|
from wf_core.models.steps import Step
|
||||||
from wf_core.runtime.ops.merges import ReducerDefinition
|
from wf_core.runtime.ops.merges import ReducerDefinition
|
||||||
|
|
||||||
from ..dsl import Expr, PathArg, PathExpr, compile_condition
|
from ..dsl import Expr, PathArg, PathExpr, compile_condition
|
||||||
@@ -38,7 +39,14 @@ from .mapping import (
|
|||||||
coerce_path,
|
coerce_path,
|
||||||
normalize_mapping,
|
normalize_mapping,
|
||||||
)
|
)
|
||||||
from .refs import BranchRef, RouteRef, StepRef, is_node_spec, step_id
|
from .refs import (
|
||||||
|
BranchRef,
|
||||||
|
BranchResult,
|
||||||
|
DecisionResult,
|
||||||
|
HandleResult,
|
||||||
|
StepRef,
|
||||||
|
step_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _condition_base(condition: CoreCondition) -> str:
|
def _condition_base(condition: CoreCondition) -> str:
|
||||||
@@ -61,7 +69,7 @@ class WorkflowBuilder:
|
|||||||
start: str | None = None
|
start: str | None = None
|
||||||
reducers: ReducerCatalog | Mapping[str, ReducerDefinition] | None = None
|
reducers: ReducerCatalog | Mapping[str, ReducerDefinition] | None = None
|
||||||
node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict)
|
node_specs: dict[str, NodeSpec[Any, Any]] = field(default_factory=dict)
|
||||||
nodes: list[Any] = field(default_factory=list)
|
nodes: list[Step] = field(default_factory=list)
|
||||||
edges: list[Edge] = field(default_factory=list)
|
edges: list[Edge] = field(default_factory=list)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
@@ -134,7 +142,13 @@ class WorkflowBuilder:
|
|||||||
|
|
||||||
def _next_step_id(self, base: str) -> str:
|
def _next_step_id(self, base: str) -> str:
|
||||||
"""Return a stable unused step id based on the requested base name."""
|
"""Return a stable unused step id based on the requested base name."""
|
||||||
return next_step_id(base, cast(list[StepRef], self.nodes))
|
return next_step_id(base, self.nodes)
|
||||||
|
|
||||||
|
def _resolve_branch_ref(self, ref: BranchRef) -> StepRef:
|
||||||
|
"""Resolve a possibly callable-backed branch ref into one step ref."""
|
||||||
|
if isinstance(ref, NodeSpec):
|
||||||
|
return self.use(ref)
|
||||||
|
return ref
|
||||||
|
|
||||||
def set_entry_point(self, step: StepRef) -> None:
|
def set_entry_point(self, step: StepRef) -> None:
|
||||||
"""Set the workflow start node explicitly."""
|
"""Set the workflow start node explicitly."""
|
||||||
@@ -232,46 +246,59 @@ class WorkflowBuilder:
|
|||||||
to: BranchRef,
|
to: BranchRef,
|
||||||
) -> tuple[StepRef, StepRef]:
|
) -> tuple[StepRef, StepRef]:
|
||||||
"""Connect one outcome, auto-using NodeSpec endpoints as fresh node uses."""
|
"""Connect one outcome, auto-using NodeSpec endpoints as fresh node uses."""
|
||||||
source = self.use(from_) if is_node_spec(from_) else from_
|
source = self._resolve_branch_ref(from_)
|
||||||
target = self.use(to) if is_node_spec(to) else to
|
target = self._resolve_branch_ref(to)
|
||||||
self.edges.append(
|
self.edges.append(
|
||||||
Edge.model_validate(
|
Edge.model_validate(
|
||||||
{
|
{
|
||||||
"from": step_id(cast(StepRef, source)),
|
"from": step_id(source),
|
||||||
"outcome": outcome,
|
"outcome": outcome,
|
||||||
"to": step_id(cast(StepRef, target)),
|
"to": step_id(target),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return cast(StepRef, source), cast(StepRef, target)
|
return source, target
|
||||||
|
|
||||||
def branch(
|
def branch(
|
||||||
self,
|
self,
|
||||||
from_: BranchRef,
|
from_: BranchRef,
|
||||||
branches: Mapping[str, BranchRef],
|
branches: Mapping[str, BranchRef],
|
||||||
) -> dict[str, StepRef]:
|
) -> BranchResult:
|
||||||
"""Connect multiple outcomes from one branch source.
|
"""Connect multiple outcomes from one branch source.
|
||||||
|
|
||||||
Passing a NodeSpec creates a node use with auto-mapping and an auto id.
|
Passing a NodeSpec creates a node use with auto-mapping and an auto id.
|
||||||
Passing an existing step or id only wires edges. Empty branch maps are
|
Passing an existing step or id only wires edges.
|
||||||
allowed but warn because they usually indicate an unfinished router.
|
|
||||||
The returned mapping is keyed by branch outcome, not generated target id.
|
The returned mapping is keyed by branch outcome, not generated target id.
|
||||||
"""
|
"""
|
||||||
if not branches:
|
if not branches:
|
||||||
warnings.warn(
|
raise ValueError("WorkflowBuilder.branch requires at least one branch")
|
||||||
"WorkflowBuilder.branch called with no branches",
|
|
||||||
UserWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
source = self.use(from_) if is_node_spec(from_) else from_
|
source = self._resolve_branch_ref(from_)
|
||||||
resolved_targets: dict[str, StepRef] = {}
|
resolved_targets: dict[str, StepRef] = {}
|
||||||
for outcome, target in branches.items():
|
for outcome, target in branches.items():
|
||||||
resolved = self.use(target) if is_node_spec(target) else target
|
resolved = self._resolve_branch_ref(target)
|
||||||
self.connect(cast(StepRef, source), outcome, cast(StepRef, resolved))
|
self.connect(source, outcome, resolved)
|
||||||
resolved_targets[outcome] = cast(StepRef, resolved)
|
resolved_targets[outcome] = resolved
|
||||||
return resolved_targets
|
return BranchResult(source=source, targets=resolved_targets)
|
||||||
|
|
||||||
|
def handle(
|
||||||
|
self,
|
||||||
|
*branches: tuple[BranchRef, str],
|
||||||
|
to: BranchRef,
|
||||||
|
) -> HandleResult:
|
||||||
|
"""Connect several source outcomes to one shared target."""
|
||||||
|
if not branches:
|
||||||
|
raise ValueError("WorkflowBuilder.handle requires at least one branch")
|
||||||
|
resolved_branches: list[tuple[StepRef, str]] = []
|
||||||
|
target = self._resolve_branch_ref(to)
|
||||||
|
for branch, outcome in branches:
|
||||||
|
resolved = self._resolve_branch_ref(branch)
|
||||||
|
self.connect(resolved, outcome, target)
|
||||||
|
resolved_branches.append((resolved, outcome))
|
||||||
|
return HandleResult(
|
||||||
|
target=target,
|
||||||
|
branches=tuple(resolved_branches),
|
||||||
|
)
|
||||||
|
|
||||||
def match(
|
def match(
|
||||||
self,
|
self,
|
||||||
@@ -280,11 +307,20 @@ class WorkflowBuilder:
|
|||||||
*,
|
*,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
default: BranchRef = runtime_error,
|
default: BranchRef = runtime_error,
|
||||||
) -> RouteRef:
|
) -> DecisionResult:
|
||||||
"""Match one graph value against ordered equality cases."""
|
"""Match one graph value against ordered equality cases.
|
||||||
|
|
||||||
|
DecisionResult.targets is keyed by case value, not generated target id.
|
||||||
|
The default case is always available under the "default" key. Case
|
||||||
|
values must be hashable and are compared using equality against the
|
||||||
|
graph value at runtime, so they should be primitives or tuples of
|
||||||
|
primitives for predictable behavior.
|
||||||
|
"""
|
||||||
|
if not cases:
|
||||||
|
raise ValueError("WorkflowBuilder.match requires at least one case")
|
||||||
resolved_targets: dict[object, StepRef] = {}
|
resolved_targets: dict[object, StepRef] = {}
|
||||||
conditions: list[ConditionNode] = []
|
conditions: list[ConditionNode] = []
|
||||||
default_target = self.use(default) if is_node_spec(default) else default
|
default_target = self._resolve_branch_ref(default)
|
||||||
previous_condition: ConditionNode | None = None
|
previous_condition: ConditionNode | None = None
|
||||||
condition_base = id or slug_id(value.path)
|
condition_base = id or slug_id(value.path)
|
||||||
for case_value, target in cases.items():
|
for case_value, target in cases.items():
|
||||||
@@ -293,17 +329,16 @@ class WorkflowBuilder:
|
|||||||
check=value.eq(case_value),
|
check=value.eq(case_value),
|
||||||
)
|
)
|
||||||
conditions.append(condition)
|
conditions.append(condition)
|
||||||
resolved = self.use(target) if is_node_spec(target) else target
|
resolved = self._resolve_branch_ref(target)
|
||||||
if previous_condition is not None:
|
if previous_condition is not None:
|
||||||
self.connect(previous_condition, "false", condition)
|
self.connect(previous_condition, "false", condition)
|
||||||
self.connect(condition, "true", cast(StepRef, resolved))
|
self.connect(condition, "true", resolved)
|
||||||
previous_condition = condition
|
previous_condition = condition
|
||||||
resolved_targets[case_value] = cast(StepRef, resolved)
|
resolved_targets[case_value] = resolved
|
||||||
if previous_condition is None:
|
assert previous_condition is not None # guarded by cases check above
|
||||||
raise ValueError("WorkflowBuilder.match requires at least one case")
|
self.connect(previous_condition, "false", default_target)
|
||||||
self.connect(previous_condition, "false", cast(StepRef, default_target))
|
resolved_targets["default"] = default_target
|
||||||
resolved_targets["default"] = cast(StepRef, default_target)
|
return DecisionResult(
|
||||||
return RouteRef(
|
|
||||||
entry=conditions[0],
|
entry=conditions[0],
|
||||||
conditions=tuple(conditions),
|
conditions=tuple(conditions),
|
||||||
targets=resolved_targets,
|
targets=resolved_targets,
|
||||||
@@ -316,21 +351,19 @@ class WorkflowBuilder:
|
|||||||
then: BranchRef,
|
then: BranchRef,
|
||||||
otherwise: BranchRef = runtime_error,
|
otherwise: BranchRef = runtime_error,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
) -> RouteRef:
|
) -> DecisionResult:
|
||||||
"""Route one boolean condition through true and false targets."""
|
"""Route one boolean condition through true and false targets."""
|
||||||
condition_node = self.condition(id=id, check=condition)
|
condition_node = self.condition(id=id, check=condition)
|
||||||
resolved_then = self.use(then) if is_node_spec(then) else then
|
resolved_then = self._resolve_branch_ref(then)
|
||||||
resolved_otherwise = (
|
resolved_otherwise = self._resolve_branch_ref(otherwise)
|
||||||
self.use(otherwise) if is_node_spec(otherwise) else otherwise
|
self.connect(condition_node, "true", resolved_then)
|
||||||
)
|
self.connect(condition_node, "false", resolved_otherwise)
|
||||||
self.connect(condition_node, "true", cast(StepRef, resolved_then))
|
return DecisionResult(
|
||||||
self.connect(condition_node, "false", cast(StepRef, resolved_otherwise))
|
|
||||||
return RouteRef(
|
|
||||||
entry=condition_node,
|
entry=condition_node,
|
||||||
conditions=(condition_node,),
|
conditions=(condition_node,),
|
||||||
targets={
|
targets={
|
||||||
True: cast(StepRef, resolved_then),
|
True: resolved_then,
|
||||||
False: cast(StepRef, resolved_otherwise),
|
False: resolved_otherwise,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -339,7 +372,7 @@ class WorkflowBuilder:
|
|||||||
*clauses: tuple[Expr, BranchRef],
|
*clauses: tuple[Expr, BranchRef],
|
||||||
default: BranchRef = runtime_error,
|
default: BranchRef = runtime_error,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
) -> RouteRef:
|
) -> DecisionResult:
|
||||||
"""Route to the first target whose ordered condition is true."""
|
"""Route to the first target whose ordered condition is true."""
|
||||||
if not clauses:
|
if not clauses:
|
||||||
raise ValueError("WorkflowBuilder.choose requires at least one clause")
|
raise ValueError("WorkflowBuilder.choose requires at least one clause")
|
||||||
@@ -354,16 +387,17 @@ class WorkflowBuilder:
|
|||||||
check=condition_expr,
|
check=condition_expr,
|
||||||
)
|
)
|
||||||
conditions.append(condition)
|
conditions.append(condition)
|
||||||
resolved = self.use(target) if is_node_spec(target) else target
|
resolved = self._resolve_branch_ref(target)
|
||||||
if previous_condition is not None:
|
if previous_condition is not None:
|
||||||
self.connect(previous_condition, "false", condition)
|
self.connect(previous_condition, "false", condition)
|
||||||
self.connect(condition, "true", cast(StepRef, resolved))
|
self.connect(condition, "true", resolved)
|
||||||
previous_condition = condition
|
previous_condition = condition
|
||||||
resolved_targets[index] = cast(StepRef, resolved)
|
resolved_targets[index] = resolved
|
||||||
default_target = self.use(default) if is_node_spec(default) else default
|
default_target = self._resolve_branch_ref(default)
|
||||||
self.connect(cast(ConditionNode, previous_condition), "false", cast(StepRef, default_target))
|
assert previous_condition is not None # guarded by clauses check above
|
||||||
resolved_targets["default"] = cast(StepRef, default_target)
|
self.connect(previous_condition, "false", default_target)
|
||||||
return RouteRef(
|
resolved_targets["default"] = default_target
|
||||||
|
return DecisionResult(
|
||||||
entry=conditions[0],
|
entry=conditions[0],
|
||||||
conditions=tuple(conditions),
|
conditions=tuple(conditions),
|
||||||
targets=resolved_targets,
|
targets=resolved_targets,
|
||||||
@@ -377,7 +411,7 @@ class WorkflowBuilder:
|
|||||||
*,
|
*,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
default: BranchRef = runtime_error,
|
default: BranchRef = runtime_error,
|
||||||
) -> RouteRef:
|
) -> DecisionResult:
|
||||||
"""Deprecated compatibility shim for the old overloaded route API."""
|
"""Deprecated compatibility shim for the old overloaded route API."""
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"WorkflowBuilder.route is deprecated; use match(...) or when(...)",
|
"WorkflowBuilder.route is deprecated; use match(...) or when(...)",
|
||||||
@@ -385,9 +419,11 @@ class WorkflowBuilder:
|
|||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
if isinstance(value, Expr):
|
if isinstance(value, Expr):
|
||||||
invalid_cases = [case for case in cases if not isinstance(case, bool)]
|
invalid_cases = any(not isinstance(case, bool) for case in cases)
|
||||||
if invalid_cases:
|
if invalid_cases:
|
||||||
raise ValueError("condition route cases must be boolean True/False keys")
|
raise ValueError(
|
||||||
|
"condition route cases must be boolean True/False keys"
|
||||||
|
)
|
||||||
if not cases:
|
if not cases:
|
||||||
raise ValueError("WorkflowBuilder.route requires at least one case")
|
raise ValueError("WorkflowBuilder.route requires at least one case")
|
||||||
return self.when(
|
return self.when(
|
||||||
@@ -401,8 +437,8 @@ class WorkflowBuilder:
|
|||||||
def compile(self) -> Workflow:
|
def compile(self) -> Workflow:
|
||||||
if self.start is None:
|
if self.start is None:
|
||||||
raise WorkflowExecutionError(
|
raise WorkflowExecutionError(
|
||||||
"workflow builder requires an explicit start; call set_entry_point(...) "
|
"workflow builder requires an explicit start; "
|
||||||
"or pass start=..."
|
"call set_entry_point(...) or pass start=..."
|
||||||
)
|
)
|
||||||
node_defs = [spec.to_node_def() for spec in self.node_specs.values()]
|
node_defs = [spec.to_node_def() for spec in self.node_specs.values()]
|
||||||
return Workflow(
|
return Workflow(
|
||||||
|
|||||||
@@ -10,19 +10,43 @@ from ..nodes import NodeSpec
|
|||||||
StepRef: TypeAlias = (
|
StepRef: TypeAlias = (
|
||||||
str | NodeUse | ConditionNode | ForeachNode | InterruptNode | JoinNode
|
str | NodeUse | ConditionNode | ForeachNode | InterruptNode | JoinNode
|
||||||
)
|
)
|
||||||
|
"""A reference to a step, which can be either a string id or a node object
|
||||||
|
that should be auto-used."""
|
||||||
BranchRef: TypeAlias = StepRef | NodeSpec[Any, Any]
|
BranchRef: TypeAlias = StepRef | NodeSpec[Any, Any]
|
||||||
|
"""A reference to a branch source or target, which can be either a step ref
|
||||||
|
or a NodeSpec that should be auto-used."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class RouteRef:
|
class BranchResult:
|
||||||
"""Reference bundle returned by control-flow helpers with generated conditions."""
|
"""Resolved branch source plus outcome-indexed targets."""
|
||||||
|
|
||||||
|
source: StepRef
|
||||||
|
targets: dict[str, StepRef]
|
||||||
|
|
||||||
|
def __getitem__(self, outcome: str) -> StepRef:
|
||||||
|
"""Keep branch outcome lookup ergonomic while exposing the source."""
|
||||||
|
return self.targets[outcome]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class HandleResult:
|
||||||
|
"""Resolved shared target plus source/outcome pairs that feed it."""
|
||||||
|
|
||||||
|
target: StepRef
|
||||||
|
branches: tuple[tuple[StepRef, str], ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DecisionResult:
|
||||||
|
"""Generated condition entry plus resolved targets for one decision helper."""
|
||||||
|
|
||||||
entry: ConditionNode
|
entry: ConditionNode
|
||||||
conditions: tuple[ConditionNode, ...]
|
conditions: tuple[ConditionNode, ...]
|
||||||
targets: dict[object, StepRef]
|
targets: dict[object, StepRef]
|
||||||
|
|
||||||
def __getitem__(self, key: object) -> StepRef:
|
def __getitem__(self, key: object) -> StepRef:
|
||||||
"""Keep helper result lookup ergonomic while exposing generated conditions."""
|
"""Keep decision result lookup ergonomic while exposing conditions."""
|
||||||
return self.targets[key]
|
return self.targets[key]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ def test_builder_branch_can_use_node_specs_as_targets() -> None:
|
|||||||
)
|
)
|
||||||
router = builder.use(branch_router)
|
router = builder.use(branch_router)
|
||||||
|
|
||||||
targets = builder.branch(router, {"left": auto_bind_node})
|
result = builder.branch(router, {"left": auto_bind_node})
|
||||||
|
assert result.source == router
|
||||||
target = targets["left"]
|
target = result["left"]
|
||||||
assert not isinstance(target, str)
|
assert not isinstance(target, str)
|
||||||
assert target.id == "test_auto_bind"
|
assert target.id == "test_auto_bind"
|
||||||
assert builder.edges[0].from_ == "test_branch_router"
|
assert builder.edges[0].from_ == "test_branch_router"
|
||||||
@@ -51,19 +51,53 @@ def test_builder_branch_can_use_node_specs_as_targets() -> None:
|
|||||||
assert builder.edges[0].to == "test_auto_bind"
|
assert builder.edges[0].to == "test_auto_bind"
|
||||||
|
|
||||||
|
|
||||||
def test_builder_branch_warns_on_empty_branch_map() -> None:
|
def test_builder_branch_rejects_empty_branch_map_without_using_source() -> None:
|
||||||
builder = WorkflowBuilder(
|
builder = WorkflowBuilder(
|
||||||
name="branch_empty_demo",
|
name="branch_empty_demo",
|
||||||
input_schema=AutoBindInput,
|
input_schema=AutoBindInput,
|
||||||
state_schema=AutoBindState,
|
state_schema=AutoBindState,
|
||||||
output_schema=AutoBindOutput,
|
output_schema=AutoBindOutput,
|
||||||
)
|
)
|
||||||
router = builder.use(branch_router)
|
|
||||||
|
|
||||||
with pytest.warns(UserWarning, match="no branches"):
|
with pytest.raises(ValueError, match="at least one branch"):
|
||||||
targets = builder.branch(router, {})
|
builder.branch(branch_router, {})
|
||||||
|
|
||||||
assert targets == {}
|
assert builder.nodes == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_builder_handle_connects_shared_target_for_duplicate_outcomes() -> None:
|
||||||
|
builder = WorkflowBuilder(
|
||||||
|
name="handle_demo",
|
||||||
|
input_schema=AutoBindInput,
|
||||||
|
state_schema=AutoBindState,
|
||||||
|
output_schema=AutoBindOutput,
|
||||||
|
)
|
||||||
|
left = builder.use(auto_bind_node, id="left")
|
||||||
|
right = builder.use(auto_bind_node, id="right")
|
||||||
|
fallback = builder.use(auto_bind_node, id="fallback")
|
||||||
|
|
||||||
|
result = builder.handle((left, "error"), (right, "error"), to=fallback)
|
||||||
|
assert result is not None
|
||||||
|
assert result.target is fallback
|
||||||
|
assert [(edge.from_, edge.outcome, edge.to) for edge in builder.edges] == [
|
||||||
|
("left", "error", "fallback"),
|
||||||
|
("right", "error", "fallback"),
|
||||||
|
]
|
||||||
|
assert result.branches == ((left, "error"), (right, "error"))
|
||||||
|
|
||||||
|
|
||||||
|
def test_builder_handle_rejects_empty_sources_without_using_target() -> None:
|
||||||
|
builder = WorkflowBuilder(
|
||||||
|
name="handle_empty_demo",
|
||||||
|
input_schema=AutoBindInput,
|
||||||
|
state_schema=AutoBindState,
|
||||||
|
output_schema=AutoBindOutput,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="at least one branch"):
|
||||||
|
builder.handle(to=auto_bind_node)
|
||||||
|
|
||||||
|
assert builder.nodes == []
|
||||||
|
|
||||||
|
|
||||||
def test_builder_match_expands_state_value_cases_into_condition_chain() -> None:
|
def test_builder_match_expands_state_value_cases_into_condition_chain() -> None:
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ def test_workflow_surface_validates_draft_without_saving() -> None:
|
|||||||
assert payload["status"] == "valid"
|
assert payload["status"] == "valid"
|
||||||
assert payload["diagnostics"] == []
|
assert payload["diagnostics"] == []
|
||||||
assert payload["compiled_plan"]["nodes"][0]["type"] == "node"
|
assert payload["compiled_plan"]["nodes"][0]["type"] == "node"
|
||||||
assert artifact_store.list_artifacts() == []
|
assert not artifact_store.list_artifacts()
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_rejects_unknown_draft_route_outcome_when_spec_is_known() -> (
|
def test_workflow_surface_rejects_unknown_draft_route_outcome_when_spec_is_known() -> (
|
||||||
@@ -340,7 +340,7 @@ def test_workflow_surface_patches_draft_without_saving() -> None:
|
|||||||
|
|
||||||
assert payload["status"] == "valid"
|
assert payload["status"] == "valid"
|
||||||
assert payload["draft"]["steps"]["echo"]["in"]["input.text"] == "message"
|
assert payload["draft"]["steps"]["echo"]["in"]["input.text"] == "message"
|
||||||
assert artifact_store.list_artifacts() == []
|
assert not artifact_store.list_artifacts()
|
||||||
|
|
||||||
|
|
||||||
def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
|
def test_raw_workflow_plan_uses_core_step_and_edge_models() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user