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