import { describe, expect, it } from 'vitest'; import type { Edge, Node } from '@xyflow/svelte'; import { appendStartFormField, buildAutoBindingPatch, buildSequentialFieldBindingPatches, buildFieldBindingPatch, buildEditorReferenceParameters, buildSingleRunModel, buildSingleRunParameters, createInitialWorkflowData, ensureStartNodeParameters, FIELD_BINDING_META_KEY, isStartFormFieldKeyAvailable, normalizeStartNodeData, normalizeWorkflowStartNodes, renameStartFieldReferencesInNodes, removeStartFormField, updateStartFormField, } from './workflowNodeFields'; describe('workflow node fields', () => { it('creates initial workflow data with fixed start input', () => { const initial = createInitialWorkflowData(); expect(initial.nodes).toHaveLength(1); expect(initial.nodes[0]?.type).toBe('startNode'); const parameters = ensureStartNodeParameters( (initial.nodes[0]?.data?.parameters || []) as any[], ); expect(parameters).toHaveLength(1); expect(parameters[0]?.name).toBe('user_input'); expect(parameters[0]?.systemReserved).toBe(true); expect(parameters[0]?.required).toBe(true); expect(parameters[0]?.formLabel).toBe(''); expect(parameters[0]?.displayName).toBe('流程开始 > 用户问题'); expect(initial.nodes[0]?.data?.startFormMeta).toMatchObject({ title: '开始问答', submitText: '开始', }); expect(initial.nodes[0]?.data?.startFormSchema?.[0]).toMatchObject({ key: 'user_input', label: '', type: 'textarea', systemReserved: true, required: true, }); }); it('preserves an empty system start title for the default placeholder', () => { const initial = createInitialWorkflowData(); const startData = initial.nodes[0]?.data as Record; const customized = updateStartFormField(startData, 'user_input', { label: '反洗钱', }); const cleared = updateStartFormField(customized, 'user_input', { label: '', }); expect(customized.startFormSchema?.[0]?.label).toBe('反洗钱'); expect(cleared.startFormSchema?.[0]?.label).toBe(''); expect((cleared.parameters as any[])?.[0]?.formLabel).toBe(''); expect((cleared.parameters as any[])?.[0]?.displayName).toBe( '流程开始 > 用户问题', ); }); it('preserves an explicitly entered default or prefixed system title', () => { const initial = createInitialWorkflowData(); const startData = initial.nodes[0]?.data as Record; const defaultTitle = updateStartFormField(startData, 'user_input', { label: '用户问题', }); const prefixedTitle = updateStartFormField(startData, 'user_input', { label: '用户问题说明', }); expect(defaultTitle.startFormSchema?.[0]?.label).toBe('用户问题'); expect((defaultTitle.parameters as any[])?.[0]?.formLabel).toBe('用户问题'); expect(prefixedTitle.startFormSchema?.[0]?.label).toBe('用户问题说明'); expect((prefixedTitle.parameters as any[])?.[0]?.formLabel).toBe( '用户问题说明', ); }); it('appends custom start form field into schema source of truth', () => { const initial = createInitialWorkflowData(); const startNode = initial.nodes[0]!; const nextData = appendStartFormField( startNode.data as Record, { type: 'select', options: ['售前', '售后'], }, ); expect(nextData.startFormSchema).toHaveLength(2); expect(nextData.startFormSchema?.[1]).toMatchObject({ key: 'select_field', label: '下拉字段', type: 'select', options: ['售前', '售后'], }); expect((nextData.parameters as any[])?.[1]).toMatchObject({ name: 'select_field', formLabel: '下拉字段', formType: 'select', enums: ['售前', '售后'], }); }); it('updates generated field key when switching field type', () => { const initial = createInitialWorkflowData(); const appended = appendStartFormField( initial.nodes[0]?.data as Record, { type: 'text', }, ); const updated = updateStartFormField(appended, 'text_field', { type: 'file', placeholder: '请选择文件', }); expect( updated.startFormSchema?.find((item: any) => item.key === 'file_field'), ).toMatchObject({ key: 'file_field', label: '文件字段', type: 'file', contentType: 'file', placeholder: '请选择文件', }); expect( (updated.parameters as any[]).find((item) => item.name === 'file_field'), ).toMatchObject({ name: 'file_field', dataType: 'File', contentType: 'file', }); }); it('updates and removes custom start form fields through schema source of truth', () => { const initial = createInitialWorkflowData(); const appended = appendStartFormField( initial.nodes[0]?.data as Record, { key: 'attachments', label: '附件', type: 'text', placeholder: '请输入内容', }, ); const updated = updateStartFormField(appended, 'attachments', { type: 'file', placeholder: '请选择文件', }); expect( updated.startFormSchema?.find((item: any) => item.key === 'attachments'), ).toMatchObject({ type: 'file', contentType: 'file', placeholder: '请选择文件', }); expect( (updated.parameters as any[]).find((item) => item.name === 'attachments'), ).toMatchObject({ dataType: 'File', contentType: 'file', }); const removed = removeStartFormField(updated, 'attachments'); expect(removed.startFormSchema).toHaveLength(1); expect((removed.parameters as any[]).map((item) => item.name)).toEqual([ 'user_input', ]); }); it('keeps custom start field parameter id stable when renaming key', () => { const initial = createInitialWorkflowData(); const appended = appendStartFormField( initial.nodes[0]?.data as Record, { type: 'text', }, ); const previousField = appended.startFormSchema?.find( (item: any) => item.key === 'text_field', ); const previousParameter = (appended.parameters as any[]).find( (item) => item.name === 'text_field', ); const updated = updateStartFormField(appended, 'text_field', { key: 'topic', label: '主题', }); const nextField = updated.startFormSchema?.find( (item: any) => item.key === 'topic', ); const nextParameter = (updated.parameters as any[]).find( (item) => item.name === 'topic', ); expect(previousField?.id).toBeTruthy(); expect(nextField?.id).toBe(previousField?.id); expect(nextParameter?.id).toBe(previousParameter?.id); }); it('aligns the default field label when renaming a generated key', () => { const initial = createInitialWorkflowData(); const appended = appendStartFormField( initial.nodes[0]?.data as Record, { type: 'text', }, ); const updated = updateStartFormField(appended, 'text_field', { key: '补充信息', }); expect( updated.startFormSchema?.find((item: any) => item.key === '补充信息'), ).toMatchObject({ key: '补充信息', label: '补充信息', }); expect( (updated.parameters as any[]).find((item) => item.name === '补充信息'), ).toMatchObject({ name: '补充信息', formLabel: '补充信息', }); }); it('keeps start fields unchanged when renaming to an existing key', () => { const initial = createInitialWorkflowData(); const withTopic = appendStartFormField( initial.nodes[0]?.data as Record, { key: 'topic', label: '主题', type: 'text', }, ); const withDetails = appendStartFormField(withTopic, { key: 'details', label: '详情', type: 'textarea', }); const previousFields = withDetails.startFormSchema; const previousParameters = withDetails.parameters; expect( isStartFormFieldKeyAvailable(withDetails, 'details', 'summary'), ).toBe(true); expect(isStartFormFieldKeyAvailable(withDetails, 'details', 'topic')).toBe( false, ); expect( isStartFormFieldKeyAvailable(withDetails, 'details', 'user_input'), ).toBe(false); const duplicatedCustomKey = updateStartFormField(withDetails, 'details', { key: 'topic', }); const duplicatedSystemKey = updateStartFormField(withDetails, 'details', { key: 'user_input', }); expect(duplicatedCustomKey.startFormSchema).toEqual(previousFields); expect(duplicatedCustomKey.parameters).toEqual(previousParameters); expect(duplicatedSystemKey.startFormSchema).toEqual(previousFields); expect(duplicatedSystemKey.parameters).toEqual(previousParameters); }); it('renames downstream token and managed references when start field key changes', () => { const initialStartData = appendStartFormField( createInitialWorkflowData().nodes[0]?.data as Record, { type: 'text', }, ); const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '开始节点', ...initialStartData, }, }; const renamedStartData = updateStartFormField( startNode.data as Record, 'text_field', { key: 'topic', label: '主题', }, ); const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', userPrompt: '请围绕 {{start_1.text_field}} 生成内容', parameters: [ { id: 'param_1', name: 'start_1.text_field', ref: 'start_1.text_field', refType: 'ref', autoManaged: true, formLabel: '开始节点 > 文本字段', displayName: '开始节点 > 文本字段', }, ], [FIELD_BINDING_META_KEY]: { userPrompt: { autoFilledFrom: 'start_1.text_field', userModified: false, }, }, }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge, ]; const nextNodes = renameStartFieldReferencesInNodes( [ { ...startNode, data: { ...(startNode.data as Record), ...renamedStartData, }, }, llmNode, ], edges, 'start_1', 'text_field', 'topic', ); const nextLlmNode = nextNodes.find((node) => node.id === 'llm_1')!; const nextParameter = (nextLlmNode.data?.parameters as any[])?.[0]; expect(nextLlmNode.data?.userPrompt).toBe( '请围绕 {{start_1.topic}} 生成内容', ); expect(nextParameter?.name).toBe('start_1.topic'); expect(nextParameter?.ref).toBe('start_1.topic'); expect(nextParameter?.displayName).toBe('开始节点 > topic'); expect( (nextLlmNode.data as any)?.[FIELD_BINDING_META_KEY]?.userPrompt, ).toMatchObject({ autoFilledFrom: 'start_1.topic', userModified: false, }); }); it('refreshes downstream reference labels when the system title changes', () => { const initial = createInitialWorkflowData(); const customStartData = updateStartFormField( initial.nodes[0]?.data as Record, 'user_input', { label: '反洗钱' }, ); const clearedStartData = updateStartFormField( customStartData, 'user_input', { label: '' }, ); const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '开始节点', ...clearedStartData, }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', userPrompt: '', parameters: [ { id: 'param_1', name: 'start_1.user_input', ref: 'start_1.user_input', refType: 'ref', autoManaged: true, formLabel: '开始节点 > 反洗钱', displayName: '开始节点 > 反洗钱', }, ], }, }; const endNode: Node = { id: 'end_1', type: 'endNode', position: { x: 240, y: 0 }, data: { title: '结束节点', parameters: [ { id: 'param_2', name: 'start_1.user_input', ref: 'start_1.user_input', refType: 'ref', formLabel: '开始节点 > 反洗钱', displayName: '开始节点 > 反洗钱', }, ], }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge, { id: 'edge_2', source: 'llm_1', target: 'end_1' } as Edge, ]; const nextNodes = renameStartFieldReferencesInNodes( [startNode, llmNode, endNode], edges, 'start_1', 'user_input', 'user_input', ); const nextLlmNode = nextNodes.find((node) => node.id === 'llm_1')!; const nextParameter = (nextLlmNode.data?.parameters as any[])?.[0]; const nextEndParameter = ( nextNodes.find((node) => node.id === 'end_1')?.data?.parameters as any[] )?.[0]; expect(nextLlmNode.data?.userPrompt).toBe(''); expect(nextParameter?.name).toBe('start_1.user_input'); expect(nextParameter?.ref).toBe('start_1.user_input'); expect(nextParameter?.displayName).toBe('开始节点 > 用户问题'); expect(nextParameter?.formLabel).toBe('开始节点 > 用户问题'); expect(nextEndParameter?.displayName).toBe('开始节点 > 用户问题'); expect(nextEndParameter?.formLabel).toBe('开始节点 > 用户问题'); expect((nextLlmNode.data as any)?.[FIELD_BINDING_META_KEY]).toBeUndefined(); }); it('preserves node identities when a title sync has nothing to update', () => { const initial = createInitialWorkflowData(); const startNode: Node = { ...initial.nodes[0]!, id: 'start_1', }; const untouchedNode: Node = { id: 'code_1', type: 'codeNode', position: { x: 120, y: 0 }, data: { title: '代码节点', parameters: [ { id: 'param_1', name: 'manual_value', displayName: '手工参数', }, ], }, }; const nodes = [startNode, untouchedNode]; const nextNodes = renameStartFieldReferencesInNodes( nodes, [], 'start_1', 'user_input', 'user_input', ); expect(nextNodes).toBe(nodes); expect(nextNodes[0]).toBe(startNode); expect(nextNodes[1]).toBe(untouchedNode); }); it('preserves resource content type independently from input type', () => { for (const contentType of ['image', 'video', 'audio', 'other'] as const) { const key = `preview_${contentType}`; const initial = createInitialWorkflowData(); const appended = appendStartFormField( initial.nodes[0]?.data as Record, { key, label: `资源-${contentType}`, type: 'text', }, ); const updated = updateStartFormField(appended, key, { contentType, }); expect( updated.startFormSchema?.find((item: any) => item.key === key), ).toMatchObject({ type: 'text', contentType, }); expect( (updated.parameters as any[]).find((item) => item.name === key), ).toMatchObject({ dataType: contentType === 'image' ? 'Object' : 'String', contentType, formType: 'input', }); } }); it('reconciles a legacy text schema with its image parameter', () => { const initial = createInitialWorkflowData(); const startData = initial.nodes[0]?.data as Record; const appended = appendStartFormField(startData, { key: 'legacy_image', label: '图片', type: 'text', }); const imageParameterData = { ...appended, parameters: (appended.parameters as any[]).map((parameter) => parameter.name === 'legacy_image' ? { ...parameter, contentType: 'image', dataType: 'Object', } : parameter, ), }; const normalized = normalizeStartNodeData(imageParameterData); expect( normalized.startFormSchema?.find( (field: any) => field.key === 'legacy_image', ), ).toMatchObject({ contentType: 'image', type: 'text', }); expect( (normalized.parameters as any[]).find( (parameter) => parameter.name === 'legacy_image', ), ).toMatchObject({ contentType: 'image', dataType: 'Object', }); const changedToText = updateStartFormField(normalized, 'legacy_image', { contentType: 'text', }); expect( changedToText.startFormSchema?.find( (field: any) => field.key === 'legacy_image', ), ).toMatchObject({ contentType: 'text', }); expect( (changedToText.parameters as any[]).find( (parameter) => parameter.name === 'legacy_image', ), ).toMatchObject({ contentType: 'text', dataType: 'String', }); }); it('uses custom start parameter name for reference display', () => { const startData = appendStartFormField( createInitialWorkflowData().nodes[0]?.data as Record, { key: 'topic_name', label: '下拉字段', type: 'select', options: ['A', 'B'], }, ); const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '开始节点', ...startData, }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', parameters: [], }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge, ]; const parameters = buildEditorReferenceParameters( 'llm_1', [startNode, llmNode], edges, [], ); const topicParameter = parameters.find( (item) => item.name === 'start_1.topic_name', ); expect(topicParameter?.displayName).toBe('开始节点 > topic_name'); expect(topicParameter?.formLabel).toBe('开始节点 > topic_name'); }); it('builds upstream reference candidates from start node', () => { const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '流程开始', parameters: ensureStartNodeParameters(), }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', parameters: [], }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge, ]; const parameters = buildEditorReferenceParameters( 'llm_1', [startNode, llmNode], edges, [], ); expect(parameters.some((item) => item.name === 'start_1.user_input')).toBe( true, ); expect( parameters.find((item) => item.name === 'start_1.user_input') ?.displayName, ).toBe('流程开始 > 用户问题'); }); it('uses output parameter name for reference display', () => { const knowledgeNode: Node = { id: 'knowledge_1', type: 'knowledgeNode', position: { x: 0, y: 0 }, data: { title: '知识库', outputDefs: [ { name: 'documents', formLabel: '文档列表', children: [ { name: 'content', formLabel: '正文内容', }, ], }, ], }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', parameters: [], }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'knowledge_1', target: 'llm_1' } as Edge, ]; const parameters = buildEditorReferenceParameters( 'llm_1', [knowledgeNode, llmNode], edges, [], ); const documentsParameter = parameters.find( (item) => item.name === 'knowledge_1.documents', ); const contentParameter = parameters.find( (item) => item.name === 'knowledge_1.documents.content', ); expect(documentsParameter?.displayName).toBe('知识库 > documents'); expect(documentsParameter?.formLabel).toBe('知识库 > documents'); expect(documentsParameter?.pathLabel).toBe('documents'); expect(documentsParameter?.isCollection).toBe(true); expect(contentParameter?.displayName).toBe('知识库 > documents.[].content'); expect(contentParameter?.formLabel).toBe('知识库 > documents.[].content'); expect(contentParameter?.pathLabel).toBe('documents.[].content'); expect(contentParameter?.itemTypeLabel).toBe('数组项字段'); }); it('exposes typed loop variables only to nodes inside the loop body', () => { const knowledgeNode: Node = { id: 'knowledge_1', type: 'knowledgeNode', position: { x: 0, y: 0 }, data: { title: '知识库', outputDefs: [ { name: 'documents', dataType: 'Array', children: [ { name: 'title', dataType: 'String' }, { name: 'content', dataType: 'String' }, ], }, ], }, }; const loopNode: Node = { id: 'loop_1', type: 'loopNode', position: { x: 120, y: 0 }, data: { title: '循环', loopInputs: { items: { name: 'items', refType: 'ref', ref: 'knowledge_1.documents', dataType: 'Array', }, }, outputDefs: [ { name: 'res', dataType: 'Array', }, ], }, }; const insideNode: Node = { id: 'llm_inside', type: 'llmNode', parentId: 'loop_1', position: { x: 240, y: 0 }, data: { title: '循环内模型', parameters: [] }, }; const outsideNode: Node = { id: 'llm_outside', type: 'llmNode', position: { x: 240, y: 120 }, data: { title: '循环外模型', parameters: [] }, }; const nodes = [knowledgeNode, loopNode, insideNode, outsideNode]; const insideParameters = buildEditorReferenceParameters( 'llm_inside', nodes, [], [], ); const outsideParameters = buildEditorReferenceParameters( 'llm_outside', nodes, [ { id: 'loop_to_outside', source: 'loop_1', target: 'llm_outside', }, ], [], ); expect(insideParameters.map((item) => item.name)).toEqual([ 'loop_1.index', 'loop_1.loopItem', 'loop_1.loopItem.title', 'loop_1.loopItem.content', ]); expect( insideParameters.find((item) => item.name === 'loop_1.loopItem.content') ?.dataType, ).toBe('String'); expect(outsideParameters.map((item) => item.name)).toEqual(['loop_1.res']); expect( outsideParameters.some((item) => ['loop_1.index', 'loop_1.loopItem'].includes(item.name || ''), ), ).toBe(false); }); it('uses document node child outputs for reference display', () => { const documentNode: Node = { id: 'doc_1', type: 'document-node', position: { x: 0, y: 0 }, data: { title: '文档解析', outputDefs: [ { name: 'documents', formLabel: '文档列表', children: [ { name: 'fileName', formLabel: '文件名', }, { name: 'content', formLabel: '正文内容', }, ], }, ], }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', parameters: [], }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'doc_1', target: 'llm_1' } as Edge, ]; const parameters = buildEditorReferenceParameters( 'llm_1', [documentNode, llmNode], edges, [], ); expect( parameters.find((item) => item.name === 'doc_1.documents')?.displayName, ).toBe('文档解析 > documents'); expect( parameters.find((item) => item.name === 'doc_1.documents.fileName') ?.displayName, ).toBe('文档解析 > documents.[].fileName'); expect( parameters.find((item) => item.name === 'doc_1.documents.content') ?.displayName, ).toBe('文档解析 > documents.[].content'); }); it('applies default binding to llm user prompt after connect', () => { const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '流程开始', parameters: ensureStartNodeParameters(), }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', parameters: [], userPrompt: '', }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'start_1', target: 'llm_1' } as Edge, ]; const patch = buildAutoBindingPatch(llmNode, [startNode, llmNode], edges); expect(patch?.userPrompt).toBe('{{start_1.user_input}}'); expect((patch?.parameters as any[])?.[0]?.name).toBe('start_1.user_input'); expect( (patch?.[FIELD_BINDING_META_KEY] as any)?.userPrompt?.userModified, ).toBe(false); }); it('does not auto-bind from legacy start nodes without user_input', () => { const legacyStartNode: Node = { id: 'start_legacy', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '开始节点', parameters: [], }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', parameters: [], userPrompt: '', }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'start_legacy', target: 'llm_1' } as Edge, ]; expect( buildAutoBindingPatch(llmNode, [legacyStartNode, llmNode], edges), ).toBeNull(); }); it('clears auto-filled start bindings after disconnect', () => { const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '流程开始', parameters: ensureStartNodeParameters(), }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', userPrompt: '{{start_1.user_input}}', parameters: [ { name: 'start_1.user_input', ref: 'start_1.user_input', refType: 'ref', autoManaged: true, }, ], [FIELD_BINDING_META_KEY]: { userPrompt: { autoFilledFrom: 'start_1.user_input', userModified: false, }, }, }, }; const patch = buildFieldBindingPatch(llmNode, [startNode, llmNode], []); expect(patch?.userPrompt).toBe(''); expect(patch?.parameters).toEqual([]); expect(patch?.[FIELD_BINDING_META_KEY]).toEqual({}); }); it('removes managed param for disconnected manual upstream refs so token becomes invalid', () => { const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '流程开始', parameters: ensureStartNodeParameters(), }, }; const knowledgeNode: Node = { id: 'knowledge_1', type: 'knowledgeNode', position: { x: 80, y: 0 }, data: { title: '知识库', outputDefs: [ { name: 'documents', dataType: 'String', }, ], }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', systemPrompt: '{{knowledge_1.documents}}', userPrompt: '', parameters: [ { name: 'knowledge_1.documents', ref: 'knowledge_1.documents', refType: 'ref', autoManaged: true, }, ], }, }; const connectedEdges: Edge[] = [ { id: 'edge_1', source: 'start_1', target: 'knowledge_1' } as Edge, { id: 'edge_2', source: 'knowledge_1', target: 'llm_1' } as Edge, ]; const connectedParameters = buildEditorReferenceParameters( 'llm_1', [startNode, knowledgeNode, llmNode], connectedEdges, (llmNode.data?.parameters || []) as any[], ); expect( connectedParameters.some((item) => item.name === 'knowledge_1.documents'), ).toBe(true); const patch = buildFieldBindingPatch( llmNode, [startNode, knowledgeNode, llmNode], [], ); expect((patch?.parameters as any[])?.[0]?.name).toBe( 'knowledge_1.documents', ); expect((patch?.parameters as any[])?.[0]?.disconnected).toBe(true); expect((patch?.parameters as any[])?.[0]?.displayName).toBe('documents'); expect(patch).not.toHaveProperty('systemPrompt'); }); it('restores auto-filled user input binding after reconnect through upstream chain', () => { const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '流程开始', parameters: ensureStartNodeParameters(), }, }; const knowledgeNode: Node = { id: 'knowledge_1', type: 'knowledgeNode', position: { x: 80, y: 0 }, data: { title: '知识库', keyword: '', parameters: [], outputDefs: [ { name: 'documents', dataType: 'String', }, ], }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', userPrompt: '', parameters: [], }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'start_1', target: 'knowledge_1' } as Edge, { id: 'edge_2', source: 'knowledge_1', target: 'llm_1' } as Edge, ]; const knowledgePatch = buildFieldBindingPatch( knowledgeNode, [startNode, knowledgeNode, llmNode], edges, ); const nextKnowledgeNode: Node = { ...knowledgeNode, data: { ...knowledgeNode.data, ...knowledgePatch, }, }; const llmPatch = buildFieldBindingPatch( llmNode, [startNode, nextKnowledgeNode, llmNode], edges, ); expect(knowledgePatch?.keyword).toBe('{{start_1.user_input}}'); expect(llmPatch?.userPrompt).toBe('{{start_1.user_input}}'); }); it('applies reconnect patches sequentially so downstream nodes can restore in the same batch', () => { const startNode: Node = { id: 'start_1', type: 'startNode', position: { x: 0, y: 0 }, data: { title: '流程开始', parameters: ensureStartNodeParameters(), }, }; const knowledgeNode: Node = { id: 'knowledge_1', type: 'knowledgeNode', position: { x: 80, y: 0 }, data: { title: '知识库', keyword: '', parameters: [], outputDefs: [ { name: 'documents', dataType: 'String', }, ], }, }; const llmNode: Node = { id: 'llm_1', type: 'llmNode', position: { x: 120, y: 0 }, data: { title: '大模型', userPrompt: '', parameters: [], }, }; const edges: Edge[] = [ { id: 'edge_1', source: 'start_1', target: 'knowledge_1' } as Edge, { id: 'edge_2', source: 'knowledge_1', target: 'llm_1' } as Edge, ]; const patches = buildSequentialFieldBindingPatches( ['knowledge_1', 'llm_1'], [startNode, knowledgeNode, llmNode], edges, ); expect(patches).toHaveLength(2); expect(patches[0]).toMatchObject({ nodeId: 'knowledge_1', patch: { keyword: '{{start_1.user_input}}', }, }); expect(patches[1]).toMatchObject({ nodeId: 'llm_1', patch: { userPrompt: '{{start_1.user_input}}', }, }); }); it('extracts only used parameters for llm single run', () => { const parameters = ensureStartNodeParameters().map((item) => ({ ...item, name: 'start_1.user_input', ref: 'start_1.user_input', formLabel: '流程开始 > 用户问题', displayName: '流程开始 > 用户问题', systemReserved: false, autoManaged: true, })); const result = buildSingleRunParameters({ type: 'llmNode', data: { userPrompt: '请回答 {{start_1.user_input}}', systemPrompt: '系统', parameters, }, }); expect(result).toHaveLength(1); expect(result[0]?.formLabel).toBe('流程开始 > 用户问题'); expect(result[0]?.required).toBe(true); }); it('builds field-mode single run model for llm node', () => { const parameters = ensureStartNodeParameters().map((item) => ({ ...item, name: 'start_1.user_input', ref: 'start_1.user_input', formLabel: '流程开始 > 用户问题', displayName: '流程开始 > 用户问题', systemReserved: false, autoManaged: true, })); const result = buildSingleRunModel({ type: 'llmNode', data: { userPrompt: '请回答 {{start_1.user_input}}', systemPrompt: '你是助手', parameters, }, }); expect(result.mode).toBe('fields'); expect(result.fields.map((item) => item.key)).toEqual([ 'systemPrompt', 'userPrompt', ]); expect(result.parameters).toHaveLength(1); expect(result.parameters[0]?.formLabel).toBe('流程开始 > 用户问题'); }); it('keeps legacy start node parameters unchanged during single run build', () => { const legacyParameters = [ { id: 'legacy_1', name: 'legacy_input', refType: 'input', dataType: 'String', }, ]; const result = buildSingleRunParameters({ type: 'startNode', data: { parameters: legacyParameters, }, }); expect(result).toEqual(legacyParameters); }); it('normalizes start node schema from parameters', () => { const normalized = normalizeStartNodeData({ parameters: [ { id: 'system_1', name: 'user_input', refType: 'input', required: true, formType: 'input', formLabel: '问题', formPlaceholder: '请输入问题', }, { id: 'file_1', name: 'attachments', refType: 'input', dataType: 'File', contentType: 'file', formLabel: '附件', required: false, }, ], }); expect(normalized.startFormSchema).toHaveLength(2); expect(normalized.startFormSchema[0]).toMatchObject({ key: 'user_input', type: 'text', systemReserved: true, required: true, }); expect(normalized.startFormSchema[1]).toMatchObject({ key: 'attachments', type: 'file', required: false, }); expect(normalized.parameters[1]).toMatchObject({ name: 'attachments', contentType: 'file', dataType: 'File', }); }); it('forces invalid user_input schema back to required text input', () => { const normalized = normalizeStartNodeData({ startFormSchema: [ { key: 'user_input', label: '主问题', type: 'radio', required: false, options: ['A', 'B'], }, ], startFormMeta: { title: '', description: '', submitText: '', }, }); expect(normalized.startFormSchema[0]).toMatchObject({ key: 'user_input', type: 'textarea', required: true, systemReserved: true, }); expect(normalized.startFormMeta).toMatchObject({ title: '', description: '', submitText: '开始', }); expect(normalized.parameters[0]).toMatchObject({ name: 'user_input', formType: 'textarea', required: true, contentType: 'text', }); }); it('normalizes only start nodes that already contain fixed user_input', () => { const normalizedWorkflow = normalizeWorkflowStartNodes({ nodes: [ { id: 'start_new', type: 'startNode', data: { parameters: [ { name: 'user_input', refType: 'input', required: false, }, ], }, }, { id: 'start_legacy', type: 'startNode', data: { parameters: [], }, }, ], edges: [], }); expect(normalizedWorkflow.nodes[0]?.data?.parameters?.[0]?.required).toBe( true, ); expect( (normalizedWorkflow.nodes[0]?.data as Record | undefined) ?.startFormSchema?.[0]?.key, ).toBe('user_input'); expect(normalizedWorkflow.nodes[1]?.data?.parameters).toEqual([]); }); });