From 9eb1f681e2aee2fe9e88ecfa9876b09a3da87ab2 Mon Sep 17 00:00:00 2001 From: William Groover Date: Tue, 2 Dec 2025 14:19:10 -0800 Subject: [PATCH 1/2] Fix: Support consumption SKU A2A workflows in parser files - Replace workflowKind check with isA2AWorkflow() helper in parser files - Fixes bug where consumption SKU agents lose runAfter after deletion/recreation - Affects both conversational and autonomous agent workflows - Updated 8 parser files in designer and designer-v2 libraries The issue was that allowRunAfterTrigger only checked for Standard SKU (workflowKind === 'agent') and missed Consumption SKU which uses metadata.agentType === 'conversational'. This caused agents to have no runAfter connection to trigger, breaking handoff addition. --- libs/designer-v2/src/lib/core/parsers/addNodeToWorkflow.ts | 4 ++-- .../src/lib/core/parsers/deleteNodeFromWorkflow.ts | 3 ++- .../designer-v2/src/lib/core/parsers/moveNodeInWorkflow.ts | 7 ++++--- .../src/lib/core/parsers/pasteScopeInWorkflow.ts | 5 +++-- libs/designer/src/lib/core/parsers/addNodeToWorkflow.ts | 4 ++-- .../src/lib/core/parsers/deleteNodeFromWorkflow.ts | 3 ++- libs/designer/src/lib/core/parsers/moveNodeInWorkflow.ts | 7 ++++--- libs/designer/src/lib/core/parsers/pasteScopeInWorkflow.ts | 5 +++-- 8 files changed, 22 insertions(+), 16 deletions(-) diff --git a/libs/designer-v2/src/lib/core/parsers/addNodeToWorkflow.ts b/libs/designer-v2/src/lib/core/parsers/addNodeToWorkflow.ts index 00246ee1d71..9ca4ddf9a11 100644 --- a/libs/designer-v2/src/lib/core/parsers/addNodeToWorkflow.ts +++ b/libs/designer-v2/src/lib/core/parsers/addNodeToWorkflow.ts @@ -2,6 +2,7 @@ import CONSTANTS from '../../common/constants'; import type { RelationshipIds } from '../state/panel/panelTypes'; import type { NodesMetadata, WorkflowState } from '../state/workflow/workflowInterfaces'; +import { isA2AWorkflow } from '../state/workflow/helper'; import { createWorkflowNode, createWorkflowEdge } from '../utils/graph'; import type { WorkflowEdge, WorkflowNode } from './models/workflowNode'; import { reassignEdgeSources, reassignEdgeTargets, addNewEdge, applyIsRootNode, removeEdge } from './restructuringHelpers'; @@ -13,7 +14,6 @@ import { isScopeOperation, WORKFLOW_NODE_TYPES, getRecordEntry, - equals, } from '@microsoft/logic-apps-shared'; export interface AddNodePayload { @@ -53,7 +53,7 @@ export const addNodeToWorkflow = ( state.isDirty = true; const isAfterTrigger = getRecordEntry(nodesMetadata, parentId ?? '')?.isTrigger; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isRoot && !isAfterTrigger); nodesMetadata[newNodeId] = { graphId: subgraphId ?? graphId, parentNodeId, isRoot, isTrigger }; state.operations[newNodeId] = { ...state.operations[newNodeId], type: operation.type, kind: operation.kind }; diff --git a/libs/designer-v2/src/lib/core/parsers/deleteNodeFromWorkflow.ts b/libs/designer-v2/src/lib/core/parsers/deleteNodeFromWorkflow.ts index fee73bf54da..ab22fce50d5 100644 --- a/libs/designer-v2/src/lib/core/parsers/deleteNodeFromWorkflow.ts +++ b/libs/designer-v2/src/lib/core/parsers/deleteNodeFromWorkflow.ts @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import type { NodesMetadata, WorkflowState } from '../state/workflow/workflowInterfaces'; +import { isA2AWorkflow } from '../state/workflow/helper'; import type { WorkflowNode } from './models/workflowNode'; import { removeEdge, reassignEdgeSources, reassignEdgeTargets } from './restructuringHelpers'; import { equals, getRecordEntry, type LogicAppsV2 } from '@microsoft/logic-apps-shared'; @@ -80,7 +81,7 @@ export const deleteNodeFromWorkflow = ( const parentId = (workflowGraph.edges ?? []).find((edge) => edge.target === nodeId)?.source ?? ''; const parentMetadata = getRecordEntry(nodesMetadata, parentId); const isAfterTrigger = parentMetadata?.isTrigger; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isRoot && !isAfterTrigger); reassignEdgeSources(state, nodeId, parentId, workflowGraph, shouldAddRunAfters); removeEdge(state, parentId, nodeId, workflowGraph); diff --git a/libs/designer-v2/src/lib/core/parsers/moveNodeInWorkflow.ts b/libs/designer-v2/src/lib/core/parsers/moveNodeInWorkflow.ts index 549b180437d..324258cebd4 100644 --- a/libs/designer-v2/src/lib/core/parsers/moveNodeInWorkflow.ts +++ b/libs/designer-v2/src/lib/core/parsers/moveNodeInWorkflow.ts @@ -1,10 +1,11 @@ /* eslint-disable no-param-reassign */ import type { RelationshipIds } from '../state/panel/panelTypes'; import type { NodesMetadata, WorkflowState } from '../state/workflow/workflowInterfaces'; +import { isA2AWorkflow } from '../state/workflow/helper'; import type { WorkflowNode } from './models/workflowNode'; import { addNewEdge, reassignEdgeSources, reassignEdgeTargets, removeEdge, applyIsRootNode } from './restructuringHelpers'; import type { LogicAppsV2 } from '@microsoft/logic-apps-shared'; -import { containsIdTag, equals, getRecordEntry } from '@microsoft/logic-apps-shared'; +import { containsIdTag, getRecordEntry } from '@microsoft/logic-apps-shared'; export interface MoveNodePayload { nodeId: string; @@ -73,7 +74,7 @@ export const moveNodeInWorkflow = ( const parentId = (oldWorkflowGraph.edges ?? []).find((edge) => edge.target === nodeId)?.source ?? ''; const parentMetadata = getRecordEntry(nodesMetadata, parentId); const isAfterTrigger = parentMetadata?.isTrigger; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isOldRoot && !isAfterTrigger); reassignEdgeSources(state, nodeId, parentId, oldWorkflowGraph, shouldAddRunAfters); removeEdge(state, parentId, nodeId, oldWorkflowGraph); @@ -105,7 +106,7 @@ export const moveNodeInWorkflow = ( const parentMetadata = getRecordEntry(nodesMetadata, parentId); const isAfterTrigger = (parentMetadata?.isRoot && newGraphId === 'root') ?? false; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isNewRoot && !isAfterTrigger); // 1 parent, 1 child diff --git a/libs/designer-v2/src/lib/core/parsers/pasteScopeInWorkflow.ts b/libs/designer-v2/src/lib/core/parsers/pasteScopeInWorkflow.ts index 7283da65424..b569ea0048b 100644 --- a/libs/designer-v2/src/lib/core/parsers/pasteScopeInWorkflow.ts +++ b/libs/designer-v2/src/lib/core/parsers/pasteScopeInWorkflow.ts @@ -1,8 +1,9 @@ import type { RelationshipIds } from '../state/panel/panelTypes'; import type { NodesMetadata, Operations, WorkflowState } from '../state/workflow/workflowInterfaces'; +import { isA2AWorkflow } from '../state/workflow/helper'; import type { WorkflowNode } from './models/workflowNode'; import { addNewEdge, reassignEdgeSources, reassignEdgeTargets, removeEdge, applyIsRootNode } from './restructuringHelpers'; -import { containsIdTag, equals, getRecordEntry } from '@microsoft/logic-apps-shared'; +import { containsIdTag, getRecordEntry } from '@microsoft/logic-apps-shared'; export interface PasteScopeNodePayload { relationshipIds: RelationshipIds; @@ -56,7 +57,7 @@ export const pasteScopeInWorkflow = ( const parentMetadata = getRecordEntry(state.nodesMetadata, parentId); const isAfterTrigger = (parentMetadata?.isRoot && newGraphId === 'root') ?? false; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isNewRoot && !isAfterTrigger); // clear the existing runAfter diff --git a/libs/designer/src/lib/core/parsers/addNodeToWorkflow.ts b/libs/designer/src/lib/core/parsers/addNodeToWorkflow.ts index 00246ee1d71..9ca4ddf9a11 100644 --- a/libs/designer/src/lib/core/parsers/addNodeToWorkflow.ts +++ b/libs/designer/src/lib/core/parsers/addNodeToWorkflow.ts @@ -2,6 +2,7 @@ import CONSTANTS from '../../common/constants'; import type { RelationshipIds } from '../state/panel/panelTypes'; import type { NodesMetadata, WorkflowState } from '../state/workflow/workflowInterfaces'; +import { isA2AWorkflow } from '../state/workflow/helper'; import { createWorkflowNode, createWorkflowEdge } from '../utils/graph'; import type { WorkflowEdge, WorkflowNode } from './models/workflowNode'; import { reassignEdgeSources, reassignEdgeTargets, addNewEdge, applyIsRootNode, removeEdge } from './restructuringHelpers'; @@ -13,7 +14,6 @@ import { isScopeOperation, WORKFLOW_NODE_TYPES, getRecordEntry, - equals, } from '@microsoft/logic-apps-shared'; export interface AddNodePayload { @@ -53,7 +53,7 @@ export const addNodeToWorkflow = ( state.isDirty = true; const isAfterTrigger = getRecordEntry(nodesMetadata, parentId ?? '')?.isTrigger; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isRoot && !isAfterTrigger); nodesMetadata[newNodeId] = { graphId: subgraphId ?? graphId, parentNodeId, isRoot, isTrigger }; state.operations[newNodeId] = { ...state.operations[newNodeId], type: operation.type, kind: operation.kind }; diff --git a/libs/designer/src/lib/core/parsers/deleteNodeFromWorkflow.ts b/libs/designer/src/lib/core/parsers/deleteNodeFromWorkflow.ts index fee73bf54da..ab22fce50d5 100644 --- a/libs/designer/src/lib/core/parsers/deleteNodeFromWorkflow.ts +++ b/libs/designer/src/lib/core/parsers/deleteNodeFromWorkflow.ts @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import type { NodesMetadata, WorkflowState } from '../state/workflow/workflowInterfaces'; +import { isA2AWorkflow } from '../state/workflow/helper'; import type { WorkflowNode } from './models/workflowNode'; import { removeEdge, reassignEdgeSources, reassignEdgeTargets } from './restructuringHelpers'; import { equals, getRecordEntry, type LogicAppsV2 } from '@microsoft/logic-apps-shared'; @@ -80,7 +81,7 @@ export const deleteNodeFromWorkflow = ( const parentId = (workflowGraph.edges ?? []).find((edge) => edge.target === nodeId)?.source ?? ''; const parentMetadata = getRecordEntry(nodesMetadata, parentId); const isAfterTrigger = parentMetadata?.isTrigger; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isRoot && !isAfterTrigger); reassignEdgeSources(state, nodeId, parentId, workflowGraph, shouldAddRunAfters); removeEdge(state, parentId, nodeId, workflowGraph); diff --git a/libs/designer/src/lib/core/parsers/moveNodeInWorkflow.ts b/libs/designer/src/lib/core/parsers/moveNodeInWorkflow.ts index 549b180437d..324258cebd4 100644 --- a/libs/designer/src/lib/core/parsers/moveNodeInWorkflow.ts +++ b/libs/designer/src/lib/core/parsers/moveNodeInWorkflow.ts @@ -1,10 +1,11 @@ /* eslint-disable no-param-reassign */ import type { RelationshipIds } from '../state/panel/panelTypes'; import type { NodesMetadata, WorkflowState } from '../state/workflow/workflowInterfaces'; +import { isA2AWorkflow } from '../state/workflow/helper'; import type { WorkflowNode } from './models/workflowNode'; import { addNewEdge, reassignEdgeSources, reassignEdgeTargets, removeEdge, applyIsRootNode } from './restructuringHelpers'; import type { LogicAppsV2 } from '@microsoft/logic-apps-shared'; -import { containsIdTag, equals, getRecordEntry } from '@microsoft/logic-apps-shared'; +import { containsIdTag, getRecordEntry } from '@microsoft/logic-apps-shared'; export interface MoveNodePayload { nodeId: string; @@ -73,7 +74,7 @@ export const moveNodeInWorkflow = ( const parentId = (oldWorkflowGraph.edges ?? []).find((edge) => edge.target === nodeId)?.source ?? ''; const parentMetadata = getRecordEntry(nodesMetadata, parentId); const isAfterTrigger = parentMetadata?.isTrigger; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isOldRoot && !isAfterTrigger); reassignEdgeSources(state, nodeId, parentId, oldWorkflowGraph, shouldAddRunAfters); removeEdge(state, parentId, nodeId, oldWorkflowGraph); @@ -105,7 +106,7 @@ export const moveNodeInWorkflow = ( const parentMetadata = getRecordEntry(nodesMetadata, parentId); const isAfterTrigger = (parentMetadata?.isRoot && newGraphId === 'root') ?? false; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isNewRoot && !isAfterTrigger); // 1 parent, 1 child diff --git a/libs/designer/src/lib/core/parsers/pasteScopeInWorkflow.ts b/libs/designer/src/lib/core/parsers/pasteScopeInWorkflow.ts index 7283da65424..b569ea0048b 100644 --- a/libs/designer/src/lib/core/parsers/pasteScopeInWorkflow.ts +++ b/libs/designer/src/lib/core/parsers/pasteScopeInWorkflow.ts @@ -1,8 +1,9 @@ import type { RelationshipIds } from '../state/panel/panelTypes'; import type { NodesMetadata, Operations, WorkflowState } from '../state/workflow/workflowInterfaces'; +import { isA2AWorkflow } from '../state/workflow/helper'; import type { WorkflowNode } from './models/workflowNode'; import { addNewEdge, reassignEdgeSources, reassignEdgeTargets, removeEdge, applyIsRootNode } from './restructuringHelpers'; -import { containsIdTag, equals, getRecordEntry } from '@microsoft/logic-apps-shared'; +import { containsIdTag, getRecordEntry } from '@microsoft/logic-apps-shared'; export interface PasteScopeNodePayload { relationshipIds: RelationshipIds; @@ -56,7 +57,7 @@ export const pasteScopeInWorkflow = ( const parentMetadata = getRecordEntry(state.nodesMetadata, parentId); const isAfterTrigger = (parentMetadata?.isRoot && newGraphId === 'root') ?? false; - const allowRunAfterTrigger = equals(state.workflowKind, 'agent'); + const allowRunAfterTrigger = isA2AWorkflow(state); const shouldAddRunAfters = allowRunAfterTrigger || (!isNewRoot && !isAfterTrigger); // clear the existing runAfter From ad64a2113105dc012dbab0a5a531ccc5c8d52007 Mon Sep 17 00:00:00 2001 From: William Groover Date: Mon, 15 Dec 2025 12:14:53 -0800 Subject: [PATCH 2/2] test: Add comprehensive unit tests for isA2AWorkflow helper - Added 21 tests covering all detection paths (Standard SKU, Consumption metadata, trigger pattern) - Tests verify case-sensitive metadata check and case-insensitive trigger check - Tests cover edge cases: empty state, missing triggers, priority/short-circuit behavior - Tests added to both designer and designer-v2 libraries for consistency --- .../state/workflow/__test__/helper.spec.ts | 312 +++++++++++++++++- .../state/workflow/__test__/helper.spec.ts | 308 ++++++++++++++++- 2 files changed, 616 insertions(+), 4 deletions(-) diff --git a/libs/designer-v2/src/lib/core/state/workflow/__test__/helper.spec.ts b/libs/designer-v2/src/lib/core/state/workflow/__test__/helper.spec.ts index 98600ee4217..fda8c3a1c0c 100644 --- a/libs/designer-v2/src/lib/core/state/workflow/__test__/helper.spec.ts +++ b/libs/designer-v2/src/lib/core/state/workflow/__test__/helper.spec.ts @@ -1,6 +1,7 @@ import { describe, test, expect } from 'vitest'; -import { collapseFlowTree, isManagedMcpOperation } from '../helper'; // Adjust the import path as needed +import { collapseFlowTree, isManagedMcpOperation, isA2AWorkflow } from '../helper'; // Adjust the import path as needed import { WorkflowNode } from '../../../parsers/models/workflowNode'; +import type { WorkflowState } from '../workflowInterfaces'; import Constants from '../../../../common/constants'; describe('collapseFlowTree', () => { @@ -130,7 +131,7 @@ describe('isManagedMcpOperation', () => { test('should return true for MCP client operations with managed kind', () => { const operation = { type: Constants.NODE.TYPE.MCP_CLIENT, - kind: Constants.NODE.KIND.MANAGED + kind: Constants.NODE.KIND.MANAGED, }; const result = isManagedMcpOperation(operation); @@ -151,7 +152,7 @@ describe('isManagedMcpOperation', () => { test('should return false for MCP operations with non-managed kind', () => { const operation = { type: Constants.NODE.TYPE.MCP_CLIENT, - kind: Constants.NODE.KIND.BUILTIN + kind: Constants.NODE.KIND.BUILTIN, }; const result = isManagedMcpOperation(operation); @@ -165,3 +166,308 @@ describe('isManagedMcpOperation', () => { expect(isManagedMcpOperation({ kind: Constants.NODE.KIND.MANAGED })).toBe(false); }); }); + +describe('isA2AWorkflow', () => { + // Helper to create minimal workflow state + const createWorkflowState = (overrides: Partial = {}): WorkflowState => ({ + graph: { + id: 'root', + type: 'GRAPH_NODE', + children: [], + edges: [], + }, + operations: {}, + nodesMetadata: {}, + collapsedGraphIds: {}, + edgeIdsBySource: {}, + idReplacements: {}, + newlyAddedOperations: {}, + isDirty: false, + runInstance: null, + ...overrides, + }); + + describe('Standard SKU detection', () => { + test('should return true for Standard SKU with workflowKind="agent"', () => { + const state = createWorkflowState({ + workflowKind: 'agent', + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should return false for Standard SKU with workflowKind="stateful"', () => { + const state = createWorkflowState({ + workflowKind: 'stateful', + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false for Standard SKU with workflowKind="stateless"', () => { + const state = createWorkflowState({ + workflowKind: 'stateless', + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + }); + + describe('Consumption SKU - metadata detection', () => { + test('should return true for Consumption SKU with agentType="conversational"', () => { + const state = createWorkflowState({ + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'conversational', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should return false for Consumption SKU with agentType="Conversational" (case sensitive)', () => { + // The equals() function is case-sensitive for metadata check + const state = createWorkflowState({ + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'Conversational', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false for Consumption SKU with different agentType', () => { + const state = createWorkflowState({ + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'autonomous', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + }); + + describe('Consumption SKU - trigger pattern detection', () => { + test('should return true for Consumption SKU with Request trigger and Agent kind', () => { + const state = createWorkflowState({ + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + operations: { + trigger1: { + type: 'Request', + kind: 'Agent', + inputs: {}, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should return true for trigger pattern with case-insensitive matching', () => { + const state = createWorkflowState({ + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + operations: { + trigger1: { + type: 'request', + kind: 'agent', + inputs: {}, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should return false for Request trigger without Agent kind', () => { + const state = createWorkflowState({ + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + operations: { + trigger1: { + type: 'Request', + kind: 'Http', + inputs: {}, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false for Agent kind without Request trigger', () => { + const state = createWorkflowState({ + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + operations: { + trigger1: { + type: 'Recurrence', + kind: 'Agent', + inputs: {}, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + }); + + describe('Edge cases and fallback behavior', () => { + test('should return false for regular workflow with no A2A indicators', () => { + const state = createWorkflowState({ + operations: { + trigger1: { + type: 'Recurrence', + kind: 'Http', + inputs: {}, + }, + }, + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false for empty workflow state', () => { + const state = createWorkflowState(); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false when no trigger exists', () => { + const state = createWorkflowState({ + operations: { + action1: { + type: 'Http', + inputs: {}, + }, + }, + nodesMetadata: { + action1: { + graphId: 'root', + isRoot: false, + isTrigger: false, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should handle workflow with multiple non-trigger nodes', () => { + const state = createWorkflowState({ + operations: { + action1: { type: 'Http', inputs: {} }, + action2: { type: 'Response', inputs: {} }, + action3: { type: 'Compose', inputs: {} }, + }, + nodesMetadata: { + action1: { graphId: 'root', isRoot: false, isTrigger: false }, + action2: { graphId: 'root', isRoot: false, isTrigger: false }, + action3: { graphId: 'root', isRoot: false, isTrigger: false }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + }); + + describe('Priority and short-circuit behavior', () => { + test('should prioritize workflowKind="agent" over other checks', () => { + // Even with conflicting metadata, workflowKind should win + const state = createWorkflowState({ + workflowKind: 'agent', + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'someOtherType', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should short-circuit on explicit non-agent workflowKind', () => { + // Should return false without checking other properties + const state = createWorkflowState({ + workflowKind: 'stateful', + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'conversational', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should fall through to metadata check when workflowKind is undefined', () => { + const state = createWorkflowState({ + workflowKind: undefined, + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'conversational', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + }); +}); diff --git a/libs/designer/src/lib/core/state/workflow/__test__/helper.spec.ts b/libs/designer/src/lib/core/state/workflow/__test__/helper.spec.ts index 24156aaea7c..352d990c2e1 100644 --- a/libs/designer/src/lib/core/state/workflow/__test__/helper.spec.ts +++ b/libs/designer/src/lib/core/state/workflow/__test__/helper.spec.ts @@ -1,6 +1,7 @@ import { describe, test, expect } from 'vitest'; -import { collapseFlowTree } from '../helper'; // Adjust the import path as needed +import { collapseFlowTree, isA2AWorkflow } from '../helper'; // Adjust the import path as needed import { WorkflowNode } from '../../../parsers/models/workflowNode'; +import type { WorkflowState } from '../workflowInterfaces'; describe('collapseFlowTree', () => { test('should return the original tree when no collapsed nodes are provided', () => { @@ -124,3 +125,308 @@ describe('collapseFlowTree', () => { expect(nodeA?.edges).toBeUndefined(); }); }); + +describe('isA2AWorkflow', () => { + // Helper to create minimal workflow state + const createWorkflowState = (overrides: Partial = {}): WorkflowState => ({ + graph: { + id: 'root', + type: 'GRAPH_NODE', + children: [], + edges: [], + }, + operations: {}, + nodesMetadata: {}, + collapsedGraphIds: {}, + edgeIdsBySource: {}, + idReplacements: {}, + newlyAddedOperations: {}, + isDirty: false, + focusedTab: undefined, + ...overrides, + }); + + describe('Standard SKU detection', () => { + test('should return true for Standard SKU with workflowKind="agent"', () => { + const state = createWorkflowState({ + workflowKind: 'agent', + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should return false for Standard SKU with workflowKind="stateful"', () => { + const state = createWorkflowState({ + workflowKind: 'stateful', + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false for Standard SKU with workflowKind="stateless"', () => { + const state = createWorkflowState({ + workflowKind: 'stateless', + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + }); + + describe('Consumption SKU - metadata detection', () => { + test('should return true for Consumption SKU with agentType="conversational"', () => { + const state = createWorkflowState({ + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'conversational', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should return false for Consumption SKU with agentType="Conversational" (case sensitive)', () => { + // The equals() function is case-sensitive for metadata check + const state = createWorkflowState({ + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'Conversational', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false for Consumption SKU with different agentType', () => { + const state = createWorkflowState({ + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'autonomous', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + }); + + describe('Consumption SKU - trigger pattern detection', () => { + test('should return true for Consumption SKU with Request trigger and Agent kind', () => { + const state = createWorkflowState({ + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + operations: { + trigger1: { + type: 'Request', + kind: 'Agent', + inputs: {}, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should return true for trigger pattern with case-insensitive matching', () => { + const state = createWorkflowState({ + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + operations: { + trigger1: { + type: 'request', + kind: 'agent', + inputs: {}, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should return false for Request trigger without Agent kind', () => { + const state = createWorkflowState({ + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + operations: { + trigger1: { + type: 'Request', + kind: 'Http', + inputs: {}, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false for Agent kind without Request trigger', () => { + const state = createWorkflowState({ + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + operations: { + trigger1: { + type: 'Recurrence', + kind: 'Agent', + inputs: {}, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + }); + + describe('Edge cases and fallback behavior', () => { + test('should return false for regular workflow with no A2A indicators', () => { + const state = createWorkflowState({ + operations: { + trigger1: { + type: 'Recurrence', + kind: 'Http', + inputs: {}, + }, + }, + nodesMetadata: { + trigger1: { + graphId: 'root', + isRoot: true, + isTrigger: true, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false for empty workflow state', () => { + const state = createWorkflowState(); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should return false when no trigger exists', () => { + const state = createWorkflowState({ + operations: { + action1: { + type: 'Http', + inputs: {}, + }, + }, + nodesMetadata: { + action1: { + graphId: 'root', + isRoot: false, + isTrigger: false, + }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should handle workflow with multiple non-trigger nodes', () => { + const state = createWorkflowState({ + operations: { + action1: { type: 'Http', inputs: {} }, + action2: { type: 'Response', inputs: {} }, + action3: { type: 'Compose', inputs: {} }, + }, + nodesMetadata: { + action1: { graphId: 'root', isRoot: false, isTrigger: false }, + action2: { graphId: 'root', isRoot: false, isTrigger: false }, + action3: { graphId: 'root', isRoot: false, isTrigger: false }, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + }); + + describe('Priority and short-circuit behavior', () => { + test('should prioritize workflowKind="agent" over other checks', () => { + // Even with conflicting metadata, workflowKind should win + const state = createWorkflowState({ + workflowKind: 'agent', + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'someOtherType', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + + test('should short-circuit on explicit non-agent workflowKind', () => { + // Should return false without checking other properties + const state = createWorkflowState({ + workflowKind: 'stateful', + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'conversational', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(false); + }); + + test('should fall through to metadata check when workflowKind is undefined', () => { + const state = createWorkflowState({ + workflowKind: undefined, + originalDefinition: { + $schema: 'https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#', + contentVersion: '1.0.0.0', + metadata: { + agentType: 'conversational', + }, + triggers: {}, + actions: {}, + }, + }); + + expect(isA2AWorkflow(state)).toBe(true); + }); + }); +});