diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/__tests__/tool-executor-workflow-action.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/__tests__/tool-executor-workflow-action.spec.ts new file mode 100644 index 00000000000..62658b3a397 --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/__tests__/tool-executor-workflow-action.spec.ts @@ -0,0 +1,192 @@ +import { Test, type TestingModule } from '@nestjs/testing'; + +import { DraftEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/draft-email-tool'; +import { SendEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/send-email-tool'; +import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool'; +import { ToolExecutorWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action'; +import { type WorkflowActionSettings } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action-settings.type'; +import { + type WorkflowAction, + WorkflowActionType, +} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; + +jest.mock( + 'src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util', + () => ({ + renderRichTextToHtml: jest.fn().mockResolvedValue('
rendered html
'), + }), +); + +const { renderRichTextToHtml } = jest.requireMock( + 'src/engine/core-modules/tool/tools/email-tool/utils/render-rich-text-to-html.util', +); + +const baseSettings: WorkflowActionSettings = { + outputSchema: {}, + errorHandlingOptions: { + retryOnFailure: { value: false }, + continueOnFailure: { value: false }, + }, + input: {}, +}; + +const emailInput = { + connectedAccountId: 'account-1', + recipients: { to: 'test@example.com' }, + subject: 'Test', +}; + +const buildEmailStep = ( + type: 'SEND_EMAIL' | 'DRAFT_EMAIL', + input: Recordrendered html
' }), + expect.any(Object), + ); + }); + + it('should resolve variableTag nodes inside TipTap JSON before rendering', async () => { + const tipTapBodyWithVariable = JSON.stringify({ + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'variableTag', + attrs: { variable: '{{trigger.name}}' }, + }, + ], + }, + ], + }); + + await executeWithBody(tipTapBodyWithVariable); + + expect(renderRichTextToHtml).toHaveBeenCalledWith({ + type: 'doc', + content: [ + { + type: 'paragraph', + content: [{ type: 'text', text: 'John' }], + }, + ], + }); + }); + + it('should pass plain text body through without rendering', async () => { + await executeWithBody('{{trigger.name}}\n{{trigger.email}}'); + + expect(renderRichTextToHtml).not.toHaveBeenCalled(); + expect(mockSendEmailTool.execute).toHaveBeenCalledWith( + expect.objectContaining({ body: 'John\njohn@example.com' }), + expect.any(Object), + ); + }); + + it('should treat non-TipTap JSON as plain text', async () => { + await executeWithBody('{"key":"value"}'); + + expect(renderRichTextToHtml).not.toHaveBeenCalled(); + expect(mockSendEmailTool.execute).toHaveBeenCalledWith( + expect.objectContaining({ body: '{"key":"value"}' }), + expect.any(Object), + ); + }); + + it('should handle empty string body without crashing', async () => { + await executeWithBody(''); + + expect(renderRichTextToHtml).not.toHaveBeenCalled(); + expect(mockSendEmailTool.execute).toHaveBeenCalled(); + }); + + it('should handle undefined body without crashing', async () => { + await executeWithBody(undefined); + + expect(renderRichTextToHtml).not.toHaveBeenCalled(); + expect(mockSendEmailTool.execute).toHaveBeenCalled(); + }); + + it('should apply the same body handling for DRAFT_EMAIL', async () => { + await executeWithBody('{{trigger.name}}', 'DRAFT_EMAIL'); + + expect(renderRichTextToHtml).not.toHaveBeenCalled(); + expect(mockDraftEmailTool.execute).toHaveBeenCalledWith( + expect.objectContaining({ body: 'John' }), + expect.any(Object), + ); + }); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts index b096ddc5e4e..bf7e5e9c311 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/tool-executor-workflow-action.ts @@ -1,6 +1,12 @@ import { Injectable } from '@nestjs/common'; +import type { JSONContent } from '@tiptap/core'; -import { resolveInput, resolveRichTextVariables } from 'twenty-shared/utils'; +import { + isDefined, + parseJson, + resolveInput, + resolveRichTextVariables, +} from 'twenty-shared/utils'; import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface'; @@ -35,6 +41,22 @@ export class ToolExecutorWorkflowAction implements WorkflowAction { ]); } + private async resolveEmailBody( + body: string, + context: Record