mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-16 08:01:18 +00:00
Test version (#4792)
* plugin node version select (#4760) * plugin node version select * type * fix * fix * perf: version list * fix node version (#4787) * change my select * fix-ui * fix test * add test * fix * remove invalid version field * filter deprecated field * fix: claude tool call * fix: test --------- Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import type { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
describe('storeNode2FlowNode with deprecated inputs/outputs', () => {
|
||||
beforeEach(() => {
|
||||
vi.mock('@fastgpt/global/core/workflow/template/constants', () => {
|
||||
return {
|
||||
moduleTemplatesFlat: [
|
||||
{
|
||||
flowNodeType: 'userInput',
|
||||
name: 'User Input',
|
||||
avatar: '',
|
||||
intro: '',
|
||||
version: '1.0',
|
||||
inputs: [
|
||||
{
|
||||
key: 'deprecatedInput',
|
||||
deprecated: true,
|
||||
label: 'Deprecated Input',
|
||||
renderTypeList: ['input'],
|
||||
selectedTypeIndex: 0
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'deprecatedOutput',
|
||||
id: 'deprecatedId',
|
||||
type: 'input',
|
||||
deprecated: true,
|
||||
label: 'Deprecated Output'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('should handle deprecated inputs and outputs', () => {
|
||||
const storeNode = {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: 'userInput' as FlowNodeTypeEnum,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [
|
||||
{
|
||||
key: 'deprecatedInput',
|
||||
value: 'old value',
|
||||
renderTypeList: ['input'],
|
||||
label: 'Deprecated Input'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'deprecatedOutput',
|
||||
id: 'deprecatedId',
|
||||
type: 'input',
|
||||
label: 'Deprecated Output'
|
||||
}
|
||||
],
|
||||
name: 'Test Node',
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const result = storeNode2FlowNode({
|
||||
item: storeNode as any,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
const deprecatedInput = result.data.inputs.find((input) => input.key === 'deprecatedInput');
|
||||
expect(deprecatedInput).toBeDefined();
|
||||
expect(deprecatedInput?.deprecated).toBe(true);
|
||||
|
||||
const deprecatedOutput = result.data.outputs.find(
|
||||
(output) => output.key === 'deprecatedOutput'
|
||||
);
|
||||
expect(deprecatedOutput).toBeDefined();
|
||||
expect(deprecatedOutput?.deprecated).toBe(true);
|
||||
});
|
||||
});
|
113
projects/app/test/cases/web/workflow/store2flow.version.test.ts
Normal file
113
projects/app/test/cases/web/workflow/store2flow.version.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { storeNode2FlowNode } from '@/web/core/workflow/utils';
|
||||
import type { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
describe('storeNode2FlowNode with version and avatar inheritance', () => {
|
||||
beforeEach(() => {
|
||||
vi.mock('@fastgpt/global/core/workflow/template/constants', () => {
|
||||
return {
|
||||
moduleTemplatesFlat: [
|
||||
{
|
||||
flowNodeType: 'userInput',
|
||||
name: 'User Input',
|
||||
avatar: 'template-avatar.png',
|
||||
intro: '',
|
||||
version: '2.0',
|
||||
inputs: [],
|
||||
outputs: []
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
vi.mock('@fastgpt/global/core/workflow/node/constant', () => {
|
||||
return {
|
||||
FlowNodeTypeEnum: { userInput: 'userInput' },
|
||||
FlowNodeInputTypeEnum: {
|
||||
addInputParam: 'addInputParam',
|
||||
input: 'input',
|
||||
reference: 'reference',
|
||||
textarea: 'textarea',
|
||||
numberInput: 'numberInput',
|
||||
switch: 'switch',
|
||||
select: 'select'
|
||||
},
|
||||
FlowNodeOutputTypeEnum: {
|
||||
dynamic: 'dynamic',
|
||||
static: 'static',
|
||||
source: 'source',
|
||||
hidden: 'hidden'
|
||||
},
|
||||
EDGE_TYPE: 'custom-edge',
|
||||
chatHistoryValueDesc: 'chat history description',
|
||||
datasetSelectValueDesc: 'dataset value description',
|
||||
datasetQuoteValueDesc: 'dataset quote value description'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('should handle version and avatar inheritance', () => {
|
||||
// 测试场景1:storeNode没有version,使用template的version
|
||||
const storeNode1 = {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: 'userInput' as FlowNodeTypeEnum,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
name: 'Test Node 1'
|
||||
};
|
||||
|
||||
// 测试场景2:storeNode没有avatar,使用template的avatar
|
||||
const storeNode2 = {
|
||||
nodeId: 'node2',
|
||||
flowNodeType: 'userInput' as FlowNodeTypeEnum,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
name: 'Test Node 2',
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
// 测试场景3:storeNode和template都有avatar,使用template的avatar
|
||||
const storeNode3 = {
|
||||
nodeId: 'node3',
|
||||
flowNodeType: 'userInput' as FlowNodeTypeEnum,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
name: 'Test Node 3',
|
||||
version: '3.0',
|
||||
avatar: 'store-avatar.png'
|
||||
};
|
||||
|
||||
const result1 = storeNode2FlowNode({
|
||||
item: storeNode1 as any,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
const result2 = storeNode2FlowNode({
|
||||
item: storeNode2 as any,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
const result3 = storeNode2FlowNode({
|
||||
item: storeNode3 as any,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
// 验证版本继承关系
|
||||
expect(result1.data.version).toBe('2.0'); // 使用template的version
|
||||
expect(result2.data.version).toBe('2.0'); // 使用storeNode的version
|
||||
expect(result3.data.version).toBe('2.0'); // 使用storeNode的version
|
||||
|
||||
// 验证avatar继承关系
|
||||
expect(result1.data.avatar).toBe('template-avatar.png'); // 使用template的avatar
|
||||
expect(result2.data.avatar).toBe('template-avatar.png'); // 使用template的avatar
|
||||
expect(result3.data.avatar).toBe('template-avatar.png'); // 根据源码,应该使用template的avatar
|
||||
});
|
||||
});
|
339
projects/app/test/cases/web/workflow/utils.test.ts
Normal file
339
projects/app/test/cases/web/workflow/utils.test.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
||||
import type {
|
||||
FlowNodeItemType,
|
||||
FlowNodeTemplateType,
|
||||
StoreNodeItemType
|
||||
} from '@fastgpt/global/core/workflow/type/node';
|
||||
import type { Node, Edge } from 'reactflow';
|
||||
import {
|
||||
FlowNodeTypeEnum,
|
||||
FlowNodeInputTypeEnum,
|
||||
FlowNodeOutputTypeEnum,
|
||||
EDGE_TYPE
|
||||
} from '@fastgpt/global/core/workflow/node/constant';
|
||||
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
|
||||
import {
|
||||
nodeTemplate2FlowNode,
|
||||
storeNode2FlowNode,
|
||||
filterWorkflowNodeOutputsByType,
|
||||
checkWorkflowNodeAndConnection,
|
||||
getLatestNodeTemplate
|
||||
} from '@/web/core/workflow/utils';
|
||||
import type { FlowNodeOutputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
|
||||
describe('nodeTemplate2FlowNode', () => {
|
||||
it('should convert template to flow node', () => {
|
||||
const template: FlowNodeTemplateType = {
|
||||
id: 'template1',
|
||||
templateType: 'formInput',
|
||||
name: 'Test Node',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [],
|
||||
outputs: []
|
||||
};
|
||||
|
||||
const result = nodeTemplate2FlowNode({
|
||||
template,
|
||||
position: { x: 100, y: 100 },
|
||||
selected: true,
|
||||
parentNodeId: 'parent1',
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
type: FlowNodeTypeEnum.formInput,
|
||||
position: { x: 100, y: 100 },
|
||||
selected: true,
|
||||
data: {
|
||||
name: 'Test Node',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
parentNodeId: 'parent1'
|
||||
}
|
||||
});
|
||||
expect(result.id).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('storeNode2FlowNode', () => {
|
||||
it('should convert store node to flow node', () => {
|
||||
const storeNode: StoreNodeItemType = {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
position: { x: 100, y: 100 },
|
||||
inputs: [],
|
||||
outputs: [],
|
||||
name: 'Test Node',
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const result = storeNode2FlowNode({
|
||||
item: storeNode,
|
||||
selected: true,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
id: 'node1',
|
||||
type: FlowNodeTypeEnum.formInput,
|
||||
position: { x: 100, y: 100 },
|
||||
selected: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle dynamic inputs and outputs', () => {
|
||||
const storeNode: StoreNodeItemType = {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
position: { x: 0, y: 0 },
|
||||
inputs: [
|
||||
{
|
||||
key: 'dynamicInput',
|
||||
label: 'Dynamic Input',
|
||||
renderTypeList: [FlowNodeInputTypeEnum.addInputParam]
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
id: 'dynamicOutput',
|
||||
key: 'dynamicOutput',
|
||||
label: 'Dynamic Output',
|
||||
type: FlowNodeOutputTypeEnum.dynamic
|
||||
}
|
||||
],
|
||||
name: 'Test Node',
|
||||
version: '1.0'
|
||||
};
|
||||
|
||||
const result = storeNode2FlowNode({
|
||||
item: storeNode,
|
||||
t: ((key: any) => key) as any
|
||||
});
|
||||
|
||||
expect(result.data.inputs).toHaveLength(3);
|
||||
expect(result.data.outputs).toHaveLength(2);
|
||||
});
|
||||
|
||||
// 这两个测试涉及到模拟冲突,请运行单独的测试文件:
|
||||
// - utils.deprecated.test.ts: 测试 deprecated inputs/outputs
|
||||
// - utils.version.test.ts: 测试 version 和 avatar inheritance
|
||||
});
|
||||
|
||||
describe('filterWorkflowNodeOutputsByType', () => {
|
||||
it('should filter outputs by type', () => {
|
||||
const outputs: FlowNodeOutputItemType[] = [
|
||||
{
|
||||
id: '1',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
key: '1',
|
||||
label: '1',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
valueType: WorkflowIOValueTypeEnum.number,
|
||||
key: '2',
|
||||
label: '2',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
valueType: WorkflowIOValueTypeEnum.boolean,
|
||||
key: '3',
|
||||
label: '3',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
}
|
||||
];
|
||||
|
||||
const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.string);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe('1');
|
||||
});
|
||||
|
||||
it('should return all outputs for any type', () => {
|
||||
const outputs: FlowNodeOutputItemType[] = [
|
||||
{
|
||||
id: '1',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
key: '1',
|
||||
label: '1',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
valueType: WorkflowIOValueTypeEnum.number,
|
||||
key: '2',
|
||||
label: '2',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
}
|
||||
];
|
||||
|
||||
const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.any);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle array types correctly', () => {
|
||||
const outputs: FlowNodeOutputItemType[] = [
|
||||
{
|
||||
id: '1',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
key: '1',
|
||||
label: '1',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
valueType: WorkflowIOValueTypeEnum.arrayString,
|
||||
key: '2',
|
||||
label: '2',
|
||||
type: FlowNodeOutputTypeEnum.static
|
||||
}
|
||||
];
|
||||
|
||||
const result = filterWorkflowNodeOutputsByType(outputs, WorkflowIOValueTypeEnum.arrayString);
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkWorkflowNodeAndConnection', () => {
|
||||
it('should validate nodes and connections', () => {
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
id: 'node1',
|
||||
type: FlowNodeTypeEnum.formInput,
|
||||
data: {
|
||||
nodeId: 'node1',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{
|
||||
key: NodeInputKeyEnum.aiChatDatasetQuote,
|
||||
required: true,
|
||||
value: undefined,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.input]
|
||||
}
|
||||
],
|
||||
outputs: []
|
||||
},
|
||||
position: { x: 0, y: 0 }
|
||||
}
|
||||
];
|
||||
|
||||
const edges: Edge[] = [
|
||||
{
|
||||
id: 'edge1',
|
||||
source: 'node1',
|
||||
target: 'node2',
|
||||
type: EDGE_TYPE
|
||||
}
|
||||
];
|
||||
|
||||
const result = checkWorkflowNodeAndConnection({ nodes, edges });
|
||||
expect(result).toEqual(['node1']);
|
||||
});
|
||||
|
||||
it('should handle empty nodes and edges', () => {
|
||||
const result = checkWorkflowNodeAndConnection({ nodes: [], edges: [] });
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLatestNodeTemplate', () => {
|
||||
it('should update node to latest template version', () => {
|
||||
const node: FlowNodeItemType = {
|
||||
id: 'node1',
|
||||
nodeId: 'node1',
|
||||
templateType: 'formInput',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{
|
||||
key: 'input1',
|
||||
value: 'test',
|
||||
renderTypeList: [FlowNodeInputTypeEnum.input],
|
||||
label: 'Input 1'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'output1',
|
||||
value: 'test',
|
||||
type: FlowNodeOutputTypeEnum.static,
|
||||
label: 'Output 1',
|
||||
id: 'output1'
|
||||
}
|
||||
],
|
||||
name: 'Old Name',
|
||||
intro: 'Old Intro'
|
||||
};
|
||||
|
||||
const template: FlowNodeTemplateType = {
|
||||
name: 'Template 1',
|
||||
id: 'template1',
|
||||
templateType: 'formInput',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{ key: 'input1', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 1' },
|
||||
{ key: 'input2', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 2' }
|
||||
],
|
||||
outputs: [
|
||||
{ id: 'output1', key: 'output1', type: FlowNodeOutputTypeEnum.static, label: 'Output 1' },
|
||||
{ id: 'output2', key: 'output2', type: FlowNodeOutputTypeEnum.static, label: 'Output 2' }
|
||||
]
|
||||
};
|
||||
|
||||
const result = getLatestNodeTemplate(node, template);
|
||||
|
||||
expect(result.inputs).toHaveLength(2);
|
||||
expect(result.outputs).toHaveLength(2);
|
||||
expect(result.name).toBe('Old Name');
|
||||
});
|
||||
|
||||
it('should preserve existing values when updating template', () => {
|
||||
const node: FlowNodeItemType = {
|
||||
id: 'node1',
|
||||
nodeId: 'node1',
|
||||
templateType: 'formInput',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{
|
||||
key: 'input1',
|
||||
value: 'existingValue',
|
||||
renderTypeList: [FlowNodeInputTypeEnum.input],
|
||||
label: 'Input 1'
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
key: 'output1',
|
||||
value: 'existingOutput',
|
||||
type: FlowNodeOutputTypeEnum.static,
|
||||
label: 'Output 1',
|
||||
id: 'output1'
|
||||
}
|
||||
],
|
||||
name: 'Node Name',
|
||||
intro: 'Node Intro'
|
||||
};
|
||||
|
||||
const template: FlowNodeTemplateType = {
|
||||
name: 'Template 1',
|
||||
id: 'template1',
|
||||
templateType: 'formInput',
|
||||
flowNodeType: FlowNodeTypeEnum.formInput,
|
||||
inputs: [
|
||||
{ key: 'input1', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 1' },
|
||||
{ key: 'input2', renderTypeList: [FlowNodeInputTypeEnum.input], label: 'Input 2' }
|
||||
],
|
||||
outputs: [
|
||||
{ id: 'output1', key: 'output1', type: FlowNodeOutputTypeEnum.static, label: 'Output 1' },
|
||||
{ id: 'output2', key: 'output2', type: FlowNodeOutputTypeEnum.static, label: 'Output 2' }
|
||||
]
|
||||
};
|
||||
|
||||
const result = getLatestNodeTemplate(node, template);
|
||||
|
||||
expect(result.inputs[0].value).toBe('existingValue');
|
||||
expect(result.outputs[0].value).toBe('existingOutput');
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user