feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
100
frontend/packages/workflow/base/__tests__/api/api.test.ts
Normal file
100
frontend/packages/workflow/base/__tests__/api/api.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { workflowApi as archWorkflowApi } from '@coze-arch/bot-api';
|
||||
|
||||
import { workflowApi, workflowQueryClient } from '../../src/api';
|
||||
|
||||
vi.mock('@coze-arch/bot-api', () => ({
|
||||
workflowApi: {
|
||||
GetHistorySchema: vi.fn(),
|
||||
GetWorkFlowProcess: vi.fn(),
|
||||
GetCanvasInfo: vi.fn(),
|
||||
OPGetHistorySchema: vi.fn(),
|
||||
OPGetWorkFlowProcess: vi.fn(),
|
||||
OPGetCanvasInfo: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockParams = {
|
||||
space_id: '123',
|
||||
workflow_id: '456',
|
||||
commit_id: '789',
|
||||
type: 1,
|
||||
};
|
||||
|
||||
describe('api/index.ts', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should export workflowApi', () => {
|
||||
expect(workflowApi).toBeDefined();
|
||||
});
|
||||
|
||||
it('should export workflowQueryClient', () => {
|
||||
expect(workflowQueryClient).toBeDefined();
|
||||
});
|
||||
|
||||
describe('workflowApi proxy', () => {
|
||||
it('should call original API methods in non-OP environment', () => {
|
||||
// @ts-expect-error IS_BOT_OP is a global variable defined in runtime
|
||||
global.IS_BOT_OP = false;
|
||||
|
||||
workflowApi.GetHistorySchema(mockParams);
|
||||
expect(archWorkflowApi.GetHistorySchema).toHaveBeenCalledWith(mockParams);
|
||||
|
||||
workflowApi.GetWorkFlowProcess({
|
||||
space_id: mockParams.space_id,
|
||||
workflow_id: mockParams.workflow_id,
|
||||
});
|
||||
expect(archWorkflowApi.GetWorkFlowProcess).toHaveBeenCalledWith({
|
||||
space_id: mockParams.space_id,
|
||||
workflow_id: mockParams.workflow_id,
|
||||
});
|
||||
|
||||
workflowApi.GetCanvasInfo({ space_id: mockParams.space_id });
|
||||
expect(archWorkflowApi.GetCanvasInfo).toHaveBeenCalledWith({
|
||||
space_id: mockParams.space_id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call OP API methods in OP environment', () => {
|
||||
// @ts-expect-error IS_BOT_OP is a global variable defined in runtime
|
||||
global.IS_BOT_OP = true;
|
||||
|
||||
workflowApi.GetHistorySchema(mockParams);
|
||||
expect(archWorkflowApi.OPGetHistorySchema).toHaveBeenCalledWith(
|
||||
mockParams,
|
||||
);
|
||||
|
||||
workflowApi.GetWorkFlowProcess({
|
||||
space_id: mockParams.space_id,
|
||||
workflow_id: mockParams.workflow_id,
|
||||
});
|
||||
expect(archWorkflowApi.OPGetWorkFlowProcess).toHaveBeenCalledWith({
|
||||
space_id: mockParams.space_id,
|
||||
workflow_id: mockParams.workflow_id,
|
||||
});
|
||||
|
||||
workflowApi.GetCanvasInfo({ space_id: mockParams.space_id });
|
||||
expect(archWorkflowApi.OPGetCanvasInfo).toHaveBeenCalledWith({
|
||||
space_id: mockParams.space_id,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import {
|
||||
workflowQueryClient,
|
||||
withQueryClient,
|
||||
} from '../../src/api/with-query-client';
|
||||
|
||||
describe('with-query-client.tsx', () => {
|
||||
beforeEach(() => {
|
||||
// 清理 QueryClient 缓存
|
||||
workflowQueryClient.clear();
|
||||
});
|
||||
|
||||
describe('workflowQueryClient', () => {
|
||||
it('should export a QueryClient instance', () => {
|
||||
expect(workflowQueryClient).toBeDefined();
|
||||
expect(workflowQueryClient.constructor.name).toBe('QueryClient');
|
||||
});
|
||||
|
||||
it('should have default configuration', () => {
|
||||
const defaultOptions = workflowQueryClient.getDefaultOptions();
|
||||
expect(defaultOptions).toBeDefined();
|
||||
});
|
||||
|
||||
it('should be able to manage queries', async () => {
|
||||
const queryKey = ['test'];
|
||||
const queryFn = vi.fn().mockResolvedValue('test data');
|
||||
|
||||
const result = await workflowQueryClient.fetchQuery({
|
||||
queryKey,
|
||||
queryFn,
|
||||
});
|
||||
|
||||
expect(result).toBe('test data');
|
||||
expect(queryFn).toHaveBeenCalled();
|
||||
expect(workflowQueryClient.getQueryData(queryKey)).toBe('test data');
|
||||
});
|
||||
});
|
||||
|
||||
describe('withQueryClient', () => {
|
||||
it('should wrap component with QueryClientProvider', () => {
|
||||
const TestComponent = () => <div>Test Component</div>;
|
||||
const WrappedComponent = withQueryClient(TestComponent);
|
||||
|
||||
const { container } = render(<WrappedComponent />);
|
||||
expect(container.innerHTML).toContain('Test Component');
|
||||
});
|
||||
|
||||
it('should pass props to wrapped component', () => {
|
||||
const TestComponent = ({ text }: { text: string }) => <div>{text}</div>;
|
||||
const WrappedComponent = withQueryClient(TestComponent);
|
||||
|
||||
const { container } = render(<WrappedComponent text="Hello" />);
|
||||
expect(container.innerHTML).toContain('Hello');
|
||||
});
|
||||
|
||||
it('should provide QueryClient context to wrapped component', async () => {
|
||||
const queryKey = ['test'];
|
||||
const queryFn = vi.fn().mockResolvedValue('test data');
|
||||
|
||||
const TestComponent = () => {
|
||||
const { data, isLoading } = useQuery({ queryKey, queryFn });
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
const WrappedComponent = withQueryClient(TestComponent);
|
||||
const { container } = render(<WrappedComponent />);
|
||||
|
||||
// 初始加载状态
|
||||
expect(container.innerHTML).toContain('Loading...');
|
||||
|
||||
// 等待数据加载完成
|
||||
await waitFor(() => {
|
||||
expect(container.innerHTML).toContain('test data');
|
||||
});
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle query errors gracefully', () => {
|
||||
const queryKey = ['test-error'];
|
||||
const queryFn = vi.fn().mockRejectedValue(new Error('Test error'));
|
||||
|
||||
const TestComponent = () => {
|
||||
const { error, isError } = useQuery({ queryKey, queryFn });
|
||||
if (isError) {
|
||||
return <div>Error: {error.message}</div>;
|
||||
}
|
||||
return <div>Loading...</div>;
|
||||
};
|
||||
|
||||
const WrappedComponent = withQueryClient(TestComponent);
|
||||
render(<WrappedComponent />);
|
||||
|
||||
expect(queryFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should maintain component type and handle complex props', () => {
|
||||
interface ComplexProps {
|
||||
text: string;
|
||||
count: number;
|
||||
items: string[];
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const TestComponent: React.FC<ComplexProps> = ({
|
||||
text,
|
||||
count,
|
||||
items,
|
||||
onClick,
|
||||
}) => (
|
||||
<div onClick={onClick}>
|
||||
<span>{text}</span>
|
||||
<span>{count}</span>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
const WrappedComponent = withQueryClient(TestComponent);
|
||||
const handleClick = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<WrappedComponent
|
||||
text="Complex Test"
|
||||
count={42}
|
||||
items={['a', 'b', 'c']}
|
||||
onClick={handleClick}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.innerHTML).toContain('Complex Test');
|
||||
expect(container.innerHTML).toContain('42');
|
||||
expect(container.innerHTML).toContain('<li>a</li>');
|
||||
expect(container.innerHTML).toContain('<li>b</li>');
|
||||
expect(container.innerHTML).toContain('<li>c</li>');
|
||||
});
|
||||
|
||||
it('should work with nested queries', async () => {
|
||||
const parentQueryKey = ['parent'];
|
||||
const childQueryKey = ['child'];
|
||||
const parentQueryFn = vi.fn().mockResolvedValue('parent data');
|
||||
const childQueryFn = vi.fn().mockResolvedValue('child data');
|
||||
|
||||
const ChildComponent = () => {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: childQueryKey,
|
||||
queryFn: childQueryFn,
|
||||
});
|
||||
if (isLoading) {
|
||||
return <div>Loading Child...</div>;
|
||||
}
|
||||
return <div className="child">{data}</div>;
|
||||
};
|
||||
|
||||
const ParentComponent = () => {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: parentQueryKey,
|
||||
queryFn: parentQueryFn,
|
||||
});
|
||||
if (isLoading) {
|
||||
return <div>Loading Parent...</div>;
|
||||
}
|
||||
return (
|
||||
<div className="parent">
|
||||
{data}
|
||||
<ChildComponent />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const WrappedComponent = withQueryClient(ParentComponent);
|
||||
const { container } = render(<WrappedComponent />);
|
||||
|
||||
// 初始加载状态
|
||||
expect(container.innerHTML).toContain('Loading Parent...');
|
||||
|
||||
// 等待所有查询完成
|
||||
await waitFor(() => {
|
||||
expect(container.innerHTML).toContain('parent data');
|
||||
expect(container.innerHTML).toContain('child data');
|
||||
});
|
||||
|
||||
expect(parentQueryFn).toHaveBeenCalledTimes(1);
|
||||
expect(childQueryFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
FlowNodeFormData,
|
||||
FlowNodeErrorData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { StandardNodeType } from '../../src/types';
|
||||
import { type WorkflowNode } from '../../src/entities';
|
||||
import { WorkflowNodeContext } from '../../src/contexts/workflow-node-context';
|
||||
|
||||
describe('WorkflowNodeContext', () => {
|
||||
const createMockWorkflowNode = (id: string): WorkflowNode => {
|
||||
const dispose = vi.fn();
|
||||
const mockFormModel = {
|
||||
initialized: true,
|
||||
getFormItemValueByPath: vi.fn(),
|
||||
getFormItemByPath: vi.fn(),
|
||||
formItemPathMap: new Map(),
|
||||
onInitialized: vi.fn().mockReturnValue({ dispose }),
|
||||
};
|
||||
|
||||
const mockFormData = {
|
||||
formModel: mockFormModel,
|
||||
onDataChange: vi.fn().mockReturnValue({ dispose }),
|
||||
};
|
||||
|
||||
const mockRegistry = {
|
||||
getNodeInputParameters: vi.fn().mockReturnValue([]),
|
||||
getNodeOutputs: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
const mockNode = {
|
||||
id,
|
||||
flowNodeType: StandardNodeType.Start,
|
||||
getNodeRegistry: vi.fn().mockReturnValue(mockRegistry),
|
||||
getNodeMeta: vi.fn().mockReturnValue({
|
||||
icon: '',
|
||||
title: '',
|
||||
description: '',
|
||||
}),
|
||||
getData: vi.fn(dataType => {
|
||||
if (dataType === FlowNodeFormData) {
|
||||
return mockFormData;
|
||||
}
|
||||
if (dataType === FlowNodeErrorData) {
|
||||
return {
|
||||
getError: vi.fn().mockReturnValue(null),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
_formModel: mockFormModel,
|
||||
};
|
||||
|
||||
const workflowNode = {
|
||||
type: StandardNodeType.Start,
|
||||
registry: mockRegistry,
|
||||
inputParameters: [],
|
||||
outputs: [],
|
||||
data: {},
|
||||
icon: '',
|
||||
title: '',
|
||||
description: '',
|
||||
isError: false,
|
||||
isInitialized: true,
|
||||
setData: vi.fn(),
|
||||
onDataChange: vi.fn().mockReturnValue({ dispose }),
|
||||
onInitialized: vi.fn().mockReturnValue({ dispose }),
|
||||
node: mockNode,
|
||||
getFormValueByPathEnds: vi.fn(),
|
||||
form: mockFormModel,
|
||||
};
|
||||
|
||||
return workflowNode as unknown as WorkflowNode;
|
||||
};
|
||||
|
||||
it('应该创建一个默认值为 undefined 的 Context', () => {
|
||||
const TestComponent = () => {
|
||||
const context = useContext(WorkflowNodeContext);
|
||||
expect(context).toBeUndefined();
|
||||
return null;
|
||||
};
|
||||
|
||||
render(<TestComponent />);
|
||||
});
|
||||
|
||||
it('应该能够通过 Provider 提供 WorkflowNode 实例', () => {
|
||||
const mockWorkflowNode = createMockWorkflowNode('1');
|
||||
|
||||
const TestComponent = () => {
|
||||
const context = useContext(WorkflowNodeContext);
|
||||
expect(context).toBe(mockWorkflowNode);
|
||||
return null;
|
||||
};
|
||||
|
||||
render(
|
||||
<WorkflowNodeContext.Provider value={mockWorkflowNode}>
|
||||
<TestComponent />
|
||||
</WorkflowNodeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
it('应该能够在嵌套组件中访问 Context', () => {
|
||||
const mockWorkflowNode = createMockWorkflowNode('1');
|
||||
|
||||
const ChildComponent = () => {
|
||||
const context = useContext(WorkflowNodeContext);
|
||||
expect(context).toBe(mockWorkflowNode);
|
||||
return null;
|
||||
};
|
||||
|
||||
const ParentComponent = () => (
|
||||
<div>
|
||||
<ChildComponent />
|
||||
</div>
|
||||
);
|
||||
|
||||
render(
|
||||
<WorkflowNodeContext.Provider value={mockWorkflowNode}>
|
||||
<ParentComponent />
|
||||
</WorkflowNodeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
it('应该能够处理 undefined 值', () => {
|
||||
const TestComponent = () => {
|
||||
const context = useContext(WorkflowNodeContext);
|
||||
expect(context).toBeUndefined();
|
||||
return null;
|
||||
};
|
||||
|
||||
render(
|
||||
<WorkflowNodeContext.Provider value={undefined}>
|
||||
<TestComponent />
|
||||
</WorkflowNodeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
it('应该能够在多层 Provider 中正确获取最近的值', () => {
|
||||
const mockWorkflowNode1 = createMockWorkflowNode('1');
|
||||
const mockWorkflowNode2 = createMockWorkflowNode('2');
|
||||
|
||||
const InnerComponent = () => {
|
||||
const context = useContext(WorkflowNodeContext);
|
||||
expect(context).toBe(mockWorkflowNode2);
|
||||
return null;
|
||||
};
|
||||
|
||||
const OuterComponent = () => {
|
||||
const context = useContext(WorkflowNodeContext);
|
||||
expect(context).toBe(mockWorkflowNode1);
|
||||
return (
|
||||
<WorkflowNodeContext.Provider value={mockWorkflowNode2}>
|
||||
<InnerComponent />
|
||||
</WorkflowNodeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
render(
|
||||
<WorkflowNodeContext.Provider value={mockWorkflowNode1}>
|
||||
<OuterComponent />
|
||||
</WorkflowNodeContext.Provider>,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
FlowNodeFormData,
|
||||
FlowNodeErrorData,
|
||||
} from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { StandardNodeType } from '../../src/types';
|
||||
import { WorkflowNode } from '../../src/entities';
|
||||
|
||||
// Mock entities
|
||||
vi.mock('../../src/entities', async () => {
|
||||
const actual = await vi.importActual('../../src/entities');
|
||||
return {
|
||||
...actual,
|
||||
// 如果需要mock其他实体,可以在这里添加
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../src/utils', () => ({
|
||||
getFormValueByPathEnds: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('WorkflowNode', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
const createMockFormModel = () => ({
|
||||
initialized: true,
|
||||
getFormItemValueByPath: vi.fn(),
|
||||
getFormItemByPath: vi.fn(),
|
||||
formItemPathMap: new Map(),
|
||||
onInitialized: vi.fn().mockReturnValue({ dispose: vi.fn() }),
|
||||
});
|
||||
|
||||
const createMockNode = () => {
|
||||
const mockFormModel = createMockFormModel();
|
||||
const mockFormData = {
|
||||
formModel: mockFormModel,
|
||||
onDataChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
|
||||
};
|
||||
|
||||
return {
|
||||
flowNodeType: StandardNodeType.Start,
|
||||
getNodeRegistry: vi.fn().mockReturnValue({
|
||||
getNodeInputParameters: vi.fn().mockReturnValue([]),
|
||||
getNodeOutputs: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
getNodeMeta: vi.fn(),
|
||||
getData: vi.fn(dataType => {
|
||||
if (dataType === FlowNodeFormData) {
|
||||
return mockFormData;
|
||||
}
|
||||
if (dataType === FlowNodeErrorData) {
|
||||
return {
|
||||
getError: vi.fn().mockReturnValue(null),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
_formModel: mockFormModel,
|
||||
};
|
||||
};
|
||||
|
||||
describe('基本属性', () => {
|
||||
it('should get node type', () => {
|
||||
const mockNode = createMockNode();
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.type).toBe(StandardNodeType.Start);
|
||||
});
|
||||
|
||||
it('should get node registry', () => {
|
||||
const mockRegistry = { test: 'registry' };
|
||||
const mockNode = createMockNode();
|
||||
mockNode.getNodeRegistry.mockReturnValue(mockRegistry);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.registry).toBe(mockRegistry);
|
||||
});
|
||||
|
||||
it('should check if node has error', () => {
|
||||
const mockNode = createMockNode();
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.isError).toBe(false);
|
||||
|
||||
mockNode.getData.mockImplementation(dataType => {
|
||||
if (dataType === FlowNodeErrorData) {
|
||||
return {
|
||||
getError: vi.fn().mockReturnValue(new Error('Test error')),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
expect(workflowNode.isError).toBe(true);
|
||||
});
|
||||
|
||||
it('should check if node is initialized', () => {
|
||||
const mockNode = createMockNode();
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.isInitialized).toBe(true);
|
||||
|
||||
mockNode._formModel.initialized = false;
|
||||
expect(workflowNode.isInitialized).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle undefined node type', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode.flowNodeType = StandardNodeType.Start;
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.type).toBe(StandardNodeType.Start);
|
||||
});
|
||||
|
||||
it('should handle null registry', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode.getNodeRegistry.mockReturnValue(null);
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.registry).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle error data access failure', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode.getData.mockReturnValue({
|
||||
getError: vi.fn().mockReturnValue(undefined),
|
||||
});
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.isError).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('表单数据管理', () => {
|
||||
it('should get form data', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode._formModel.getFormItemValueByPath.mockReturnValue({
|
||||
test: 'data',
|
||||
});
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.data).toEqual({ test: 'data' });
|
||||
expect(mockNode._formModel.getFormItemValueByPath).toHaveBeenCalledWith(
|
||||
'/',
|
||||
);
|
||||
});
|
||||
|
||||
it('should set form data', () => {
|
||||
const mockNode = createMockNode();
|
||||
const mockFormItem = { value: null };
|
||||
mockNode._formModel.getFormItemByPath.mockReturnValue(mockFormItem);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
const newData = { test: 'new data' };
|
||||
workflowNode.setData(newData);
|
||||
|
||||
expect(mockNode._formModel.getFormItemByPath).toHaveBeenCalledWith('/');
|
||||
expect(mockFormItem.value).toEqual(newData);
|
||||
});
|
||||
|
||||
it('should handle setting data when form item does not exist', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode._formModel.getFormItemByPath.mockReturnValue(null);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
workflowNode.setData({ test: 'data' });
|
||||
|
||||
expect(mockNode._formModel.getFormItemByPath).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
it('should handle invalid form data', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode._formModel.getFormItemValueByPath.mockReturnValue(undefined);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle form item value mutation', () => {
|
||||
const mockNode = createMockNode();
|
||||
const mockFormItem = { value: { test: 'original' } };
|
||||
mockNode._formModel.getFormItemByPath.mockReturnValue(mockFormItem);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
const newData = { test: 'new data' };
|
||||
workflowNode.setData(newData);
|
||||
|
||||
// 验证原始数据没有被修改
|
||||
expect(newData).toEqual({ test: 'new data' });
|
||||
expect(mockFormItem.value).toEqual({ test: 'new data' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('输入输出参数', () => {
|
||||
it('should get input parameters from registry', () => {
|
||||
const mockInputParams = [{ name: 'test', type: 'string' }];
|
||||
const mockNode = createMockNode();
|
||||
mockNode.getNodeRegistry.mockReturnValue({
|
||||
getNodeInputParameters: vi.fn().mockReturnValue(mockInputParams),
|
||||
});
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.inputParameters).toEqual(mockInputParams);
|
||||
});
|
||||
|
||||
it('should get outputs from registry', () => {
|
||||
const mockOutputs = [{ name: 'test', type: 'string' }];
|
||||
const mockNode = createMockNode();
|
||||
mockNode.getNodeRegistry.mockReturnValue({
|
||||
getNodeOutputs: vi.fn().mockReturnValue(mockOutputs),
|
||||
});
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.outputs).toEqual(mockOutputs);
|
||||
});
|
||||
|
||||
it('should get outputs from form', () => {
|
||||
const mockOutputs = [{ name: 'test', type: 'string' }];
|
||||
const mockNode = createMockNode();
|
||||
mockNode.getNodeRegistry.mockReturnValue({
|
||||
getNodeInputParameters: vi.fn().mockReturnValue([]),
|
||||
getNodeOutputs: vi.fn().mockReturnValue(mockOutputs),
|
||||
});
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.outputs).toEqual(mockOutputs);
|
||||
});
|
||||
|
||||
it('should handle invalid input parameters path', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode.getNodeMeta.mockReturnValue({
|
||||
inputParametersPath: null,
|
||||
});
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.inputParameters).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle circular references in parameters', () => {
|
||||
const mockNode = createMockNode();
|
||||
const circularObj: any = { name: 'test' };
|
||||
circularObj.self = circularObj;
|
||||
|
||||
mockNode.getNodeRegistry.mockReturnValue({
|
||||
getNodeInputParameters: vi.fn().mockReturnValue([circularObj]),
|
||||
});
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(() => JSON.stringify(workflowNode.inputParameters)).toThrow();
|
||||
});
|
||||
|
||||
it('should handle empty outputs path', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode._formModel.getFormItemValueByPath.mockReturnValue(null);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.outputs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('元数据', () => {
|
||||
it('should get node metadata', () => {
|
||||
const mockNode = createMockNode();
|
||||
const mockData = {
|
||||
nodeMeta: {
|
||||
icon: 'test-icon',
|
||||
title: 'Test Node',
|
||||
description: 'Test Description',
|
||||
},
|
||||
};
|
||||
mockNode._formModel.getFormItemValueByPath.mockReturnValue(mockData);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.icon).toBe(mockData.nodeMeta.icon);
|
||||
expect(workflowNode.title).toBe(mockData.nodeMeta.title);
|
||||
expect(workflowNode.description).toBe(mockData.nodeMeta.description);
|
||||
});
|
||||
|
||||
it('should handle missing metadata', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode._formModel.getFormItemValueByPath.mockReturnValue({});
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.icon).toBeUndefined();
|
||||
expect(workflowNode.title).toBeUndefined();
|
||||
expect(workflowNode.description).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null metadata', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode._formModel.getFormItemValueByPath.mockReturnValue(null);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.icon).toBeUndefined();
|
||||
expect(workflowNode.title).toBeUndefined();
|
||||
expect(workflowNode.description).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle metadata access error', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode._formModel.getFormItemValueByPath.mockReturnValue(null);
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.icon).toBeUndefined();
|
||||
expect(workflowNode.title).toBeUndefined();
|
||||
expect(workflowNode.description).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle partial metadata', () => {
|
||||
const mockNode = createMockNode();
|
||||
mockNode._formModel.getFormItemValueByPath.mockReturnValue({
|
||||
nodeMeta: {
|
||||
icon: 'test-icon',
|
||||
// title and description missing
|
||||
},
|
||||
});
|
||||
|
||||
const workflowNode = new WorkflowNode(mockNode as any);
|
||||
expect(workflowNode.icon).toBe('test-icon');
|
||||
expect(workflowNode.title).toBeUndefined();
|
||||
expect(workflowNode.description).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useNodeTestId } from '../../src/hooks/use-node-test-id';
|
||||
|
||||
const mockUseCurrentEntity = vi.fn();
|
||||
|
||||
vi.mock('@flowgram-adapter/free-layout-editor', () => ({
|
||||
useCurrentEntity: () => mockUseCurrentEntity(),
|
||||
}));
|
||||
|
||||
// Mock @coze-arch/bot-error
|
||||
class MockCustomError extends Error {
|
||||
code: string;
|
||||
constructor(message: string, code: string) {
|
||||
super(message);
|
||||
this.name = 'CustomError';
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
vi.mock('@coze-arch/bot-error', () => ({
|
||||
CustomError: vi
|
||||
.fn()
|
||||
.mockImplementation(
|
||||
() =>
|
||||
new MockCustomError(
|
||||
'useNodeTestId must be called in a workflow node',
|
||||
'',
|
||||
),
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock utils
|
||||
const mockConcatTestId = vi.fn();
|
||||
vi.mock('../../src/utils', () => ({
|
||||
concatTestId: (...args: string[]) => mockConcatTestId(...args),
|
||||
}));
|
||||
|
||||
describe('useNodeTestId', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// 设置 concatTestId 的默认行为
|
||||
mockConcatTestId.mockImplementation((...args: string[]) => args.join('.'));
|
||||
});
|
||||
|
||||
it('应该在没有当前节点时抛出错误', () => {
|
||||
mockUseCurrentEntity.mockReturnValue(null);
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useNodeTestId());
|
||||
}).toThrow(
|
||||
new MockCustomError(
|
||||
'useNodeTestId must be called in a workflow node',
|
||||
'',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('应该在节点没有 id 时抛出错误', () => {
|
||||
mockUseCurrentEntity.mockReturnValue({});
|
||||
|
||||
expect(() => {
|
||||
renderHook(() => useNodeTestId());
|
||||
}).toThrow(
|
||||
new MockCustomError(
|
||||
'useNodeTestId must be called in a workflow node',
|
||||
'',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('应该返回正确的节点测试 ID', () => {
|
||||
mockUseCurrentEntity.mockReturnValue({ id: '123' });
|
||||
mockConcatTestId.mockReturnValueOnce('playground.node.123');
|
||||
|
||||
const { result } = renderHook(() => useNodeTestId());
|
||||
|
||||
expect(result.current.getNodeTestId()).toBe('playground.node.123');
|
||||
expect(mockConcatTestId).toHaveBeenCalledWith('playground.node', '123');
|
||||
});
|
||||
|
||||
it('应该返回正确的节点设置器 ID', () => {
|
||||
mockUseCurrentEntity.mockReturnValue({ id: '123' });
|
||||
mockConcatTestId
|
||||
.mockReturnValueOnce('playground.node.123') // 用于 getNodeTestId
|
||||
.mockReturnValueOnce('playground.node.123.llm'); // 用于 getNodeSetterId
|
||||
|
||||
const { result } = renderHook(() => useNodeTestId());
|
||||
|
||||
expect(result.current.getNodeSetterId('llm')).toBe(
|
||||
'playground.node.123.llm',
|
||||
);
|
||||
expect(mockConcatTestId).toHaveBeenCalledTimes(2);
|
||||
expect(mockConcatTestId).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'playground.node',
|
||||
'123',
|
||||
);
|
||||
expect(mockConcatTestId).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'playground.node.123',
|
||||
'llm',
|
||||
);
|
||||
});
|
||||
|
||||
it('应该正确连接测试 ID', () => {
|
||||
mockUseCurrentEntity.mockReturnValue({ id: '123' });
|
||||
mockConcatTestId
|
||||
.mockReturnValueOnce('a.b')
|
||||
.mockReturnValueOnce('a.b.c')
|
||||
.mockReturnValueOnce('a.b.c');
|
||||
|
||||
const { result } = renderHook(() => useNodeTestId());
|
||||
|
||||
expect(result.current.concatTestId('a', 'b')).toBe('a.b');
|
||||
expect(mockConcatTestId).toHaveBeenCalledWith('a', 'b');
|
||||
|
||||
expect(result.current.concatTestId('a.b', 'c')).toBe('a.b.c');
|
||||
expect(mockConcatTestId).toHaveBeenCalledWith('a.b', 'c');
|
||||
|
||||
expect(result.current.concatTestId('a', 'b', 'c')).toBe('a.b.c');
|
||||
expect(mockConcatTestId).toHaveBeenCalledWith('a', 'b', 'c');
|
||||
});
|
||||
|
||||
it('应该在多个组件中返回不同的节点测试 ID', () => {
|
||||
// 第一个组件
|
||||
mockUseCurrentEntity.mockReturnValue({ id: '123' });
|
||||
mockConcatTestId.mockReturnValueOnce('playground.node.123');
|
||||
|
||||
const { result: result1 } = renderHook(() => useNodeTestId());
|
||||
expect(result1.current.getNodeTestId()).toBe('playground.node.123');
|
||||
expect(mockConcatTestId).toHaveBeenCalledWith('playground.node', '123');
|
||||
|
||||
// 第二个组件
|
||||
mockUseCurrentEntity.mockReturnValue({ id: '456' });
|
||||
mockConcatTestId.mockReturnValueOnce('playground.node.456');
|
||||
|
||||
const { result: result2 } = renderHook(() => useNodeTestId());
|
||||
expect(result2.current.getNodeTestId()).toBe('playground.node.456');
|
||||
expect(mockConcatTestId).toHaveBeenCalledWith('playground.node', '456');
|
||||
});
|
||||
|
||||
it('应该在多个组件中返回不同的节点设置器 ID', () => {
|
||||
// 第一个组件
|
||||
mockUseCurrentEntity.mockReturnValue({ id: '123' });
|
||||
mockConcatTestId
|
||||
.mockReturnValueOnce('playground.node.123') // 用于 getNodeTestId
|
||||
.mockReturnValueOnce('playground.node.123.llm'); // 用于 getNodeSetterId
|
||||
|
||||
const { result: result1 } = renderHook(() => useNodeTestId());
|
||||
expect(result1.current.getNodeSetterId('llm')).toBe(
|
||||
'playground.node.123.llm',
|
||||
);
|
||||
expect(mockConcatTestId).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'playground.node',
|
||||
'123',
|
||||
);
|
||||
expect(mockConcatTestId).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'playground.node.123',
|
||||
'llm',
|
||||
);
|
||||
|
||||
// 第二个组件
|
||||
mockUseCurrentEntity.mockReturnValue({ id: '456' });
|
||||
mockConcatTestId
|
||||
.mockReturnValueOnce('playground.node.456') // 用于 getNodeTestId
|
||||
.mockReturnValueOnce('playground.node.456.llm'); // 用于 getNodeSetterId
|
||||
|
||||
const { result: result2 } = renderHook(() => useNodeTestId());
|
||||
expect(result2.current.getNodeSetterId('llm')).toBe(
|
||||
'playground.node.456.llm',
|
||||
);
|
||||
expect(mockConcatTestId).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'playground.node',
|
||||
'456',
|
||||
);
|
||||
expect(mockConcatTestId).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'playground.node.456',
|
||||
'llm',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act, cleanup } from '@testing-library/react';
|
||||
|
||||
import { StandardNodeType } from '../../src/types';
|
||||
import { useWorkflowNode } from '../../src/hooks/use-workflow-node';
|
||||
import { type WorkflowNode } from '../../src/entities';
|
||||
import { WorkflowNodeContext } from '../../src/contexts';
|
||||
|
||||
describe('useWorkflowNode', () => {
|
||||
const createMockWorkflowNode = (id: string): WorkflowNode => {
|
||||
const mockRegistry = {
|
||||
getNodeInputParameters: vi.fn().mockReturnValue([]),
|
||||
getNodeOutputs: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
const workflowNode = {
|
||||
type: StandardNodeType.Start,
|
||||
registry: mockRegistry,
|
||||
inputParameters: [],
|
||||
outputs: [],
|
||||
data: {},
|
||||
icon: '',
|
||||
title: '',
|
||||
description: '',
|
||||
isError: false,
|
||||
isInitialized: true,
|
||||
setData: vi.fn(),
|
||||
};
|
||||
|
||||
return workflowNode as unknown as WorkflowNode;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('应该返回 WorkflowNode 的观察值', () => {
|
||||
const mockWorkflowNode = createMockWorkflowNode('1');
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<WorkflowNodeContext.Provider value={mockWorkflowNode}>
|
||||
{children}
|
||||
</WorkflowNodeContext.Provider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useWorkflowNode(), { wrapper });
|
||||
|
||||
expect(result.current).toEqual({
|
||||
type: StandardNodeType.Start,
|
||||
inputParameters: [],
|
||||
outputs: [],
|
||||
data: {},
|
||||
icon: '',
|
||||
title: '',
|
||||
description: '',
|
||||
isError: false,
|
||||
isInitialized: true,
|
||||
registry: mockWorkflowNode.registry,
|
||||
setData: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('应该正确绑定 setData 方法', async () => {
|
||||
const mockWorkflowNode = createMockWorkflowNode('1');
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<WorkflowNodeContext.Provider value={mockWorkflowNode}>
|
||||
{children}
|
||||
</WorkflowNodeContext.Provider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useWorkflowNode(), { wrapper });
|
||||
|
||||
const newData = { test: 'new data' };
|
||||
await act(() => {
|
||||
result.current.setData(newData);
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
expect(mockWorkflowNode.setData).toHaveBeenCalledWith(newData);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import type { WorkflowEdgeJSON } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import type { WorkflowNodeJSON } from '../../../src/types';
|
||||
import { useWorkflowStore } from '../../../src/store/workflow';
|
||||
|
||||
describe('useWorkflowStore', () => {
|
||||
beforeEach(() => {
|
||||
// 重置 store 状态
|
||||
useWorkflowStore.setState({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
isCreatingWorkflow: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('应该有正确的初始状态', () => {
|
||||
const state = useWorkflowStore.getState();
|
||||
expect(state.nodes).toEqual([]);
|
||||
expect(state.edges).toEqual([]);
|
||||
expect(state.isCreatingWorkflow).toBe(false);
|
||||
});
|
||||
|
||||
describe('setNodes', () => {
|
||||
it('应该正确设置节点数据', () => {
|
||||
const mockNodes: WorkflowNodeJSON[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'test',
|
||||
data: {},
|
||||
meta: {
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'test',
|
||||
data: {},
|
||||
meta: {
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
useWorkflowStore.getState().setNodes(mockNodes);
|
||||
expect(useWorkflowStore.getState().nodes).toEqual(mockNodes);
|
||||
});
|
||||
|
||||
it('应该能够重置为空数组', () => {
|
||||
// 先设置一些数据
|
||||
useWorkflowStore.getState().setNodes([
|
||||
{
|
||||
id: '1',
|
||||
type: 'test',
|
||||
data: {},
|
||||
meta: {
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
// 设置为空数组
|
||||
useWorkflowStore.getState().setNodes([]);
|
||||
expect(useWorkflowStore.getState().nodes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setEdges', () => {
|
||||
it('应该正确设置边数据', () => {
|
||||
const mockEdges: WorkflowEdgeJSON[] = [
|
||||
{
|
||||
sourceNodeID: '1',
|
||||
targetNodeID: '2',
|
||||
},
|
||||
{
|
||||
sourceNodeID: '2',
|
||||
targetNodeID: '3',
|
||||
},
|
||||
];
|
||||
|
||||
useWorkflowStore.getState().setEdges(mockEdges);
|
||||
expect(useWorkflowStore.getState().edges).toEqual(mockEdges);
|
||||
});
|
||||
|
||||
it('应该能够重置为空数组', () => {
|
||||
// 先设置一些数据
|
||||
useWorkflowStore.getState().setEdges([
|
||||
{
|
||||
sourceNodeID: '1',
|
||||
targetNodeID: '2',
|
||||
},
|
||||
]);
|
||||
|
||||
// 设置为空数组
|
||||
useWorkflowStore.getState().setEdges([]);
|
||||
expect(useWorkflowStore.getState().edges).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setIsCreatingWorkflow', () => {
|
||||
it('应该正确设置创建状态', () => {
|
||||
// 设置为 true
|
||||
useWorkflowStore.getState().setIsCreatingWorkflow(true);
|
||||
expect(useWorkflowStore.getState().isCreatingWorkflow).toBe(true);
|
||||
|
||||
// 设置为 false
|
||||
useWorkflowStore.getState().setIsCreatingWorkflow(false);
|
||||
expect(useWorkflowStore.getState().isCreatingWorkflow).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('状态更新', () => {
|
||||
it('应该能够同时更新多个状态', () => {
|
||||
const mockNodes: WorkflowNodeJSON[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'test',
|
||||
data: {},
|
||||
meta: {
|
||||
position: { x: 0, y: 0 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const mockEdges: WorkflowEdgeJSON[] = [
|
||||
{
|
||||
sourceNodeID: '1',
|
||||
targetNodeID: '2',
|
||||
},
|
||||
];
|
||||
|
||||
useWorkflowStore.setState({
|
||||
nodes: mockNodes,
|
||||
edges: mockEdges,
|
||||
isCreatingWorkflow: true,
|
||||
});
|
||||
|
||||
const state = useWorkflowStore.getState();
|
||||
expect(state.nodes).toEqual(mockNodes);
|
||||
expect(state.edges).toEqual(mockEdges);
|
||||
expect(state.isCreatingWorkflow).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { BlockInput } from '../../src/types/block-input-dto';
|
||||
import { ViewVariableType } from '../../src/types';
|
||||
|
||||
describe('block-input-dto', () => {
|
||||
describe('create', () => {
|
||||
it('应该能够创建基本的 BlockInput', () => {
|
||||
const input = BlockInput.create('testName', 'testValue');
|
||||
expect(input).toEqual({
|
||||
name: 'testName',
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'testValue',
|
||||
rawMeta: {
|
||||
type: ViewVariableType.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能够创建指定类型的 BlockInput', () => {
|
||||
const input = BlockInput.create('testName', '42', 'integer');
|
||||
expect(input).toEqual({
|
||||
name: 'testName',
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: '42',
|
||||
rawMeta: {
|
||||
type: ViewVariableType.Integer,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('应该在没有提供值时使用空字符串', () => {
|
||||
const input = BlockInput.create('testName');
|
||||
expect(input.input.value.content).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createString', () => {
|
||||
it('应该创建字符串类型的 BlockInput', () => {
|
||||
const input = BlockInput.createString('testName', 'testValue');
|
||||
expect(input).toEqual({
|
||||
name: 'testName',
|
||||
input: {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'testValue',
|
||||
rawMeta: {
|
||||
type: ViewVariableType.String,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createInteger', () => {
|
||||
it('应该创建整数类型的 BlockInput', () => {
|
||||
const input = BlockInput.createInteger('testName', '42');
|
||||
expect(input).toEqual({
|
||||
name: 'testName',
|
||||
input: {
|
||||
type: 'integer',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: '42',
|
||||
rawMeta: {
|
||||
type: ViewVariableType.Integer,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createFloat', () => {
|
||||
it('应该创建浮点数类型的 BlockInput', () => {
|
||||
const input = BlockInput.createFloat('testName', '3.14');
|
||||
expect(input).toEqual({
|
||||
name: 'testName',
|
||||
input: {
|
||||
type: 'float',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: '3.14',
|
||||
rawMeta: {
|
||||
type: ViewVariableType.Number,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createArray', () => {
|
||||
it('应该创建数组类型的 BlockInput', () => {
|
||||
const schema = { type: 'string' };
|
||||
const input = BlockInput.createArray('testName', ['a', 'b', 'c'], schema);
|
||||
expect(input).toEqual({
|
||||
name: 'testName',
|
||||
input: {
|
||||
type: 'list',
|
||||
schema: { type: 'string' },
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: ['a', 'b', 'c'],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('应该支持不同类型的数组元素', () => {
|
||||
const schema = { type: 'number' };
|
||||
const input = BlockInput.createArray('testName', [1, 2, 3], schema);
|
||||
expect(input.input.value.content).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createBoolean', () => {
|
||||
it('应该创建布尔类型的 BlockInput', () => {
|
||||
const input = BlockInput.createBoolean('testName', true);
|
||||
expect(input).toEqual({
|
||||
name: 'testName',
|
||||
input: {
|
||||
type: 'boolean',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: true,
|
||||
rawMeta: {
|
||||
type: ViewVariableType.Boolean,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('应该正确处理 false 值', () => {
|
||||
const input = BlockInput.createBoolean('testName', false);
|
||||
expect(input.input.value.content).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toLiteral', () => {
|
||||
it('应该提取 BlockInput 的内容值', () => {
|
||||
const input = BlockInput.createString('testName', 'testValue');
|
||||
const value = BlockInput.toLiteral<string>(input);
|
||||
expect(value).toBe('testValue');
|
||||
});
|
||||
|
||||
it('应该能够提取不同类型的值', () => {
|
||||
const boolInput = BlockInput.createBoolean('testName', true);
|
||||
const boolValue = BlockInput.toLiteral<boolean>(boolInput);
|
||||
expect(boolValue).toBe(true);
|
||||
|
||||
const arrayInput = BlockInput.createArray('testName', [1, 2, 3], {
|
||||
type: 'number',
|
||||
});
|
||||
const arrayValue = BlockInput.toLiteral<number[]>(arrayInput);
|
||||
expect(arrayValue).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBlockInput', () => {
|
||||
it('应该正确识别有效的 BlockInput', () => {
|
||||
const input = BlockInput.createString('testName', 'testValue');
|
||||
expect(BlockInput.isBlockInput(input)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确识别无效的 BlockInput', () => {
|
||||
expect(BlockInput.isBlockInput(null)).toBe(false);
|
||||
expect(BlockInput.isBlockInput(undefined)).toBe(false);
|
||||
expect(BlockInput.isBlockInput({})).toBe(false);
|
||||
expect(
|
||||
BlockInput.isBlockInput({
|
||||
name: 'test',
|
||||
input: { value: { content: undefined } },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
BlockInput.isBlockInput({
|
||||
input: { value: { content: 'test' } },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确识别所有类型的有效 BlockInput', () => {
|
||||
const stringInput = BlockInput.createString('test', 'value');
|
||||
const integerInput = BlockInput.createInteger('test', '42');
|
||||
const floatInput = BlockInput.createFloat('test', '3.14');
|
||||
const booleanInput = BlockInput.createBoolean('test', true);
|
||||
const arrayInput = BlockInput.createArray('test', [1, 2, 3], {
|
||||
type: 'number',
|
||||
});
|
||||
|
||||
expect(BlockInput.isBlockInput(stringInput)).toBe(true);
|
||||
expect(BlockInput.isBlockInput(integerInput)).toBe(true);
|
||||
expect(BlockInput.isBlockInput(floatInput)).toBe(true);
|
||||
expect(BlockInput.isBlockInput(booleanInput)).toBe(true);
|
||||
expect(BlockInput.isBlockInput(arrayInput)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
StandardNodeType,
|
||||
NODE_ORDER,
|
||||
CONVERSATION_NODES,
|
||||
} from '../../src/types/node-type';
|
||||
|
||||
describe('node-type', () => {
|
||||
describe('StandardNodeType', () => {
|
||||
it('应该包含所有预定义的节点类型', () => {
|
||||
// 测试一些关键的节点类型
|
||||
expect(StandardNodeType.Start).toBe('1');
|
||||
expect(StandardNodeType.End).toBe('2');
|
||||
expect(StandardNodeType.LLM).toBe('3');
|
||||
expect(StandardNodeType.Api).toBe('4');
|
||||
expect(StandardNodeType.Code).toBe('5');
|
||||
expect(StandardNodeType.Dataset).toBe('6');
|
||||
expect(StandardNodeType.If).toBe('8');
|
||||
expect(StandardNodeType.SubWorkflow).toBe('9');
|
||||
expect(StandardNodeType.Variable).toBe('11');
|
||||
expect(StandardNodeType.Database).toBe('12');
|
||||
expect(StandardNodeType.Output).toBe('13');
|
||||
expect(StandardNodeType.Http).toBe('45');
|
||||
});
|
||||
|
||||
it('应该不包含已废弃的节点类型', () => {
|
||||
const nodeTypeValues = Object.values(StandardNodeType);
|
||||
expect(nodeTypeValues).not.toContain('33'); // TriggerCreate
|
||||
});
|
||||
});
|
||||
|
||||
describe('NODE_ORDER', () => {
|
||||
it('应该为每个节点类型定义正确的顺序', () => {
|
||||
expect(NODE_ORDER[StandardNodeType.Start]).toBe(1);
|
||||
expect(NODE_ORDER[StandardNodeType.End]).toBe(2);
|
||||
expect(NODE_ORDER[StandardNodeType.Api]).toBe(3);
|
||||
expect(NODE_ORDER[StandardNodeType.LLM]).toBe(4);
|
||||
expect(NODE_ORDER[StandardNodeType.Code]).toBe(5);
|
||||
expect(NODE_ORDER[StandardNodeType.Dataset]).toBe(6);
|
||||
expect(NODE_ORDER[StandardNodeType.SubWorkflow]).toBe(7);
|
||||
expect(NODE_ORDER[StandardNodeType.Imageflow]).toBe(8);
|
||||
expect(NODE_ORDER[StandardNodeType.If]).toBe(9);
|
||||
expect(NODE_ORDER[StandardNodeType.Loop]).toBe(10);
|
||||
});
|
||||
|
||||
it('应该不包含已废弃节点类型的顺序', () => {
|
||||
expect(NODE_ORDER).not.toHaveProperty('33'); // TriggerCreate
|
||||
});
|
||||
|
||||
it('应该为所有需要排序的节点类型定义顺序', () => {
|
||||
const nodeTypesWithOrder = Object.keys(NODE_ORDER);
|
||||
const expectedNodeTypes = [
|
||||
StandardNodeType.Start,
|
||||
StandardNodeType.End,
|
||||
StandardNodeType.Api,
|
||||
StandardNodeType.LLM,
|
||||
StandardNodeType.Code,
|
||||
StandardNodeType.Dataset,
|
||||
StandardNodeType.SubWorkflow,
|
||||
StandardNodeType.Imageflow,
|
||||
StandardNodeType.If,
|
||||
StandardNodeType.Loop,
|
||||
StandardNodeType.Intent,
|
||||
StandardNodeType.Text,
|
||||
StandardNodeType.Output,
|
||||
StandardNodeType.Question,
|
||||
StandardNodeType.Variable,
|
||||
StandardNodeType.Database,
|
||||
StandardNodeType.LTM,
|
||||
StandardNodeType.Batch,
|
||||
StandardNodeType.Input,
|
||||
StandardNodeType.SetVariable,
|
||||
StandardNodeType.Break,
|
||||
StandardNodeType.Continue,
|
||||
StandardNodeType.SceneChat,
|
||||
StandardNodeType.SceneVariable,
|
||||
StandardNodeType.TriggerUpsert,
|
||||
StandardNodeType.TriggerRead,
|
||||
StandardNodeType.TriggerDelete,
|
||||
];
|
||||
|
||||
expectedNodeTypes.forEach(nodeType => {
|
||||
expect(nodeTypesWithOrder).toContain(nodeType);
|
||||
expect(NODE_ORDER[nodeType]).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('CONVERSATION_NODES', () => {
|
||||
it('应该包含所有会话相关的节点类型', () => {
|
||||
expect(CONVERSATION_NODES).toEqual([
|
||||
StandardNodeType.CreateConversation,
|
||||
StandardNodeType.UpdateConversation,
|
||||
StandardNodeType.DeleteConversation,
|
||||
StandardNodeType.QueryConversationList,
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该只包含会话相关的节点类型', () => {
|
||||
CONVERSATION_NODES.forEach(nodeType => {
|
||||
expect([
|
||||
StandardNodeType.CreateConversation,
|
||||
StandardNodeType.UpdateConversation,
|
||||
StandardNodeType.DeleteConversation,
|
||||
StandardNodeType.QueryConversationList,
|
||||
]).toContain(nodeType);
|
||||
});
|
||||
});
|
||||
|
||||
it('应该是一个数组', () => {
|
||||
expect(Array.isArray(CONVERSATION_NODES)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
160
frontend/packages/workflow/base/__tests__/types/node.test.ts
Normal file
160
frontend/packages/workflow/base/__tests__/types/node.test.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { StandardNodeType } from '../../src/types/node-type';
|
||||
import type { WorkflowNodeJSON, WorkflowJSON } from '../../src/types/node';
|
||||
|
||||
describe('node types', () => {
|
||||
describe('WorkflowNodeJSON', () => {
|
||||
it('应该能够创建基本的节点 JSON', () => {
|
||||
const node: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.Start,
|
||||
data: {},
|
||||
};
|
||||
|
||||
expect(node).toEqual({
|
||||
id: '1',
|
||||
type: StandardNodeType.Start,
|
||||
data: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('应该能够创建带有元数据的节点 JSON', () => {
|
||||
const node: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.Start,
|
||||
meta: {
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
data: {},
|
||||
};
|
||||
|
||||
expect(node.meta).toBeDefined();
|
||||
expect(node.meta?.position).toEqual({ x: 100, y: 100 });
|
||||
});
|
||||
|
||||
it('应该能够创建带有版本的节点 JSON', () => {
|
||||
const node: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.Start,
|
||||
data: {},
|
||||
version: '1.0.0',
|
||||
};
|
||||
|
||||
expect(node.version).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('应该能够创建带有子节点的节点 JSON', () => {
|
||||
const node: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.Start,
|
||||
data: {},
|
||||
blocks: [
|
||||
{
|
||||
id: '2',
|
||||
type: StandardNodeType.Code,
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(node.blocks).toHaveLength(1);
|
||||
expect(node.blocks?.[0].type).toBe(StandardNodeType.Code);
|
||||
});
|
||||
|
||||
it('应该能够创建带有边的节点 JSON', () => {
|
||||
const node: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.Start,
|
||||
data: {},
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: '1',
|
||||
targetNodeID: '2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(node.edges).toHaveLength(1);
|
||||
expect(node.edges?.[0].sourceNodeID).toBe('1');
|
||||
expect(node.edges?.[0].targetNodeID).toBe('2');
|
||||
});
|
||||
|
||||
it('应该能够使用泛型数据类型', () => {
|
||||
interface CustomData {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const node: WorkflowNodeJSON<CustomData> = {
|
||||
id: '1',
|
||||
type: StandardNodeType.Start,
|
||||
data: {
|
||||
name: 'test',
|
||||
value: 42,
|
||||
},
|
||||
};
|
||||
|
||||
expect(node.data.name).toBe('test');
|
||||
expect(node.data.value).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WorkflowJSON', () => {
|
||||
it('应该能够创建基本的工作流 JSON', () => {
|
||||
const workflow: WorkflowJSON = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
|
||||
expect(workflow.nodes).toEqual([]);
|
||||
expect(workflow.edges).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该能够创建包含节点和边的工作流 JSON', () => {
|
||||
const workflow: WorkflowJSON = {
|
||||
nodes: [
|
||||
{
|
||||
id: '1',
|
||||
type: StandardNodeType.Start,
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: StandardNodeType.End,
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
sourceNodeID: '1',
|
||||
targetNodeID: '2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(workflow.nodes).toHaveLength(2);
|
||||
expect(workflow.edges).toHaveLength(1);
|
||||
expect(workflow.nodes[0].type).toBe(StandardNodeType.Start);
|
||||
expect(workflow.nodes[1].type).toBe(StandardNodeType.End);
|
||||
expect(workflow.edges[0].sourceNodeID).toBe('1');
|
||||
expect(workflow.edges[0].targetNodeID).toBe('2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { ViewVariableType } from '../../src/types/view-variable-type';
|
||||
import type { RecursedParamDefinition } from '../../src/types/param-definition';
|
||||
import { ParamValueType } from '../../src/types/param-definition';
|
||||
|
||||
describe('param-definition', () => {
|
||||
describe('RecursedParamDefinition', () => {
|
||||
it('应该能够创建基本的参数定义', () => {
|
||||
const param: RecursedParamDefinition = {
|
||||
name: 'testParam',
|
||||
type: ViewVariableType.String,
|
||||
};
|
||||
|
||||
expect(param.name).toBe('testParam');
|
||||
expect(param.type).toBe(ViewVariableType.String);
|
||||
});
|
||||
|
||||
it('应该能够创建带有随机键的参数定义', () => {
|
||||
const param: RecursedParamDefinition = {
|
||||
name: 'testParam',
|
||||
fieldRandomKey: 'random123',
|
||||
type: ViewVariableType.String,
|
||||
};
|
||||
|
||||
expect(param.fieldRandomKey).toBe('random123');
|
||||
});
|
||||
|
||||
it('应该能够创建带有描述和必填标记的参数定义', () => {
|
||||
const param: RecursedParamDefinition = {
|
||||
name: 'testParam',
|
||||
desc: 'This is a test parameter',
|
||||
required: true,
|
||||
type: ViewVariableType.String,
|
||||
};
|
||||
|
||||
expect(param.desc).toBe('This is a test parameter');
|
||||
expect(param.required).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能够创建带有子参数的参数定义', () => {
|
||||
const param: RecursedParamDefinition = {
|
||||
name: 'parentParam',
|
||||
type: ViewVariableType.Object,
|
||||
children: [
|
||||
{
|
||||
name: 'childParam1',
|
||||
type: ViewVariableType.String,
|
||||
},
|
||||
{
|
||||
name: 'childParam2',
|
||||
type: ViewVariableType.Number,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(param.children).toHaveLength(2);
|
||||
expect(param.children?.[0].name).toBe('childParam1');
|
||||
expect(param.children?.[1].name).toBe('childParam2');
|
||||
});
|
||||
|
||||
it('应该能够创建带有固定值的参数定义', () => {
|
||||
const param: RecursedParamDefinition = {
|
||||
name: 'testParam',
|
||||
type: ViewVariableType.String,
|
||||
isQuote: ParamValueType.FIXED,
|
||||
fixedValue: 'test value',
|
||||
};
|
||||
|
||||
expect(param.isQuote).toBe(ParamValueType.FIXED);
|
||||
expect(param.fixedValue).toBe('test value');
|
||||
});
|
||||
|
||||
it('应该能够创建带有引用值的参数定义', () => {
|
||||
const param: RecursedParamDefinition = {
|
||||
name: 'testParam',
|
||||
type: ViewVariableType.String,
|
||||
isQuote: ParamValueType.QUOTE,
|
||||
quotedValue: ['node1', 'path', 'to', 'value'],
|
||||
};
|
||||
|
||||
expect(param.isQuote).toBe(ParamValueType.QUOTE);
|
||||
expect(param.quotedValue).toEqual(['node1', 'path', 'to', 'value']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ParamValueType', () => {
|
||||
it('应该定义正确的参数值类型', () => {
|
||||
expect(ParamValueType.QUOTE).toBe('quote');
|
||||
expect(ParamValueType.FIXED).toBe('fixed');
|
||||
});
|
||||
|
||||
it('应该只包含 QUOTE 和 FIXED 两种类型', () => {
|
||||
const valueTypes = Object.values(ParamValueType);
|
||||
expect(valueTypes).toHaveLength(2);
|
||||
expect(valueTypes).toContain('quote');
|
||||
expect(valueTypes).toContain('fixed');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { ViewVariableType } from '../../src/types/view-variable-type';
|
||||
import {
|
||||
ViewVariableTreeNode,
|
||||
type ViewVariableMeta,
|
||||
} from '../../src/types/view-variable-tree';
|
||||
|
||||
describe('view-variable-tree', () => {
|
||||
describe('ViewVariableTreeNode 类型', () => {
|
||||
it('应该能够创建基本的变量树节点', () => {
|
||||
const node: ViewVariableTreeNode = {
|
||||
key: 'test',
|
||||
type: ViewVariableType.String,
|
||||
name: 'Test Node',
|
||||
};
|
||||
|
||||
expect(node.key).toBe('test');
|
||||
expect(node.type).toBe(ViewVariableType.String);
|
||||
expect(node.name).toBe('Test Node');
|
||||
});
|
||||
|
||||
it('应该能够创建带有可选属性的变量树节点', () => {
|
||||
const node: ViewVariableTreeNode = {
|
||||
key: 'test',
|
||||
type: ViewVariableType.String,
|
||||
name: 'Test Node',
|
||||
required: true,
|
||||
description: 'Test Description',
|
||||
isPreset: true,
|
||||
enabled: true,
|
||||
label: 'Custom Label',
|
||||
defaultValue: 'default',
|
||||
};
|
||||
|
||||
expect(node.required).toBe(true);
|
||||
expect(node.description).toBe('Test Description');
|
||||
expect(node.isPreset).toBe(true);
|
||||
expect(node.enabled).toBe(true);
|
||||
expect(node.label).toBe('Custom Label');
|
||||
expect(node.defaultValue).toBe('default');
|
||||
});
|
||||
|
||||
it('应该能够创建带有子节点的变量树节点', () => {
|
||||
const node: ViewVariableTreeNode = {
|
||||
key: 'parent',
|
||||
type: ViewVariableType.Object,
|
||||
name: 'Parent Node',
|
||||
children: [
|
||||
{
|
||||
key: 'child1',
|
||||
type: ViewVariableType.String,
|
||||
name: 'Child Node 1',
|
||||
},
|
||||
{
|
||||
key: 'child2',
|
||||
type: ViewVariableType.Number,
|
||||
name: 'Child Node 2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(node.children).toHaveLength(2);
|
||||
expect(node.children?.[0].key).toBe('child1');
|
||||
expect(node.children?.[1].type).toBe(ViewVariableType.Number);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ViewVariableMeta 类型', () => {
|
||||
it('应该能够创建基本的变量元数据', () => {
|
||||
const meta: ViewVariableMeta = {
|
||||
key: 'test',
|
||||
type: ViewVariableType.String,
|
||||
name: 'Test Meta',
|
||||
required: true,
|
||||
description: 'Test Description',
|
||||
readonly: true,
|
||||
mutable: false,
|
||||
};
|
||||
|
||||
expect(meta.key).toBe('test');
|
||||
expect(meta.type).toBe(ViewVariableType.String);
|
||||
expect(meta.name).toBe('Test Meta');
|
||||
expect(meta.required).toBe(true);
|
||||
expect(meta.description).toBe('Test Description');
|
||||
expect(meta.readonly).toBe(true);
|
||||
expect(meta.mutable).toBe(false);
|
||||
});
|
||||
|
||||
it('应该继承 ViewVariableTreeNode 的所有属性', () => {
|
||||
const meta: ViewVariableMeta = {
|
||||
key: 'test',
|
||||
type: ViewVariableType.String,
|
||||
name: 'Test Meta',
|
||||
isPreset: true,
|
||||
enabled: true,
|
||||
label: 'Custom Label',
|
||||
defaultValue: 'default',
|
||||
readonly: true,
|
||||
mutable: false,
|
||||
};
|
||||
|
||||
expect(meta.isPreset).toBe(true);
|
||||
expect(meta.enabled).toBe(true);
|
||||
expect(meta.label).toBe('Custom Label');
|
||||
expect(meta.defaultValue).toBe('default');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ViewVariableTreeNode 命名空间函数', () => {
|
||||
const createTestTree = (): ViewVariableTreeNode => ({
|
||||
key: 'root',
|
||||
type: ViewVariableType.Object,
|
||||
name: 'Root',
|
||||
children: [
|
||||
{
|
||||
key: 'child1',
|
||||
type: ViewVariableType.Object,
|
||||
name: 'Child1',
|
||||
children: [
|
||||
{
|
||||
key: 'grandchild1',
|
||||
type: ViewVariableType.String,
|
||||
name: 'GrandChild1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'child2',
|
||||
type: ViewVariableType.String,
|
||||
name: 'Child2',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
describe('getVariableTreeNodeByPath', () => {
|
||||
it('应该能够通过路径找到正确的节点', () => {
|
||||
const tree = createTestTree();
|
||||
const node = ViewVariableTreeNode.getVariableTreeNodeByPath(tree, [
|
||||
'root',
|
||||
'child1',
|
||||
'grandchild1',
|
||||
]);
|
||||
|
||||
expect(node).toBeDefined();
|
||||
expect(node?.key).toBe('grandchild1');
|
||||
expect(node?.name).toBe('GrandChild1');
|
||||
});
|
||||
|
||||
it('应该在路径无效时返回 undefined', () => {
|
||||
const tree = createTestTree();
|
||||
const node = ViewVariableTreeNode.getVariableTreeNodeByPath(tree, [
|
||||
'root',
|
||||
'nonexistent',
|
||||
]);
|
||||
|
||||
expect(node).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该能处理空路径', () => {
|
||||
const tree = createTestTree();
|
||||
const node = ViewVariableTreeNode.getVariableTreeNodeByPath(tree, []);
|
||||
|
||||
expect(node).toBeDefined();
|
||||
expect(node?.key).toBe('root');
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyPathToNameString', () => {
|
||||
it('应该能够将路径转换为名称字符串', () => {
|
||||
const tree = createTestTree();
|
||||
const nameStr = ViewVariableTreeNode.keyPathToNameString(tree, [
|
||||
'root',
|
||||
'child1',
|
||||
'grandchild1',
|
||||
]);
|
||||
|
||||
expect(nameStr).toBe('Root.Child1.GrandChild1');
|
||||
});
|
||||
|
||||
it('应该在路径无效时返回空字符串', () => {
|
||||
const tree = createTestTree();
|
||||
const nameStr = ViewVariableTreeNode.keyPathToNameString(tree, [
|
||||
'root',
|
||||
'nonexistent',
|
||||
]);
|
||||
|
||||
expect(nameStr).toBe('');
|
||||
});
|
||||
|
||||
it('应该能处理空路径', () => {
|
||||
const tree = createTestTree();
|
||||
const nameStr = ViewVariableTreeNode.keyPathToNameString(tree, []);
|
||||
|
||||
expect(nameStr).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('nameStringToKeyPath', () => {
|
||||
it('应该能够将名称字符串转换为路径', () => {
|
||||
const tree = createTestTree();
|
||||
const keyPath = ViewVariableTreeNode.nameStringToKeyPath(
|
||||
tree,
|
||||
'Root.Child1.GrandChild1',
|
||||
);
|
||||
|
||||
expect(keyPath).toEqual(['root', 'child1', 'grandchild1']);
|
||||
});
|
||||
|
||||
it('应该在名称无效时返回空数组', () => {
|
||||
const tree = createTestTree();
|
||||
const keyPath = ViewVariableTreeNode.nameStringToKeyPath(
|
||||
tree,
|
||||
'Root.NonExistent',
|
||||
);
|
||||
|
||||
expect(keyPath).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该能处理空字符串', () => {
|
||||
const tree = createTestTree();
|
||||
const keyPath = ViewVariableTreeNode.nameStringToKeyPath(tree, '');
|
||||
|
||||
expect(keyPath).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
372
frontend/packages/workflow/base/__tests__/types/vo.test.ts
Normal file
372
frontend/packages/workflow/base/__tests__/types/vo.test.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
ValueExpressionType,
|
||||
ValueExpression,
|
||||
type RefExpressionContent,
|
||||
type LiteralExpression,
|
||||
type RefExpression,
|
||||
type InputValueVO,
|
||||
type InputTypeValueVO,
|
||||
BatchMode,
|
||||
type BatchVOInputList,
|
||||
type BatchVO,
|
||||
type OutputValueVO,
|
||||
} from '../../src/types/vo';
|
||||
import { ViewVariableType } from '../../src/types/view-variable-type';
|
||||
|
||||
describe('vo', () => {
|
||||
describe('RefExpressionContent', () => {
|
||||
it('应该能够创建引用表达式内容', () => {
|
||||
const content: RefExpressionContent = {
|
||||
keyPath: ['node1', 'output1'],
|
||||
};
|
||||
|
||||
expect(content.keyPath).toEqual(['node1', 'output1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ValueExpressionType', () => {
|
||||
it('应该定义正确的表达式类型', () => {
|
||||
expect(ValueExpressionType.LITERAL).toBe('literal');
|
||||
expect(ValueExpressionType.REF).toBe('ref');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LiteralExpression', () => {
|
||||
it('应该能够创建字符串字面量表达式', () => {
|
||||
const expr: LiteralExpression = {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test string',
|
||||
};
|
||||
|
||||
expect(expr.type).toBe(ValueExpressionType.LITERAL);
|
||||
expect(expr.content).toBe('test string');
|
||||
});
|
||||
|
||||
it('应该能够创建数字字面量表达式', () => {
|
||||
const expr: LiteralExpression = {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 42,
|
||||
};
|
||||
|
||||
expect(expr.content).toBe(42);
|
||||
});
|
||||
|
||||
it('应该能够创建布尔字面量表达式', () => {
|
||||
const expr: LiteralExpression = {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: true,
|
||||
};
|
||||
|
||||
expect(expr.content).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能够创建数组字面量表达式', () => {
|
||||
const expr: LiteralExpression = {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: [1, 'two', true],
|
||||
};
|
||||
|
||||
expect(expr.content).toEqual([1, 'two', true]);
|
||||
});
|
||||
|
||||
it('应该能够包含原始元数据', () => {
|
||||
const expr: LiteralExpression = {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test',
|
||||
rawMeta: {
|
||||
source: 'user input',
|
||||
timestamp: 123456789,
|
||||
},
|
||||
};
|
||||
|
||||
expect(expr.rawMeta).toEqual({
|
||||
source: 'user input',
|
||||
timestamp: 123456789,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RefExpression', () => {
|
||||
it('应该能够创建引用表达式', () => {
|
||||
const expr: RefExpression = {
|
||||
type: ValueExpressionType.REF,
|
||||
content: {
|
||||
keyPath: ['node1', 'output1'],
|
||||
},
|
||||
};
|
||||
|
||||
expect(expr.type).toBe(ValueExpressionType.REF);
|
||||
expect(expr.content?.keyPath).toEqual(['node1', 'output1']);
|
||||
});
|
||||
|
||||
it('应该允许内容为可选', () => {
|
||||
const expr: RefExpression = {
|
||||
type: ValueExpressionType.REF,
|
||||
};
|
||||
|
||||
expect(expr.content).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ValueExpression 命名空间函数', () => {
|
||||
describe('isRef', () => {
|
||||
it('应该正确识别引用表达式', () => {
|
||||
const refExpr: ValueExpression = {
|
||||
type: ValueExpressionType.REF,
|
||||
content: { keyPath: ['node1', 'output1'] },
|
||||
};
|
||||
const literalExpr: ValueExpression = {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test',
|
||||
};
|
||||
|
||||
expect(ValueExpression.isRef(refExpr)).toBe(true);
|
||||
expect(ValueExpression.isRef(literalExpr)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isLiteral', () => {
|
||||
it('应该正确识别字面量表达式', () => {
|
||||
const refExpr: ValueExpression = {
|
||||
type: ValueExpressionType.REF,
|
||||
content: { keyPath: ['node1', 'output1'] },
|
||||
};
|
||||
const literalExpr: ValueExpression = {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test',
|
||||
};
|
||||
|
||||
expect(ValueExpression.isLiteral(literalExpr)).toBe(true);
|
||||
expect(ValueExpression.isLiteral(refExpr)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExpression', () => {
|
||||
it('应该正确识别有效的表达式', () => {
|
||||
const refExpr: ValueExpression = {
|
||||
type: ValueExpressionType.REF,
|
||||
content: { keyPath: ['node1', 'output1'] },
|
||||
};
|
||||
const literalExpr: ValueExpression = {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test',
|
||||
};
|
||||
|
||||
expect(ValueExpression.isExpression(refExpr)).toBe(true);
|
||||
expect(ValueExpression.isExpression(literalExpr)).toBe(true);
|
||||
expect(ValueExpression.isExpression(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEmpty', () => {
|
||||
it('应该正确识别空的字面量表达式', () => {
|
||||
expect(
|
||||
ValueExpression.isEmpty({
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: '',
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
ValueExpression.isEmpty({
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: undefined,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
ValueExpression.isEmpty({
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test',
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
ValueExpression.isEmpty({
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确识别空的引用表达式', () => {
|
||||
expect(
|
||||
ValueExpression.isEmpty({
|
||||
type: ValueExpressionType.REF,
|
||||
content: { keyPath: [] },
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
ValueExpression.isEmpty({
|
||||
type: ValueExpressionType.REF,
|
||||
content: { keyPath: ['node1'] },
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确处理 undefined 值', () => {
|
||||
expect(ValueExpression.isEmpty(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('InputValueVO', () => {
|
||||
it('应该能够创建输入值对象', () => {
|
||||
const vo: InputValueVO = {
|
||||
name: 'test input',
|
||||
input: {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test value',
|
||||
},
|
||||
};
|
||||
|
||||
expect(vo.name).toBe('test input');
|
||||
expect(vo.input.type).toBe(ValueExpressionType.LITERAL);
|
||||
expect(vo.input.content).toBe('test value');
|
||||
});
|
||||
|
||||
it('应该允许名称为可选', () => {
|
||||
const vo: InputValueVO = {
|
||||
input: {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test value',
|
||||
},
|
||||
};
|
||||
|
||||
expect(vo.name).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('InputTypeValueVO', () => {
|
||||
it('应该能够创建带类型的输入值对象', () => {
|
||||
const vo: InputTypeValueVO = {
|
||||
name: 'test input',
|
||||
type: ViewVariableType.String,
|
||||
input: {
|
||||
type: ValueExpressionType.LITERAL,
|
||||
content: 'test value',
|
||||
},
|
||||
};
|
||||
|
||||
expect(vo.name).toBe('test input');
|
||||
expect(vo.type).toBe(ViewVariableType.String);
|
||||
expect(vo.input.type).toBe(ValueExpressionType.LITERAL);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BatchMode', () => {
|
||||
it('应该定义正确的批处理模式', () => {
|
||||
expect(BatchMode.Single).toBe('single');
|
||||
expect(BatchMode.Batch).toBe('batch');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BatchVOInputList', () => {
|
||||
it('应该能够创建批处理输入列表项', () => {
|
||||
const inputList: BatchVOInputList = {
|
||||
id: '1',
|
||||
name: 'test batch input',
|
||||
input: {
|
||||
type: ValueExpressionType.REF,
|
||||
content: { keyPath: ['node1', 'output1'] },
|
||||
},
|
||||
};
|
||||
|
||||
expect(inputList.id).toBe('1');
|
||||
expect(inputList.name).toBe('test batch input');
|
||||
expect(inputList.input.type).toBe(ValueExpressionType.REF);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BatchVO', () => {
|
||||
it('应该能够创建批处理配置对象', () => {
|
||||
const vo: BatchVO = {
|
||||
batchSize: 10,
|
||||
concurrentSize: 2,
|
||||
inputLists: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'input1',
|
||||
input: {
|
||||
type: ValueExpressionType.REF,
|
||||
content: { keyPath: ['node1', 'output1'] },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(vo.batchSize).toBe(10);
|
||||
expect(vo.concurrentSize).toBe(2);
|
||||
expect(vo.inputLists).toHaveLength(1);
|
||||
expect(vo.inputLists[0].id).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OutputValueVO', () => {
|
||||
it('应该能够创建基本的输出值对象', () => {
|
||||
const vo: OutputValueVO = {
|
||||
key: 'output1',
|
||||
name: 'Output 1',
|
||||
type: ViewVariableType.String,
|
||||
};
|
||||
|
||||
expect(vo.key).toBe('output1');
|
||||
expect(vo.name).toBe('Output 1');
|
||||
expect(vo.type).toBe(ViewVariableType.String);
|
||||
});
|
||||
|
||||
it('应该能够创建带可选属性的输出值对象', () => {
|
||||
const vo: OutputValueVO = {
|
||||
key: 'output1',
|
||||
name: 'Output 1',
|
||||
type: ViewVariableType.String,
|
||||
description: 'Test output',
|
||||
readonly: true,
|
||||
required: true,
|
||||
};
|
||||
|
||||
expect(vo.description).toBe('Test output');
|
||||
expect(vo.readonly).toBe(true);
|
||||
expect(vo.required).toBe(true);
|
||||
});
|
||||
|
||||
it('应该能够创建带子节点的输出值对象', () => {
|
||||
const vo: OutputValueVO = {
|
||||
key: 'parent',
|
||||
name: 'Parent Output',
|
||||
type: ViewVariableType.Object,
|
||||
children: [
|
||||
{
|
||||
key: 'child1',
|
||||
name: 'Child 1',
|
||||
type: ViewVariableType.String,
|
||||
},
|
||||
{
|
||||
key: 'child2',
|
||||
name: 'Child 2',
|
||||
type: ViewVariableType.Number,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(vo.children).toHaveLength(2);
|
||||
expect(vo.children?.[0].key).toBe('child1');
|
||||
expect(vo.children?.[1].type).toBe(ViewVariableType.Number);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { concatTestId } from '../../src/utils/concat-test-id';
|
||||
|
||||
describe('concat-test-id', () => {
|
||||
it('应该正确连接多个测试 ID', () => {
|
||||
const result = concatTestId('a', 'b', 'c');
|
||||
expect(result).toBe('a.b.c');
|
||||
});
|
||||
|
||||
it('应该过滤掉空字符串', () => {
|
||||
const result = concatTestId('a', '', 'c');
|
||||
expect(result).toBe('a.c');
|
||||
});
|
||||
|
||||
it('应该过滤掉 undefined 和 null', () => {
|
||||
const result = concatTestId('a', undefined as any, 'c', null as any);
|
||||
expect(result).toBe('a.c');
|
||||
});
|
||||
|
||||
it('应该在只有一个有效 ID 时正确返回', () => {
|
||||
const result = concatTestId('a');
|
||||
expect(result).toBe('a');
|
||||
});
|
||||
|
||||
it('应该在所有 ID 都无效时返回空字符串', () => {
|
||||
const result = concatTestId('', undefined as any, null as any);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('应该在没有参数时返回空字符串', () => {
|
||||
const result = concatTestId();
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('应该正确处理包含点号的 ID', () => {
|
||||
const result = concatTestId('a.x', 'b', 'c.y');
|
||||
expect(result).toBe('a.x.b.c.y');
|
||||
});
|
||||
|
||||
it('应该正确处理数字 ID', () => {
|
||||
const result = concatTestId('1', '2', '3');
|
||||
expect(result).toBe('1.2.3');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { FlowNodeFormData } from '@flowgram-adapter/free-layout-editor';
|
||||
import type { FlowNodeEntity } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { getFormValueByPathEnds } from '../../src/utils/form-helpers';
|
||||
|
||||
// Mock lodash-es
|
||||
vi.mock('lodash-es', () => ({
|
||||
cloneDeep: vi.fn(val => {
|
||||
if (val === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(val));
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('form-helpers', () => {
|
||||
describe('getFormValueByPathEnds', () => {
|
||||
const createMockFormModel = (paths: Record<string, unknown>) => {
|
||||
const formItemPathMap = new Map<string, unknown>();
|
||||
Object.entries(paths).forEach(([path, value]) => {
|
||||
formItemPathMap.set(path, value);
|
||||
});
|
||||
|
||||
return {
|
||||
formItemPathMap,
|
||||
getFormItemValueByPath: vi.fn(path => paths[path]),
|
||||
};
|
||||
};
|
||||
|
||||
const createMockNode = (
|
||||
formModel: ReturnType<typeof createMockFormModel>,
|
||||
) =>
|
||||
({
|
||||
getData: vi.fn((dataType: symbol) => {
|
||||
if (Object.is(dataType, FlowNodeFormData)) {
|
||||
return { formModel };
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
getNodeRegistry: () => ({}),
|
||||
}) as unknown as FlowNodeEntity;
|
||||
|
||||
it('应该返回匹配路径结尾的表单项值', () => {
|
||||
const formModel = createMockFormModel({
|
||||
'/form/test/value': 'test value',
|
||||
'/form/other/path': 'other value',
|
||||
});
|
||||
const node = createMockNode(formModel);
|
||||
|
||||
const result = getFormValueByPathEnds(node, 'test/value');
|
||||
expect(result).toBe('test value');
|
||||
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
|
||||
'/form/test/value',
|
||||
);
|
||||
});
|
||||
|
||||
it('应该在找不到匹配路径时返回 undefined', () => {
|
||||
const formModel = createMockFormModel({
|
||||
'/form/test/value': 'test value',
|
||||
});
|
||||
const node = createMockNode(formModel);
|
||||
|
||||
const result = getFormValueByPathEnds(node, 'non/existent');
|
||||
expect(result).toBeUndefined();
|
||||
expect(formModel.getFormItemValueByPath).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('应该正确处理复杂对象值', () => {
|
||||
const complexValue = {
|
||||
name: 'test',
|
||||
value: 42,
|
||||
nested: {
|
||||
field: 'nested value',
|
||||
},
|
||||
};
|
||||
const formModel = createMockFormModel({
|
||||
'/form/complex/value': complexValue,
|
||||
});
|
||||
const node = createMockNode(formModel);
|
||||
|
||||
const result = getFormValueByPathEnds(node, 'complex/value');
|
||||
expect(result).toEqual(complexValue);
|
||||
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
|
||||
'/form/complex/value',
|
||||
);
|
||||
});
|
||||
|
||||
it('应该返回深拷贝的值而不是引用', () => {
|
||||
const originalValue = { name: 'test' };
|
||||
const formModel = createMockFormModel({
|
||||
'/form/object/value': originalValue,
|
||||
});
|
||||
const node = createMockNode(formModel);
|
||||
|
||||
const result = getFormValueByPathEnds(node, 'object/value');
|
||||
expect(result).toEqual(originalValue);
|
||||
expect(result).not.toBe(originalValue);
|
||||
});
|
||||
|
||||
it('应该在有多个匹配路径时返回第一个匹配的值', () => {
|
||||
const formModel = createMockFormModel({
|
||||
'/form/path/test/value': 'first value',
|
||||
'/other/path/test/value': 'second value',
|
||||
});
|
||||
const node = createMockNode(formModel);
|
||||
|
||||
const result = getFormValueByPathEnds(node, 'test/value');
|
||||
expect(result).toBe('first value');
|
||||
expect(formModel.getFormItemValueByPath).toHaveBeenCalledWith(
|
||||
'/form/path/test/value',
|
||||
);
|
||||
});
|
||||
|
||||
it('应该正确处理空值', () => {
|
||||
const formModel = createMockFormModel({
|
||||
'/form/empty/value': null,
|
||||
'/form/undefined/value': undefined,
|
||||
});
|
||||
const node = createMockNode(formModel);
|
||||
|
||||
const nullResult = getFormValueByPathEnds(node, 'empty/value');
|
||||
expect(nullResult).toBeNull();
|
||||
|
||||
const undefinedResult = getFormValueByPathEnds(node, 'undefined/value');
|
||||
expect(undefinedResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该正确处理数组值', () => {
|
||||
const arrayValue = [1, 2, { name: 'test' }];
|
||||
const formModel = createMockFormModel({
|
||||
'/form/array/value': arrayValue,
|
||||
});
|
||||
const node = createMockNode(formModel);
|
||||
|
||||
const result = getFormValueByPathEnds(node, 'array/value');
|
||||
expect(result).toEqual(arrayValue);
|
||||
expect(Array.isArray(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { WorkflowMode } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { isGeneralWorkflow } from '../../src/utils/is-general-workflow';
|
||||
|
||||
describe('is-general-workflow', () => {
|
||||
it('应该在 flowMode 为 Workflow 时返回 true', () => {
|
||||
expect(isGeneralWorkflow(WorkflowMode.Workflow)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该在 flowMode 为 ChatFlow 时返回 true', () => {
|
||||
expect(isGeneralWorkflow(WorkflowMode.ChatFlow)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该在 flowMode 为其他值时返回 false', () => {
|
||||
// 测试其他可能的 WorkflowMode 值
|
||||
expect(isGeneralWorkflow(WorkflowMode.Imageflow)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import { defaultParser } from '../../../../src/utils/node-result-extractor/parsers';
|
||||
import { StandardNodeType } from '../../../../src/types';
|
||||
import type { WorkflowJSON } from '../../../../src/types';
|
||||
import { TerminatePlanType } from '../../../../src/api';
|
||||
import type { NodeResult } from '../../../../src/api';
|
||||
|
||||
// Mock @coze-arch/bot-utils
|
||||
vi.mock('@coze-arch/bot-utils', () => ({
|
||||
typeSafeJSONParse: (str: string) => {
|
||||
try {
|
||||
const result = JSON.parse(str);
|
||||
// 如果是批处理数据,确保返回数组类型
|
||||
if (str === 'invalid json') {
|
||||
return str;
|
||||
}
|
||||
// 如果是批处理数据,确保返回数组类型
|
||||
if (str.includes('batch')) {
|
||||
return Array.isArray(result) ? result : [];
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
// 如果是批处理数据,返回空数组
|
||||
if (str.includes('batch')) {
|
||||
return [];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock parseImagesFromOutputData
|
||||
vi.mock('../../../../src/utils/output-image-parser', () => ({
|
||||
parseImagesFromOutputData: vi.fn(({ outputData }) => {
|
||||
if (typeof outputData === 'string' && outputData.includes('image')) {
|
||||
return ['https://example.com/image.png'];
|
||||
}
|
||||
if (typeof outputData === 'object' && outputData !== null) {
|
||||
const str = JSON.stringify(outputData);
|
||||
if (str.includes('image')) {
|
||||
return ['https://example.com/image.png'];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('../../../../src/api', () => ({
|
||||
TerminatePlanType: {
|
||||
USESETTING: 2,
|
||||
},
|
||||
}));
|
||||
|
||||
describe('default-parser', () => {
|
||||
const createMockNodeResult = (
|
||||
nodeId: string,
|
||||
overrides: Partial<NodeResult> = {},
|
||||
): NodeResult => ({
|
||||
nodeId,
|
||||
isBatch: false,
|
||||
input: 'test input',
|
||||
output: 'test output',
|
||||
raw_output: 'test raw output',
|
||||
extra: '{}',
|
||||
items: '[]',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createMockWorkflowSchema = (
|
||||
nodeId: string,
|
||||
nodeType = StandardNodeType.LLM,
|
||||
): WorkflowJSON => ({
|
||||
nodes: [
|
||||
{
|
||||
id: nodeId,
|
||||
type: nodeType,
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
});
|
||||
|
||||
describe('非批处理节点', () => {
|
||||
it('应该正确解析 LLM 节点结果', () => {
|
||||
const nodeResult = createMockNodeResult('1');
|
||||
const workflowSchema = createMockWorkflowSchema(
|
||||
'1',
|
||||
StandardNodeType.LLM,
|
||||
);
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.nodeId).toBe('1');
|
||||
expect(result.nodeType).toBe(StandardNodeType.LLM);
|
||||
expect(result.isBatch).toBe(false);
|
||||
expect(result.caseResult).toHaveLength(1);
|
||||
expect(result.caseResult?.[0].dataList).toHaveLength(3); // 输入、原始输出、最终输出
|
||||
});
|
||||
|
||||
it('应该正确解析 Code 节点结果', () => {
|
||||
const nodeResult = createMockNodeResult('1');
|
||||
const workflowSchema = createMockWorkflowSchema(
|
||||
'1',
|
||||
StandardNodeType.Code,
|
||||
);
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.nodeType).toBe(StandardNodeType.Code);
|
||||
expect(result.caseResult?.[0].dataList).toHaveLength(3); // 输入、原始输出、最终输出
|
||||
});
|
||||
|
||||
it('应该正确解析 Start 节点结果', () => {
|
||||
const nodeResult = createMockNodeResult('1');
|
||||
const workflowSchema = createMockWorkflowSchema(
|
||||
'1',
|
||||
StandardNodeType.Start,
|
||||
);
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.nodeType).toBe(StandardNodeType.Start);
|
||||
expect(result.caseResult?.[0].dataList).toHaveLength(1); // 只有输入
|
||||
});
|
||||
|
||||
it('应该正确解析 End 节点结果', () => {
|
||||
const nodeResult = createMockNodeResult('1', {
|
||||
extra: JSON.stringify({
|
||||
response_extra: {
|
||||
terminal_plan: TerminatePlanType.USESETTING,
|
||||
},
|
||||
}),
|
||||
output: JSON.stringify({ content: 'test content' }),
|
||||
raw_output: JSON.stringify({ content: 'test raw content' }),
|
||||
});
|
||||
const workflowSchema = createMockWorkflowSchema(
|
||||
'1',
|
||||
StandardNodeType.End,
|
||||
);
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.nodeType).toBe(StandardNodeType.End);
|
||||
expect(result.caseResult?.[0].dataList).toHaveLength(2); // 输出变量、回答内容
|
||||
});
|
||||
|
||||
it('应该正确解析 Message 节点结果', () => {
|
||||
const nodeResult = createMockNodeResult('1');
|
||||
const workflowSchema = createMockWorkflowSchema(
|
||||
'1',
|
||||
StandardNodeType.Output,
|
||||
);
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.nodeType).toBe(StandardNodeType.Output);
|
||||
expect(result.caseResult?.[0].dataList).toHaveLength(2); // 输出变量、回答内容
|
||||
});
|
||||
|
||||
it('应该正确解析包含图片的输出', () => {
|
||||
const nodeResult = createMockNodeResult('1', {
|
||||
output: JSON.stringify({ content: 'test output with image' }),
|
||||
});
|
||||
const workflowSchema = createMockWorkflowSchema('1');
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.caseResult?.[0].imgList).toEqual([
|
||||
'https://example.com/image.png',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('批处理节点', () => {
|
||||
it('应该正确解析批处理节点结果', () => {
|
||||
const nodeResult = createMockNodeResult('1', {
|
||||
isBatch: true,
|
||||
batch: JSON.stringify([
|
||||
createMockNodeResult('1'),
|
||||
createMockNodeResult('1'),
|
||||
]),
|
||||
});
|
||||
const workflowSchema = createMockWorkflowSchema('1');
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.isBatch).toBe(true);
|
||||
expect(result.caseResult).toHaveLength(2);
|
||||
expect(result.caseResult?.[0].dataList).toBeDefined();
|
||||
expect(result.caseResult?.[1].dataList).toBeDefined();
|
||||
});
|
||||
|
||||
it('应该正确处理空的批处理结果', () => {
|
||||
const nodeResult = createMockNodeResult('1', {
|
||||
isBatch: true,
|
||||
batch: '[]',
|
||||
});
|
||||
const workflowSchema = createMockWorkflowSchema('1');
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.isBatch).toBe(true);
|
||||
expect(result.caseResult).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确处理无效的批处理 JSON', () => {
|
||||
const nodeResult = createMockNodeResult('1', {
|
||||
isBatch: true,
|
||||
batch: 'invalid batch json',
|
||||
});
|
||||
const workflowSchema = createMockWorkflowSchema('1');
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.isBatch).toBe(true);
|
||||
expect(result.caseResult).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确处理批处理中的 null 或 undefined 结果', () => {
|
||||
const nodeResult = createMockNodeResult('1', {
|
||||
isBatch: true,
|
||||
batch: JSON.stringify([null, createMockNodeResult('1'), undefined]),
|
||||
});
|
||||
const workflowSchema = createMockWorkflowSchema('1');
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.isBatch).toBe(true);
|
||||
expect(result.caseResult).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('特殊情况处理', () => {
|
||||
it('应该正确处理无效的 JSON 输入', () => {
|
||||
const nodeResult = createMockNodeResult('1', {
|
||||
input: 'invalid json',
|
||||
output: 'invalid json',
|
||||
raw_output: 'invalid json',
|
||||
});
|
||||
const workflowSchema = createMockWorkflowSchema('1');
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(
|
||||
result.caseResult?.[0].dataList?.some(
|
||||
item => item.data === 'invalid json',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确处理 Text 节点的原始输出', () => {
|
||||
const nodeResult = createMockNodeResult('1', {
|
||||
raw_output: '{"key": "value"}', // 即使是有效的 JSON 字符串也应该保持原样
|
||||
});
|
||||
const workflowSchema = createMockWorkflowSchema(
|
||||
'1',
|
||||
StandardNodeType.Text,
|
||||
);
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
const rawOutput = result.caseResult?.[0].dataList?.find(
|
||||
item => item.title === '原始输出',
|
||||
);
|
||||
expect(rawOutput?.data).toBe('{"key": "value"}');
|
||||
});
|
||||
|
||||
it('应该正确处理不存在的节点类型', () => {
|
||||
const nodeResult = createMockNodeResult('1');
|
||||
const workflowSchema = createMockWorkflowSchema('2'); // 不同的节点 ID
|
||||
const result = defaultParser(nodeResult, workflowSchema);
|
||||
|
||||
expect(result.nodeType).toBeUndefined();
|
||||
expect(result.caseResult).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
type CaseResultData,
|
||||
type NodeResultExtracted,
|
||||
} from '../../../src/utils/node-result-extractor/type';
|
||||
import { StandardNodeType } from '../../../src/types';
|
||||
|
||||
describe('node-result-extractor/type', () => {
|
||||
describe('CaseResultData', () => {
|
||||
it('应该能够创建基本的 CaseResultData 对象', () => {
|
||||
const data: CaseResultData = {
|
||||
dataList: [
|
||||
{
|
||||
title: 'test',
|
||||
data: 'test data',
|
||||
},
|
||||
],
|
||||
imgList: ['https://example.com/image.png'],
|
||||
};
|
||||
|
||||
expect(data.dataList).toHaveLength(1);
|
||||
expect(data.dataList?.[0].title).toBe('test');
|
||||
expect(data.dataList?.[0].data).toBe('test data');
|
||||
expect(data.imgList).toHaveLength(1);
|
||||
expect(data.imgList?.[0]).toBe('https://example.com/image.png');
|
||||
});
|
||||
|
||||
it('应该允许所有属性为可选', () => {
|
||||
const data: CaseResultData = {};
|
||||
expect(data.dataList).toBeUndefined();
|
||||
expect(data.imgList).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该允许 dataList 中的 data 为任意类型', () => {
|
||||
const data: CaseResultData = {
|
||||
dataList: [
|
||||
{ title: 'string', data: 'string data' },
|
||||
{ title: 'number', data: 123 },
|
||||
{ title: 'boolean', data: true },
|
||||
{ title: 'object', data: { key: 'value' } },
|
||||
{ title: 'array', data: [1, 2, 3] },
|
||||
],
|
||||
};
|
||||
|
||||
expect(data.dataList).toHaveLength(5);
|
||||
expect(typeof data.dataList?.[0].data).toBe('string');
|
||||
expect(typeof data.dataList?.[1].data).toBe('number');
|
||||
expect(typeof data.dataList?.[2].data).toBe('boolean');
|
||||
expect(typeof data.dataList?.[3].data).toBe('object');
|
||||
expect(Array.isArray(data.dataList?.[4].data)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NodeResultExtracted', () => {
|
||||
it('应该能够创建基本的 NodeResultExtracted 对象', () => {
|
||||
const result: NodeResultExtracted = {
|
||||
nodeId: '123',
|
||||
nodeType: StandardNodeType.LLM,
|
||||
isBatch: false,
|
||||
caseResult: [
|
||||
{
|
||||
dataList: [{ title: 'test', data: 'test data' }],
|
||||
imgList: ['https://example.com/image.png'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(result.nodeId).toBe('123');
|
||||
expect(result.nodeType).toBe(StandardNodeType.LLM);
|
||||
expect(result.isBatch).toBe(false);
|
||||
expect(result.caseResult).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('应该允许所有属性为可选', () => {
|
||||
const result: NodeResultExtracted = {};
|
||||
expect(result.nodeId).toBeUndefined();
|
||||
expect(result.nodeType).toBeUndefined();
|
||||
expect(result.isBatch).toBeUndefined();
|
||||
expect(result.caseResult).toBeUndefined();
|
||||
});
|
||||
|
||||
it('应该允许 caseResult 为空数组', () => {
|
||||
const result: NodeResultExtracted = {
|
||||
nodeId: '123',
|
||||
nodeType: StandardNodeType.LLM,
|
||||
isBatch: false,
|
||||
caseResult: [],
|
||||
};
|
||||
|
||||
expect(result.caseResult).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('应该允许多个 caseResult', () => {
|
||||
const result: NodeResultExtracted = {
|
||||
nodeId: '123',
|
||||
nodeType: StandardNodeType.LLM,
|
||||
isBatch: true,
|
||||
caseResult: [
|
||||
{
|
||||
dataList: [{ title: 'case1', data: 'data1' }],
|
||||
},
|
||||
{
|
||||
dataList: [{ title: 'case2', data: 'data2' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(result.caseResult).toHaveLength(2);
|
||||
expect(result.caseResult?.[0].dataList?.[0].title).toBe('case1');
|
||||
expect(result.caseResult?.[1].dataList?.[0].title).toBe('case2');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
|
||||
|
||||
import { parseImagesFromOutputData } from '../../src/utils/output-image-parser';
|
||||
import {
|
||||
StandardNodeType,
|
||||
VariableTypeDTO,
|
||||
AssistTypeDTO,
|
||||
} from '../../src/types';
|
||||
|
||||
describe('output-image-parser', () => {
|
||||
describe('parseImagesFromOutputData', () => {
|
||||
it('应该在没有 nodeSchema 或 outputData 时返回空数组', () => {
|
||||
expect(parseImagesFromOutputData({})).toEqual([]);
|
||||
expect(
|
||||
parseImagesFromOutputData({
|
||||
outputData: 'test',
|
||||
nodeSchema: undefined,
|
||||
}),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
parseImagesFromOutputData({
|
||||
outputData: undefined,
|
||||
nodeSchema: {},
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该在节点类型被排除时返回空数组', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.LLM,
|
||||
data: {
|
||||
outputs: [
|
||||
{
|
||||
name: 'image',
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
parseImagesFromOutputData({
|
||||
outputData: { image: 'https://example.com/image.png' },
|
||||
nodeSchema,
|
||||
excludeNodeTypes: [StandardNodeType.LLM],
|
||||
}),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确解析 End 节点的图片链接', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.End,
|
||||
data: {
|
||||
inputs: {
|
||||
inputParameters: [
|
||||
{
|
||||
name: 'image',
|
||||
input: {
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseImagesFromOutputData({
|
||||
outputData: { image: 'https://example.com/image.png' },
|
||||
nodeSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['https://example.com/image.png']);
|
||||
});
|
||||
|
||||
it('应该正确解析 Message 节点的图片链接', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.Output,
|
||||
data: {
|
||||
inputs: {
|
||||
inputParameters: [
|
||||
{
|
||||
name: 'image',
|
||||
input: {
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseImagesFromOutputData({
|
||||
outputData: { image: 'https://example.com/image.png' },
|
||||
nodeSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['https://example.com/image.png']);
|
||||
});
|
||||
|
||||
it('应该正确解析其他节点类型的图片链接', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.LLM,
|
||||
data: {
|
||||
outputs: [
|
||||
{
|
||||
name: 'image',
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseImagesFromOutputData({
|
||||
outputData: { image: 'https://example.com/image.png' },
|
||||
nodeSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['https://example.com/image.png']);
|
||||
});
|
||||
|
||||
it('应该正确解析图片列表', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.LLM,
|
||||
data: {
|
||||
outputs: [
|
||||
{
|
||||
name: 'images',
|
||||
type: VariableTypeDTO.list,
|
||||
schema: {
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseImagesFromOutputData({
|
||||
outputData: {
|
||||
images: [
|
||||
'https://example.com/image1.png',
|
||||
'https://example.com/image2.png',
|
||||
],
|
||||
},
|
||||
nodeSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
'https://example.com/image1.png',
|
||||
'https://example.com/image2.png',
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确解析对象中的图片', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.LLM,
|
||||
data: {
|
||||
outputs: [
|
||||
{
|
||||
name: 'data',
|
||||
type: VariableTypeDTO.object,
|
||||
schema: [
|
||||
{
|
||||
name: 'image',
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
type: VariableTypeDTO.string,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseImagesFromOutputData({
|
||||
outputData: {
|
||||
data: {
|
||||
image: 'https://example.com/image.png',
|
||||
text: 'Some text',
|
||||
},
|
||||
},
|
||||
nodeSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['https://example.com/image.png']);
|
||||
});
|
||||
|
||||
it('应该正确处理 SVG 类型的图片', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.LLM,
|
||||
data: {
|
||||
outputs: [
|
||||
{
|
||||
name: 'svg',
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.svg,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseImagesFromOutputData({
|
||||
outputData: { svg: 'https://example.com/image.svg' },
|
||||
nodeSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['https://example.com/image.svg']);
|
||||
});
|
||||
|
||||
it('应该正确处理原生图片类型', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.LLM,
|
||||
data: {
|
||||
outputs: [
|
||||
{
|
||||
name: 'image',
|
||||
type: VariableTypeDTO.image,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseImagesFromOutputData({
|
||||
outputData: { image: 'https://example.com/image.png' },
|
||||
nodeSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['https://example.com/image.png']);
|
||||
});
|
||||
|
||||
it('应该过滤掉空的图片链接', () => {
|
||||
const nodeSchema: WorkflowNodeJSON = {
|
||||
id: '1',
|
||||
type: StandardNodeType.LLM,
|
||||
data: {
|
||||
outputs: [
|
||||
{
|
||||
name: 'images',
|
||||
type: VariableTypeDTO.list,
|
||||
schema: {
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = parseImagesFromOutputData({
|
||||
outputData: {
|
||||
images: [
|
||||
'https://example.com/image1.png',
|
||||
'',
|
||||
null,
|
||||
undefined,
|
||||
'https://example.com/image2.png',
|
||||
],
|
||||
},
|
||||
nodeSchema,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
'https://example.com/image1.png',
|
||||
'https://example.com/image2.png',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { expressionParser } from '../../../../src/utils/schema-extractor/parsers/expression-parser';
|
||||
import type { ValueExpressionDTO } from '../../../../src/types/dto';
|
||||
|
||||
describe('expression-parser', () => {
|
||||
it('should handle empty input', () => {
|
||||
const result = expressionParser([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse string literal expression', () => {
|
||||
const expression: ValueExpressionDTO = {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'hello',
|
||||
},
|
||||
};
|
||||
const result = expressionParser(expression);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
value: 'hello',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse image url expression', () => {
|
||||
const expression: ValueExpressionDTO = {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'https://example.com/tos-cn-i-mdko3gqilj/test.png',
|
||||
},
|
||||
};
|
||||
const result = expressionParser(expression);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
value: 'https://example.com/tos-cn-i-mdko3gqilj/test.png',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse block output expression', () => {
|
||||
const expression: ValueExpressionDTO = {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'ref',
|
||||
content: {
|
||||
source: 'block-output',
|
||||
blockID: 'block1',
|
||||
name: 'output',
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = expressionParser(expression);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
value: 'output',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse global variable expression', () => {
|
||||
const expression: ValueExpressionDTO = {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'ref',
|
||||
content: {
|
||||
source: 'global_variable_test',
|
||||
path: ['user', 'name'],
|
||||
blockID: 'global',
|
||||
name: 'user.name',
|
||||
},
|
||||
},
|
||||
};
|
||||
const result = expressionParser(expression);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
value: 'user.name',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle invalid expressions', () => {
|
||||
const expression: ValueExpressionDTO = {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: undefined,
|
||||
},
|
||||
};
|
||||
const result = expressionParser(expression);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should filter out invalid inputs', () => {
|
||||
const result = expressionParser(undefined as any);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { imageReferenceParser } from '../../../../src/utils/schema-extractor/parsers/image-reference';
|
||||
import type { ValueExpressionDTO } from '../../../../src/types/dto';
|
||||
|
||||
interface ImageReferenceDTO {
|
||||
url: ValueExpressionDTO;
|
||||
}
|
||||
|
||||
describe('image-reference-parser', () => {
|
||||
it('应该处理空输入', () => {
|
||||
const result = imageReferenceParser([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该处理非数组输入', () => {
|
||||
const result = imageReferenceParser(undefined as any);
|
||||
expect(result).toEqual([]);
|
||||
|
||||
const result2 = imageReferenceParser({} as any);
|
||||
expect(result2).toEqual([]);
|
||||
|
||||
const result3 = imageReferenceParser(null as any);
|
||||
expect(result3).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确解析图片引用', () => {
|
||||
const references: ImageReferenceDTO[] = [
|
||||
{
|
||||
url: {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'https://example.com/test.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = imageReferenceParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: '-',
|
||||
value: 'https://example.com/test.png',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确解析多个图片引用', () => {
|
||||
const references: ImageReferenceDTO[] = [
|
||||
{
|
||||
url: {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'https://example.com/test1.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
url: {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'https://example.com/test2.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = imageReferenceParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: '-',
|
||||
value: 'https://example.com/test1.png',
|
||||
isImage: false,
|
||||
},
|
||||
{
|
||||
name: '-',
|
||||
value: 'https://example.com/test2.png',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该过滤掉无效的图片引用', () => {
|
||||
const references: ImageReferenceDTO[] = [
|
||||
{
|
||||
url: {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
url: {
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'https://example.com/test.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = imageReferenceParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: '-',
|
||||
value: 'https://example.com/test.png',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import { outputsParser } from '../../../../src/utils/schema-extractor/parsers/output';
|
||||
import { AssistTypeDTO, VariableTypeDTO } from '../../../../src/types/dto';
|
||||
|
||||
// Mock isWorkflowImageTypeURL
|
||||
vi.mock('../../../../src/utils/schema-extractor/utils', () => ({
|
||||
isWorkflowImageTypeURL: (url: string) =>
|
||||
url.startsWith('https://example.com/'),
|
||||
}));
|
||||
|
||||
describe('output-parser', () => {
|
||||
it('应该处理空输入', () => {
|
||||
const result = outputsParser([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该处理非数组输入', () => {
|
||||
const result = outputsParser(undefined as any);
|
||||
expect(result).toEqual([]);
|
||||
|
||||
const result2 = outputsParser({} as any);
|
||||
expect(result2).toEqual([]);
|
||||
|
||||
const result3 = outputsParser(null as any);
|
||||
expect(result3).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确解析基本输出', () => {
|
||||
const outputs = [
|
||||
{
|
||||
name: 'test',
|
||||
description: 'test description',
|
||||
type: VariableTypeDTO.string,
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'test',
|
||||
description: 'test description',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确解析对象类型输出', () => {
|
||||
const outputs = [
|
||||
{
|
||||
name: 'obj',
|
||||
type: VariableTypeDTO.object,
|
||||
schema: [
|
||||
{
|
||||
name: 'field1',
|
||||
type: VariableTypeDTO.string,
|
||||
},
|
||||
{
|
||||
name: 'field2',
|
||||
type: VariableTypeDTO.float,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'obj',
|
||||
children: [
|
||||
{
|
||||
name: 'field1',
|
||||
},
|
||||
{
|
||||
name: 'field2',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确解析列表类型输出', () => {
|
||||
const outputs = [
|
||||
{
|
||||
name: 'list',
|
||||
type: VariableTypeDTO.list,
|
||||
schema: {
|
||||
schema: [
|
||||
{
|
||||
name: 'item1',
|
||||
type: VariableTypeDTO.string,
|
||||
},
|
||||
{
|
||||
name: 'item2',
|
||||
type: VariableTypeDTO.float,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'list',
|
||||
children: [
|
||||
{
|
||||
name: 'item1',
|
||||
},
|
||||
{
|
||||
name: 'item2',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理带有默认值的字符串输出', () => {
|
||||
const outputs = [
|
||||
{
|
||||
name: 'text',
|
||||
type: VariableTypeDTO.string,
|
||||
defaultValue: 'hello world',
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'text',
|
||||
value: 'hello world',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理图片 URL 输出', () => {
|
||||
const outputs = [
|
||||
{
|
||||
name: 'image',
|
||||
type: VariableTypeDTO.string,
|
||||
defaultValue: 'https://example.com/test.png',
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'image',
|
||||
value: 'https://example.com/test.png',
|
||||
images: ['https://example.com/test.png'],
|
||||
isImage: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理图片类型输出', () => {
|
||||
const outputs = [
|
||||
{
|
||||
name: 'image',
|
||||
type: VariableTypeDTO.string,
|
||||
assistType: AssistTypeDTO.image,
|
||||
defaultValue: 'https://example.com/test.png',
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'image',
|
||||
value: 'https://example.com/test.png',
|
||||
images: ['https://example.com/test.png'],
|
||||
isImage: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理图片列表输出', () => {
|
||||
const outputs = [
|
||||
{
|
||||
name: 'images',
|
||||
type: VariableTypeDTO.list,
|
||||
schema: {
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
defaultValue: JSON.stringify([
|
||||
'https://example.com/test1.png',
|
||||
'https://example.com/test2.png',
|
||||
]),
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'images',
|
||||
value: JSON.stringify([
|
||||
'https://example.com/test1.png',
|
||||
'https://example.com/test2.png',
|
||||
]),
|
||||
images: [
|
||||
'https://example.com/test1.png',
|
||||
'https://example.com/test2.png',
|
||||
],
|
||||
isImage: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理文件列表中的图片', () => {
|
||||
const outputs = [
|
||||
{
|
||||
name: 'files',
|
||||
type: VariableTypeDTO.list,
|
||||
schema: {
|
||||
assistType: AssistTypeDTO.file,
|
||||
},
|
||||
defaultValue: JSON.stringify([
|
||||
'https://example.com/test.png',
|
||||
'https://example1.com/document.pdf',
|
||||
]),
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
console.log(result);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'files',
|
||||
value: JSON.stringify([
|
||||
'https://example.com/test.png',
|
||||
'https://example1.com/document.pdf',
|
||||
]),
|
||||
images: ['https://example.com/test.png'],
|
||||
isImage: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理无效的 JSON 字符串', () => {
|
||||
const consoleError = vi.spyOn(console, 'error');
|
||||
const outputs = [
|
||||
{
|
||||
name: 'invalid',
|
||||
type: VariableTypeDTO.list,
|
||||
schema: {
|
||||
assistType: AssistTypeDTO.image,
|
||||
},
|
||||
defaultValue: 'invalid json',
|
||||
},
|
||||
];
|
||||
|
||||
const result = outputsParser(outputs);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'invalid',
|
||||
value: 'invalid json',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { refInputParametersParser } from '../../../../src/utils/schema-extractor/parsers/ref-input-parameters';
|
||||
|
||||
// Mock isWorkflowImageTypeURL
|
||||
vi.mock('../../../../src/utils/schema-extractor/utils', () => ({
|
||||
isWorkflowImageTypeURL: (url: string) =>
|
||||
typeof url === 'string' && url.startsWith('https://example.com/'),
|
||||
}));
|
||||
|
||||
describe('ref-input-parameters-parser', () => {
|
||||
it('应该处理空输入', () => {
|
||||
const result = refInputParametersParser([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确解析单个引用参数', () => {
|
||||
const references = [
|
||||
{
|
||||
param1: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'test value',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = refInputParametersParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'param1',
|
||||
value: 'test value',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确解析多个引用参数', () => {
|
||||
const references = [
|
||||
{
|
||||
param1: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'value1',
|
||||
},
|
||||
},
|
||||
param2: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'value2',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = refInputParametersParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'param1',
|
||||
value: 'value1',
|
||||
isImage: false,
|
||||
},
|
||||
{
|
||||
name: 'param2',
|
||||
value: 'value2',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确解析多个引用对象', () => {
|
||||
const references = [
|
||||
{
|
||||
param1: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'value1',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
param2: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'value2',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = refInputParametersParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'param1',
|
||||
value: 'value1',
|
||||
isImage: false,
|
||||
},
|
||||
{
|
||||
name: 'param2',
|
||||
value: 'value2',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确识别图片 URL', () => {
|
||||
const references = [
|
||||
{
|
||||
image: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'https://example.com/test.png',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = refInputParametersParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'image',
|
||||
value: 'https://example.com/test.png',
|
||||
isImage: true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该忽略非字符串类型的参数', () => {
|
||||
const references = [
|
||||
{
|
||||
param1: {
|
||||
type: 'number',
|
||||
value: {
|
||||
content: '123',
|
||||
},
|
||||
},
|
||||
param2: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'valid',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = refInputParametersParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'param2',
|
||||
value: 'valid',
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该忽略无效的参数结构', () => {
|
||||
const references = [
|
||||
{
|
||||
param1: 'invalid',
|
||||
param2: {
|
||||
type: 'string',
|
||||
value: {
|
||||
content: 'valid',
|
||||
},
|
||||
},
|
||||
param3: null,
|
||||
param4: undefined,
|
||||
param5: {
|
||||
type: 'string',
|
||||
value: {}, // 空的 value 对象
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = refInputParametersParser(references);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'param2',
|
||||
value: 'valid',
|
||||
isImage: false,
|
||||
},
|
||||
{
|
||||
name: 'param5',
|
||||
value: undefined,
|
||||
isImage: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
import { variableMergeGroupsParser } from '../../../../src/utils/schema-extractor/parsers/variable-merge-groups-parser';
|
||||
import type { ValueExpressionDTO } from '../../../../src/types/dto';
|
||||
|
||||
// Mock expressionParser
|
||||
vi.mock(
|
||||
'../../../../src/utils/schema-extractor/parsers/expression-parser',
|
||||
() => ({
|
||||
expressionParser: vi.fn(variables => {
|
||||
if (!Array.isArray(variables)) {
|
||||
return [];
|
||||
}
|
||||
return variables.map(variable => ({
|
||||
value: variable.value?.content,
|
||||
isImage:
|
||||
typeof variable.value?.content === 'string' &&
|
||||
variable.value?.content.startsWith('https://example.com/'),
|
||||
}));
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('variable-merge-groups-parser', () => {
|
||||
it('应该处理空输入', () => {
|
||||
const result = variableMergeGroupsParser([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('应该正确解析单个变量组', () => {
|
||||
const mergeGroups = [
|
||||
{
|
||||
name: 'group1',
|
||||
variables: [
|
||||
{
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'test value',
|
||||
},
|
||||
} as ValueExpressionDTO,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = variableMergeGroupsParser(mergeGroups);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
groupName: 'group1',
|
||||
variables: [
|
||||
{
|
||||
value: 'test value',
|
||||
isImage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确解析多个变量组', () => {
|
||||
const mergeGroups = [
|
||||
{
|
||||
name: 'group1',
|
||||
variables: [
|
||||
{
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'value1',
|
||||
},
|
||||
} as ValueExpressionDTO,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'group2',
|
||||
variables: [
|
||||
{
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'value2',
|
||||
},
|
||||
} as ValueExpressionDTO,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = variableMergeGroupsParser(mergeGroups);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
groupName: 'group1',
|
||||
variables: [
|
||||
{
|
||||
value: 'value1',
|
||||
isImage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
groupName: 'group2',
|
||||
variables: [
|
||||
{
|
||||
value: 'value2',
|
||||
isImage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理包含图片 URL 的变量组', () => {
|
||||
const mergeGroups = [
|
||||
{
|
||||
name: 'images',
|
||||
variables: [
|
||||
{
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'https://example.com/test.png',
|
||||
},
|
||||
} as ValueExpressionDTO,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = variableMergeGroupsParser(mergeGroups);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
groupName: 'images',
|
||||
variables: [
|
||||
{
|
||||
value: 'https://example.com/test.png',
|
||||
isImage: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理空变量组', () => {
|
||||
const mergeGroups = [
|
||||
{
|
||||
name: 'emptyGroup',
|
||||
variables: [],
|
||||
},
|
||||
];
|
||||
|
||||
const result = variableMergeGroupsParser(mergeGroups);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
groupName: 'emptyGroup',
|
||||
variables: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('应该正确处理包含多个变量的组', () => {
|
||||
const mergeGroups = [
|
||||
{
|
||||
name: 'mixedGroup',
|
||||
variables: [
|
||||
{
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'text value',
|
||||
},
|
||||
} as ValueExpressionDTO,
|
||||
{
|
||||
type: 'string',
|
||||
value: {
|
||||
type: 'literal',
|
||||
content: 'https://example.com/test.png',
|
||||
},
|
||||
} as ValueExpressionDTO,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = variableMergeGroupsParser(mergeGroups);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
groupName: 'mixedGroup',
|
||||
variables: [
|
||||
{
|
||||
value: 'text value',
|
||||
isImage: false,
|
||||
},
|
||||
{
|
||||
value: 'https://example.com/test.png',
|
||||
isImage: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
isPresetStartParams,
|
||||
isUserInputStartParams,
|
||||
} from '../../src/utils/start-params';
|
||||
import {
|
||||
BOT_USER_INPUT,
|
||||
USER_INPUT,
|
||||
CONVERSATION_NAME,
|
||||
} from '../../src/constants';
|
||||
|
||||
describe('start-params', () => {
|
||||
describe('isPresetStartParams', () => {
|
||||
it('应该正确识别预设的开始节点参数', () => {
|
||||
expect(isPresetStartParams(BOT_USER_INPUT)).toBe(true);
|
||||
expect(isPresetStartParams(USER_INPUT)).toBe(true);
|
||||
expect(isPresetStartParams(CONVERSATION_NAME)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确识别非预设的开始节点参数', () => {
|
||||
expect(isPresetStartParams('other_param')).toBe(false);
|
||||
expect(isPresetStartParams('custom_input')).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确处理 undefined 和空字符串', () => {
|
||||
expect(isPresetStartParams(undefined)).toBe(false);
|
||||
expect(isPresetStartParams('')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUserInputStartParams', () => {
|
||||
it('应该正确识别用户输入的开始节点参数', () => {
|
||||
expect(isUserInputStartParams(BOT_USER_INPUT)).toBe(true);
|
||||
expect(isUserInputStartParams(USER_INPUT)).toBe(true);
|
||||
});
|
||||
|
||||
it('应该正确识别非用户输入的开始节点参数', () => {
|
||||
expect(isUserInputStartParams(CONVERSATION_NAME)).toBe(false);
|
||||
expect(isUserInputStartParams('other_param')).toBe(false);
|
||||
expect(isUserInputStartParams('custom_input')).toBe(false);
|
||||
});
|
||||
|
||||
it('应该正确处理 undefined 和空字符串', () => {
|
||||
expect(isUserInputStartParams(undefined)).toBe(false);
|
||||
expect(isUserInputStartParams('')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user