Skip to content

Commit 69e9ca4

Browse files
authored
fix(workflow): generate template-compatible snippet node IDs (#43838)
1 parent 1e311fa commit 69e9ca4

2 files changed

Lines changed: 64 additions & 12 deletions

File tree

‎web/app/components/workflow/block-selector/snippets/__tests__/use-insert-snippet.spec.tsx‎

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type TestNode = {
2020
_connectedSourceHandleIds?: string[]
2121
_connectedTargetHandleIds?: string[]
2222
variables?: { variable: string; value_selector: string[] }[]
23+
prompt_template?: { role: string; text: string }[]
2324
items?: {
2425
variable_selector: string[]
2526
input_type: 'variable' | 'constant'
@@ -141,6 +142,58 @@ describe('useInsertSnippet', () => {
141142
})
142143

143144
describe('Insert Flow', () => {
145+
it('should keep prompt references parseable across insertions at the same timestamp', async () => {
146+
const timestamp = vi.spyOn(Date, 'now').mockReturnValue(1791619342702)
147+
try {
148+
seedPublishedWorkflow(queryClient, {
149+
graph: {
150+
nodes: [
151+
{
152+
id: 'extractor-with-hyphens',
153+
position: { x: 0, y: 0 },
154+
data: { type: 'document-extractor' },
155+
},
156+
{
157+
id: 'llm-with-hyphens',
158+
position: { x: 300, y: 0 },
159+
data: {
160+
type: 'llm',
161+
model: { mode: 'chat' },
162+
prompt_template: [{ role: 'system', text: '{{#extractor-with-hyphens.text#}}' }],
163+
},
164+
},
165+
],
166+
edges: [{ id: 'edge', source: 'extractor-with-hyphens', target: 'llm-with-hyphens' }],
167+
},
168+
})
169+
const { result } = renderUseInsertSnippet()
170+
171+
for (let insertion = 0; insertion < 2; insertion++) {
172+
await act(async () => {
173+
await result.current.handleInsertSnippet('snippet-1')
174+
})
175+
const nextNodes = mockSetNodes.mock.calls[insertion]![0] as TestNode[]
176+
const insertedNodes = nextNodes.slice(-2)
177+
const extractor = insertedNodes[0]!
178+
const llm = insertedNodes[1]!
179+
// Runtime variable selectors accept at most 50 alphanumeric/underscore characters.
180+
for (const node of insertedNodes) expect(node.id).toMatch(/^\w{1,50}$/)
181+
expect(llm.data.prompt_template).toEqual([
182+
{ role: 'system', text: `{{#${extractor.id}.text#}}` },
183+
])
184+
expect(new Set(nextNodes.map((node) => node.id)).size).toBe(nextNodes.length)
185+
const edges = mockSetEdges.mock.calls[insertion]![0] as TestEdge[]
186+
expect(edges.at(-1)).toEqual(
187+
expect.objectContaining({ source: extractor.id, target: llm.id }),
188+
)
189+
mockGetNodes.mockReturnValue(nextNodes)
190+
mockEdges = edges
191+
}
192+
} finally {
193+
timestamp.mockRestore()
194+
}
195+
})
196+
144197
it('should append remapped snippet graph into current workflow graph', async () => {
145198
seedPublishedWorkflow(queryClient, {
146199
graph: {
@@ -269,8 +322,8 @@ describe('useInsertSnippet', () => {
269322
})
270323

271324
const nextNodes = mockSetNodes.mock.calls[0]![0] as TestNode[]
272-
const insertedLLMNode = nextNodes.find((node) => node.id.includes('snippet-llm'))!
273-
const insertedCodeNode = nextNodes.find((node) => node.id.includes('snippet-code'))!
325+
const insertedLLMNode = nextNodes.find((node) => node.data.type === 'llm')!
326+
const insertedCodeNode = nextNodes.find((node) => node.data.type === 'code')!
274327

275328
expect(insertedLLMNode.id).not.toBe('snippet-llm')
276329
expect(insertedCodeNode.data.variables).toEqual([
@@ -461,22 +514,22 @@ describe('useInsertSnippet', () => {
461514
})
462515

463516
const firstInsertion = mockSetNodes.mock.calls[0]![0] as TestNode[]
464-
const firstSource = firstInsertion.find((node) => node.id.includes('snippet-source'))!
465-
const firstAssigner = firstInsertion.find((node) => node.id.includes('snippet-assigner'))!
517+
const firstSource = firstInsertion.find((node) => node.data.type === 'code')!
518+
const firstAssigner = firstInsertion.find((node) => node.data.type === 'assigner')!
466519
mockGetNodes.mockReturnValue(firstInsertion)
467520

468521
await act(async () => {
469522
await result.current.handleInsertSnippet('snippet-1')
470523
})
471524

472525
const secondInsertion = mockSetNodes.mock.calls[1]![0] as TestNode[]
473-
const insertedSources = secondInsertion.filter((node) => node.id.includes('snippet-source'))
526+
const insertedSources = secondInsertion.filter((node) => node.data.type === 'code')
474527
const secondSource = insertedSources.find((node) => node.id !== firstSource.id)!
475528
const firstAssignerAfterSecondInsert = secondInsertion.find(
476529
(node) => node.id === firstAssigner.id,
477530
)!
478531
const secondAssigner = secondInsertion.find(
479-
(node) => node.id.includes('snippet-assigner') && node.id !== firstAssigner.id,
532+
(node) => node.data.type === 'assigner' && node.id !== firstAssigner.id,
480533
)!
481534

482535
expect(firstAssignerAfterSecondInsert.data.items![0]).toEqual(
@@ -727,12 +780,10 @@ describe('useInsertSnippet', () => {
727780

728781
const nextNodes = mockSetNodes.mock.calls[0]![0] as TestNode[]
729782
const insertedEntry = nextNodes.find(
730-
(node) =>
731-
node.id !== 'prev-node' && node.id !== 'next-node' && node.id.includes('snippet-entry'),
783+
(node) => node.id !== 'prev-node' && node.id !== 'next-node' && node.data.type === 'llm',
732784
)!
733785
const insertedExit = nextNodes.find(
734-
(node) =>
735-
node.id !== 'prev-node' && node.id !== 'next-node' && node.id.includes('snippet-exit'),
786+
(node) => node.id !== 'prev-node' && node.id !== 'next-node' && node.data.type === 'code',
736787
)!
737788
const shiftedNextNode = nextNodes.find((node) => node.id === 'next-node')!
738789
expect(insertedEntry.position).toEqual({ x: 300, y: 0 })

‎web/app/components/workflow/block-selector/snippets/use-insert-snippet.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,9 @@ const remapSnippetGraph = (
230230
const offsetY = (insertAnchor?.y ?? (currentNodes.length ? currentMinY : 80)) - minRootY
231231

232232
snippetNodes.forEach((node, index) => {
233-
let nextId = `${node.id}-${Date.now()}-${index}`
234-
while (existingIds.has(nextId)) nextId = `${nextId}-1`
233+
// Keep IDs compatible with runtime variable templates, including on collisions.
234+
let nextId = `${Date.now() + index}`
235+
while (existingIds.has(nextId)) nextId = `${Number(nextId) + 1}`
235236
existingIds.add(nextId)
236237
idMapping.set(node.id, nextId)
237238
})

0 commit comments

Comments
 (0)