feat: M28 增加工作流汇聚可视化配置
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import type { Edge, Node } from '@xyflow/svelte';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
analyzeJoinMode,
|
||||
getJoinModeBadge,
|
||||
parseJoinMode,
|
||||
} from './joinMode';
|
||||
|
||||
const node = (
|
||||
id: string,
|
||||
type = 'codeNode',
|
||||
data: Record<string, unknown> = {},
|
||||
parentId?: string,
|
||||
) => ({ id, type, data, parentId, position: { x: 0, y: 0 } }) as Node;
|
||||
|
||||
const edge = (
|
||||
id: string,
|
||||
source: string,
|
||||
target: string,
|
||||
condition = '',
|
||||
) => ({
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
data: condition ? { condition } : {},
|
||||
}) as Edge;
|
||||
|
||||
describe('join mode graph analysis', () => {
|
||||
it('defaults to any and only exposes a multi-inbound setting', () => {
|
||||
const nodes = [node('start', 'startNode'), node('join')];
|
||||
|
||||
expect(analyzeJoinMode(nodes, [], 'join')).toMatchObject({
|
||||
incomingCount: 0,
|
||||
mode: 'any',
|
||||
allAllowed: true,
|
||||
});
|
||||
expect(analyzeJoinMode(
|
||||
nodes,
|
||||
[edge('e1', 'start', 'join')],
|
||||
'join',
|
||||
).incomingCount).toBe(1);
|
||||
});
|
||||
|
||||
it('allows all when every parallel source is guaranteed', () => {
|
||||
const nodes = [
|
||||
node('start', 'startNode'),
|
||||
node('a'),
|
||||
node('b'),
|
||||
node('join', 'codeNode', { joinMode: 'all' }),
|
||||
];
|
||||
const edges = [
|
||||
edge('start-a', 'start', 'a'),
|
||||
edge('start-b', 'start', 'b'),
|
||||
edge('a-join', 'a', 'join'),
|
||||
edge('b-join', 'b', 'join'),
|
||||
];
|
||||
|
||||
expect(analyzeJoinMode(nodes, edges, 'join')).toMatchObject({
|
||||
incomingCount: 2,
|
||||
mode: 'all',
|
||||
invalidMode: false,
|
||||
allAllowed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('blocks conditional and custom-conditioned upstream paths', () => {
|
||||
const conditionalNodes = [
|
||||
node('start', 'startNode'),
|
||||
node('a'),
|
||||
node('b'),
|
||||
node('join'),
|
||||
];
|
||||
const conditionalEdges = [
|
||||
edge('start-a', 'start', 'a', 'matched === true'),
|
||||
edge('start-b', 'start', 'b'),
|
||||
edge('a-join', 'a', 'join'),
|
||||
edge('b-join', 'b', 'join'),
|
||||
];
|
||||
|
||||
const conditional = analyzeJoinMode(
|
||||
conditionalNodes,
|
||||
conditionalEdges,
|
||||
'join',
|
||||
);
|
||||
expect(conditional.allAllowed).toBe(false);
|
||||
expect(conditional.allDisabledReason).toContain('无法证明必达');
|
||||
|
||||
const directConditional = analyzeJoinMode(
|
||||
conditionalNodes,
|
||||
[
|
||||
edge('start-a', 'start', 'a'),
|
||||
edge('start-b', 'start', 'b'),
|
||||
edge('a-join', 'a', 'join', 'matched === true'),
|
||||
edge('b-join', 'b', 'join'),
|
||||
],
|
||||
'join',
|
||||
);
|
||||
expect(directConditional.allAllowed).toBe(false);
|
||||
expect(directConditional.allDisabledReason).toContain('直接入边');
|
||||
|
||||
const customCondition = analyzeJoinMode(
|
||||
[
|
||||
node('start', 'startNode'),
|
||||
node('a', 'codeNode', { condition: 'score > 0' }),
|
||||
node('b'),
|
||||
node('join'),
|
||||
],
|
||||
[
|
||||
edge('start-a', 'start', 'a'),
|
||||
edge('start-b', 'start', 'b'),
|
||||
edge('a-join', 'a', 'join'),
|
||||
edge('b-join', 'b', 'join'),
|
||||
],
|
||||
'join',
|
||||
);
|
||||
expect(customCondition.allAllowed).toBe(false);
|
||||
expect(customCondition.allDisabledReason).toContain('高级执行条件');
|
||||
});
|
||||
|
||||
it('blocks loop children without silently rewriting existing all', () => {
|
||||
const analysis = analyzeJoinMode(
|
||||
[
|
||||
node('loop', 'loopNode'),
|
||||
node('a', 'codeNode', {}, 'loop'),
|
||||
node('b', 'codeNode', {}, 'loop'),
|
||||
node('join', 'codeNode', { joinMode: 'all' }, 'loop'),
|
||||
],
|
||||
[
|
||||
edge('a-join', 'a', 'join'),
|
||||
edge('b-join', 'b', 'join'),
|
||||
],
|
||||
'join',
|
||||
);
|
||||
|
||||
expect(analysis.mode).toBe('all');
|
||||
expect(analysis.allAllowed).toBe(false);
|
||||
expect(analysis.allDisabledReason).toContain('显式循环子图');
|
||||
});
|
||||
|
||||
it('keeps invalid raw values visible and exposes the all badge', () => {
|
||||
expect(parseJoinMode('unexpected')).toBeNull();
|
||||
expect(parseJoinMode('')).toBeNull();
|
||||
expect(parseJoinMode(null)).toBeNull();
|
||||
expect(parseJoinMode(undefined)).toBe('any');
|
||||
expect(getJoinModeBadge('ALL')).toBe('等待全部');
|
||||
expect(getJoinModeBadge('any')).toBe('');
|
||||
|
||||
const analysis = analyzeJoinMode(
|
||||
[node('join', 'codeNode', { joinMode: 'unexpected' })],
|
||||
[],
|
||||
'join',
|
||||
);
|
||||
expect(analysis.invalidMode).toBe(true);
|
||||
expect(analysis.mode).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user