Download tests/test_routing.py from FlyingNunchucks/09-agentic-workflow-sys: direct link, hf CLI and curl.
- Browser
- Download file 12.6 kB
-
https://huggingface.co/spaces/FlyingNunchucks/09-agentic-workflow-sys/resolve/main/tests/test_routing.py
- Command line
-
hf download hf://spaces/FlyingNunchucks/09-agentic-workflow-sys/tests/test_routing.py
-
curl -L -o test_routing.py https://huggingface.co/spaces/FlyingNunchucks/09-agentic-workflow-sys/resolve/main/tests/test_routing.py
12.6 kB
| import pytest | |
| from pydantic import ValidationError | |
| from src.executor import WorkflowExecutionError, execute_workflow, submit_human_decision | |
| from src.persistence import InMemoryStateStore | |
| from src.registry import HandlerRegistry | |
| from src.schemas import ( | |
| EventType, | |
| HumanDecision, | |
| NodeDefinition, | |
| NodeStatus, | |
| NodeType, | |
| WorkflowDefinition, | |
| WorkflowStatus, | |
| ) | |
| from src.validator import WorkflowValidationError, validate_workflow | |
| def make_risk_workflow() -> WorkflowDefinition: | |
| return WorkflowDefinition( | |
| workflow_id="risk-routing-workflow", | |
| name="Risk Routing Workflow", | |
| nodes=[ | |
| NodeDefinition( | |
| node_id="intake", | |
| name="Intake", | |
| node_type=NodeType.TASK, | |
| handler="intake_handler", | |
| ), | |
| NodeDefinition( | |
| node_id="risk_gate", | |
| name="Risk Gate", | |
| node_type=NodeType.DECISION, | |
| handler="risk_handler", | |
| depends_on=["intake"], | |
| routes={ | |
| "LOW_RISK": "low_finalize", | |
| "HIGH_RISK": "human_review", | |
| }, | |
| ), | |
| NodeDefinition( | |
| node_id="low_finalize", | |
| name="Low Risk Finalize", | |
| node_type=NodeType.TASK, | |
| handler="low_finalize_handler", | |
| depends_on=["risk_gate"], | |
| ), | |
| NodeDefinition( | |
| node_id="human_review", | |
| name="Human Review", | |
| node_type=NodeType.HUMAN_GATE, | |
| depends_on=["risk_gate"], | |
| config={"reason": "High-risk request requires approval."}, | |
| ), | |
| NodeDefinition( | |
| node_id="high_finalize", | |
| name="High Risk Finalize", | |
| node_type=NodeType.TASK, | |
| handler="high_finalize_handler", | |
| depends_on=["human_review"], | |
| ), | |
| ], | |
| ) | |
| def build_registry(route: str, counters: dict[str, int]) -> HandlerRegistry: | |
| registry = HandlerRegistry() | |
| def intake(payload: dict) -> dict: | |
| counters["intake"] += 1 | |
| return {"request_id": "REQ-001"} | |
| def risk(payload: dict) -> dict: | |
| counters["risk"] += 1 | |
| assert payload["dependencies"] == {"intake": {"request_id": "REQ-001"}} | |
| return {"route": route} | |
| def low_finalize(payload: dict) -> dict: | |
| counters["low"] += 1 | |
| assert payload["dependencies"]["risk_gate"]["route"] == "LOW_RISK" | |
| return {"outcome": "auto_finalized"} | |
| def high_finalize(payload: dict) -> dict: | |
| counters["high"] += 1 | |
| assert payload["dependencies"]["human_review"]["decision"] == "APPROVE" | |
| return {"outcome": "human_approved"} | |
| registry.register("intake_handler", intake) | |
| registry.register("risk_handler", risk) | |
| registry.register("low_finalize_handler", low_finalize) | |
| registry.register("high_finalize_handler", high_finalize) | |
| return registry | |
| def test_decision_node_requires_handler_and_multiple_routes() -> None: | |
| with pytest.raises(ValidationError, match="must declare a handler"): | |
| NodeDefinition( | |
| node_id="decision", | |
| name="Decision", | |
| node_type=NodeType.DECISION, | |
| routes={"A": "alpha", "B": "beta"}, | |
| ) | |
| with pytest.raises(ValidationError, match="at least two routes"): | |
| NodeDefinition( | |
| node_id="decision", | |
| name="Decision", | |
| node_type=NodeType.DECISION, | |
| handler="handler", | |
| routes={"A": "alpha"}, | |
| ) | |
| def test_routes_are_rejected_on_non_decision_nodes() -> None: | |
| with pytest.raises(ValidationError, match="only allowed on DECISION"): | |
| NodeDefinition( | |
| node_id="task", | |
| name="Task", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| routes={"A": "alpha", "B": "beta"}, | |
| ) | |
| def test_validator_rejects_unknown_route_target() -> None: | |
| workflow = WorkflowDefinition( | |
| workflow_id="bad-route", | |
| name="Bad Route", | |
| nodes=[ | |
| NodeDefinition( | |
| node_id="decision", | |
| name="Decision", | |
| node_type=NodeType.DECISION, | |
| handler="handler", | |
| routes={"A": "alpha", "B": "missing"}, | |
| ), | |
| NodeDefinition( | |
| node_id="alpha", | |
| name="Alpha", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| depends_on=["decision"], | |
| ), | |
| ], | |
| ) | |
| with pytest.raises(WorkflowValidationError, match="unknown node 'missing'"): | |
| validate_workflow(workflow) | |
| def test_validator_requires_route_target_to_directly_depend_on_decision() -> None: | |
| workflow = WorkflowDefinition( | |
| workflow_id="bad-dependency", | |
| name="Bad Dependency", | |
| nodes=[ | |
| NodeDefinition( | |
| node_id="decision", | |
| name="Decision", | |
| node_type=NodeType.DECISION, | |
| handler="handler", | |
| routes={"A": "alpha", "B": "beta"}, | |
| ), | |
| NodeDefinition( | |
| node_id="alpha", | |
| name="Alpha", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| ), | |
| NodeDefinition( | |
| node_id="beta", | |
| name="Beta", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| depends_on=["decision"], | |
| ), | |
| ], | |
| ) | |
| with pytest.raises(WorkflowValidationError, match="must directly depend"): | |
| validate_workflow(workflow) | |
| def test_validator_rejects_unrouted_direct_dependent() -> None: | |
| workflow = WorkflowDefinition( | |
| workflow_id="unrouted", | |
| name="Unrouted", | |
| nodes=[ | |
| NodeDefinition( | |
| node_id="decision", | |
| name="Decision", | |
| node_type=NodeType.DECISION, | |
| handler="handler", | |
| routes={"A": "alpha", "B": "beta"}, | |
| ), | |
| NodeDefinition( | |
| node_id="alpha", | |
| name="Alpha", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| depends_on=["decision"], | |
| ), | |
| NodeDefinition( | |
| node_id="beta", | |
| name="Beta", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| depends_on=["decision"], | |
| ), | |
| NodeDefinition( | |
| node_id="gamma", | |
| name="Gamma", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| depends_on=["decision"], | |
| ), | |
| ], | |
| ) | |
| with pytest.raises(WorkflowValidationError, match="unrouted dependents: gamma"): | |
| validate_workflow(workflow) | |
| def test_validator_rejects_branch_reconvergence_in_current_mvp() -> None: | |
| workflow = WorkflowDefinition( | |
| workflow_id="reconverge", | |
| name="Reconverge", | |
| nodes=[ | |
| NodeDefinition( | |
| node_id="decision", | |
| name="Decision", | |
| node_type=NodeType.DECISION, | |
| handler="handler", | |
| routes={"A": "alpha", "B": "beta"}, | |
| ), | |
| NodeDefinition( | |
| node_id="alpha", | |
| name="Alpha", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| depends_on=["decision"], | |
| ), | |
| NodeDefinition( | |
| node_id="beta", | |
| name="Beta", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| depends_on=["decision"], | |
| ), | |
| NodeDefinition( | |
| node_id="join", | |
| name="Join", | |
| node_type=NodeType.TASK, | |
| handler="handler", | |
| depends_on=["alpha", "beta"], | |
| ), | |
| ], | |
| ) | |
| with pytest.raises(WorkflowValidationError, match="reconvergence is not supported"): | |
| validate_workflow(workflow) | |
| def test_low_risk_route_executes_only_selected_branch_and_skips_other_branch() -> None: | |
| counters = {"intake": 0, "risk": 0, "low": 0, "high": 0} | |
| run = execute_workflow( | |
| make_risk_workflow(), | |
| build_registry("LOW_RISK", counters), | |
| run_id="low-risk", | |
| state_store=InMemoryStateStore(), | |
| ) | |
| assert run.status is WorkflowStatus.COMPLETED | |
| assert counters == {"intake": 1, "risk": 1, "low": 1, "high": 0} | |
| assert run.node_runs["risk_gate"].output == {"route": "LOW_RISK"} | |
| assert run.node_runs["low_finalize"].status is NodeStatus.COMPLETED | |
| assert run.node_runs["human_review"].status is NodeStatus.SKIPPED | |
| assert run.node_runs["high_finalize"].status is NodeStatus.SKIPPED | |
| assert run.final_output == {"low_finalize": {"outcome": "auto_finalized"}} | |
| routed = [event for event in run.events if event.event_type is EventType.DECISION_ROUTED] | |
| assert len(routed) == 1 | |
| assert routed[0].details == { | |
| "route": "LOW_RISK", | |
| "selected_target": "low_finalize", | |
| } | |
| assert EventType.HUMAN_REVIEW_REQUESTED not in [ | |
| event.event_type for event in run.events | |
| ] | |
| def test_high_risk_route_pauses_for_human_then_resumes_without_replaying_decision() -> None: | |
| counters = {"intake": 0, "risk": 0, "low": 0, "high": 0} | |
| workflow = make_risk_workflow() | |
| registry = build_registry("HIGH_RISK", counters) | |
| store = InMemoryStateStore() | |
| paused = execute_workflow( | |
| workflow, | |
| registry, | |
| run_id="high-risk", | |
| state_store=store, | |
| ) | |
| assert paused.status is WorkflowStatus.WAITING_FOR_HUMAN | |
| assert counters == {"intake": 1, "risk": 1, "low": 0, "high": 0} | |
| assert paused.node_runs["low_finalize"].status is NodeStatus.SKIPPED | |
| assert paused.node_runs["human_review"].status is NodeStatus.WAITING_FOR_HUMAN | |
| review_id = paused.human_reviews[0].review_id | |
| completed = submit_human_decision( | |
| workflow, | |
| registry, | |
| run_id="high-risk", | |
| review_id=review_id, | |
| decision=HumanDecision.APPROVE, | |
| state_store=store, | |
| ) | |
| assert completed.status is WorkflowStatus.COMPLETED | |
| assert counters == {"intake": 1, "risk": 1, "low": 0, "high": 1} | |
| assert completed.node_runs["risk_gate"].attempt == 1 | |
| assert completed.node_runs["low_finalize"].status is NodeStatus.SKIPPED | |
| assert completed.node_runs["human_review"].status is NodeStatus.COMPLETED | |
| assert completed.node_runs["high_finalize"].status is NodeStatus.COMPLETED | |
| assert completed.final_output == {"high_finalize": {"outcome": "human_approved"}} | |
| def test_unknown_decision_route_fails_safely_before_any_branch_executes() -> None: | |
| counters = {"intake": 0, "risk": 0, "low": 0, "high": 0} | |
| with pytest.raises(WorkflowExecutionError, match="unknown route 'UNAPPROVED'") as exc_info: | |
| execute_workflow( | |
| make_risk_workflow(), | |
| build_registry("UNAPPROVED", counters), | |
| run_id="bad-route-output", | |
| state_store=InMemoryStateStore(), | |
| ) | |
| run = exc_info.value.run | |
| assert run.status is WorkflowStatus.FAILED | |
| assert counters == {"intake": 1, "risk": 1, "low": 0, "high": 0} | |
| assert run.node_runs["risk_gate"].status is NodeStatus.FAILED | |
| assert run.node_runs["low_finalize"].status is NodeStatus.PENDING | |
| assert run.node_runs["human_review"].status is NodeStatus.PENDING | |
| assert EventType.DECISION_ROUTED not in [event.event_type for event in run.events] | |
| def test_decision_route_must_be_string() -> None: | |
| counters = {"intake": 0, "risk": 0, "low": 0, "high": 0} | |
| registry = build_registry("LOW_RISK", counters) | |
| def malformed_route(payload: dict) -> dict: | |
| counters["risk"] += 1 | |
| return {"route": 42} | |
| registry = HandlerRegistry() | |
| def intake(payload: dict) -> dict: | |
| counters["intake"] += 1 | |
| return {"request_id": "REQ-001"} | |
| registry.register("intake_handler", intake) | |
| registry.register("risk_handler", malformed_route) | |
| registry.register("low_finalize_handler", lambda payload: {"outcome": "low"}) | |
| registry.register("high_finalize_handler", lambda payload: {"outcome": "high"}) | |
| with pytest.raises(WorkflowExecutionError, match="non-blank string route"): | |
| execute_workflow( | |
| make_risk_workflow(), | |
| registry, | |
| state_store=InMemoryStateStore(), | |
| ) | |