Skip to content

Commit 39c12a0

Browse files
committed
fix(search): keep the dot-free name on a legacy reference-prefix collision
Creating or renaming a block enforces uniqueness at the normalized level, but legacy workflows can still hold two names that collide only now that `normalizeName` strips dots. `BlockResolver` settles that tie by letting the dot-free name keep ownership of the key, so previously working references never change targets. The prefix map took whichever block was iterated last instead, so search could name a reference after the dotted block while execution resolved it to the dot-free one - search reporting the wrong block, which is what this is meant to stop. Mirror the resolver's rule so both agree.
1 parent c4e2fb7 commit 39c12a0

2 files changed

Lines changed: 45 additions & 3 deletions

File tree

apps/sim/lib/workflows/search-replace/indexer.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,36 @@ describe('indexWorkflowSearchMatches', () => {
202202
matches.filter((match) => match.kind === 'environment').map((match) => match.searchText)
203203
).toEqual(['OLD_SECRET', 'OLD_SECRET'])
204204
})
205+
206+
/**
207+
* Legacy workflows can hold two names that collide only now that
208+
* `normalizeName` strips dots. `BlockResolver` gives the key to the dot-free
209+
* name whichever order the blocks arrive in, so search has to name the same
210+
* block or it would label the reference with a title that block does not own
211+
* at execution time.
212+
*/
213+
it.each([
214+
['dotted first', ['Hunter.io 1', 'Hunterio 1']],
215+
['dot-free first', ['Hunterio 1', 'Hunter.io 1']],
216+
])('names a legacy dot collision after the dot-free block (%s)', (_order, names) => {
217+
const workflow = createSearchReplaceWorkflowFixture()
218+
workflow.blocks['knowledge-1'].name = names[0]
219+
workflow.blocks['api-1'].name = names[1]
220+
workflow.blocks['agent-1'].subBlocks.systemPrompt.value = 'Read <hunterio1.email>.'
221+
222+
const matches = indexWorkflowSearchMatches({
223+
workflow,
224+
mode: 'all',
225+
includeResourceMatchesWithoutQuery: true,
226+
blockConfigs: SEARCH_REPLACE_BLOCK_CONFIGS,
227+
})
228+
229+
expect(
230+
matches
231+
.filter((match) => match.kind === 'workflow-reference')
232+
.map((match) => match.searchText)
233+
).toEqual(['Hunterio 1.email'])
234+
})
205235
})
206236

207237
it('does not index internal row metadata in structured subblock values', () => {

apps/sim/lib/workflows/search-replace/resources/references.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,15 @@ export function parseInlineReferences(value: string): ParsedInlineReference[] {
7272
* Indexes a workflow's block names by the prefix their references carry, so a
7373
* parsed reference can be read back as the name the canvas shows.
7474
*
75-
* Block names are unique by normalized form, so a prefix identifies at most one
76-
* block. Blank names are skipped rather than mapped to an empty prefix.
75+
* Creating or renaming a block enforces uniqueness at the normalized level, but
76+
* legacy workflows can still hold two names that collide only now that
77+
* `normalizeName` strips dots. `BlockResolver` settles that tie by letting the
78+
* dot-free name keep ownership of the key, so previously working references
79+
* never change targets; this mirrors that rule rather than taking whichever
80+
* block happens to be iterated last, so search names the block a reference
81+
* actually resolves to at execution time.
82+
*
83+
* Blank names are skipped rather than mapped to an empty prefix.
7784
*/
7885
export function buildBlockNamesByReferencePrefix(
7986
blocks: Record<string, { name?: string }>
@@ -83,7 +90,12 @@ export function buildBlockNamesByReferencePrefix(
8390
for (const block of Object.values(blocks)) {
8491
if (typeof block.name !== 'string') continue
8592
const prefix = normalizeName(block.name)
86-
if (prefix) namesByPrefix.set(prefix, block.name)
93+
if (!prefix) continue
94+
95+
const incumbent = namesByPrefix.get(prefix)
96+
if (incumbent === undefined || incumbent.includes(REFERENCE.PATH_DELIMITER)) {
97+
namesByPrefix.set(prefix, block.name)
98+
}
8799
}
88100

89101
return namesByPrefix

0 commit comments

Comments
 (0)