Skip to content

Commit 01577a1

Browse files
authored
fix(change-detection): resolve false positive trigger block change detection (#3204)
1 parent 52aff4d commit 01577a1

File tree

6 files changed

+535
-5
lines changed

6 files changed

+535
-5
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,21 @@ export function useChangeDetection({
5757
}
5858
}
5959

60+
if (block.triggerMode) {
61+
const triggerConfigValue = blockSubValues?.triggerConfig
62+
if (
63+
triggerConfigValue &&
64+
typeof triggerConfigValue === 'object' &&
65+
!subBlocks.triggerConfig
66+
) {
67+
subBlocks.triggerConfig = {
68+
id: 'triggerConfig',
69+
type: 'short-input',
70+
value: triggerConfigValue,
71+
}
72+
}
73+
}
74+
6075
blocksWithSubBlocks[blockId] = {
6176
...block,
6277
subBlocks,

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

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2364,6 +2364,261 @@ describe('hasWorkflowChanged', () => {
23642364
})
23652365
})
23662366

2367+
describe('Trigger Config Normalization (False Positive Prevention)', () => {
2368+
it.concurrent(
2369+
'should not detect change when deployed has null fields but current has values from triggerConfig',
2370+
() => {
2371+
// Core scenario: deployed state has null individual fields, current state has
2372+
// values populated from triggerConfig at runtime by populateTriggerFieldsFromConfig
2373+
const deployedState = createWorkflowState({
2374+
blocks: {
2375+
block1: createBlock('block1', {
2376+
type: 'starter',
2377+
subBlocks: {
2378+
signingSecret: { id: 'signingSecret', type: 'short-input', value: null },
2379+
botToken: { id: 'botToken', type: 'short-input', value: null },
2380+
triggerConfig: {
2381+
id: 'triggerConfig',
2382+
type: 'short-input',
2383+
value: { signingSecret: 'secret123', botToken: 'token456' },
2384+
},
2385+
},
2386+
}),
2387+
},
2388+
})
2389+
2390+
const currentState = createWorkflowState({
2391+
blocks: {
2392+
block1: createBlock('block1', {
2393+
type: 'starter',
2394+
subBlocks: {
2395+
signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' },
2396+
botToken: { id: 'botToken', type: 'short-input', value: 'token456' },
2397+
triggerConfig: {
2398+
id: 'triggerConfig',
2399+
type: 'short-input',
2400+
value: { signingSecret: 'secret123', botToken: 'token456' },
2401+
},
2402+
},
2403+
}),
2404+
},
2405+
})
2406+
2407+
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
2408+
}
2409+
)
2410+
2411+
it.concurrent(
2412+
'should detect change when user edits a trigger field to a different value',
2413+
() => {
2414+
const deployedState = createWorkflowState({
2415+
blocks: {
2416+
block1: createBlock('block1', {
2417+
type: 'starter',
2418+
subBlocks: {
2419+
signingSecret: { id: 'signingSecret', type: 'short-input', value: null },
2420+
triggerConfig: {
2421+
id: 'triggerConfig',
2422+
type: 'short-input',
2423+
value: { signingSecret: 'old-secret' },
2424+
},
2425+
},
2426+
}),
2427+
},
2428+
})
2429+
2430+
const currentState = createWorkflowState({
2431+
blocks: {
2432+
block1: createBlock('block1', {
2433+
type: 'starter',
2434+
subBlocks: {
2435+
signingSecret: { id: 'signingSecret', type: 'short-input', value: 'new-secret' },
2436+
triggerConfig: {
2437+
id: 'triggerConfig',
2438+
type: 'short-input',
2439+
value: { signingSecret: 'old-secret' },
2440+
},
2441+
},
2442+
}),
2443+
},
2444+
})
2445+
2446+
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
2447+
}
2448+
)
2449+
2450+
it.concurrent('should not detect change when both sides have no triggerConfig', () => {
2451+
const deployedState = createWorkflowState({
2452+
blocks: {
2453+
block1: createBlock('block1', {
2454+
type: 'starter',
2455+
subBlocks: {
2456+
signingSecret: { id: 'signingSecret', type: 'short-input', value: null },
2457+
},
2458+
}),
2459+
},
2460+
})
2461+
2462+
const currentState = createWorkflowState({
2463+
blocks: {
2464+
block1: createBlock('block1', {
2465+
type: 'starter',
2466+
subBlocks: {
2467+
signingSecret: { id: 'signingSecret', type: 'short-input', value: null },
2468+
},
2469+
}),
2470+
},
2471+
})
2472+
2473+
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
2474+
})
2475+
2476+
it.concurrent(
2477+
'should not detect change when deployed has empty fields and triggerConfig populates them',
2478+
() => {
2479+
// Empty string is also treated as "empty" by normalizeTriggerConfigValues
2480+
const deployedState = createWorkflowState({
2481+
blocks: {
2482+
block1: createBlock('block1', {
2483+
type: 'starter',
2484+
subBlocks: {
2485+
signingSecret: { id: 'signingSecret', type: 'short-input', value: '' },
2486+
triggerConfig: {
2487+
id: 'triggerConfig',
2488+
type: 'short-input',
2489+
value: { signingSecret: 'secret123' },
2490+
},
2491+
},
2492+
}),
2493+
},
2494+
})
2495+
2496+
const currentState = createWorkflowState({
2497+
blocks: {
2498+
block1: createBlock('block1', {
2499+
type: 'starter',
2500+
subBlocks: {
2501+
signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' },
2502+
triggerConfig: {
2503+
id: 'triggerConfig',
2504+
type: 'short-input',
2505+
value: { signingSecret: 'secret123' },
2506+
},
2507+
},
2508+
}),
2509+
},
2510+
})
2511+
2512+
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
2513+
}
2514+
)
2515+
2516+
it.concurrent('should not detect change when triggerId differs', () => {
2517+
const deployedState = createWorkflowState({
2518+
blocks: {
2519+
block1: createBlock('block1', {
2520+
type: 'starter',
2521+
subBlocks: {
2522+
model: { value: 'gpt-4' },
2523+
triggerId: { value: null },
2524+
},
2525+
}),
2526+
},
2527+
})
2528+
2529+
const currentState = createWorkflowState({
2530+
blocks: {
2531+
block1: createBlock('block1', {
2532+
type: 'starter',
2533+
subBlocks: {
2534+
model: { value: 'gpt-4' },
2535+
triggerId: { value: 'slack_webhook' },
2536+
},
2537+
}),
2538+
},
2539+
})
2540+
2541+
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
2542+
})
2543+
2544+
it.concurrent(
2545+
'should not detect change for namespaced system subBlock IDs like samplePayload_slack_webhook',
2546+
() => {
2547+
const deployedState = createWorkflowState({
2548+
blocks: {
2549+
block1: createBlock('block1', {
2550+
type: 'starter',
2551+
subBlocks: {
2552+
model: { value: 'gpt-4' },
2553+
samplePayload_slack_webhook: { value: 'old payload' },
2554+
triggerInstructions_slack_webhook: { value: 'old instructions' },
2555+
},
2556+
}),
2557+
},
2558+
})
2559+
2560+
const currentState = createWorkflowState({
2561+
blocks: {
2562+
block1: createBlock('block1', {
2563+
type: 'starter',
2564+
subBlocks: {
2565+
model: { value: 'gpt-4' },
2566+
samplePayload_slack_webhook: { value: 'new payload' },
2567+
triggerInstructions_slack_webhook: { value: 'new instructions' },
2568+
},
2569+
}),
2570+
},
2571+
})
2572+
2573+
expect(hasWorkflowChanged(currentState, deployedState)).toBe(false)
2574+
}
2575+
)
2576+
2577+
it.concurrent(
2578+
'should handle mixed scenario: some fields from triggerConfig, some user-edited',
2579+
() => {
2580+
const deployedState = createWorkflowState({
2581+
blocks: {
2582+
block1: createBlock('block1', {
2583+
type: 'starter',
2584+
subBlocks: {
2585+
signingSecret: { id: 'signingSecret', type: 'short-input', value: null },
2586+
botToken: { id: 'botToken', type: 'short-input', value: null },
2587+
includeFiles: { id: 'includeFiles', type: 'switch', value: false },
2588+
triggerConfig: {
2589+
id: 'triggerConfig',
2590+
type: 'short-input',
2591+
value: { signingSecret: 'secret123', botToken: 'token456' },
2592+
},
2593+
},
2594+
}),
2595+
},
2596+
})
2597+
2598+
const currentState = createWorkflowState({
2599+
blocks: {
2600+
block1: createBlock('block1', {
2601+
type: 'starter',
2602+
subBlocks: {
2603+
signingSecret: { id: 'signingSecret', type: 'short-input', value: 'secret123' },
2604+
botToken: { id: 'botToken', type: 'short-input', value: 'token456' },
2605+
includeFiles: { id: 'includeFiles', type: 'switch', value: true },
2606+
triggerConfig: {
2607+
id: 'triggerConfig',
2608+
type: 'short-input',
2609+
value: { signingSecret: 'secret123', botToken: 'token456' },
2610+
},
2611+
},
2612+
}),
2613+
},
2614+
})
2615+
2616+
// includeFiles changed from false to true — this IS a real change
2617+
expect(hasWorkflowChanged(currentState, deployedState)).toBe(true)
2618+
}
2619+
)
2620+
})
2621+
23672622
describe('Trigger Runtime Metadata (Should Not Trigger Change)', () => {
23682623
it.concurrent('should not detect change when webhookId differs', () => {
23692624
const deployedState = createWorkflowState({

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
normalizeLoop,
1010
normalizeParallel,
1111
normalizeSubBlockValue,
12+
normalizeTriggerConfigValues,
1213
normalizeValue,
1314
normalizeVariables,
1415
sanitizeVariable,
@@ -172,14 +173,18 @@ export function generateWorkflowDiffSummary(
172173
}
173174
}
174175

176+
// Normalize trigger config values for both states before comparison
177+
const normalizedCurrentSubs = normalizeTriggerConfigValues(currentSubBlocks)
178+
const normalizedPreviousSubs = normalizeTriggerConfigValues(previousSubBlocks)
179+
175180
// Compare subBlocks using shared helper for filtering (single source of truth)
176181
const allSubBlockIds = filterSubBlockIds([
177-
...new Set([...Object.keys(currentSubBlocks), ...Object.keys(previousSubBlocks)]),
182+
...new Set([...Object.keys(normalizedCurrentSubs), ...Object.keys(normalizedPreviousSubs)]),
178183
])
179184

180185
for (const subId of allSubBlockIds) {
181-
const currentSub = currentSubBlocks[subId] as Record<string, unknown> | undefined
182-
const previousSub = previousSubBlocks[subId] as Record<string, unknown> | undefined
186+
const currentSub = normalizedCurrentSubs[subId] as Record<string, unknown> | undefined
187+
const previousSub = normalizedPreviousSubs[subId] as Record<string, unknown> | undefined
183188

184189
if (!currentSub || !previousSub) {
185190
changes.push({

0 commit comments

Comments
 (0)