Skip to content

Unit test result missing expected action #8864

@asos-martinsmith

Description

@asos-martinsmith

Severity

P3 - Medium (Minor functionality affected)

Describe the Bug with repro steps

In the example below

The scope "My_Scope" has Status of "Failed".

There is an exception handler Exception_Handler set to

"runAfter": {
                    "My_Scope": [
                        "FAILED",
                        "TIMEDOUT"
                    ]

After running the test BugRepro_FunctionFailed_ExceptionHandlerMissing the "My_Scope" action has Status of "Failed" but testRun.Actions does not contain the Exception_Handler action.

Why not? It should have been called if "My_Scope" has Status of "Failed"?

What type of Logic App Is this happening in?

Standard (Local Development)

Are you experiencing a regression?

No response

Which operating system are you using?

Windows

Did you refer to the TSG before filing this issue? https://aka.ms/lauxtsg

Yes

Workflow JSON

{
    "definition": {
        "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
        "contentVersion": "1.0.0.0",
        "outputs": {},
        "triggers": {
            "manual": {
                "type": "Request",
                "kind": "Http",
                "inputs": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "input": { "type": "string" }
                        }
                    }
                }
            }
        },
        "actions": {
            "My_Scope": {
                "type": "Scope",
                "actions": {
                    "Call_Function": {
                        "type": "InvokeFunction",
                        "inputs": {
                            "functionName": "MyFunction",
                            "parameters": {
                                "input": "@triggerBody()?['input']"
                            }
                        },
                        "runAfter": {}
                    }
                },
                "runAfter": {}
            },
            "Exception_Handler": {
                "type": "Scope",
                "actions": {
                    "Filter_Errors": {
                        "type": "Query",
                        "inputs": {
                            "from": "@result('My_Scope')",
                            "where": "@not(equals(item()?['status'], 'Succeeded'))"
                        },
                        "runAfter": {}
                    },
                    "Compose_Code": {
                        "type": "Compose",
                        "inputs": {
                            "code": "@coalesce(body('Filter_Errors')?[0]?['error']?['code'], 500)",
                            "message": "@coalesce(body('Filter_Errors')?[0]?['error']?['message'], 'An unexpected error occurred')"
                        },
                        "runAfter": {
                            "Filter_Errors": [ "SUCCEEDED" ]
                        }
                    },
                    "Return_Response": {
                        "type": "Response",
                        "inputs": {
                            "statusCode": "@outputs('Compose_Code')?['code']",
                            "body": { "error": true }
                        },
                        "runAfter": {
                            "Compose_Code": [ "SUCCEEDED" ]
                        }
                    },
                    "Terminate_Exception": {
                        "type": "Terminate",
                        "inputs": {
                            "runStatus": "Failed",
                            "runError": {
                                "code": "@outputs('Compose_Code')?['code']",
                                "message": "Exception caught"
                            }
                        },
                        "runAfter": {
                            "Return_Response": [ "SUCCEEDED" ]
                        }
                    }
                },
                "runAfter": {
                    "My_Scope": [
                        "FAILED",
                        "TIMEDOUT"
                    ]
                }
            }
        }
    },
    "kind": "Stateful"
}

Screenshots or Videos

No response

Environment

Additional context

using Microsoft.Azure.Workflows.UnitTesting;
using Microsoft.Azure.Workflows.UnitTesting.Definitions;
using Microsoft.Azure.Workflows.UnitTesting.ErrorResponses;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Tests.LogicApp.BugRepro_InvokeFunction_ScopeFailure.Test
{
    /// <summary>
    /// Minimal reproduction of a Logic Apps Unit Testing Framework bug (extension v1.127.24.0).
    ///
    /// BUG: When an InvokeFunction action inside a Scope is mocked as Failed (TestErrorInfo),
    /// AND the Exception_Handler scope (runAfter: FAILED/TIMEDOUT) contains:
    ///   1. A Query action using @result('ScopeName') to get scope results
    ///   2. A Compose action using @coalesce(body('Filter_Errors')...) referencing Query output
    /// then the framework fails to include Exception_Handler in testRun.Actions.
    /// The Exception_Handler is never evaluated despite the scope failing.
    ///
    /// Without the @coalesce(body('Filter_Errors')...) expression → passes.
    /// Without the @result('My_Scope') Query → passes.
    /// With both present → Exception_Handler is missing from testRun.Actions.
    ///
    /// Workaround: Mock the InvokeFunction as Succeeded with outputs that cause
    /// a downstream action (e.g. ParseJson) to fail naturally, instead of using TestErrorInfo.
    ///
    /// Workflow structure:
    ///   My_Scope (Scope)
    ///     └── Call_Function (InvokeFunction)
    ///   Exception_Handler (Scope, runAfter: My_Scope [FAILED, TIMEDOUT])
    ///     ├── Filter_Errors (Query, from: @result('My_Scope'))
    ///     ├── Compose_Code (Compose, @coalesce(body('Filter_Errors')...))
    ///     ├── Return_Response (Response)
    ///     └── Terminate_Exception (Terminate, Failed)
    /// </summary>
    [TestClass]
    public class BugRepro_InvokeFunctionScopeFailureTest
    {
        /// <summary>
        /// CONTROL: Function succeeds → Exception_Handler is skipped. Confirms workflow is valid.
        /// </summary>
        [TestMethod]
        public async Task Control_FunctionSucceeds_ExceptionHandlerSkipped()
        {
            var executor = new TestExecutor("BugRepro_InvokeFunction_ScopeFailure/testSettings.config");
            var triggerMock = new Mocks.BugRepro_InvokeFunction_ScopeFailure.TriggerMock();
            var functionMock = new Mocks.BugRepro_InvokeFunction_ScopeFailure.CallFunctionActionMock(
                name: "Call_Function");

            var testMock = new TestMockDefinition(
                triggerMock: triggerMock,
                actionMocks: new Dictionary<string, ActionMock>
                {
                    { functionMock.Name, functionMock }
                });

            var testRun = await executor.Create().RunWorkflowAsync(testMock: testMock);

            Assert.AreEqual(TestWorkflowStatus.Succeeded, testRun.Status,
                "Workflow should succeed when function succeeds");
            Assert.IsTrue(testRun.Actions.ContainsKey("Exception_Handler"),
                "Exception_Handler should be present (as Skipped)");
            Assert.AreEqual(TestWorkflowStatus.Skipped, testRun.Actions["Exception_Handler"].Status);
        }

        /// <summary>
        /// BUG REPRO: Function mocked as Failed via TestErrorInfo.
        ///
        /// EXPECTED: My_Scope fails → Exception_Handler runs (Filter_Errors → Compose_Code → 
        ///           Return_Response → Terminate_Exception).
        /// ACTUAL:   testRun.Actions only contains My_Scope. Exception_Handler is missing.
        ///
        /// Root cause: The combination of a Query action using @result('My_Scope') and a 
        /// Compose action referencing body('Filter_Errors') via @coalesce() causes the 
        /// framework to skip the Exception_Handler entirely when an InvokeFunction inside 
        /// the scope is mocked as Failed with TestErrorInfo.
        /// </summary>
        [TestMethod]
        public async Task BugRepro_FunctionFailed_ExceptionHandlerMissing()
        {
            var executor = new TestExecutor("BugRepro_InvokeFunction_ScopeFailure/testSettings.config");
            var triggerMock = new Mocks.BugRepro_InvokeFunction_ScopeFailure.TriggerMock();

            var error = new TestErrorInfo(
                Microsoft.Azure.Workflows.Common.ErrorResponses.ErrorResponseCode.ActionFailed,
                "Simulated function failure");
            var functionMock = new Mocks.BugRepro_InvokeFunction_ScopeFailure.CallFunctionActionMock(
                status: TestWorkflowStatus.Failed,
                name: "Call_Function",
                error: error);

            var testMock = new TestMockDefinition(
                triggerMock: triggerMock,
                actionMocks: new Dictionary<string, ActionMock>
                {
                    { functionMock.Name, functionMock }
                });

            var testRun = await executor.Create().RunWorkflowAsync(testMock: testMock);

            // Diagnostic output
            System.Console.WriteLine($"\nWorkflow Status: {testRun.Status}");
            System.Console.WriteLine($"Actions: {string.Join(", ", testRun.Actions.Keys)}");
            foreach (var a in testRun.Actions)
            {
                System.Console.WriteLine($"  {a.Key}: {a.Value.Status}");
                if (a.Value.ChildActions != null)
                    foreach (var c in a.Value.ChildActions)
                        System.Console.WriteLine($"    {c.Key}: {c.Value.Status}");
            }

            // ASSERT
            Assert.AreEqual(TestWorkflowStatus.Failed, testRun.Status,
                "Workflow should be Failed");
            Assert.IsTrue(testRun.Actions.ContainsKey("My_Scope"),
                "My_Scope should be present");
            Assert.AreEqual(TestWorkflowStatus.Failed, testRun.Actions["My_Scope"].Status);

            // BUG: This assertion fails — Exception_Handler is not in testRun.Actions
            Assert.IsTrue(testRun.Actions.ContainsKey("Exception_Handler"),
                "BUG: Exception_Handler is missing from testRun.Actions. "
                + "The framework skips the Exception_Handler when a Query uses @result('ScopeName') "
                + "and a Compose uses @coalesce(body('Filter_Errors')...), combined with "
                + "an InvokeFunction mocked as Failed via TestErrorInfo.");
        }
    }
}



namespace Tests.LogicApp.Mocks.BugRepro_InvokeFunction_ScopeFailure
{
    // ── Trigger ──

    public class TriggerMock : Microsoft.Azure.Workflows.UnitTesting.Definitions.TriggerMock
    {
        public TriggerMock(TestWorkflowStatus status = TestWorkflowStatus.Succeeded,
            string name = null, TriggerOutput outputs = null)
            : base(status: status, name: name, outputs: outputs ?? new TriggerOutput())
        {
        }
    }

    public class TriggerOutput : MockOutput
    {
        public JObject Body { get; set; }

        public TriggerOutput()
        {
            this.Body = new JObject
            {
                ["input"] = "test"
            };
        }
    }

    // ── Call_Function (InvokeFunction) ──

    public class CallFunctionActionMock : ActionMock
    {
        /// <summary>Constructor for successful mock with outputs.</summary>
        public CallFunctionActionMock(TestWorkflowStatus status = TestWorkflowStatus.Succeeded,
            string name = null, CallFunctionActionOutput outputs = null)
            : base(status: status, name: name, outputs: outputs ?? new CallFunctionActionOutput())
        {
        }

        /// <summary>Constructor for failed mock with TestErrorInfo.</summary>
        public CallFunctionActionMock(TestWorkflowStatus status, string name = null, TestErrorInfo error = null)
            : base(status: status, name: name, error: error)
        {
        }
    }

    public class CallFunctionActionOutput : MockOutput
    {
        public JObject Body { get; set; }

        public CallFunctionActionOutput()
        {
            this.Body = new JObject
            {
                ["result"] = "ok"
            };
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions