@@ -180,6 +180,32 @@ describe('buildWorkflowSearchReplacePlan', () => {
180180 ] )
181181 } )
182182
183+ it ( 'replaces duplicate structured resources with blank comma segments consistently' , ( ) => {
184+ const workflow = createSearchReplaceWorkflowFixture ( )
185+ workflow . blocks [ 'knowledge-1' ] . subBlocks . knowledgeBaseIds . value = 'kb-old,,kb-second,kb-old'
186+
187+ const matches = indexWorkflowSearchMatches ( {
188+ workflow,
189+ query : 'kb-old' ,
190+ mode : 'resource' ,
191+ blockConfigs : SEARCH_REPLACE_BLOCK_CONFIGS ,
192+ } ) . filter ( ( match ) => match . kind === 'knowledge-base' )
193+
194+ const plan = buildWorkflowSearchReplacePlan ( {
195+ blocks : workflow . blocks ,
196+ matches,
197+ selectedMatchIds : new Set ( [ matches [ 1 ] . id ] ) ,
198+ defaultReplacement : 'kb-new' ,
199+ resourceReplacementOptions : [
200+ { kind : 'knowledge-base' , value : 'kb-new' , label : 'New Knowledge Base' } ,
201+ ] ,
202+ } )
203+
204+ expect ( plan . conflicts ) . toEqual ( [ ] )
205+ expect ( plan . updates ) . toHaveLength ( 1 )
206+ expect ( plan . updates [ 0 ] . nextValue ) . toBe ( 'kb-old,,kb-second,kb-new' )
207+ } )
208+
183209 it ( 'replaces all compatible knowledge base references across blocks' , ( ) => {
184210 const workflow = createSearchReplaceWorkflowFixture ( )
185211 workflow . blocks [ 'knowledge-2' ] = {
@@ -588,6 +614,101 @@ describe('buildWorkflowSearchReplacePlan', () => {
588614 ] )
589615 } )
590616
617+ it ( 'conflicts when a selected duplicate file occurrence becomes a single file object' , ( ) => {
618+ const workflow = createSearchReplaceWorkflowFixture ( )
619+ const firstFile = {
620+ name : 'first.pdf' ,
621+ key : 'file-key-old' ,
622+ path : '/first.pdf' ,
623+ size : 12 ,
624+ type : 'application/pdf' ,
625+ }
626+ const secondFile = {
627+ name : 'second.pdf' ,
628+ key : 'file-key-old' ,
629+ path : '/second.pdf' ,
630+ size : 14 ,
631+ type : 'application/pdf' ,
632+ }
633+ workflow . blocks [ 'tool-input-1' ] = {
634+ id : 'tool-input-1' ,
635+ type : 'custom' ,
636+ name : 'Tool Input Block' ,
637+ position : { x : 0 , y : 0 } ,
638+ enabled : true ,
639+ outputs : { } ,
640+ subBlocks : {
641+ tools : {
642+ id : 'tools' ,
643+ type : 'tool-input' ,
644+ value : [
645+ {
646+ type : 'slack' ,
647+ toolId : 'slack_message' ,
648+ operation : 'send' ,
649+ title : 'Slack message' ,
650+ params : {
651+ authMethod : 'oauth' ,
652+ credential : 'slack-credential' ,
653+ text : 'message with files' ,
654+ attachmentFiles : [ firstFile , secondFile ] ,
655+ } ,
656+ } ,
657+ ] ,
658+ } ,
659+ } ,
660+ }
661+
662+ const matches = indexWorkflowSearchMatches ( {
663+ workflow,
664+ query : 'file-key-old' ,
665+ mode : 'resource' ,
666+ blockConfigs : {
667+ ...SEARCH_REPLACE_BLOCK_CONFIGS ,
668+ custom : {
669+ subBlocks : [ { id : 'tools' , title : 'Tools' , type : 'tool-input' } ] ,
670+ } ,
671+ } ,
672+ } ) . filter ( ( match ) => match . kind === 'file' )
673+
674+ workflow . blocks [ 'tool-input-1' ] . subBlocks . tools . value = [
675+ {
676+ type : 'slack' ,
677+ toolId : 'slack_message' ,
678+ operation : 'send' ,
679+ title : 'Slack message' ,
680+ params : {
681+ authMethod : 'oauth' ,
682+ credential : 'slack-credential' ,
683+ text : 'message with files' ,
684+ attachmentFiles : firstFile ,
685+ } ,
686+ } ,
687+ ]
688+
689+ const replacementFile = {
690+ name : 'replacement.pdf' ,
691+ key : 'file-key-new' ,
692+ path : '/replacement.pdf' ,
693+ size : 24 ,
694+ type : 'application/pdf' ,
695+ }
696+ const plan = buildWorkflowSearchReplacePlan ( {
697+ blocks : workflow . blocks ,
698+ matches,
699+ selectedMatchIds : new Set ( [ matches [ 1 ] . id ] ) ,
700+ defaultReplacement : JSON . stringify ( replacementFile ) ,
701+ resourceReplacementOptions : [
702+ { kind : 'file' , value : JSON . stringify ( replacementFile ) , label : replacementFile . name } ,
703+ ] ,
704+ } )
705+
706+ expect ( plan . updates ) . toEqual ( [ ] )
707+ expect ( plan . conflicts ) . toEqual ( [
708+ { matchId : matches [ 1 ] . id , reason : 'Target resource changed since search' } ,
709+ ] )
710+ } )
711+
591712 it ( 'clears nested tool-input dependents when replacing a parent resource' , ( ) => {
592713 const workflow = createSearchReplaceWorkflowFixture ( )
593714 workflow . blocks [ 'tool-input-1' ] = {
0 commit comments