From 1adfaf1ca0294a6c4959336c6c2232e3017541ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Thallis?= Date: Thu, 10 Sep 2026 20:37:45 -0300 Subject: [PATCH 1/2] fix: return an error from Core.Case when no exit matches and there is no default exit FlowRunner.Spec.Blocks.Case.evaluate_outgoing/5 bang-matched Block.evaluate_exits/2, so the {:error, "No default exit available"} that #258 introduced as a graceful return raised a MatchError in the caller's process instead. The error now returns through Block.evaluate_outgoing/5, which already propagates the same error from fetch_next_block/3. Co-Authored-By: Claude Fable 5.1 --- lib/flow_runner/spec/block.ex | 13 ++++++++++++- lib/flow_runner/spec/blocks/case.ex | 16 +++++++-------- mix.exs | 2 +- test/block_test.exs | 30 +++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/lib/flow_runner/spec/block.ex b/lib/flow_runner/spec/block.ex index c1ae4a5..565c6ce 100644 --- a/lib/flow_runner/spec/block.ex +++ b/lib/flow_runner/spec/block.ex @@ -54,11 +54,16 @@ defmodule FlowRunner.Spec.Block do * `{:invalid, reason}` — the flow runner will exit through the block's default response. + + * `{:error, reason}` — the block could not pick an exit at all (for + example a `Core.Case` whose tests all evaluated to false and which + has no default exit); the error is returned to the caller. """ @callback evaluate_outgoing(Container.t(), Flow.t(), Block.t(), Context.t(), user_input :: any) :: {:ok, user_input :: any} | {:ok, user_input :: any, opts :: Keyword.t()} | {:invalid, reason :: String.t()} + | {:error, reason :: String.t()} @derive Jason.Encoder defstruct uuid: nil, @@ -228,7 +233,9 @@ defmodule FlowRunner.Spec.Block do @decorate with_span("FlowRunner.Spec.Block.evaluate_outgoing") @spec evaluate_outgoing(Container.t(), Flow.t(), Block.t(), Context.t(), user_input :: any) :: - {:ok, Context.t(), Block.t()} | {:invalid, reason :: String.t()} + {:ok, Context.t(), Block.t() | nil} + | {:invalid, reason :: String.t()} + | {:error, reason :: String.t()} def evaluate_outgoing( container, flow, @@ -270,12 +277,16 @@ defmodule FlowRunner.Spec.Block do {:ok, context} = evaluate_user_input(block, context, user_input) fetch_default_block(block, flow, context) + + {:error, reason} -> + {:error, reason} end end defp normalize_outgoing({:ok, user_input}), do: {:ok, user_input, []} defp normalize_outgoing({:ok, user_input, opts}) when is_list(opts), do: {:ok, user_input, opts} defp normalize_outgoing({:invalid, _} = invalid), do: invalid + defp normalize_outgoing({:error, _} = error), do: error @spec fetch_default_block(Block.t(), Flow.t(), Context.t()) :: {:error, String.t()} | {:ok, Context.t(), Block.t() | nil} diff --git a/lib/flow_runner/spec/blocks/case.ex b/lib/flow_runner/spec/blocks/case.ex index b1ac3b4..49d2571 100644 --- a/lib/flow_runner/spec/blocks/case.ex +++ b/lib/flow_runner/spec/blocks/case.ex @@ -33,14 +33,14 @@ defmodule FlowRunner.Spec.Blocks.Case do @impl FlowRunner.Spec.Block def evaluate_outgoing(_container, _flow, block, context, nil) do - {:ok, block_exit} = Block.evaluate_exits(block, context) - - case FlowRunner.evaluate_expression_block(block_exit.name, context) do - # We didn't manage to parse it and it returned a parsing error - {:error, _error, _reason} -> {:ok, block_exit.name} - # We managed to evaluate it and it returned nil - nil -> {:ok, block_exit.name} - value -> {:ok, value} + with {:ok, block_exit} <- Block.evaluate_exits(block, context) do + case FlowRunner.evaluate_expression_block(block_exit.name, context) do + # We didn't manage to parse it and it returned a parsing error + {:error, _error, _reason} -> {:ok, block_exit.name} + # We managed to evaluate it and it returned nil + nil -> {:ok, block_exit.name} + value -> {:ok, value} + end end end end diff --git a/mix.exs b/mix.exs index 506893b..03fa3d5 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule FlowRunner.MixProject do use Mix.Project - @version "6.15.1" + @version "6.15.2" def project do [ diff --git a/test/block_test.exs b/test/block_test.exs index f889e56..bf08089 100644 --- a/test/block_test.exs +++ b/test/block_test.exs @@ -4,6 +4,7 @@ defmodule BlockTest do alias FlowRunner.Context alias FlowRunner.Spec.Block + alias FlowRunner.Spec.Container alias FlowRunner.Spec.Exit alias FlowRunner.Spec.Flow @@ -259,6 +260,35 @@ defmodule BlockTest do assert {:ok, %Exit{uuid: "failure-exit"}} = Block.evaluate_exits(block, context_false) end + describe "evaluate_outgoing/5 on a Core.Case block" do + test "returns an error instead of raising when no exit matches and there is no default exit" do + routing_block = %Block{ + uuid: "5a0a4c1e-1d0e-4a4e-9a8e-3c1d7b2f6e01", + name: "Routing for check", + type: "Core.Case", + exits: [ + %Exit{ + uuid: "b586afa7-0097-4805-9951-f6d3156c08db", + name: "Exit for CheckYes", + test: "block.value = 5", + destination_block: "8e2f8c2d-4b0b-4a3f-8a6c-1f2e3d4c5b6a" + } + ] + } + + context = %Context{vars: %{"block" => %{"value" => 10}}} + + assert {:error, "No default exit available"} = + Block.evaluate_outgoing( + %Container{}, + %Flow{blocks: [routing_block]}, + routing_block, + context, + nil + ) + end + end + describe "load_config_for_set_contact_property!/1" do test "accepts the singular shape" do assert %{ From ff1ad4360b0eca49f86e458ba3fbd9aed79a0d6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Thallis?= Date: Fri, 11 Sep 2026 20:54:22 -0300 Subject: [PATCH 2/2] test: cover the Case exit name evaluating to nil Co-Authored-By: Claude Fable 5.1 --- test/block_test.exs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/block_test.exs b/test/block_test.exs index bf08089..ebeb6c1 100644 --- a/test/block_test.exs +++ b/test/block_test.exs @@ -287,6 +287,37 @@ defmodule BlockTest do nil ) end + + test "stores the exit name when the name evaluates to nil" do + destination_uuid = "8e2f8c2d-4b0b-4a3f-8a6c-1f2e3d4c5b6a" + + routing_block = %Block{ + uuid: "5a0a4c1e-1d0e-4a4e-9a8e-3c1d7b2f6e02", + name: "Routing for check", + type: "Core.Case", + exits: [ + %Exit{ + uuid: "c2f1e9a0-6d3b-4f8e-9c1a-2b3d4e5f6a70", + name: "answer", + test: "true", + destination_block: destination_uuid + } + ] + } + + destination_block = %Block{uuid: destination_uuid, name: "destination", type: "Core.Log"} + context = %Context{vars: %{"answer" => nil}} + + assert {:ok, %Context{vars: %{"Routing for check" => "answer"}}, + %Block{uuid: ^destination_uuid}} = + Block.evaluate_outgoing( + %Container{}, + %Flow{blocks: [routing_block, destination_block]}, + routing_block, + context, + nil + ) + end end describe "load_config_for_set_contact_property!/1" do