Files
EasyFlow/easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/joinMode.test.ts

158 lines
4.2 KiB
TypeScript

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();
});
});