Azure / Azure/LogicAppsUX

Unit test result missing expected action

Open
#8,864 0 comments 0 reactions 1 assignee Claimed by @andrew-eldridge View on GitHub
Dominant language
TypeScript
Stars
111
Forks
109
Avg merge
1d 23h
Merged PRs (30d)
20

Description

### 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

```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
{
///
/// 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)
///
[TestClass]
public class BugRepro_InvokeFunctionScopeFailureTest
{
///
/// CONTROL: Function succeeds → Exception_Handler is skipped. Confirms workflow is valid.
///
[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
{
{ 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);
}

///
/// 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.
///
[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
{
{ 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
{
/// Constructor for successful mock with outputs.
public CallFunctionActionMock(TestWorkflowStatus status = TestWorkflowStatus.Succeeded,
string name = null, CallFunctionActionOutput outputs = null)
: base(status: status, name: name, outputs: outputs ?? new CallFunctionActionOutput())
{
}

/// Constructor for failed mock with TestErrorInfo.
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"
};
}
}
}

```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.