feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
/*
* 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 { injectable } from 'inversify';
import { type EncapsulateApiService } from '../src/api';
import { complexMock } from './workflow.mock';
@injectable()
export class MockEncapsulateApiService implements EncapsulateApiService {
encapsulateWorkflow() {
return Promise.resolve({ workflowId: 'mockWorkflowId' });
}
validateWorkflow() {
return Promise.resolve([]);
}
getWorkflow(_spaceId: string, _workflowId: string) {
return Promise.resolve(complexMock);
}
}

View File

@@ -0,0 +1,142 @@
/*
* 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 { ContainerModule, injectable } from 'inversify';
import {
createFreeHistoryPlugin,
FormModelV2,
type FlowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import {
FlowNodeFormData,
createNodeContainerModules,
createNodeEntityDatas,
} from '@flowgram-adapter/free-layout-editor';
import {
FlowDocumentContainerModule,
FlowDocumentContribution,
} from '@flowgram-adapter/free-layout-editor';
import {
PlaygroundMockTools,
PlaygroundContext,
bindContributions,
Playground,
loadPlugins,
EntityManagerContribution,
type EntityManager,
} from '@flowgram-adapter/free-layout-editor';
import {
type WorkflowDocument,
WorkflowDocumentContainerModule,
} from '@flowgram-adapter/free-layout-editor';
import { createWorkflowVariablePlugins } from '@coze-workflow/variable';
import { WorkflowNodesService } from '@coze-workflow/nodes';
import { StandardNodeType } from '@coze-workflow/base/types';
import { ValidationService } from '@coze-workflow/base/services';
import { WorkflowEncapsulateContainerModule } from '../src/workflow-encapsulate-container-module';
import { EncapsulateValidatorsContainerModule } from '../src/validators';
import {
EncapsulateWorkflowJSONValidator,
type EncapsulateValidateResult,
} from '../src/validate';
import { EncapsulateApiService } from '../src/api';
import { MockValidationService } from './validation-service.mock';
import { MockEncapsulateApiService } from './api-service.mock';
@injectable()
export class MockPlaygroundContext implements PlaygroundContext {
getNodeTemplateInfoByType() {
return {};
}
}
@injectable()
export class MockWorkflowEncapsulateValidator
implements EncapsulateWorkflowJSONValidator
{
validate(_json, _result: EncapsulateValidateResult): void | Promise<void> {
return;
}
}
@injectable()
export class MockWorkflowForm
implements FlowDocumentContribution, EntityManagerContribution
{
registerDocument(document: WorkflowDocument): void {
document.registerNodeDatas(...createNodeEntityDatas());
document.registerFlowNodes({
type: StandardNodeType.SubWorkflow,
formMeta: {
render: () => null,
},
});
}
registerEntityManager(entityManager: EntityManager): void {
const formModelFactory = (entity: FlowNodeEntity) =>
new FormModelV2(entity);
entityManager.registerEntityData(
FlowNodeFormData,
() =>
({
formModelFactory,
}) as any,
);
}
}
// eslint-disable-next-line max-params
const TestModule = new ContainerModule((bind, _unbind, _isBound, rebind) => {
rebind(PlaygroundContext).to(MockPlaygroundContext);
rebind(EncapsulateApiService)
.to(MockEncapsulateApiService)
.inSingletonScope();
bind(ValidationService).to(MockValidationService).inSingletonScope();
bindContributions(bind, MockWorkflowEncapsulateValidator, [
EncapsulateWorkflowJSONValidator,
]);
bind(WorkflowNodesService).toSelf().inSingletonScope();
bindContributions(bind, MockWorkflowForm, [
FlowDocumentContribution,
EntityManagerContribution,
]);
});
export function createContainer() {
const container = PlaygroundMockTools.createContainer([
FlowDocumentContainerModule,
WorkflowDocumentContainerModule,
WorkflowEncapsulateContainerModule,
EncapsulateValidatorsContainerModule,
...createNodeContainerModules(),
TestModule,
]);
const playground = container.get(Playground);
loadPlugins(
[
createFreeHistoryPlugin({ enable: true, limit: 50 }),
...createWorkflowVariablePlugins({}),
],
container,
);
playground.init();
return container;
}

View File

@@ -0,0 +1,17 @@
/*
* 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.
*/
export default {};

View File

@@ -0,0 +1,112 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`encapsulate-nodes-service > should decapsulateLayout 1`] = `
{
"edges": [
{
"sourceNodeID": "100001",
"targetNodeID": "154702",
},
{
"sourceNodeID": "102906",
"targetNodeID": "900001",
},
{
"sourceNodeID": "177547",
"targetNodeID": "154702",
},
{
"sourceNodeID": "154702",
"targetNodeID": "102906",
},
{
"sourceNodeID": "177547",
"targetNodeID": "109408",
},
],
"nodes": [
{
"data": undefined,
"id": "100001",
"meta": {
"position": {
"x": 180,
"y": 26.700000000000017,
},
},
"type": "1",
},
{
"data": undefined,
"id": "900001",
"meta": {
"position": {
"x": 1743.609729023942,
"y": 176.16991217034956,
},
},
"type": "2",
},
{
"data": undefined,
"id": "154702",
"meta": {
"position": {
"x": 987.7405729256998,
"y": 245.06502111375198,
},
},
"type": "3",
},
{
"data": {
"inputs": {
"spaceId": "test_space_id",
"workflowId": "test_workflow_id",
},
},
"id": "102906",
"meta": {
"position": {
"x": 848.7417419605051,
"y": -297.0809682834506,
},
},
"type": "9",
},
{
"data": undefined,
"id": "177547",
"meta": {
"position": {
"x": -125.93331336641754,
"y": 357.10168132837816,
},
},
"type": "3",
},
{
"data": undefined,
"id": "109408",
"meta": {
"position": {
"x": 1141.6692827086551,
"y": 537.1820787709282,
},
},
"type": "3",
},
{
"data": undefined,
"id": "156471",
"meta": {
"position": {
"x": 984.2265002815602,
"y": 712.1169030826604,
},
},
"type": "3",
},
],
}
`;

View File

@@ -0,0 +1,161 @@
/*
* 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 { cloneDeep } from 'lodash-es';
import {
WorkflowDocument,
type WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import {
EncapsulateLinesService,
EncapsulateNodesService,
} from '../../src/encapsulate';
describe('encapsulate-lines-service', () => {
let workflowDocument: WorkflowDocument;
let encapsulateNodesService: EncapsulateNodesService;
let encapsulateLinesService: EncapsulateLinesService;
beforeEach(() => {
const container = createContainer();
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
encapsulateLinesService = container.get<EncapsulateLinesService>(
EncapsulateLinesService,
);
encapsulateNodesService = container.get<EncapsulateNodesService>(
EncapsulateNodesService,
);
});
it('should create empty decapsulate lines', async () => {
await workflowDocument.fromJSON(complexMock);
const { inputLines, outputLines } =
encapsulateLinesService.createDecapsulateLines({
node: workflowDocument.getNode('102906') as WorkflowNodeEntity,
workflowJSON: {
edges: [],
nodes: [],
},
startNodeId: '',
endNodeId: '',
idsMap: new Map(),
});
expect(inputLines).toEqual([]);
expect(outputLines).toEqual([]);
});
// 解封节点有多个输入且解封流程start节点有多个输出不能创建输入连线
it('should not create decapsulate input lines', async () => {
const json = {
...cloneDeep(complexMock),
edges: [
...complexMock.edges,
{
sourceNodeID: '100001',
targetNodeID: '177547',
},
],
};
await workflowDocument.fromJSON(json);
const sourceNode = workflowDocument.getNode('154702') as WorkflowNodeEntity;
const { idsMap, startNode, endNode } =
await encapsulateNodesService.createDecapsulateNodes(
sourceNode,
json.nodes,
);
const { inputLines } = encapsulateLinesService.createDecapsulateLines({
node: sourceNode,
workflowJSON: {
nodes: [],
edges: json.edges,
},
startNodeId: startNode.id,
endNodeId: endNode.id,
idsMap,
});
expect(inputLines).toEqual([]);
});
// 解封节点有多个输出且解封流程end节点有多个输入不能创建输出连线
it('should not create decapsulate output lines', async () => {
const json = {
...cloneDeep(complexMock),
edges: [
...complexMock.edges,
{
sourceNodeID: '109408',
targetNodeID: '900001',
},
],
};
await workflowDocument.fromJSON(json);
const sourceNode = workflowDocument.getNode('177547') as WorkflowNodeEntity;
const { idsMap, startNode, endNode } =
await encapsulateNodesService.createDecapsulateNodes(
sourceNode,
json.nodes,
);
const { outputLines } = encapsulateLinesService.createDecapsulateLines({
node: sourceNode,
workflowJSON: {
nodes: [],
edges: json.edges,
},
startNodeId: startNode.id,
endNodeId: endNode.id,
idsMap,
});
expect(outputLines).toEqual([]);
});
it('should create decapsulate lines', async () => {
await workflowDocument.fromJSON(cloneDeep(complexMock));
const sourceNode = workflowDocument.getNode('102906') as WorkflowNodeEntity;
const { idsMap, startNode, endNode } =
await encapsulateNodesService.createDecapsulateNodes(
sourceNode,
complexMock.nodes,
);
const { inputLines, outputLines, internalLines } =
encapsulateLinesService.createDecapsulateLines({
node: sourceNode,
workflowJSON: {
nodes: [],
edges: complexMock.edges,
},
startNodeId: startNode.id,
endNodeId: endNode.id,
idsMap,
});
expect(internalLines.length).toEqual(3);
expect(inputLines.length).toEqual(1);
expect(outputLines.length).toEqual(1);
});
});

View File

@@ -0,0 +1,126 @@
/*
* 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, beforeEach, it, expect } from 'vitest';
import { FlowNodeTransformData } from '@flowgram-adapter/free-layout-editor';
import {
WorkflowDocument,
type WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import { EncapsulateNodesService } from '../../src/encapsulate';
describe('encapsulate-nodes-service', () => {
let workflowDocument: WorkflowDocument;
let encapsulateNodesService: EncapsulateNodesService;
beforeEach(() => {
const container = createContainer();
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
encapsulateNodesService = container.get<EncapsulateNodesService>(
EncapsulateNodesService,
);
});
it('should getNodesMiddlePoint', async () => {
await workflowDocument.fromJSON(complexMock);
const nodes = ['109408', '156471'].map(id =>
workflowDocument.getNode(id),
) as WorkflowNodeEntity[];
const point = encapsulateNodesService.getNodesMiddlePoint(nodes);
expect(point).toEqual({
x: 993.4484760125105,
y: 489.11299357749374,
});
});
it('should getNodesMiddlePoint by json', () => {
const point = encapsulateNodesService.getNodesMiddlePoint(
complexMock.nodes,
);
expect(point).toEqual({
x: 808.8382078287623,
y: 207.51796739960494,
});
});
it('should createDecapsulateNode', async () => {
await workflowDocument.fromJSON(complexMock);
const json = {
id: '1',
type: 'test',
meta: {
position: {
x: 0,
y: 0,
},
},
blocks: [
{
id: '2',
type: 'test',
meta: {
position: {
x: 5,
y: 6,
},
},
},
],
};
const idsMap = new Map<string, string>();
const node = await encapsulateNodesService.createDecapsulateNode(
json,
{
x: 170,
y: 16.700000000000017,
},
idsMap,
);
expect(node.id).not.toBe('1');
expect(node.flowNodeType).toBe(json.type);
const transformData = node.getData(FlowNodeTransformData);
expect(transformData.position).toEqual({ x: 170, y: 16.700000000000017 });
const child = node.collapsedChildren[0];
expect(child.id).not.toBe('2');
expect(child.flowNodeType).toBe(json.blocks[0].type);
const childTransformData = child.getData(FlowNodeTransformData);
expect(childTransformData.position).toEqual({
x: 175,
y: 22.700000000000017,
});
expect(idsMap.get('1')).toBe(node.id);
expect(idsMap.get('2')).toBe(child.id);
});
it('should decapsulateLayout', async () => {
await workflowDocument.fromJSON(complexMock);
await encapsulateNodesService.decapsulateLayout(
workflowDocument.getNode('100001') as WorkflowNodeEntity,
[complexMock.nodes[1], complexMock.nodes[2], complexMock.nodes[3]],
);
const json = await workflowDocument.toJSON();
expect(json).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,84 @@
/*
* 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, beforeEach } from 'vitest';
import {
WorkflowDocument,
WorkflowSelectService,
type WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import {
EncapsulateService,
type EncapsulateSuccessResult,
} from '../../src/encapsulate';
describe('encapsulate-service', () => {
let workflowDocument: WorkflowDocument;
let encapsulateService: EncapsulateService;
let workflowSelectService: WorkflowSelectService;
beforeEach(async () => {
const container = createContainer();
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
workflowSelectService = container.get<WorkflowSelectService>(
WorkflowSelectService,
);
encapsulateService = container.get<EncapsulateService>(EncapsulateService);
await workflowDocument.fromJSON(complexMock);
});
it('should can encapsulate', () => {
expect(encapsulateService.canEncapsulate()).toBeTruthy();
});
it('should can decapsulate', () => {
const node = workflowDocument.getNode('102906') as WorkflowNodeEntity;
expect(encapsulateService.canDecapsulate(node)).toBeTruthy();
});
it('should encapsulate nodes', async () => {
['102906', '154702'].forEach(id =>
workflowSelectService.toggleSelect(
workflowDocument.getNode(id) as WorkflowNodeEntity,
),
);
const res =
(await encapsulateService.encapsulate()) as EncapsulateSuccessResult;
if (!res.success) {
console.log(res);
}
expect(res.success).toBeTruthy();
expect(res.subFlowNode).toBeDefined();
expect(res.inputLines.length).toEqual(2);
res.inputLines.forEach(line => {
expect(line.to).toBe(res.subFlowNode);
});
expect(res.outputLines.length).toEqual(1);
res.outputLines.forEach(line => {
expect(line.from).toBe(res.subFlowNode);
});
expect(workflowSelectService.selectedNodes.length).toEqual(1);
expect(workflowSelectService.selectedNodes[0]).toBe(res.subFlowNode);
});
});

View File

@@ -0,0 +1,98 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`encapsulate-generate-service > should generate sub workflow node 1`] = `
{
"data": {
"inputs": {
"spaceId": "1",
"workflowId": "1",
"workflowVersion": "v0.0.1",
},
"nodeMeta": {
"description": "test",
"icon": undefined,
"isImageflow": false,
"title": "test",
},
},
}
`;
exports[`encapsulate-generate-service > should generate workflow json 1`] = `
{
"edges": [
{
"sourceNodeID": "154702",
"targetNodeID": "102906",
},
{
"sourceNodeID": "100001",
"targetNodeID": "154702",
},
{
"sourceNodeID": "102906",
"targetNodeID": "900001",
},
],
"nodes": [
{
"data": {
"nodeMeta": {},
"outputs": [],
},
"id": "100001",
"meta": {
"position": {
"x": 0,
"y": 0,
},
},
"type": "1",
},
{
"data": {
"inputs": {
"inputParameters": [],
"terminatePlan": "returnVariables",
},
"nodeMeta": {},
},
"id": "900001",
"meta": {
"position": {
"x": 1000,
"y": 0,
},
},
"type": "2",
},
{
"data": {},
"id": "154702",
"meta": {
"position": {
"x": 918.2411574431025,
"y": 109.52852376445134,
},
},
"type": "3",
},
{
"data": {
"inputs": {
"spaceId": "test_space_id",
"workflowId": "test_workflow_id",
},
},
"id": "102906",
"meta": {
"position": {
"x": 779.2423264779079,
"y": -161.54447093414998,
},
},
"type": "9",
},
],
}
`;

View File

@@ -0,0 +1,62 @@
/*
* 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, beforeEach, it, expect } from 'vitest';
import {
WorkflowDocument,
type WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import {
EncapsulateGenerateService,
type GenerateSubWorkflowNodeOptions,
} from '../../src/generate';
describe('encapsulate-generate-service', () => {
let encapsulateGenerateService: EncapsulateGenerateService;
let workflowDocument: WorkflowDocument;
beforeEach(async () => {
const container = createContainer();
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
encapsulateGenerateService = container.get<EncapsulateGenerateService>(
EncapsulateGenerateService,
);
await workflowDocument.fromJSON(complexMock);
});
it('should generate workflow json', async () => {
const nodes = ['102906', '154702'].map(id =>
workflowDocument.getNode(id),
) as WorkflowNodeEntity[];
const res = await encapsulateGenerateService.generateWorkflowJSON(nodes);
expect(res).toMatchSnapshot();
});
it('should generate sub workflow node', async () => {
const options: GenerateSubWorkflowNodeOptions = {
workflowId: '1',
name: 'test',
desc: 'test',
spaceId: '1',
};
const res =
await encapsulateGenerateService.generateSubWorkflowNode(options);
expect(res).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,39 @@
/*
* 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 'reflect-metadata';
vi.mock('@coze-arch/i18n', () => ({
I18n: {
t: vi.fn(),
},
}));
vi.mock('@coze-arch/bot-flags', () => ({
getFlags: () => ({
'bot.automation.encapsulate': true,
}),
}));
vi.mock('@coze-arch/coze-design', () => ({
Typography: {
Text: vi.fn(),
},
withField: vi.fn(),
}));
vi.mock('@coze-workflow/components', () => ({}));
vi.stubGlobal('IS_DEV_MODE', true);
vi.stubGlobal('IS_OVERSEA', false);
vi.stubGlobal('IS_BOE', false);
vi.stubGlobal('REGION', 'cn');

View File

@@ -0,0 +1,51 @@
/*
* 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 { beforeEach, describe, expect, it } from 'vitest';
import {
WorkflowDocument,
type WorkflowNodeEntity,
type WorkflowNodeJSON,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import { getNodePoint } from '../../src/utils';
describe('get-node-point', () => {
let workflowDocument: WorkflowDocument;
beforeEach(() => {
const container = createContainer();
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
});
it('should get empty node point', () => {
const node: WorkflowNodeJSON = {
type: 'test',
id: '1',
};
const point = getNodePoint(node);
expect(point).toEqual({ x: 0, y: 0 });
});
it('should get node point by entity', async () => {
await workflowDocument.fromJSON(complexMock);
const node = workflowDocument.getNode('900001') as WorkflowNodeEntity;
const point = getNodePoint(node);
expect(point).toEqual({ x: 1674.1103135413448, y: 40.63341482104891 });
});
});

View File

@@ -0,0 +1,51 @@
/*
* 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, beforeEach, expect } from 'vitest';
import {
WorkflowDocument,
type WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import { getSubWorkflowInfo } from '../../src/utils';
describe('get-sub-workflow-info', () => {
let workflowDocument: WorkflowDocument;
beforeEach(() => {
const container = createContainer();
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
});
it('should get sub workflow info', async () => {
await workflowDocument.fromJSON(complexMock);
expect(
getSubWorkflowInfo(
workflowDocument.getNode('102906') as WorkflowNodeEntity,
),
).toEqual({
spaceId: 'test_space_id',
workflowId: 'test_workflow_id',
});
expect(
getSubWorkflowInfo(
workflowDocument.getNode('154702') as WorkflowNodeEntity,
),
).toBeUndefined();
});
});

View File

@@ -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, expect, it } from 'vitest';
import { type WorkflowNodeJSON } from '@flowgram-adapter/free-layout-editor';
import { setNodePosition } from '../../src/utils';
describe('set-node-position', () => {
it('should set node position', () => {
const node: WorkflowNodeJSON = {
type: 'test',
id: '1',
};
setNodePosition(node, {
x: 10,
y: 10,
});
expect(node.meta?.position).toEqual({ x: 10, y: 10 });
});
});

View File

@@ -0,0 +1,24 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`EncapsulateValidateResult > should add different source error 1`] = `
[
{
"code": "1001",
"message": "test",
},
{
"code": "1001",
"message": "test",
"source": "1",
},
]
`;
exports[`EncapsulateValidateResult > should get errors 1`] = `
[
{
"code": "1001",
"message": "test",
},
]
`;

View File

@@ -0,0 +1,53 @@
/*
* 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, beforeEach, expect } from 'vitest';
import { StandardNodeType } from '@coze-workflow/base/types';
import { createContainer } from '../create-container';
import { EncapsulateValidateManager } from '../../src/validate';
describe('encapsulate-validate-manager', () => {
let encapsulateValidateManager: EncapsulateValidateManager;
beforeEach(() => {
const container = createContainer();
encapsulateValidateManager = container.get<EncapsulateValidateManager>(
EncapsulateValidateManager,
);
});
it('should register validator', () => {
const validators = encapsulateValidateManager.getNodeValidators();
expect(validators.length > 0).toBeTruthy();
});
it('should register nodes validators', () => {
const validators = encapsulateValidateManager.getNodesValidators();
expect(validators.length > 0).toBeTruthy();
});
it('should get validators by type', () => {
const validators = encapsulateValidateManager.getNodeValidatorsByType(
StandardNodeType.Start,
);
expect(validators.length > 0).toBeTruthy();
});
it('should register workflow json validators', () => {
const validators = encapsulateValidateManager.getWorkflowJSONValidators();
expect(validators.length > 0).toBeTruthy();
});
});

View File

@@ -0,0 +1,89 @@
/*
* 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 { createContainer } from '../create-container';
import {
EncapsulateValidateResultFactory,
type EncapsulateValidateResult,
EncapsulateValidateErrorCode,
} from '../../src/validate';
describe('EncapsulateValidateResult', () => {
let encapsulateValidateResult: EncapsulateValidateResult;
let encapsulateValidateResultFactory: EncapsulateValidateResultFactory;
beforeEach(() => {
const container = createContainer();
encapsulateValidateResultFactory =
container.get<EncapsulateValidateResultFactory>(
EncapsulateValidateResultFactory,
);
encapsulateValidateResult = encapsulateValidateResultFactory();
});
it('should be defined', () => {
expect(encapsulateValidateResult).toBeDefined();
});
it('should be different instance', () => {
expect(encapsulateValidateResultFactory()).not.toBe(
encapsulateValidateResult,
);
});
it('should add error', () => {
encapsulateValidateResult.addError({
code: EncapsulateValidateErrorCode.NO_START_END,
message: 'test',
});
expect(encapsulateValidateResult.hasError()).toBeTruthy();
});
it('should add different source error', () => {
encapsulateValidateResult.addError({
code: EncapsulateValidateErrorCode.NO_START_END,
message: 'test',
});
encapsulateValidateResult.addError({
code: EncapsulateValidateErrorCode.NO_START_END,
message: 'test',
source: '1',
});
expect(encapsulateValidateResult.getErrors()).toMatchSnapshot();
});
it('should get errors', () => {
encapsulateValidateResult.addError({
code: EncapsulateValidateErrorCode.NO_START_END,
message: 'test',
});
expect(encapsulateValidateResult.getErrors()).toMatchSnapshot();
});
it('should has error code', () => {
encapsulateValidateResult.addError({
code: EncapsulateValidateErrorCode.NO_START_END,
message: 'test',
});
expect(
encapsulateValidateResult.hasErrorCode(
EncapsulateValidateErrorCode.NO_START_END,
),
).toBeTruthy();
});
});

View File

@@ -0,0 +1,24 @@
/*
* 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 { injectable } from 'inversify';
@injectable()
export class MockValidationService {
validateNode(node) {
return { hasError: false } as any;
}
}

View File

@@ -0,0 +1,59 @@
/*
* 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 {
WorkflowDocument,
type WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import {
EncapsulateValidateErrorCode,
EncapsulateValidateService,
} from '../../src/validate';
describe('input-lines', () => {
let encapsulateValidateService: EncapsulateValidateService;
let workflowDocument: WorkflowDocument;
beforeEach(async () => {
const container = createContainer();
encapsulateValidateService = container.get<EncapsulateValidateService>(
EncapsulateValidateService,
);
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
await workflowDocument.fromJSON(complexMock);
});
it('should validate without error', async () => {
const nodes = ['102906', '154702'].map(id =>
workflowDocument.getNode(id),
) as WorkflowNodeEntity[];
const res = await encapsulateValidateService.validate(nodes);
expect(res.hasError()).toBeFalsy();
});
it('should validate two input ports return error', async () => {
const nodes = ['109408', '154702'].map(id =>
workflowDocument.getNode(id),
) as WorkflowNodeEntity[];
const res = await encapsulateValidateService.validate(nodes);
expect(
res.hasErrorCode(EncapsulateValidateErrorCode.ENCAPSULATE_LINES),
).toBeTruthy();
});
});

View File

@@ -0,0 +1,50 @@
/*
* 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 { StandardNodeType } from '@coze-workflow/base';
import { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
import { baseMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import {
EncapsulateValidateErrorCode,
EncapsulateValidateService,
} from '../../src/validate';
describe('loop-nodes', () => {
let encapsulateValidateService: EncapsulateValidateService;
let workflowDocument: WorkflowDocument;
beforeEach(() => {
const container = createContainer();
encapsulateValidateService = container.get<EncapsulateValidateService>(
EncapsulateValidateService,
);
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
workflowDocument.fromJSON(baseMock);
});
it('should validate loop nodes error', async () => {
const breakNode = await workflowDocument.createWorkflowNodeByType(
StandardNodeType.Break,
);
const res = await encapsulateValidateService.validate([breakNode]);
expect(
res.hasErrorCode(EncapsulateValidateErrorCode.INVALID_LOOP_NODES),
).toBeTruthy();
});
});

View File

@@ -0,0 +1,67 @@
/*
* 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 {
WorkflowDocument,
type WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import {
EncapsulateValidateErrorCode,
EncapsulateValidateService,
} from '../../src/validate';
describe('output-lines', () => {
let encapsulateValidateService: EncapsulateValidateService;
let workflowDocument: WorkflowDocument;
beforeEach(async () => {
const container = createContainer();
encapsulateValidateService = container.get<EncapsulateValidateService>(
EncapsulateValidateService,
);
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
await workflowDocument.fromJSON(complexMock);
});
it('should validate two output ports return error', async () => {
await workflowDocument.createWorkflowNode({
id: 'output1',
type: 'test',
});
await workflowDocument.createWorkflowNode({
id: 'output2',
type: 'test',
});
workflowDocument.linesManager.createLine({
from: '154702',
to: 'output1',
});
workflowDocument.linesManager.createLine({
from: 'output1',
to: 'output2',
});
const nodes = ['154702', '102906'].map(id =>
workflowDocument.getNode(id),
) as WorkflowNodeEntity[];
const res = await encapsulateValidateService.validate(nodes);
expect(
res.hasErrorCode(EncapsulateValidateErrorCode.ENCAPSULATE_LINES),
).toBeTruthy();
});
});

View File

@@ -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, beforeEach } from 'vitest';
import {
WorkflowDocument,
type WorkflowNodeEntity,
} from '@flowgram-adapter/free-layout-editor';
import { complexMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import {
EncapsulateValidateErrorCode,
EncapsulateValidateService,
} from '../../src/validate';
describe('ports', () => {
let encapsulateValidateService: EncapsulateValidateService;
let workflowDocument: WorkflowDocument;
beforeEach(async () => {
const container = createContainer();
encapsulateValidateService = container.get<EncapsulateValidateService>(
EncapsulateValidateService,
);
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
await workflowDocument.fromJSON(complexMock);
});
it('should validate no input ports return error', async () => {
const nodes = ['100001', '177547'].map(id =>
workflowDocument.getNode(id),
) as WorkflowNodeEntity[];
const res = await encapsulateValidateService.validate(nodes);
expect(
res.hasErrorCode(EncapsulateValidateErrorCode.INVALID_PORTS),
).toBeTruthy();
});
it('should validate no output ports return error', async () => {
const nodes = ['109408', '156471'].map(id =>
workflowDocument.getNode(id),
) as WorkflowNodeEntity[];
const res = await encapsulateValidateService.validate(nodes);
expect(
res.hasErrorCode(EncapsulateValidateErrorCode.INVALID_PORTS),
).toBeTruthy();
});
});

View File

@@ -0,0 +1,47 @@
/*
* 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 { WorkflowDocument } from '@flowgram-adapter/free-layout-editor';
import { baseMock } from '../workflow.mock';
import { createContainer } from '../create-container';
import {
EncapsulateValidateErrorCode,
EncapsulateValidateService,
} from '../../src/validate';
describe('start-end', () => {
let encapsulateValidateService: EncapsulateValidateService;
let workflowDocument: WorkflowDocument;
beforeEach(async () => {
const container = createContainer();
encapsulateValidateService = container.get<EncapsulateValidateService>(
EncapsulateValidateService,
);
workflowDocument = container.get<WorkflowDocument>(WorkflowDocument);
await workflowDocument.fromJSON(baseMock);
});
it('should validate return no-start-end error', async () => {
const startNode = workflowDocument.getNode('1')!;
const res = await encapsulateValidateService.validate([startNode]);
expect(
res.hasErrorCode(EncapsulateValidateErrorCode.NO_START_END),
).toBeTruthy();
});
});

View File

@@ -0,0 +1,176 @@
/*
* 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 { StandardNodeType } from '@coze-workflow/base/types';
import { type WorkflowJSON } from '@flowgram-adapter/free-layout-editor';
export const baseMock = {
nodes: [
{
id: '1',
type: StandardNodeType.Start,
},
{
id: '2',
type: StandardNodeType.End,
},
],
edges: [],
};
/**
* start(100001) -> 154702 -> 102906 -> end(900001)
* 177547 -> 154702
* 177547 -> 109408
*/
export const complexMock: WorkflowJSON = {
nodes: [
{
id: '100001',
type: '1',
meta: {
position: {
x: 180,
y: 26.700000000000017,
},
},
},
{
id: '900001',
type: '2',
meta: {
position: {
x: 1674.1103135413448,
y: 40.63341482104891,
},
},
},
{
id: '154702',
type: '3',
meta: {
position: {
x: 918.2411574431025,
y: 109.52852376445134,
},
},
},
{
id: '102906',
type: StandardNodeType.SubWorkflow,
data: {
inputs: {
spaceId: 'test_space_id',
workflowId: 'test_workflow_id',
},
},
meta: {
position: {
x: 779.2423264779079,
y: -161.54447093414998,
},
},
},
{
id: '177547',
type: '3',
meta: {
position: {
x: -56.433897883820265,
y: 221.56518397907752,
},
},
},
{
id: '109408',
type: '3',
meta: {
position: {
x: 1072.169867226058,
y: 401.6455814216276,
},
},
},
{
id: '156471',
type: '3',
meta: {
position: {
x: 914.727084798963,
y: 576.5804057333598,
},
},
},
],
edges: [
{
sourceNodeID: '102906',
targetNodeID: '900001',
},
{
sourceNodeID: '100001',
targetNodeID: '154702',
},
{
sourceNodeID: '177547',
targetNodeID: '154702',
},
{
sourceNodeID: '154702',
targetNodeID: '102906',
},
{
sourceNodeID: '177547',
targetNodeID: '109408',
},
],
};
export const loopJSON: WorkflowJSON = {
nodes: [
...complexMock.nodes,
{
id: 'loop_0',
type: 'loop',
meta: {
position: { x: 1200, y: 0 },
},
blocks: [
{
id: 'break_0',
type: 'break',
meta: {
position: { x: 0, y: 0 },
},
},
{
id: 'variable_0',
type: 'variable',
meta: {
position: { x: 400, y: 0 },
},
},
],
edges: [
{
sourceNodeID: 'break_0',
targetNodeID: 'variable_0',
},
],
},
],
edges: [...complexMock.edges],
};