import type { BlockFactoryOptions } from '@sim/testing' import { createAgentBlock, createBlock, createFunctionBlock, createLoopBlock, createStarterBlock, } from '@sim/testing' import type { Edge } from 'reactflow' import { describe, expect, it } from 'vitest' import { normalizeName } from '@/executor/constants' import { getUniqueBlockName, regenerateBlockIds } from './utils' describe('normalizeName', () => { it.concurrent('should convert to lowercase', () => { expect(normalizeName('MyVariable')).toBe('myvariable') expect(normalizeName('UPPERCASE')).toBe('uppercase') expect(normalizeName('MixedCase')).toBe('mixedcase') }) it.concurrent('should remove spaces', () => { expect(normalizeName('my variable')).toBe('myvariable') expect(normalizeName('my variable')).toBe('myvariable') expect(normalizeName(' spaced ')).toBe('spaced') }) it.concurrent('should handle both lowercase and space removal', () => { expect(normalizeName('JIRA TEAM UUID')).toBe('jirateamuuid') expect(normalizeName('My Block Name')).toBe('myblockname') expect(normalizeName('API 1')).toBe('api1') }) it.concurrent('should handle edge cases', () => { expect(normalizeName('')).toBe('') expect(normalizeName(' ')).toBe('') expect(normalizeName('a')).toBe('a') expect(normalizeName('already_normalized')).toBe('already_normalized') }) it.concurrent('should preserve non-space special characters except dots', () => { expect(normalizeName('my-variable')).toBe('my-variable') expect(normalizeName('my_variable')).toBe('my_variable') }) it.concurrent('should strip dots since they conflict with the reference path delimiter', () => { expect(normalizeName('my.variable')).toBe('myvariable') expect(normalizeName('Trigger.dev 1')).toBe('triggerdev1') expect(normalizeName('Hunter.io 2')).toBe('hunterio2') }) it.concurrent('should handle tabs and newlines as whitespace', () => { expect(normalizeName('my\tvariable')).toBe('myvariable') expect(normalizeName('my\nvariable')).toBe('myvariable') expect(normalizeName('my\r\nvariable')).toBe('myvariable') }) it.concurrent('should handle unicode characters', () => { expect(normalizeName('Café')).toBe('café') expect(normalizeName('日本語')).toBe('日本語') }) it.concurrent('should normalize block names correctly', () => { expect(normalizeName('Agent 1')).toBe('agent1') expect(normalizeName('API Block')).toBe('apiblock') expect(normalizeName('My Custom Block')).toBe('mycustomblock') }) it.concurrent('should normalize variable names correctly', () => { expect(normalizeName('jira1')).toBe('jira1') expect(normalizeName('JIRA TEAM UUID')).toBe('jirateamuuid') expect(normalizeName('My Variable')).toBe('myvariable') }) it.concurrent('should produce consistent results for references', () => { const originalName = 'JIRA TEAM UUID' const normalized1 = normalizeName(originalName) const normalized2 = normalizeName(originalName) expect(normalized1).toBe(normalized2) expect(normalized1).toBe('jirateamuuid') }) it.concurrent('should allow matching block references to variable references', () => { const name = 'API Block' const blockRef = `<${normalizeName(name)}.output>` const varRef = `` expect(blockRef).toBe('') expect(varRef).toBe('') }) it.concurrent('should handle real-world naming patterns consistently', () => { const realWorldNames = [ { input: 'User ID', expected: 'userid' }, { input: 'API Key', expected: 'apikey' }, { input: 'OAuth Token', expected: 'oauthtoken' }, { input: 'Database URL', expected: 'databaseurl' }, { input: 'STRIPE SECRET KEY', expected: 'stripesecretkey' }, { input: 'openai api key', expected: 'openaiapikey' }, { input: 'Customer Name', expected: 'customername' }, { input: 'Order Total', expected: 'ordertotal' }, ] for (const { input, expected } of realWorldNames) { expect(normalizeName(input)).toBe(expected) } }) }) describe('getUniqueBlockName', () => { it('should return "Start" for starter blocks', () => { expect(getUniqueBlockName('Start', {})).toBe('Start') expect(getUniqueBlockName('Starter', {})).toBe('Start') expect(getUniqueBlockName('start', {})).toBe('Start') }) it('should return name with number 1 when no existing blocks', () => { expect(getUniqueBlockName('Agent', {})).toBe('Agent 1') expect(getUniqueBlockName('Function', {})).toBe('Function 1') expect(getUniqueBlockName('Loop', {})).toBe('Loop 1') }) it('should increment number when existing blocks have same base name', () => { const existingBlocks = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 1' }), } expect(getUniqueBlockName('Agent', existingBlocks)).toBe('Agent 2') }) it('should find highest number and increment', () => { const existingBlocks = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 1' }), 'block-2': createAgentBlock({ id: 'block-2', name: 'Agent 3' }), 'block-3': createAgentBlock({ id: 'block-3', name: 'Agent 2' }), } expect(getUniqueBlockName('Agent', existingBlocks)).toBe('Agent 4') }) it('should handle base name with existing number suffix', () => { const existingBlocks = { 'block-1': createFunctionBlock({ id: 'block-1', name: 'Function 1' }), 'block-2': createFunctionBlock({ id: 'block-2', name: 'Function 2' }), } expect(getUniqueBlockName('Function 1', existingBlocks)).toBe('Function 3') expect(getUniqueBlockName('Function 5', existingBlocks)).toBe('Function 3') }) it('should be case insensitive when matching base names', () => { const existingBlocks = { 'block-1': createBlock({ id: 'block-1', name: 'API 1' }), 'block-2': createBlock({ id: 'block-2', name: 'api 2' }), } expect(getUniqueBlockName('API', existingBlocks)).toBe('API 3') expect(getUniqueBlockName('api', existingBlocks)).toBe('api 3') }) it('should handle different block types independently', () => { const existingBlocks = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 1' }), 'block-2': createFunctionBlock({ id: 'block-2', name: 'Function 1' }), 'block-3': createLoopBlock({ id: 'block-3', name: 'Loop 1' }), } expect(getUniqueBlockName('Agent', existingBlocks)).toBe('Agent 2') expect(getUniqueBlockName('Function', existingBlocks)).toBe('Function 2') expect(getUniqueBlockName('Loop', existingBlocks)).toBe('Loop 2') expect(getUniqueBlockName('Router', existingBlocks)).toBe('Router 1') }) it('should handle blocks without numbers as having number 0', () => { const existingBlocks = { 'block-1': createBlock({ id: 'block-1', name: 'Custom' }), } expect(getUniqueBlockName('Custom', existingBlocks)).toBe('Custom 1') }) it('should handle multi-word base names', () => { const existingBlocks = { 'block-1': createBlock({ id: 'block-1', name: 'API Block 1' }), 'block-2': createBlock({ id: 'block-2', name: 'API Block 2' }), } expect(getUniqueBlockName('API Block', existingBlocks)).toBe('API Block 3') }) it('should handle starter blocks even with existing starters', () => { const existingBlocks = { 'block-1': createStarterBlock({ id: 'block-1', name: 'Start' }), } expect(getUniqueBlockName('Start', existingBlocks)).toBe('Start') expect(getUniqueBlockName('Starter', existingBlocks)).toBe('Start') }) it('should handle empty string base name', () => { const existingBlocks = { 'block-1': createBlock({ id: 'block-1', name: ' 1' }), } expect(getUniqueBlockName('', existingBlocks)).toBe(' 1') }) it('should handle complex real-world scenarios', () => { const existingBlocks = { starter: createStarterBlock({ id: 'starter', name: 'Start' }), agent1: createAgentBlock({ id: 'agent1', name: 'Agent 1' }), agent2: createAgentBlock({ id: 'agent2', name: 'Agent 2' }), func1: createFunctionBlock({ id: 'func1', name: 'Function 1' }), loop1: createLoopBlock({ id: 'loop1', name: 'Loop 1' }), } expect(getUniqueBlockName('Agent', existingBlocks)).toBe('Agent 3') expect(getUniqueBlockName('Function', existingBlocks)).toBe('Function 2') expect(getUniqueBlockName('Start', existingBlocks)).toBe('Start') expect(getUniqueBlockName('Condition', existingBlocks)).toBe('Condition 1') }) it('should preserve original base name casing in result', () => { const existingBlocks = { 'block-1': createBlock({ id: 'block-1', name: 'MyBlock 1' }), } expect(getUniqueBlockName('MyBlock', existingBlocks)).toBe('MyBlock 2') expect(getUniqueBlockName('MYBLOCK', existingBlocks)).toBe('MYBLOCK 2') expect(getUniqueBlockName('myblock', existingBlocks)).toBe('myblock 2') }) }) describe('regenerateBlockIds', () => { const positionOffset = { x: 50, y: 50 } it('should preserve parentId and use same offset when duplicating a block inside an existing subflow', () => { const loopId = 'loop-1' const childId = 'child-1' const existingBlocks = { [loopId]: createLoopBlock({ id: loopId, name: 'Loop 1' }), } const blocksToCopy = { [childId]: createAgentBlock({ id: childId, name: 'Agent 1', position: { x: 100, y: 50 }, data: { parentId: loopId, extent: 'parent' }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, // { x: 50, y: 50 } - small offset, used as-is existingBlocks, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(1) const duplicatedBlock = newBlocks[0] expect(duplicatedBlock.data?.parentId).toBe(loopId) expect(duplicatedBlock.data?.extent).toBe('parent') expect(duplicatedBlock.position).toEqual({ x: 150, y: 100 }) }) it('should clear parentId when parent does not exist in paste set or existing blocks', () => { const nonExistentParentId = 'non-existent-loop' const childId = 'child-1' const blocksToCopy = { [childId]: createAgentBlock({ id: childId, name: 'Agent 1', position: { x: 100, y: 50 }, data: { parentId: nonExistentParentId, extent: 'parent' }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(1) const duplicatedBlock = newBlocks[0] expect(duplicatedBlock.data?.parentId).toBeUndefined() expect(duplicatedBlock.data?.extent).toBeUndefined() }) it('should remap parentId when copying both parent and child together', () => { const loopId = 'loop-1' const childId = 'child-1' const blocksToCopy = { [loopId]: createLoopBlock({ id: loopId, name: 'Loop 1', position: { x: 200, y: 200 }, }), [childId]: createAgentBlock({ id: childId, name: 'Agent 1', position: { x: 100, y: 50 }, data: { parentId: loopId, extent: 'parent' }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(2) const newLoop = newBlocks.find((b) => b.type === 'loop') const newChild = newBlocks.find((b) => b.type === 'agent') expect(newLoop).toBeDefined() expect(newChild).toBeDefined() expect(newChild!.data?.parentId).toBe(newLoop!.id) expect(newChild!.data?.extent).toBe('parent') expect(newLoop!.position).toEqual({ x: 250, y: 250 }) expect(newChild!.position).toEqual({ x: 100, y: 50 }) }) it('should apply offset to top-level blocks', () => { const blockId = 'block-1' const blocksToCopy = { [blockId]: createAgentBlock({ id: blockId, name: 'Agent 1', position: { x: 100, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(1) expect(newBlocks[0].position).toEqual({ x: 150, y: 150 }) }) it('should generate unique names for duplicated blocks', () => { const blockId = 'block-1' const existingBlocks = { existing: createAgentBlock({ id: 'existing', name: 'Agent 1' }), } const blocksToCopy = { [blockId]: createAgentBlock({ id: blockId, name: 'Agent 1', position: { x: 100, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, existingBlocks, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(1) expect(newBlocks[0].name).toBe('Agent 2') }) it('should ignore large viewport offset for blocks inside existing subflows', () => { const loopId = 'loop-1' const childId = 'child-1' const existingBlocks = { [loopId]: createLoopBlock({ id: loopId, name: 'Loop 1' }), } const blocksToCopy = { [childId]: createAgentBlock({ id: childId, name: 'Agent 1', position: { x: 100, y: 50 }, data: { parentId: loopId, extent: 'parent' }, }), } const largeViewportOffset = { x: 2000, y: 1500 } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, largeViewportOffset, existingBlocks, getUniqueBlockName ) const duplicatedBlock = Object.values(result.blocks)[0] expect(duplicatedBlock.position).toEqual({ x: 280, y: 70 }) expect(duplicatedBlock.data?.parentId).toBe(loopId) }) /** * Regression: a fallback writer stamped a condition block's `conditions` * subblock `short-input`. The id remap must key on block type + subblock key * (not the drifted stored type) so the condition row ids and the outgoing * edge's sourceHandle move together — previously the handle remapped while * the row ids stayed stale, orphaning the edge. */ it('keeps condition row ids and edge handles consistent when the stored subblock type drifted', () => { const conditionId = 'condition-1' const targetId = 'target-1' const blocksToCopy = { [conditionId]: createBlock({ id: conditionId, type: 'condition', name: 'botFilter', subBlocks: { conditions: { id: 'conditions', type: 'short-input', value: JSON.stringify([ { id: `${conditionId}-if`, title: 'if', value: '' }, { id: `${conditionId}-else`, title: 'else', value: '' }, ]), }, }, }), [targetId]: createAgentBlock({ id: targetId, name: 'Agent 1' }), } const edges = [ { id: 'edge-1', source: conditionId, sourceHandle: `condition-${conditionId}-else`, target: targetId, targetHandle: 'target', }, ] as Edge[] const result = regenerateBlockIds( blocksToCopy, edges, {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newCondition = Object.values(result.blocks).find((b) => b.type === 'condition')! const newEdge = result.edges[0] const rowIds = JSON.parse(newCondition.subBlocks.conditions.value as string).map( (row: { id: string }) => row.id ) expect(rowIds).toEqual([`${newCondition.id}-if`, `${newCondition.id}-else`]) expect(newEdge.sourceHandle).toBe(`condition-${newCondition.id}-else`) }) it('should unlock pasted block when source is locked', () => { const blockId = 'block-1' const blocksToCopy = { [blockId]: createAgentBlock({ id: blockId, name: 'Locked Agent', position: { x: 100, y: 50 }, locked: true, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(1) // Pasted blocks are always unlocked so users can edit them const pastedBlock = newBlocks[0] expect(pastedBlock.locked).toBe(false) }) it('should keep pasted block unlocked when source is unlocked', () => { const blockId = 'block-1' const blocksToCopy = { [blockId]: createAgentBlock({ id: blockId, name: 'Unlocked Agent', position: { x: 100, y: 50 }, locked: false, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(1) const pastedBlock = newBlocks[0] expect(pastedBlock.locked).toBe(false) }) it('should unlock all pasted blocks regardless of source locked state', () => { const lockedId = 'locked-1' const unlockedId = 'unlocked-1' const blocksToCopy = { [lockedId]: createAgentBlock({ id: lockedId, name: 'Originally Locked Agent', position: { x: 100, y: 50 }, locked: true, }), [unlockedId]: createFunctionBlock({ id: unlockedId, name: 'Originally Unlocked Function', position: { x: 200, y: 50 }, locked: false, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(2) for (const block of newBlocks) { expect(block.locked).toBe(false) } }) it('should preserve original name when no conflicting block exists', () => { const blockId = 'block-1' const blocksToCopy = { [blockId]: createAgentBlock({ id: blockId, name: 'Agent 1', position: { x: 100, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) expect(newBlocks).toHaveLength(1) expect(newBlocks[0].name).toBe('Agent 1') }) it('should preserve original name with number suffix when no conflict', () => { const blocksToCopy = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 3', position: { x: 100, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) expect(Object.values(result.blocks)[0].name).toBe('Agent 3') }) it('should increment name when an exact match exists in destination', () => { const existingBlocks = { existing: createAgentBlock({ id: 'existing', name: 'Agent 1' }), } const blocksToCopy = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 1', position: { x: 100, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, existingBlocks, getUniqueBlockName ) expect(Object.values(result.blocks)[0].name).toBe('Agent 2') }) it('should preserve name when only a different-numbered sibling exists', () => { const existingBlocks = { existing: createAgentBlock({ id: 'existing', name: 'Agent 2' }), } const blocksToCopy = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 5', position: { x: 100, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, existingBlocks, getUniqueBlockName ) expect(Object.values(result.blocks)[0].name).toBe('Agent 5') }) it('should preserve names for multiple blocks when no conflicts', () => { const blocksToCopy = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 1', position: { x: 100, y: 100 }, }), 'block-2': createFunctionBlock({ id: 'block-2', name: 'Function 3', position: { x: 200, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) const agentBlock = newBlocks.find((b) => b.type === 'agent') const functionBlock = newBlocks.find((b) => b.type === 'function') expect(agentBlock!.name).toBe('Agent 1') expect(functionBlock!.name).toBe('Function 3') }) it('should handle mixed conflicts: preserve non-conflicting, increment conflicting', () => { const existingBlocks = { existing: createAgentBlock({ id: 'existing', name: 'Agent 1' }), } const blocksToCopy = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 1', position: { x: 100, y: 100 }, }), 'block-2': createFunctionBlock({ id: 'block-2', name: 'Function 1', position: { x: 200, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, existingBlocks, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) const agentBlock = newBlocks.find((b) => b.type === 'agent') const functionBlock = newBlocks.find((b) => b.type === 'function') expect(agentBlock!.name).toBe('Agent 2') expect(functionBlock!.name).toBe('Function 1') }) it('should detect conflicts case-insensitively', () => { const existingBlocks = { existing: createBlock({ id: 'existing', name: 'api 1' }), } const blocksToCopy = { 'block-1': createBlock({ id: 'block-1', name: 'API 1', position: { x: 100, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, existingBlocks, getUniqueBlockName ) expect(Object.values(result.blocks)[0].name).toBe('API 2') }) it('should preserve name without number suffix when no conflict', () => { const blocksToCopy = { 'block-1': createBlock({ id: 'block-1', name: 'Custom Block', position: { x: 100, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) expect(Object.values(result.blocks)[0].name).toBe('Custom Block') }) it('should avoid collisions between pasted blocks themselves', () => { const blocksToCopy = { 'block-1': createAgentBlock({ id: 'block-1', name: 'Agent 1', position: { x: 100, y: 100 }, }), 'block-2': createAgentBlock({ id: 'block-2', name: 'Agent 1', position: { x: 200, y: 100 }, }), } const result = regenerateBlockIds( blocksToCopy, [], {}, {}, {}, positionOffset, {}, getUniqueBlockName ) const newBlocks = Object.values(result.blocks) const names = newBlocks.map((b) => b.name) expect(names).toHaveLength(2) expect(new Set(names).size).toBe(2) expect(names).toContain('Agent 1') expect(names).toContain('Agent 2') }) }) describe('regenerateBlockIds — cloned webhook path', () => { const positionOffset = { x: 50, y: 50 } const sourceId = 'webhook-source' const deployedPath = 'webhook-source' function pasteOne(block: Partial, values?: Record) { const blocks = { [sourceId]: createBlock({ id: sourceId, ...block }) } const result = regenerateBlockIds( blocks, [], {}, {}, values ? { [sourceId]: values } : {}, positionOffset, {}, getUniqueBlockName ) return { newId: Object.keys(result.blocks)[0], result } } /** * The reported bug. A Webhook Trigger added from the toolbar carries `triggerMode: true` * (confirmed against production rows), and after a deploy its `triggerPath` holds the registered * path — its own block id. A clone that copies that value renders the SOURCE's URL. */ it('clears triggerPath on a pasted webhook trigger (triggerMode true)', () => { const { newId, result } = pasteOne( { type: 'generic_webhook', name: 'Webhook 1', triggerMode: true, subBlocks: { triggerPath: { id: 'triggerPath', type: 'short-input', value: deployedPath }, }, }, { triggerPath: deployedPath } ) expect(newId).not.toBe(sourceId) // Both sources must be cleared: the value map overrides the structure in mergeSubblockState. expect(result.blocks[newId].subBlocks.triggerPath?.value).toBeNull() expect(result.subBlockValues[newId].triggerPath).toBeNull() }) /** Rows written by the API/import path can carry `triggerMode: false`; same requirement. */ it('clears triggerPath on a pasted webhook trigger (triggerMode false)', () => { const { newId, result } = pasteOne( { type: 'generic_webhook', name: 'Webhook 1', triggerMode: false, subBlocks: { triggerPath: { id: 'triggerPath', type: 'short-input', value: deployedPath }, }, }, { triggerPath: deployedPath } ) expect(result.blocks[newId].subBlocks.triggerPath?.value).toBeNull() expect(result.subBlockValues[newId].triggerPath).toBeNull() }) it('clears it when the path lives only in the value map', () => { const { newId, result } = pasteOne( { type: 'generic_webhook', name: 'Webhook 1', triggerMode: true, subBlocks: {} }, { triggerPath: deployedPath } ) expect(result.subBlockValues[newId].triggerPath).toBeNull() }) it('clears it when the block has no value-map entry at all', () => { const { newId, result } = pasteOne({ type: 'generic_webhook', name: 'Webhook 1', triggerMode: true, subBlocks: { triggerPath: { id: 'triggerPath', type: 'short-input', value: deployedPath }, }, }) expect(result.blocks[newId].subBlocks.triggerPath?.value).toBeNull() }) /** * `webhookId` is a user-entered action field on Attio, Vercel, and Discord — and Attio/Vercel are * trigger-capable, so any predicate keyed on trigger-ness would wipe it in trigger mode. It is * deliberately NOT cleared: nothing reads it as trigger state. */ it('preserves a user-entered webhookId on a trigger-capable block in trigger mode', () => { const { newId, result } = pasteOne( { type: 'attio', name: 'Attio 1', triggerMode: true, subBlocks: { webhookId: { id: 'webhookId', type: 'short-input', value: 'attio-wh-42' }, }, }, { webhookId: 'attio-wh-42' } ) expect(result.blocks[newId].subBlocks.webhookId?.value).toBe('attio-wh-42') expect(result.subBlockValues[newId].webhookId).toBe('attio-wh-42') }) it('preserves a user-entered webhookId on an action block', () => { const { newId, result } = pasteOne( { type: 'discord', name: 'Discord 1', triggerMode: false, subBlocks: { webhookId: { id: 'webhookId', type: 'short-input', value: '1234567890' }, webhookToken: { id: 'webhookToken', type: 'short-input', value: 'tok_abc' }, }, }, { webhookId: '1234567890', webhookToken: 'tok_abc' } ) expect(result.subBlockValues[newId].webhookId).toBe('1234567890') expect(result.subBlockValues[newId].webhookToken).toBe('tok_abc') }) /** Trigger configuration is user setup and must survive the copy. */ it('preserves trigger configuration on a cloned trigger block', () => { const { newId, result } = pasteOne( { type: 'generic_webhook', name: 'Webhook 1', triggerMode: true, subBlocks: { triggerPath: { id: 'triggerPath', type: 'short-input', value: deployedPath }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { labelIds: ['a'] } }, triggerId: { id: 'triggerId', type: 'short-input', value: 'generic_webhook' }, token: { id: 'token', type: 'short-input', value: 'user-secret' }, }, }, { triggerPath: deployedPath, triggerConfig: { labelIds: ['a'] }, triggerId: 'generic_webhook', token: 'user-secret', } ) expect(result.subBlockValues[newId].triggerPath).toBeNull() expect(result.subBlockValues[newId].triggerConfig).toEqual({ labelIds: ['a'] }) expect(result.subBlockValues[newId].triggerId).toBe('generic_webhook') expect(result.subBlockValues[newId].token).toBe('user-secret') }) })