Skip to content

Commit 5344f6c

Browse files
committed
fix normalization logic
1 parent 8cce371 commit 5344f6c

File tree

4 files changed

+283
-174
lines changed

4 files changed

+283
-174
lines changed

apps/sim/lib/workflows/comparison/compare.ts

Lines changed: 32 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,18 @@
1-
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
2-
import { SYSTEM_SUBBLOCK_IDS, TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
1+
import type { WorkflowState } from '@/stores/workflows/workflow/types'
32
import {
3+
extractBlockFieldsForComparison,
4+
extractSubBlockRest,
5+
filterSubBlockIds,
46
normalizedStringify,
57
normalizeEdge,
68
normalizeLoop,
79
normalizeParallel,
10+
normalizeSubBlockValue,
811
normalizeValue,
912
normalizeVariables,
10-
sanitizeInputFormat,
11-
sanitizeTools,
1213
sanitizeVariable,
1314
} from './normalize'
1415

15-
/** Block with optional diff markers added by copilot */
16-
type BlockWithDiffMarkers = BlockState & {
17-
is_diff?: string
18-
field_diffs?: Record<string, unknown>
19-
}
20-
21-
/** SubBlock with optional diff marker */
22-
type SubBlockWithDiffMarker = {
23-
id: string
24-
type: string
25-
value: unknown
26-
is_diff?: string
27-
}
28-
2916
/**
3017
* Compare the current workflow state with the deployed state to detect meaningful changes.
3118
* Uses generateWorkflowDiffSummary internally to ensure consistent change detection.
@@ -125,51 +112,21 @@ export function generateWorkflowDiffSummary(
125112
for (const id of currentBlockIds) {
126113
if (!previousBlockIds.has(id)) continue
127114

128-
const currentBlock = currentBlocks[id] as BlockWithDiffMarkers
129-
const previousBlock = previousBlocks[id] as BlockWithDiffMarkers
115+
const currentBlock = currentBlocks[id]
116+
const previousBlock = previousBlocks[id]
130117
const changes: FieldChange[] = []
131118

132-
// Compare block-level properties (excluding visual-only fields)
133-
const {
134-
position: _currentPos,
135-
subBlocks: currentSubBlocks = {},
136-
layout: _currentLayout,
137-
height: _currentHeight,
138-
outputs: _currentOutputs,
139-
is_diff: _currentIsDiff,
140-
field_diffs: _currentFieldDiffs,
141-
...currentRest
142-
} = currentBlock
143-
119+
// Use shared helpers for block field extraction (single source of truth)
144120
const {
145-
position: _previousPos,
146-
subBlocks: previousSubBlocks = {},
147-
layout: _previousLayout,
148-
height: _previousHeight,
149-
outputs: _previousOutputs,
150-
is_diff: _previousIsDiff,
151-
field_diffs: _previousFieldDiffs,
152-
...previousRest
153-
} = previousBlock
154-
155-
// Exclude from data object:
156-
// - width/height: container dimensions from autolayout
157-
// - nodes: subflow node membership (derived/runtime for parallel/loop blocks)
158-
// - distribution: parallel distribution (derived/runtime)
121+
blockRest: currentRest,
122+
normalizedData: currentDataRest,
123+
subBlocks: currentSubBlocks,
124+
} = extractBlockFieldsForComparison(currentBlock)
159125
const {
160-
width: _cw,
161-
height: _ch,
162-
nodes: _cn,
163-
distribution: _cd,
164-
...currentDataRest
165-
} = (currentRest.data || {}) as Record<string, unknown>
166-
const {
167-
width: _pw,
168-
height: _ph,
169-
nodes: _pn,
170-
distribution: _pd,
171-
...previousDataRest
172-
} = (previousRest.data || {}) as Record<string, unknown>
126+
blockRest: previousRest,
127+
normalizedData: previousDataRest,
128+
subBlocks: previousSubBlocks,
129+
} = extractBlockFieldsForComparison(previousBlock)
173130

174131
const normalizedCurrentBlock = { ...currentRest, data: currentDataRest, subBlocks: undefined }
175132
const normalizedPreviousBlock = {
@@ -194,10 +151,11 @@ export function generateWorkflowDiffSummary(
194151
newValue: currentBlock.enabled,
195152
})
196153
}
197-
// Check other block properties
154+
// Check other block properties (boolean fields)
155+
// Use !! to normalize: null/undefined/false are all equivalent (falsy)
198156
const blockFields = ['horizontalHandles', 'advancedMode', 'triggerMode'] as const
199157
for (const field of blockFields) {
200-
if (currentBlock[field] !== previousBlock[field]) {
158+
if (!!currentBlock[field] !== !!previousBlock[field]) {
201159
changes.push({
202160
field,
203161
oldValue: previousBlock[field],
@@ -210,42 +168,27 @@ export function generateWorkflowDiffSummary(
210168
}
211169
}
212170

213-
// Compare subBlocks
214-
const allSubBlockIds = [
171+
// Compare subBlocks using shared helper for filtering (single source of truth)
172+
const allSubBlockIds = filterSubBlockIds([
215173
...new Set([...Object.keys(currentSubBlocks), ...Object.keys(previousSubBlocks)]),
216-
]
217-
.filter(
218-
(subId) =>
219-
!TRIGGER_RUNTIME_SUBBLOCK_IDS.includes(subId) && !SYSTEM_SUBBLOCK_IDS.includes(subId)
220-
)
221-
.sort()
174+
])
222175

223176
for (const subId of allSubBlockIds) {
224-
const currentSub = currentSubBlocks[subId]
225-
const previousSub = previousSubBlocks[subId]
177+
const currentSub = currentSubBlocks[subId] as Record<string, unknown> | undefined
178+
const previousSub = previousSubBlocks[subId] as Record<string, unknown> | undefined
226179

227180
if (!currentSub || !previousSub) {
228181
changes.push({
229182
field: subId,
230-
oldValue: previousSub?.value ?? null,
231-
newValue: currentSub?.value ?? null,
183+
oldValue: (previousSub as Record<string, unknown> | undefined)?.value ?? null,
184+
newValue: (currentSub as Record<string, unknown> | undefined)?.value ?? null,
232185
})
233186
continue
234187
}
235188

236-
// Compare subBlock values with sanitization
237-
let currentValue: unknown = currentSub.value ?? null
238-
let previousValue: unknown = previousSub.value ?? null
239-
240-
if (subId === 'tools' && Array.isArray(currentValue) && Array.isArray(previousValue)) {
241-
currentValue = sanitizeTools(currentValue)
242-
previousValue = sanitizeTools(previousValue)
243-
}
244-
245-
if (subId === 'inputFormat' && Array.isArray(currentValue) && Array.isArray(previousValue)) {
246-
currentValue = sanitizeInputFormat(currentValue)
247-
previousValue = sanitizeInputFormat(previousValue)
248-
}
189+
// Use shared helper for subBlock value normalization (single source of truth)
190+
const currentValue = normalizeSubBlockValue(subId, currentSub.value)
191+
const previousValue = normalizeSubBlockValue(subId, previousSub.value)
249192

250193
// For string values, compare directly to catch even small text changes
251194
if (typeof currentValue === 'string' && typeof previousValue === 'string') {
@@ -260,11 +203,9 @@ export function generateWorkflowDiffSummary(
260203
}
261204
}
262205

263-
// Compare subBlock REST properties (type, id, etc. - excluding value and is_diff)
264-
const currentSubWithDiff = currentSub as SubBlockWithDiffMarker
265-
const previousSubWithDiff = previousSub as SubBlockWithDiffMarker
266-
const { value: _cv, is_diff: _cd, ...currentSubRest } = currentSubWithDiff
267-
const { value: _pv, is_diff: _pd, ...previousSubRest } = previousSubWithDiff
206+
// Use shared helper for subBlock REST extraction (single source of truth)
207+
const currentSubRest = extractSubBlockRest(currentSub)
208+
const previousSubRest = extractSubBlockRest(previousSub)
268209

269210
if (normalizedStringify(currentSubRest) !== normalizedStringify(previousSubRest)) {
270211
changes.push({

apps/sim/lib/workflows/comparison/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,22 @@ export {
44
generateWorkflowDiffSummary,
55
hasWorkflowChanged,
66
} from './compare'
7-
export type { NormalizedWorkflowState } from './normalize'
7+
export type {
8+
BlockWithDiffMarkers,
9+
NormalizedWorkflowState,
10+
SubBlockWithDiffMarker,
11+
} from './normalize'
812
export {
13+
EXCLUDED_BLOCK_DATA_FIELDS,
14+
extractBlockFieldsForComparison,
15+
extractSubBlockRest,
16+
filterSubBlockIds,
17+
normalizeBlockData,
918
normalizedStringify,
1019
normalizeEdge,
1120
normalizeLoop,
1221
normalizeParallel,
22+
normalizeSubBlockValue,
1323
normalizeValue,
1424
normalizeVariables,
1525
normalizeWorkflowState,

apps/sim/lib/workflows/comparison/normalize.test.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ describe('Workflow Normalization Utilities', () => {
2121
expect(normalizeValue('hello')).toBe('hello')
2222
expect(normalizeValue(true)).toBe(true)
2323
expect(normalizeValue(false)).toBe(false)
24-
expect(normalizeValue(null)).toBe(null)
24+
})
25+
26+
it.concurrent('should normalize null and undefined to undefined', () => {
27+
// null and undefined are semantically equivalent in our system
28+
expect(normalizeValue(null)).toBe(undefined)
2529
expect(normalizeValue(undefined)).toBe(undefined)
2630
})
2731

@@ -81,7 +85,7 @@ describe('Workflow Normalization Utilities', () => {
8185
expect(result[0]).toBe(1)
8286
expect(result[1]).toBe('string')
8387
expect(Object.keys(result[2] as Record<string, unknown>)).toEqual(['a', 'b'])
84-
expect(result[3]).toBe(null)
88+
expect(result[3]).toBe(undefined) // null normalized to undefined
8589
expect(result[4]).toEqual([3, 2, 1]) // Array order preserved
8690
})
8791

@@ -131,7 +135,11 @@ describe('Workflow Normalization Utilities', () => {
131135
expect(normalizedStringify(42)).toBe('42')
132136
expect(normalizedStringify('hello')).toBe('"hello"')
133137
expect(normalizedStringify(true)).toBe('true')
134-
expect(normalizedStringify(null)).toBe('null')
138+
})
139+
140+
it.concurrent('should treat null and undefined equivalently', () => {
141+
// Both null and undefined normalize to undefined, which JSON.stringify returns as undefined
142+
expect(normalizedStringify(null)).toBe(normalizedStringify(undefined))
135143
})
136144

137145
it.concurrent('should produce different strings for different values', () => {
@@ -143,8 +151,9 @@ describe('Workflow Normalization Utilities', () => {
143151
})
144152

145153
describe('normalizeLoop', () => {
146-
it.concurrent('should return null/undefined as-is', () => {
147-
expect(normalizeLoop(null)).toBe(null)
154+
it.concurrent('should normalize null/undefined to undefined', () => {
155+
// null and undefined are semantically equivalent
156+
expect(normalizeLoop(null)).toBe(undefined)
148157
expect(normalizeLoop(undefined)).toBe(undefined)
149158
})
150159

@@ -246,8 +255,9 @@ describe('Workflow Normalization Utilities', () => {
246255
})
247256

248257
describe('normalizeParallel', () => {
249-
it.concurrent('should return null/undefined as-is', () => {
250-
expect(normalizeParallel(null)).toBe(null)
258+
it.concurrent('should normalize null/undefined to undefined', () => {
259+
// null and undefined are semantically equivalent
260+
expect(normalizeParallel(null)).toBe(undefined)
251261
expect(normalizeParallel(undefined)).toBe(undefined)
252262
})
253263

0 commit comments

Comments
 (0)