from pathlib import Path import yaml from task_flow_engine import AssertionDef, FlowDef, FlowEngine, WorkstationDef, resolve_target def test_all_assertions_satisfied_reports_reachable_workstations(): flow = FlowDef( id="workstream.test", entity_type="workstream", workstations=[ WorkstationDef(name="active"), WorkstationDef( name="finished", entry_assertions=[ AssertionDef( id="tasks.all_done", target="tasks.*.status", op="all_eq", value=["done", "cancelled"], ) ], ), ], ) result = FlowEngine().evaluate( {"status": "active", "tasks": [{"status": "done"}, {"status": "cancelled"}]}, flow, ) assert result.exit_blocked is False assert result.reachable == ["active", "finished"] assert result.unreachable == [] def test_failing_exit_assertion_identifies_blocking_assertion(): flow = FlowDef( id="workstream.test", entity_type="workstream", workstations=[ WorkstationDef( name="active", exit_assertions=[ AssertionDef( id="tasks.all_done", target="tasks.*.status", op="all_eq", value=["done", "cancelled"], ) ], ) ], ) result = FlowEngine().evaluate( {"status": "active", "tasks": [{"status": "done"}, {"status": "todo"}]}, flow, ) assert result.exit_blocked is True assert [item.id for item in result.blocking_assertions] == ["tasks.all_done"] assert result.blocking_assertions[0].actual == ["done", "todo"] def test_can_reach_checks_current_exit_assertions_before_target_entry(): flow = FlowDef( id="workstream.test", entity_type="workstream", workstations=[ WorkstationDef( name="active", exit_assertions=[ AssertionDef( id="dependencies.all_complete", target="dependencies.*.workstation", op="all_eq", value=["finished", "archived"], ) ], ), WorkstationDef( name="finished", entry_assertions=[ AssertionDef( id="tasks.all_done", target="tasks.*.status", op="all_eq", value=["done", "cancelled"], ) ], ), ], ) can_reach, failures = FlowEngine().can_reach( { "status": "active", "tasks": [{"status": "done"}], "dependencies": [{"workstation": "active"}], }, flow, "finished", ) assert can_reach is False assert [item.id for item in failures] == ["dependencies.all_complete"] def test_custom_op_callable_is_invoked(): calls = [] def has_review(assertion, obj, values): calls.append((assertion.id, values)) return bool(obj.get("review")) flow = FlowDef( id="review.test", entity_type="contribution", workstations=[ WorkstationDef(name="draft"), WorkstationDef( name="accepted", entry_assertions=[ AssertionDef(id="contribution.has_review", target="review", op="custom") ], ), ], ) result = FlowEngine(custom_ops={"contribution.has_review": has_review}).evaluate( {"status": "draft", "review": {"approved_by": "bernd"}}, flow, ) assert "accepted" in result.reachable assert calls == [("contribution.has_review", [{"approved_by": "bernd"}])] def test_empty_assertions_make_all_workstations_reachable(): flow = FlowDef( id="empty.test", entity_type="task", workstations=[ WorkstationDef(name="todo"), WorkstationDef(name="in_progress"), WorkstationDef(name="done"), ], ) result = FlowEngine().evaluate({"status": "todo"}, flow) assert result.exit_blocked is False assert result.reachable == ["todo", "in_progress", "done"] def test_circular_reference_in_target_path_does_not_loop_forever(): obj = {"status": "active"} obj["self"] = obj assert resolve_target(obj, "self.self.self.status") == [] def test_yaml_flow_definitions_load_and_evaluate_representative_entities(): flow_dir = Path(__file__).resolve().parents[1] / "flows" flows = { path.stem: FlowDef.from_dict(yaml.safe_load(path.read_text())) for path in sorted(flow_dir.glob("*.yaml")) } assert sorted(flows) == [ "capability_request", "contribution", "task", "workstream", ] workstream_result = FlowEngine( custom_ops={ "dependencies.any_incomplete": lambda assertion, obj, values: any( value not in assertion.value for value in values ) } ).evaluate( { "status": "active", "tasks": [{"status": "done"}], "dependencies": [{"workstation": "finished"}], }, flows["workstream"], ) assert "finished" in workstream_result.reachable assert "blocked" in [item.workstation for item in workstream_result.unreachable] task_result = FlowEngine().evaluate( {"status": "blocked", "needs_human": False}, flows["task"], ) assert "in_progress" in task_result.reachable contribution_result = FlowEngine().evaluate( {"status": "acknowledged", "previous_workstation": "acknowledged"}, flows["contribution"], ) assert "accepted" in contribution_result.reachable assert "merged" not in contribution_result.reachable capability_result = FlowEngine().evaluate( {"status": "ready_for_review", "previous_workstation": "ready_for_review"}, flows["capability_request"], ) assert "completed" in capability_result.reachable assert "rejected" not in capability_result.reachable