/** * Tests for workflow change detection comparison logic */ import { createBlock as createTestBlock, createWorkflowState as createTestWorkflowState, } from '@sim/testing' import { describe, expect, it } from 'vitest' import type { WorkflowState } from '@/stores/workflows/workflow/types' import { formatDiffSummaryForDescription, generateWorkflowDiffSummary, hasWorkflowChanged, } from './compare' /** * Type helper for converting test workflow state to app workflow state. */ function asAppState(state: T): WorkflowState { return state as unknown as WorkflowState } /** * Helper to create a minimal valid workflow state using @sim/testing factory. */ function createWorkflowState(overrides: Partial = {}): WorkflowState { return asAppState(createTestWorkflowState(overrides as any)) } /** * Helper to create a block with common fields using @sim/testing factory. */ function createBlock(id: string, overrides: Record = {}): any { return createTestBlock({ id, name: overrides.name ?? `Block ${id}`, type: overrides.type ?? 'agent', position: overrides.position ?? { x: 100, y: 100 }, subBlocks: overrides.subBlocks ?? {}, outputs: overrides.outputs ?? {}, enabled: overrides.enabled ?? true, horizontalHandles: overrides.horizontalHandles ?? true, advancedMode: overrides.advancedMode ?? false, height: overrides.height ?? 200, ...overrides, }) } describe('hasWorkflowChanged', () => { describe('Basic Cases', () => { it.concurrent('should return true when deployedState is null', () => { const currentState = createWorkflowState() expect(hasWorkflowChanged(currentState, null)).toBe(true) }) it.concurrent('should return false for identical empty states', () => { const state1 = createWorkflowState() const state2 = createWorkflowState() expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should return false for identical states with blocks', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello' } } }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello' } } }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) }) describe('Position and Layout Changes (Should Not Trigger Change)', () => { it.concurrent('should ignore position changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { position: { x: 0, y: 0 } }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { position: { x: 500, y: 500 } }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should ignore layout changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { layout: { measuredWidth: 100, measuredHeight: 200 } }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { layout: { measuredWidth: 300, measuredHeight: 400 } }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should ignore height changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { height: 100 }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { height: 500 }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should ignore width/height in data object', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { data: { width: 100, height: 200, name: 'test' } }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { data: { width: 300, height: 400, name: 'test' } }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should ignore multiple visual-only changes combined', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { position: { x: 0, y: 0 }, layout: { measuredWidth: 100, measuredHeight: 200 }, height: 100, data: { width: 100, height: 200, name: 'test' }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { position: { x: 999, y: 999 }, layout: { measuredWidth: 999, measuredHeight: 999 }, height: 999, data: { width: 999, height: 999, name: 'test' }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) }) describe('Edge Changes', () => { it.concurrent('should detect added edges', () => { const state1 = createWorkflowState({ edges: [] }) const state2 = createWorkflowState({ edges: [{ id: 'edge1', source: 'block1', target: 'block2' }], }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect removed edges', () => { const state1 = createWorkflowState({ edges: [{ id: 'edge1', source: 'block1', target: 'block2' }], }) const state2 = createWorkflowState({ edges: [] }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect changed edge connections', () => { const state1 = createWorkflowState({ edges: [{ id: 'edge1', source: 'block1', target: 'block2' }], }) const state2 = createWorkflowState({ edges: [{ id: 'edge1', source: 'block1', target: 'block3' }], }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect changed edge handles', () => { const state1 = createWorkflowState({ edges: [{ id: 'edge1', source: 'block1', sourceHandle: 'out1', target: 'block2' }], }) const state2 = createWorkflowState({ edges: [{ id: 'edge1', source: 'block1', sourceHandle: 'out2', target: 'block2' }], }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should ignore edge ID changes', () => { const state1 = createWorkflowState({ edges: [{ id: 'edge-old', source: 'block1', target: 'block2' }], }) const state2 = createWorkflowState({ edges: [{ id: 'edge-new', source: 'block1', target: 'block2' }], }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should ignore edge order differences', () => { const state1 = createWorkflowState({ edges: [ { id: 'edge1', source: 'a', target: 'b' }, { id: 'edge2', source: 'b', target: 'c' }, { id: 'edge3', source: 'c', target: 'd' }, ], }) const state2 = createWorkflowState({ edges: [ { id: 'edge3', source: 'c', target: 'd' }, { id: 'edge1', source: 'a', target: 'b' }, { id: 'edge2', source: 'b', target: 'c' }, ], }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should ignore non-functional edge properties', () => { const state1 = createWorkflowState({ edges: [ { id: 'edge1', source: 'block1', target: 'block2', type: 'smoothstep', animated: true, style: { stroke: 'red' }, }, ], }) const state2 = createWorkflowState({ edges: [ { id: 'edge1', source: 'block1', target: 'block2', type: 'bezier', animated: false, style: { stroke: 'blue' }, }, ], }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) }) describe('Block Changes', () => { it.concurrent('should detect added blocks', () => { const state1 = createWorkflowState({ blocks: {} }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1') }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect removed blocks', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1') }, }) const state2 = createWorkflowState({ blocks: {} }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect block type changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'agent' }) }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'function' }) }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect block name changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { name: 'Original Name' }) }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { name: 'Changed Name' }) }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect enabled/disabled changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { enabled: true }) }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { enabled: false }) }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should not detect locked/unlocked toggle as a workflow change', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { locked: false }) }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { locked: true }) }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should not detect changes when locked state is the same', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { locked: true }) }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { locked: true }) }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should not include locked changes in diff summary', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { locked: false }) }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { locked: true }) }, }) const summary = generateWorkflowDiffSummary(state2, state1) expect(summary.hasChanges).toBe(false) expect(summary.modifiedBlocks).toEqual([]) }) }) describe('SubBlock Changes', () => { it.concurrent('should detect subBlock value changes (string)', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello world' } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Goodbye world' } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect subBlock value changes (number)', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { temperature: { value: 0.7 } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { temperature: { value: 0.9 } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect subBlock value changes (boolean)', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { stream: { value: true } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { stream: { value: false } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect subBlock value changes (object)', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { config: { value: { model: 'gpt-4', temp: 0.7 } } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { config: { value: { model: 'gpt-4o', temp: 0.7 } } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect added subBlocks', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello' } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello' }, model: { value: 'gpt-4' }, }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect removed subBlocks', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello' }, model: { value: 'gpt-4' }, }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello' } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should ignore subBlock type changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { type: 'short-input', value: 'Hello' } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { type: 'long-input', value: 'Hello' } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should handle null/undefined subBlock values consistently', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: null } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: undefined } }, }), }, }) // Both should be treated as null expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should detect empty string vs null difference', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: '' } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: null } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) }) describe('Tools SubBlock Special Handling', () => { it.concurrent('should ignore isExpanded field in tools', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { id: 'tool1', name: 'Search', isExpanded: true }, { id: 'tool2', name: 'Calculator', isExpanded: false }, ], }, }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { id: 'tool1', name: 'Search', isExpanded: false }, { id: 'tool2', name: 'Calculator', isExpanded: true }, ], }, }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should detect actual tool changes despite isExpanded', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [{ id: 'tool1', name: 'Search', isExpanded: true }], }, }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { id: 'tool1', name: 'Web Search', isExpanded: true }, // Changed name ], }, }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect tool count changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [{ id: 'tool1', name: 'Search' }], }, }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { id: 'tool1', name: 'Search' }, { id: 'tool2', name: 'Calculator' }, ], }, }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) }) describe('Block-based tool params (synthetic projection)', () => { const knowledgeTool = (kbId: string) => ({ type: 'knowledge', toolId: 'knowledge_search', operation: 'search', params: { knowledgeBaseSelector: kbId }, usageControl: 'auto', }) it.concurrent('ignores synthetic tool subblocks present only in the current state', () => { const current = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [knowledgeTool('kb-123')] }, 'tools-tool-0-knowledgeBaseSelector': { value: 'kb-123' }, }, }), }, }) const deployed = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [knowledgeTool('kb-123')] } }, }), }, }) expect(hasWorkflowChanged(current, deployed)).toBe(false) }) it.concurrent('detects an actual change to a tool param value', () => { const current = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [knowledgeTool('kb-999')] }, 'tools-tool-0-knowledgeBaseSelector': { value: 'kb-999' }, }, }), }, }) const deployed = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [knowledgeTool('kb-123')] } }, }), }, }) expect(hasWorkflowChanged(current, deployed)).toBe(true) }) it.concurrent('treats a cleared tool param as a change (deselect symptom)', () => { const current = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [knowledgeTool('')] } }, }), }, }) const deployed = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [knowledgeTool('kb-123')] } }, }), }, }) expect(hasWorkflowChanged(current, deployed)).toBe(true) }) it.concurrent('ignores synthetic subblocks for object-typed tool params', () => { const fileTool = { type: 'someservice', toolId: 'someservice_upload', operation: 'upload', params: { file: '{"name":"a.pdf"}' }, usageControl: 'auto', } const current = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [fileTool] }, 'tools-tool-0-file': { value: { name: 'a.pdf' } }, }, }), }, }) const deployed = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [fileTool] } }, }), }, }) expect(hasWorkflowChanged(current, deployed)).toBe(false) }) }) describe('InputFormat SubBlock Special Handling', () => { it.concurrent('should ignore collapsed field but detect value changes in inputFormat', () => { // Only collapsed changes - should NOT detect as change const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [ { id: 'input1', name: 'Name', value: 'John', collapsed: true }, { id: 'input2', name: 'Age', value: 25, collapsed: false }, ], }, }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [ { id: 'input1', name: 'Name', value: 'John', collapsed: false }, { id: 'input2', name: 'Age', value: 25, collapsed: true }, ], }, }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should detect value changes in inputFormat', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'input1', name: 'Name', value: 'John' }], }, }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'input1', name: 'Name', value: 'Jane' }], }, }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect actual inputFormat changes', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'input1', name: 'Name', type: 'string' }], }, }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'input1', name: 'Name', type: 'number' }], // Changed type }, }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) }) describe('Loop Changes', () => { it.concurrent('should detect added loops', () => { const state1 = createWorkflowState({ loops: {} }) const state2 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5 }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect removed loops', () => { const state1 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5 }, }, }) const state2 = createWorkflowState({ loops: {} }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect loop iteration changes', () => { const state1 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5 }, }, }) const state2 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 10 }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect loop type changes', () => { const state1 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5 }, }, }) const state2 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'forEach', forEachItems: '[]', iterations: 0, }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect loop nodes changes', () => { const state1 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5 }, }, }) const state2 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1', 'block2'], loopType: 'for', iterations: 5 }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect forEach items changes', () => { const state1 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'forEach', forEachItems: '', iterations: 0, }, }, }) const state2 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'forEach', forEachItems: '', iterations: 0, }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect while condition changes', () => { const state1 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'while', whileCondition: ' < 10', iterations: 0, }, }, }) const state2 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'while', whileCondition: ' < 20', iterations: 0, }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should ignore irrelevant loop fields', () => { const state1 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5, forEachItems: 'should-be-ignored', whileCondition: 'should-be-ignored', }, }, }) const state2 = createWorkflowState({ loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5, forEachItems: 'different-value', whileCondition: 'different-condition', }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) }) describe('Parallel Changes', () => { it.concurrent('should detect added parallels', () => { const state1 = createWorkflowState({ parallels: {} }) const state2 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'count', count: 3 }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect removed parallels', () => { const state1 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'count', count: 3 }, }, }) const state2 = createWorkflowState({ parallels: {} }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect parallel count changes', () => { const state1 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'count', count: 3 }, }, }) const state2 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'count', count: 5 }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect parallel type changes', () => { const state1 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'count', count: 3 }, }, }) const state2 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'collection', distribution: '', }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect parallel distribution changes', () => { const state1 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'collection', distribution: '', }, }, }) const state2 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'collection', distribution: '', }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should ignore irrelevant parallel fields', () => { const state1 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'count', count: 3, distribution: 'should-be-ignored', }, }, }) const state2 = createWorkflowState({ parallels: { parallel1: { id: 'parallel1', nodes: ['block1'], parallelType: 'count', count: 3, distribution: 'different-value', }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) }) describe('Complex Scenarios', () => { it.concurrent( 'should handle complex workflow with multiple blocks, edges, loops, and parallels', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { position: { x: 0, y: 0 }, subBlocks: { prompt: { value: 'Hello' } }, }), block2: createBlock('block2', { position: { x: 200, y: 0 }, subBlocks: { model: { value: 'gpt-4' } }, }), block3: createBlock('block3', { position: { x: 400, y: 0 }, subBlocks: { temperature: { value: 0.7 } }, }), }, edges: [ { id: 'edge1', source: 'block1', target: 'block2' }, { id: 'edge2', source: 'block2', target: 'block3' }, ], loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5 }, }, parallels: { parallel1: { id: 'parallel1', nodes: ['block2', 'block3'], parallelType: 'count', count: 3, }, }, }) // Same workflow with different positions const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { position: { x: 100, y: 100 }, subBlocks: { prompt: { value: 'Hello' } }, }), block2: createBlock('block2', { position: { x: 300, y: 100 }, subBlocks: { model: { value: 'gpt-4' } }, }), block3: createBlock('block3', { position: { x: 500, y: 100 }, subBlocks: { temperature: { value: 0.7 } }, }), }, edges: [ { id: 'edge2', source: 'block2', target: 'block3' }, { id: 'edge1', source: 'block1', target: 'block2' }, ], loops: { loop1: { id: 'loop1', nodes: ['block1'], loopType: 'for', iterations: 5 }, }, parallels: { parallel1: { id: 'parallel1', nodes: ['block2', 'block3'], parallelType: 'count', count: 3, }, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) } ) it.concurrent('should detect even small text changes in prompts', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'You are a helpful assistant.' } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'You are a helpful assistant' } }, // Missing period }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should detect whitespace changes in text', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello World' } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello World' } }, // Extra space }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should handle empty vs missing blocks/edges/loops/parallels', () => { const state1 = createWorkflowState({ blocks: {}, edges: [], loops: {}, parallels: {}, }) const state2 = createWorkflowState() expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should handle object key order differences in subBlock values', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { config: { value: { model: 'gpt-4', temperature: 0.7, maxTokens: 1000 } }, }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { config: { value: { maxTokens: 1000, model: 'gpt-4', temperature: 0.7 } }, }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) }) describe('Edge Cases', () => { it.concurrent('should handle undefined blocks in state', () => { const state1 = { edges: [], loops: {}, parallels: {} } as unknown as WorkflowState const state2 = createWorkflowState() expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should handle undefined edges in state', () => { const state1 = { blocks: {}, loops: {}, parallels: {} } as unknown as WorkflowState const state2 = createWorkflowState() expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should handle undefined loops in state', () => { const state1 = { blocks: {}, edges: [], parallels: {} } as unknown as WorkflowState const state2 = createWorkflowState() expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should handle undefined parallels in state', () => { const state1 = { blocks: {}, edges: [], loops: {} } as unknown as WorkflowState const state2 = createWorkflowState() expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should handle blocks with no subBlocks', () => { const state1 = createWorkflowState({ blocks: { block1: { id: 'block1', name: 'Test', type: 'agent', position: { x: 0, y: 0 }, } as any, }, }) const state2 = createWorkflowState({ blocks: { block1: { id: 'block1', name: 'Test', type: 'agent', position: { x: 100, y: 100 }, subBlocks: {}, } as any, }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should handle very long string values', () => { const longString1 = 'a'.repeat(10000) const longString2 = `${'a'.repeat(9999)}b` const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: longString1 } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: longString2 } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) it.concurrent('should handle deeply nested subBlock values', () => { const deepNested = { level1: { level2: { level3: { level4: { level5: { value: 'deep' }, }, }, }, }, } const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { config: { value: deepNested } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { config: { value: { ...deepNested } } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should handle array subBlock values', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { items: { value: [1, 2, 3] } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { items: { value: [1, 2, 3] } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(false) }) it.concurrent('should detect array order differences in subBlock values', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { items: { value: [1, 2, 3] } }, }), }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { items: { value: [3, 2, 1] } }, }), }, }) expect(hasWorkflowChanged(state1, state2)).toBe(true) }) }) describe('Tool Input Scenarios', () => { it.concurrent( 'should not detect change when tool param is typed and cleared back to empty string', () => { // User adds a tool, types in a field, then clears it back to empty const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: { query: '' }, usageControl: 'auto', }, ], }, }, }), }, }) // Current state after typing and clearing const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: { query: '' }, usageControl: 'auto', }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) it.concurrent('should detect change when tool param has actual content', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: { query: '' }, usageControl: 'auto', }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: { query: 'hello' }, // Has content usageControl: 'auto', }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should not detect change when tool isExpanded toggles', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: {}, isExpanded: false, usageControl: 'auto', }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: {}, isExpanded: true, // Changed expansion state usageControl: 'auto', }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should detect change when tool usageControl changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: {}, usageControl: 'auto', }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: {}, usageControl: 'force', // Changed from auto to force }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change when tool is added then params filled', () => { // Deployed state has no tools const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [] }, }, }), }, }) // Current state has a tool with empty params (just added) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: { query: '' }, usageControl: 'auto', }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent( 'should not detect change when adding and removing tool returns to original', () => { // Original deployed state has no tools const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [] }, }, }), }, }) // User added a tool, then removed it - back to empty array const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [] }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) it.concurrent('should handle empty string vs undefined in tool params', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: { query: undefined }, usageControl: 'auto', }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: { query: '' }, // Empty string instead of undefined usageControl: 'auto', }, ], }, }, }), }, }) // This IS a meaningful difference - undefined vs empty string expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should handle missing params object vs empty params object', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', // No params property at all usageControl: 'auto', }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'google_search', params: {}, // Empty params object usageControl: 'auto', }, ], }, }, }), }, }) // Missing property vs empty object IS a difference expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect tool order changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'search', title: 'Search', toolId: 'search', usageControl: 'auto' }, { type: 'calculator', title: 'Calculator', toolId: 'calc', usageControl: 'auto' }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'calculator', title: 'Calculator', toolId: 'calc', usageControl: 'auto' }, { type: 'search', title: 'Search', toolId: 'search', usageControl: 'auto' }, ], }, }, }), }, }) // Tool order matters - affects execution order expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect operation changes in multi-operation tools', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'slack', title: 'Slack', toolId: 'slack_send_message', operation: 'send_message', params: {}, usageControl: 'auto', }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'slack', title: 'Slack', toolId: 'slack_list_channels', operation: 'list_channels', // Different operation params: {}, usageControl: 'auto', }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should handle custom tool reference vs inline definition', () => { // New format: reference only const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'custom-tool', customToolId: 'tool-123', usageControl: 'auto', }, ], }, }, }), }, }) // Same tool, same ID const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'custom-tool', customToolId: 'tool-123', usageControl: 'auto', }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should detect custom tool ID changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'custom-tool', customToolId: 'tool-123', usageControl: 'auto', }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'custom-tool', customToolId: 'tool-456', // Different tool usageControl: 'auto', }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should handle MCP tool with schema changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'mcp', title: 'MCP Tool', toolId: 'mcp-server-tool', params: { serverId: 'server-1', toolName: 'tool-1' }, schema: { properties: { input: { type: 'string' } } }, usageControl: 'auto', }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { tools: { value: [ { type: 'mcp', title: 'MCP Tool', toolId: 'mcp-server-tool', params: { serverId: 'server-1', toolName: 'tool-1' }, schema: { properties: { input: { type: 'number' } } }, // Changed schema usageControl: 'auto', }, ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) }) describe('Input Format Field Scenarios', () => { it.concurrent('should not detect change when only inputFormat collapsed changes', () => { // The "collapsed" field in inputFormat is UI-only and should be ignored const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [ { id: 'field1', name: 'Name', type: 'string', value: 'test', collapsed: false }, ], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [ { id: 'field1', name: 'Name', type: 'string', value: 'test', collapsed: true, }, ], }, }, }), }, }) // collapsed is UI-only field - should NOT detect as change expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should detect change when inputFormat value changes', () => { // The "value" field in inputFormat is meaningful and should trigger change detection const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'field1', name: 'Name', type: 'string', value: '' }], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'field1', name: 'Name', type: 'string', value: 'new value' }], }, }, }), }, }) // value changes should be detected expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change when inputFormat field name changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'field1', name: 'Name', type: 'string' }], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'field1', name: 'Full Name', type: 'string' }], // Changed name }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change when inputFormat field type changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'field1', name: 'Count', type: 'string' }], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'field1', name: 'Count', type: 'number' }], // Changed type }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change when inputFormat field is added or removed', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [{ id: 'field1', name: 'Name', type: 'string' }], }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { inputFormat: { value: [ { id: 'field1', name: 'Name', type: 'string' }, { id: 'field2', name: 'Email', type: 'string' }, // Added field ], }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) }) describe('Prompt and Text Field Scenarios', () => { it.concurrent('should not detect change when text is typed and fully deleted', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: '' } }, }), }, }) // User typed something, then selected all and deleted const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: '' } }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should detect change when there is remaining text', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Original prompt' } }, }), }, }) // User edited the prompt const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Original promp' } }, // Missing last character }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change for leading/trailing whitespace', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello' } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: ' Hello' } }, // Leading space }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change for trailing whitespace', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello' } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello ' } }, // Trailing space }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change for newline differences', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Line 1\nLine 2' } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Line 1\n\nLine 2' } }, // Extra newline }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should handle case sensitivity in text', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'Hello World' } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { value: 'hello world' } }, // Different case }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) }) describe('Model and Dropdown Scenarios', () => { it.concurrent('should detect model changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { model: { value: 'gpt-4' } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { model: { value: 'gpt-4o' } }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent( 'should not detect change when selecting then deselecting back to original', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { model: { value: 'gpt-4' } }, }), }, }) // User changed to gpt-4o, then changed back to gpt-4 const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { model: { value: 'gpt-4' } }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) it.concurrent('should detect slider value changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { temperature: { value: 0.7 } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { temperature: { value: 0.8 } }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should not detect slider change when moved back to original value', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { temperature: { value: 0.7 } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { temperature: { value: 0.7 } }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should detect boolean toggle changes', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { stream: { value: true } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { stream: { value: false } }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should not detect change when toggle is toggled back', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { stream: { value: true } }, }), }, }) // User toggled off, then toggled back on const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { stream: { value: true } }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) }) describe('Variable Changes', () => { it.concurrent('should detect added variables', () => { const deployedState = { ...createWorkflowState({}), variables: {}, } const currentState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' }, }, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true) }) it.concurrent('should detect removed variables', () => { const deployedState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' }, }, } const currentState = { ...createWorkflowState({}), variables: {}, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true) }) it.concurrent('should detect variable value changes', () => { const deployedState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' }, }, } const currentState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'string', value: 'world' }, }, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true) }) it.concurrent('should detect variable type changes', () => { const deployedState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'string', value: '123' }, }, } const currentState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'number', value: 123 }, }, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true) }) it.concurrent('should detect variable name changes', () => { const deployedState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'oldName', type: 'string', value: 'hello' }, }, } const currentState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'newName', type: 'string', value: 'hello' }, }, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true) }) it.concurrent('should not detect change for identical variables', () => { const deployedState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' }, var2: { id: 'var2', name: 'count', type: 'number', value: 42 }, }, } const currentState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' }, var2: { id: 'var2', name: 'count', type: 'number', value: 42 }, }, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false) }) it.concurrent('should not detect change for empty variables on both sides', () => { const deployedState = { ...createWorkflowState({}), variables: {}, } const currentState = { ...createWorkflowState({}), variables: {}, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false) }) it.concurrent('should not detect change for undefined vs empty object variables', () => { const deployedState = { ...createWorkflowState({}), variables: undefined, } const currentState = { ...createWorkflowState({}), variables: {}, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false) }) it.concurrent('should handle complex variable values (objects)', () => { const deployedState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'config', type: 'object', value: { key: 'value1' } }, }, } const currentState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'config', type: 'object', value: { key: 'value2' } }, }, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true) }) it.concurrent('should handle complex variable values (arrays)', () => { const deployedState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'items', type: 'array', value: [1, 2, 3] }, }, } const currentState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'items', type: 'array', value: [1, 2, 4] }, }, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(true) }) it.concurrent('should not detect change when variable key order differs', () => { const deployedState = { ...createWorkflowState({}), variables: { var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' }, var2: { id: 'var2', name: 'count', type: 'number', value: 42 }, }, } const currentState = { ...createWorkflowState({}), variables: { var2: { id: 'var2', name: 'count', type: 'number', value: 42 }, var1: { id: 'var1', name: 'myVar', type: 'string', value: 'hello' }, }, } expect(hasWorkflowChanged(currentState as any, deployedState as any)).toBe(false) }) }) describe('Trigger Config Normalization (False Positive Prevention)', () => { it.concurrent( 'should not detect change when deployed has null fields but current has values from triggerConfig', () => { // Core scenario: deployed state has null individual fields, current state has // values populated from triggerConfig at runtime by populateTriggerFieldsFromConfig const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, botToken: { id: 'botToken', type: 'short-input', value: null }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { signingSecret: 'secret123', botToken: 'token456' }, }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' }, botToken: { id: 'botToken', type: 'short-input', value: 'token456' }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { signingSecret: 'secret123', botToken: 'token456' }, }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) it.concurrent( 'should detect change when user edits a trigger field to a different value', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { signingSecret: 'old-secret' }, }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: 'new-secret' }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { signingSecret: 'old-secret' }, }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) } ) it.concurrent('should not detect change when both sides have no triggerConfig', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent( 'should not detect change when deployed has empty fields and triggerConfig populates them', () => { // Empty string is also treated as "empty" by normalizeTriggerConfigValues const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: '' }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { signingSecret: 'secret123' }, }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { signingSecret: 'secret123' }, }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) it.concurrent('should not detect change when triggerId differs', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, triggerId: { value: null }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, triggerId: { value: 'slack_webhook' }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent( 'should not detect change for namespaced system subBlock IDs like samplePayload_slack_webhook', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, samplePayload_slack_webhook: { value: 'old payload' }, triggerInstructions_slack_webhook: { value: 'old instructions' }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, samplePayload_slack_webhook: { value: 'new payload' }, triggerInstructions_slack_webhook: { value: 'new instructions' }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) it.concurrent( 'should handle mixed scenario: some fields from triggerConfig, some user-edited', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: null }, botToken: { id: 'botToken', type: 'short-input', value: null }, includeFiles: { id: 'includeFiles', type: 'switch', value: false }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { signingSecret: 'secret123', botToken: 'token456' }, }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' }, botToken: { id: 'botToken', type: 'short-input', value: 'token456' }, includeFiles: { id: 'includeFiles', type: 'switch', value: true }, triggerConfig: { id: 'triggerConfig', type: 'short-input', value: { signingSecret: 'secret123', botToken: 'token456' }, }, }, }), }, }) // includeFiles changed from false to true — this IS a real change expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) } ) }) describe('Trigger Runtime Metadata (Should Not Trigger Change)', () => { it.concurrent('should not detect change when webhookId differs', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, webhookId: { value: null }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should not detect change when triggerPath differs', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, triggerPath: { value: '' }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, triggerPath: { value: '/api/webhooks/abc123' }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should not detect change when all runtime metadata differs', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, webhookId: { value: null }, triggerPath: { value: '' }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, triggerPath: { value: '/api/webhooks/abc123' }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent( 'should detect change when actual config differs but runtime metadata also differs', () => { // Test that when a real config field changes along with runtime metadata, // the change is still detected. Using 'model' as the config field since // triggerConfig is now excluded from comparison (individual trigger fields // are compared separately). const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, webhookId: { value: null }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4o' }, webhookId: { value: 'wh_123456' }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) } ) it.concurrent( 'should not detect change when triggerConfig differs (individual fields compared separately)', () => { // triggerConfig is excluded from comparison because: // 1. Individual trigger fields are stored as separate subblocks and compared individually // 2. The client populates triggerConfig with default values from trigger definitions, // which aren't present in the deployed state, causing false positive change detection const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { triggerConfig: { value: { event: 'push' } }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { triggerConfig: { value: { event: 'pull_request', extraField: true } }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) it.concurrent( 'should not detect change when runtime metadata is added to current state', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, webhookId: { value: 'wh_123456' }, triggerPath: { value: '/api/webhooks/abc123' }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) it.concurrent( 'should not detect change when runtime metadata is removed from current state', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, webhookId: { value: 'wh_old123' }, triggerPath: { value: '/api/webhooks/old' }, }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { type: 'starter', subBlocks: { model: { value: 'gpt-4' }, }, }), }, }) expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) } ) }) describe('Variables (UI-only fields should not trigger change)', () => { it.concurrent('should not detect change when validationError differs', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(deployedState as any).variables = { var1: { id: 'var1', workflowId: 'workflow1', name: 'myVar', type: 'plain', value: 'test', }, } const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(currentState as any).variables = { var1: { id: 'var1', workflowId: 'workflow1', name: 'myVar', type: 'plain', value: 'test', validationError: undefined, }, } expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should not detect change when validationError has value vs missing', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(deployedState as any).variables = { var1: { id: 'var1', workflowId: 'workflow1', name: 'myVar', type: 'number', value: 'invalid', }, } const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(currentState as any).variables = { var1: { id: 'var1', workflowId: 'workflow1', name: 'myVar', type: 'number', value: 'invalid', validationError: 'Not a valid number', }, } expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) it.concurrent('should detect change when variable value differs', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(deployedState as any).variables = { var1: { id: 'var1', workflowId: 'workflow1', name: 'myVar', type: 'plain', value: 'old value', }, } const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(currentState as any).variables = { var1: { id: 'var1', workflowId: 'workflow1', name: 'myVar', type: 'plain', value: 'new value', validationError: undefined, }, } expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change when variable is added', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(deployedState as any).variables = {} const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(currentState as any).variables = { var1: { id: 'var1', workflowId: 'workflow1', name: 'myVar', type: 'plain', value: 'test', }, } expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should detect change when variable is removed', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(deployedState as any).variables = { var1: { id: 'var1', workflowId: 'workflow1', name: 'myVar', type: 'plain', value: 'test', }, } const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(currentState as any).variables = {} expect(hasWorkflowChanged(currentState, deployedState)).toBe(true) }) it.concurrent('should not detect change when empty array vs empty object', () => { const deployedState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(deployedState as any).variables = [] const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), }, }) ;(currentState as any).variables = {} expect(hasWorkflowChanged(currentState, deployedState)).toBe(false) }) }) }) describe('generateWorkflowDiffSummary', () => { describe('Basic Cases', () => { it.concurrent('should return hasChanges=true when previousState is null', () => { const currentState = createWorkflowState({ blocks: { block1: createBlock('block1') }, }) const result = generateWorkflowDiffSummary(currentState, null) expect(result.hasChanges).toBe(true) expect(result.addedBlocks).toHaveLength(1) expect(result.addedBlocks[0].id).toBe('block1') }) it.concurrent('should return hasChanges=false for identical states', () => { const state = createWorkflowState({ blocks: { block1: createBlock('block1') }, }) const result = generateWorkflowDiffSummary(state, state) expect(result.hasChanges).toBe(false) expect(result.addedBlocks).toHaveLength(0) expect(result.removedBlocks).toHaveLength(0) expect(result.modifiedBlocks).toHaveLength(0) }) }) describe('Block Changes', () => { it.concurrent('should detect added blocks', () => { const previousState = createWorkflowState({ blocks: { block1: createBlock('block1') }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), block2: createBlock('block2'), }, }) const result = generateWorkflowDiffSummary(currentState, previousState) expect(result.hasChanges).toBe(true) expect(result.addedBlocks).toHaveLength(1) expect(result.addedBlocks[0].id).toBe('block2') }) it.concurrent('should detect removed blocks', () => { const previousState = createWorkflowState({ blocks: { block1: createBlock('block1'), block2: createBlock('block2'), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1') }, }) const result = generateWorkflowDiffSummary(currentState, previousState) expect(result.hasChanges).toBe(true) expect(result.removedBlocks).toHaveLength(1) expect(result.removedBlocks[0].id).toBe('block2') }) it.concurrent('should detect modified blocks with field changes', () => { const previousState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { model: { id: 'model', type: 'dropdown', value: 'gpt-4o' } }, }), }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { model: { id: 'model', type: 'dropdown', value: 'claude-sonnet' } }, }), }, }) const result = generateWorkflowDiffSummary(currentState, previousState) expect(result.hasChanges).toBe(true) expect(result.modifiedBlocks).toHaveLength(1) expect(result.modifiedBlocks[0].id).toBe('block1') expect(result.modifiedBlocks[0].changes.length).toBeGreaterThan(0) const modelChange = result.modifiedBlocks[0].changes.find((c) => c.field === 'model') expect(modelChange).toBeDefined() expect(modelChange?.oldValue).toBe('gpt-4o') expect(modelChange?.newValue).toBe('claude-sonnet') }) }) describe('Edge Changes', () => { it.concurrent('should detect added edges', () => { const previousState = createWorkflowState({ blocks: { block1: createBlock('block1'), block2: createBlock('block2'), }, edges: [], }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), block2: createBlock('block2'), }, edges: [{ id: 'e1', source: 'block1', target: 'block2' }], }) const result = generateWorkflowDiffSummary(currentState, previousState) expect(result.hasChanges).toBe(true) expect(result.edgeChanges.added).toBe(1) expect(result.edgeChanges.removed).toBe(0) }) it.concurrent('should detect removed edges', () => { const previousState = createWorkflowState({ blocks: { block1: createBlock('block1'), block2: createBlock('block2'), }, edges: [{ id: 'e1', source: 'block1', target: 'block2' }], }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1'), block2: createBlock('block2'), }, edges: [], }) const result = generateWorkflowDiffSummary(currentState, previousState) expect(result.hasChanges).toBe(true) expect(result.edgeChanges.added).toBe(0) expect(result.edgeChanges.removed).toBe(1) }) }) describe('Variable Changes', () => { it.concurrent('should detect added variables', () => { const previousState = createWorkflowState({ blocks: { block1: createBlock('block1') }, variables: {}, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1') }, variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'hello' } }, }) const result = generateWorkflowDiffSummary(currentState, previousState) expect(result.hasChanges).toBe(true) expect(result.variableChanges.added).toBe(1) }) it.concurrent('should detect modified variables', () => { const previousState = createWorkflowState({ blocks: { block1: createBlock('block1') }, variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'hello' } }, }) const currentState = createWorkflowState({ blocks: { block1: createBlock('block1') }, variables: { var1: { id: 'var1', name: 'test', type: 'string', value: 'world' } }, }) const result = generateWorkflowDiffSummary(currentState, previousState) expect(result.hasChanges).toBe(true) expect(result.variableChanges.modified).toBe(1) }) }) describe('Consistency with hasWorkflowChanged', () => { it.concurrent('hasChanges should match hasWorkflowChanged result', () => { const state1 = createWorkflowState({ blocks: { block1: createBlock('block1') }, }) const state2 = createWorkflowState({ blocks: { block1: createBlock('block1', { subBlocks: { prompt: { id: 'prompt', type: 'long-input', value: 'new value' } }, }), }, }) const diffResult = generateWorkflowDiffSummary(state2, state1) const hasChangedResult = hasWorkflowChanged(state2, state1) expect(diffResult.hasChanges).toBe(hasChangedResult) }) it.concurrent('should return same result as hasWorkflowChanged for no changes', () => { const state = createWorkflowState({ blocks: { block1: createBlock('block1') }, }) const diffResult = generateWorkflowDiffSummary(state, state) const hasChangedResult = hasWorkflowChanged(state, state) expect(diffResult.hasChanges).toBe(hasChangedResult) expect(diffResult.hasChanges).toBe(false) }) }) }) describe('formatDiffSummaryForDescription', () => { it.concurrent('should return no changes message when hasChanges is false', () => { const summary = { addedBlocks: [], removedBlocks: [], modifiedBlocks: [], edgeChanges: { added: 0, removed: 0 }, loopChanges: { added: 0, removed: 0, modified: 0 }, parallelChanges: { added: 0, removed: 0, modified: 0 }, variableChanges: { added: 0, removed: 0, modified: 0 }, hasChanges: false, } const result = formatDiffSummaryForDescription(summary) expect(result).toContain('No structural changes') }) it.concurrent('should format added blocks', () => { const summary = { addedBlocks: [{ id: 'block1', type: 'agent', name: 'My Agent' }], removedBlocks: [], modifiedBlocks: [], edgeChanges: { added: 0, removed: 0 }, loopChanges: { added: 0, removed: 0, modified: 0 }, parallelChanges: { added: 0, removed: 0, modified: 0 }, variableChanges: { added: 0, removed: 0, modified: 0 }, hasChanges: true, } const result = formatDiffSummaryForDescription(summary) expect(result).toContain('Added block: My Agent (agent)') }) it.concurrent('should format removed blocks', () => { const summary = { addedBlocks: [], removedBlocks: [{ id: 'block1', type: 'function', name: 'Old Function' }], modifiedBlocks: [], edgeChanges: { added: 0, removed: 0 }, loopChanges: { added: 0, removed: 0, modified: 0 }, parallelChanges: { added: 0, removed: 0, modified: 0 }, variableChanges: { added: 0, removed: 0, modified: 0 }, hasChanges: true, } const result = formatDiffSummaryForDescription(summary) expect(result).toContain('Removed block: Old Function (function)') }) it.concurrent('should format modified blocks with field changes', () => { const summary = { addedBlocks: [], removedBlocks: [], modifiedBlocks: [ { id: 'block1', type: 'agent', name: 'Agent 1', changes: [{ field: 'model', oldValue: 'gpt-4o', newValue: 'claude-sonnet' }], }, ], edgeChanges: { added: 0, removed: 0 }, loopChanges: { added: 0, removed: 0, modified: 0 }, parallelChanges: { added: 0, removed: 0, modified: 0 }, variableChanges: { added: 0, removed: 0, modified: 0 }, hasChanges: true, } const result = formatDiffSummaryForDescription(summary) expect(result).toContain('Modified Agent 1') expect(result).toContain('model') expect(result).toContain('gpt-4o') expect(result).toContain('claude-sonnet') }) it.concurrent('should format edge changes', () => { const summary = { addedBlocks: [], removedBlocks: [], modifiedBlocks: [], edgeChanges: { added: 2, removed: 1 }, loopChanges: { added: 0, removed: 0, modified: 0 }, parallelChanges: { added: 0, removed: 0, modified: 0 }, variableChanges: { added: 0, removed: 0, modified: 0 }, hasChanges: true, } const result = formatDiffSummaryForDescription(summary) expect(result).toContain('Added 2 connection(s)') expect(result).toContain('Removed 1 connection(s)') }) it.concurrent('should format variable changes', () => { const summary = { addedBlocks: [], removedBlocks: [], modifiedBlocks: [], edgeChanges: { added: 0, removed: 0 }, loopChanges: { added: 0, removed: 0 }, parallelChanges: { added: 0, removed: 0 }, variableChanges: { added: 1, removed: 0, modified: 2 }, hasChanges: true, } const result = formatDiffSummaryForDescription(summary) expect(result).toContain('Variables:') expect(result).toContain('1 added') expect(result).toContain('2 modified') }) })