This commit is contained in:
301
web/__tests__/workflow-parallel-limit.test.tsx
Normal file
301
web/__tests__/workflow-parallel-limit.test.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* MAX_PARALLEL_LIMIT Configuration Bug Test
|
||||
*
|
||||
* This test reproduces and verifies the fix for issue #23083:
|
||||
* MAX_PARALLEL_LIMIT environment variable does not take effect in iteration panel
|
||||
*/
|
||||
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
|
||||
// Mock environment variables before importing constants
|
||||
const originalEnv = process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
|
||||
|
||||
// Test with different environment values
|
||||
function setupEnvironment(value?: string) {
|
||||
if (value)
|
||||
process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT = value
|
||||
else
|
||||
delete process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
|
||||
|
||||
// Clear module cache to force re-evaluation
|
||||
jest.resetModules()
|
||||
}
|
||||
|
||||
function restoreEnvironment() {
|
||||
if (originalEnv)
|
||||
process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT = originalEnv
|
||||
else
|
||||
delete process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT
|
||||
|
||||
jest.resetModules()
|
||||
}
|
||||
|
||||
// Mock i18next with proper implementation
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
if (key.includes('MaxParallelismTitle')) return 'Max Parallelism'
|
||||
if (key.includes('MaxParallelismDesc')) return 'Maximum number of parallel executions'
|
||||
if (key.includes('parallelMode')) return 'Parallel Mode'
|
||||
if (key.includes('parallelPanelDesc')) return 'Enable parallel execution'
|
||||
if (key.includes('errorResponseMethod')) return 'Error Response Method'
|
||||
return key
|
||||
},
|
||||
}),
|
||||
initReactI18next: {
|
||||
type: '3rdParty',
|
||||
init: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock i18next module completely to prevent initialization issues
|
||||
jest.mock('i18next', () => ({
|
||||
use: jest.fn().mockReturnThis(),
|
||||
init: jest.fn().mockReturnThis(),
|
||||
t: jest.fn(key => key),
|
||||
isInitialized: true,
|
||||
}))
|
||||
|
||||
// Mock the useConfig hook
|
||||
jest.mock('@/app/components/workflow/nodes/iteration/use-config', () => ({
|
||||
__esModule: true,
|
||||
default: () => ({
|
||||
inputs: {
|
||||
is_parallel: true,
|
||||
parallel_nums: 5,
|
||||
error_handle_mode: 'terminated',
|
||||
},
|
||||
changeParallel: jest.fn(),
|
||||
changeParallelNums: jest.fn(),
|
||||
changeErrorHandleMode: jest.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock other components
|
||||
jest.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => {
|
||||
return function MockVarReferencePicker() {
|
||||
return <div data-testid="var-reference-picker">VarReferencePicker</div>
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/workflow/nodes/_base/components/split', () => {
|
||||
return function MockSplit() {
|
||||
return <div data-testid="split">Split</div>
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/workflow/nodes/_base/components/field', () => {
|
||||
return function MockField({ title, children }: { title: string, children: React.ReactNode }) {
|
||||
return (
|
||||
<div data-testid="field">
|
||||
<label>{title}</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/base/switch', () => {
|
||||
return function MockSwitch({ defaultValue }: { defaultValue: boolean }) {
|
||||
return <input type="checkbox" defaultChecked={defaultValue} data-testid="switch" />
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('@/app/components/base/select', () => {
|
||||
return function MockSelect() {
|
||||
return <select data-testid="select">Select</select>
|
||||
}
|
||||
})
|
||||
|
||||
// Use defaultValue to avoid controlled input warnings
|
||||
jest.mock('@/app/components/base/slider', () => {
|
||||
return function MockSlider({ value, max, min }: { value: number, max: number, min: number }) {
|
||||
return (
|
||||
<input
|
||||
type="range"
|
||||
defaultValue={value}
|
||||
max={max}
|
||||
min={min}
|
||||
data-testid="slider"
|
||||
data-max={max}
|
||||
data-min={min}
|
||||
readOnly
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Use defaultValue to avoid controlled input warnings
|
||||
jest.mock('@/app/components/base/input', () => {
|
||||
return function MockInput({ type, max, min, value }: { type: string, max: number, min: number, value: number }) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
defaultValue={value}
|
||||
max={max}
|
||||
min={min}
|
||||
data-testid="number-input"
|
||||
data-max={max}
|
||||
data-min={min}
|
||||
readOnly
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('MAX_PARALLEL_LIMIT Configuration Bug', () => {
|
||||
const mockNodeData = {
|
||||
id: 'test-iteration-node',
|
||||
type: 'iteration' as const,
|
||||
data: {
|
||||
title: 'Test Iteration',
|
||||
desc: 'Test iteration node',
|
||||
iterator_selector: ['test'],
|
||||
output_selector: ['output'],
|
||||
is_parallel: true,
|
||||
parallel_nums: 5,
|
||||
error_handle_mode: 'terminated' as const,
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnvironment()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
restoreEnvironment()
|
||||
})
|
||||
|
||||
describe('Environment Variable Parsing', () => {
|
||||
it('should parse MAX_PARALLEL_LIMIT from NEXT_PUBLIC_MAX_PARALLEL_LIMIT environment variable', () => {
|
||||
setupEnvironment('25')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(25)
|
||||
})
|
||||
|
||||
it('should fallback to default when environment variable is not set', () => {
|
||||
setupEnvironment() // No environment variable
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10)
|
||||
})
|
||||
|
||||
it('should handle invalid environment variable values', () => {
|
||||
setupEnvironment('invalid')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
|
||||
// Should fall back to default when parsing fails
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10)
|
||||
})
|
||||
|
||||
it('should handle empty environment variable', () => {
|
||||
setupEnvironment('')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
|
||||
// Should fall back to default when empty
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10)
|
||||
})
|
||||
|
||||
// Edge cases for boundary values
|
||||
it('should clamp MAX_PARALLEL_LIMIT to MIN when env is 0 or negative', () => {
|
||||
setupEnvironment('0')
|
||||
let { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10) // Falls back to default
|
||||
|
||||
setupEnvironment('-5')
|
||||
;({ MAX_PARALLEL_LIMIT } = require('@/config'))
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(10) // Falls back to default
|
||||
})
|
||||
|
||||
it('should handle float numbers by parseInt behavior', () => {
|
||||
setupEnvironment('12.7')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
// parseInt truncates to integer
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(12)
|
||||
})
|
||||
})
|
||||
|
||||
describe('UI Component Integration (Main Fix Verification)', () => {
|
||||
it('should render iteration panel with environment-configured max value', () => {
|
||||
// Set environment variable to a different value
|
||||
setupEnvironment('30')
|
||||
|
||||
// Import Panel after setting environment
|
||||
const Panel = require('@/app/components/workflow/nodes/iteration/panel').default
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
|
||||
render(
|
||||
<Panel
|
||||
id="test-node"
|
||||
data={mockNodeData.data}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Behavior-focused assertion: UI max should equal MAX_PARALLEL_LIMIT
|
||||
const numberInput = screen.getByTestId('number-input')
|
||||
expect(numberInput).toHaveAttribute('data-max', String(MAX_PARALLEL_LIMIT))
|
||||
|
||||
const slider = screen.getByTestId('slider')
|
||||
expect(slider).toHaveAttribute('data-max', String(MAX_PARALLEL_LIMIT))
|
||||
|
||||
// Verify the actual values
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(30)
|
||||
expect(numberInput.getAttribute('data-max')).toBe('30')
|
||||
expect(slider.getAttribute('data-max')).toBe('30')
|
||||
})
|
||||
|
||||
it('should maintain UI consistency with different environment values', () => {
|
||||
setupEnvironment('15')
|
||||
const Panel = require('@/app/components/workflow/nodes/iteration/panel').default
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
|
||||
render(
|
||||
<Panel
|
||||
id="test-node"
|
||||
data={mockNodeData.data}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Both input and slider should use the same max value from MAX_PARALLEL_LIMIT
|
||||
const numberInput = screen.getByTestId('number-input')
|
||||
const slider = screen.getByTestId('slider')
|
||||
|
||||
expect(numberInput.getAttribute('data-max')).toBe(slider.getAttribute('data-max'))
|
||||
expect(numberInput.getAttribute('data-max')).toBe(String(MAX_PARALLEL_LIMIT))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Legacy Constant Verification (For Transition Period)', () => {
|
||||
// Marked as transition/deprecation tests
|
||||
it('should maintain MAX_ITERATION_PARALLEL_NUM for backward compatibility', () => {
|
||||
const { MAX_ITERATION_PARALLEL_NUM } = require('@/app/components/workflow/constants')
|
||||
expect(typeof MAX_ITERATION_PARALLEL_NUM).toBe('number')
|
||||
expect(MAX_ITERATION_PARALLEL_NUM).toBe(10) // Hardcoded legacy value
|
||||
})
|
||||
|
||||
it('should demonstrate MAX_PARALLEL_LIMIT vs legacy constant difference', () => {
|
||||
setupEnvironment('50')
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MAX_ITERATION_PARALLEL_NUM } = require('@/app/components/workflow/constants')
|
||||
|
||||
// MAX_PARALLEL_LIMIT is configurable, MAX_ITERATION_PARALLEL_NUM is not
|
||||
expect(MAX_PARALLEL_LIMIT).toBe(50)
|
||||
expect(MAX_ITERATION_PARALLEL_NUM).toBe(10)
|
||||
expect(MAX_PARALLEL_LIMIT).not.toBe(MAX_ITERATION_PARALLEL_NUM)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Constants Validation', () => {
|
||||
it('should validate that required constants exist and have correct types', () => {
|
||||
const { MAX_PARALLEL_LIMIT } = require('@/config')
|
||||
const { MIN_ITERATION_PARALLEL_NUM } = require('@/app/components/workflow/constants')
|
||||
expect(typeof MAX_PARALLEL_LIMIT).toBe('number')
|
||||
expect(typeof MIN_ITERATION_PARALLEL_NUM).toBe('number')
|
||||
expect(MAX_PARALLEL_LIMIT).toBeGreaterThanOrEqual(MIN_ITERATION_PARALLEL_NUM)
|
||||
})
|
||||
})
|
||||
})
|
@@ -437,14 +437,6 @@ export const NODE_LAYOUT_HORIZONTAL_PADDING = 60
|
||||
export const NODE_LAYOUT_VERTICAL_PADDING = 60
|
||||
export const NODE_LAYOUT_MIN_DISTANCE = 100
|
||||
|
||||
let maxParallelLimit = 10
|
||||
|
||||
if (process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT && process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT !== '')
|
||||
maxParallelLimit = Number.parseInt(process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT)
|
||||
else if (globalThis.document?.body?.getAttribute('data-public-max-parallel-limit') && globalThis.document.body.getAttribute('data-public-max-parallel-limit') !== '')
|
||||
maxParallelLimit = Number.parseInt(globalThis.document.body.getAttribute('data-public-max-parallel-limit') as string)
|
||||
|
||||
export const PARALLEL_LIMIT = maxParallelLimit
|
||||
export const PARALLEL_DEPTH_LIMIT = 3
|
||||
|
||||
export const RETRIEVAL_OUTPUT_STRUCT = `{
|
||||
|
@@ -30,7 +30,6 @@ import {
|
||||
} from '../utils'
|
||||
import {
|
||||
PARALLEL_DEPTH_LIMIT,
|
||||
PARALLEL_LIMIT,
|
||||
SUPPORT_OUTPUT_VARS_NODE,
|
||||
} from '../constants'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
@@ -48,6 +47,7 @@ import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/ite
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { MAX_PARALLEL_LIMIT } from '@/config'
|
||||
|
||||
export const useIsChatMode = () => {
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
@@ -270,8 +270,6 @@ export const useWorkflow = () => {
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [store])
|
||||
|
||||
const isVarUsedInNodes = useCallback((varSelector: ValueSelector) => {
|
||||
@@ -310,9 +308,9 @@ export const useWorkflow = () => {
|
||||
edges,
|
||||
} = store.getState()
|
||||
const connectedEdges = edges.filter(edge => edge.source === nodeId && edge.sourceHandle === nodeHandle)
|
||||
if (connectedEdges.length > PARALLEL_LIMIT - 1) {
|
||||
if (connectedEdges.length > MAX_PARALLEL_LIMIT - 1) {
|
||||
const { setShowTips } = workflowStore.getState()
|
||||
setShowTips(t('workflow.common.parallelTip.limit', { num: PARALLEL_LIMIT }))
|
||||
setShowTips(t('workflow.common.parallelTip.limit', { num: MAX_PARALLEL_LIMIT }))
|
||||
return false
|
||||
}
|
||||
|
||||
|
@@ -3,7 +3,7 @@ import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import VarReferencePicker from '../_base/components/variable/var-reference-picker'
|
||||
import Split from '../_base/components/split'
|
||||
import { MAX_ITERATION_PARALLEL_NUM, MIN_ITERATION_PARALLEL_NUM } from '../../constants'
|
||||
import { MIN_ITERATION_PARALLEL_NUM } from '../../constants'
|
||||
import type { IterationNodeType } from './types'
|
||||
import useConfig from './use-config'
|
||||
import { ErrorHandleMode, type NodePanelProps } from '@/app/components/workflow/types'
|
||||
@@ -12,6 +12,7 @@ import Switch from '@/app/components/base/switch'
|
||||
import Select from '@/app/components/base/select'
|
||||
import Slider from '@/app/components/base/slider'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { MAX_PARALLEL_LIMIT } from '@/config'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.iteration'
|
||||
|
||||
@@ -96,11 +97,11 @@ const Panel: FC<NodePanelProps<IterationNodeType>> = ({
|
||||
inputs.is_parallel && (<div className='px-4 pb-2'>
|
||||
<Field title={t(`${i18nPrefix}.MaxParallelismTitle`)} isSubTitle tooltip={<div className='w-[230px]'>{t(`${i18nPrefix}.MaxParallelismDesc`)}</div>}>
|
||||
<div className='row flex'>
|
||||
<Input type='number' wrapperClassName='w-18 mr-4 ' max={MAX_ITERATION_PARALLEL_NUM} min={MIN_ITERATION_PARALLEL_NUM} value={inputs.parallel_nums} onChange={(e) => { changeParallelNums(Number(e.target.value)) }} />
|
||||
<Input type='number' wrapperClassName='w-18 mr-4 ' max={MAX_PARALLEL_LIMIT} min={MIN_ITERATION_PARALLEL_NUM} value={inputs.parallel_nums} onChange={(e) => { changeParallelNums(Number(e.target.value)) }} />
|
||||
<Slider
|
||||
value={inputs.parallel_nums}
|
||||
onChange={changeParallelNums}
|
||||
max={MAX_ITERATION_PARALLEL_NUM}
|
||||
max={MAX_PARALLEL_LIMIT}
|
||||
min={MIN_ITERATION_PARALLEL_NUM}
|
||||
className=' mt-4 flex-1 shrink-0'
|
||||
/>
|
||||
|
@@ -13,12 +13,18 @@ const getBooleanConfig = (envVar: string | undefined, dataAttrKey: DatasetAttr,
|
||||
}
|
||||
|
||||
const getNumberConfig = (envVar: string | undefined, dataAttrKey: DatasetAttr, defaultValue: number) => {
|
||||
if (envVar)
|
||||
return Number.parseInt(envVar)
|
||||
if (envVar) {
|
||||
const parsed = Number.parseInt(envVar)
|
||||
if (!Number.isNaN(parsed) && parsed > 0)
|
||||
return parsed
|
||||
}
|
||||
|
||||
const attrValue = globalThis.document?.body?.getAttribute(dataAttrKey)
|
||||
if (attrValue)
|
||||
return Number.parseInt(attrValue)
|
||||
if (attrValue) {
|
||||
const parsed = Number.parseInt(attrValue)
|
||||
if (!Number.isNaN(parsed) && parsed > 0)
|
||||
return parsed
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
@@ -265,6 +271,7 @@ export const FULL_DOC_PREVIEW_LENGTH = 50
|
||||
export const JSON_SCHEMA_MAX_DEPTH = 10
|
||||
|
||||
export const MAX_TOOLS_NUM = getNumberConfig(process.env.NEXT_PUBLIC_MAX_TOOLS_NUM, DatasetAttr.DATA_PUBLIC_MAX_TOOLS_NUM, 10)
|
||||
export const MAX_PARALLEL_LIMIT = getNumberConfig(process.env.NEXT_PUBLIC_MAX_PARALLEL_LIMIT, DatasetAttr.DATA_PUBLIC_MAX_PARALLEL_LIMIT, 10)
|
||||
export const TEXT_GENERATION_TIMEOUT_MS = getNumberConfig(process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS, DatasetAttr.DATA_PUBLIC_TEXT_GENERATION_TIMEOUT_MS, 60000)
|
||||
export const LOOP_NODE_MAX_COUNT = getNumberConfig(process.env.NEXT_PUBLIC_LOOP_NODE_MAX_COUNT, DatasetAttr.DATA_PUBLIC_LOOP_NODE_MAX_COUNT, 100)
|
||||
export const MAX_ITERATIONS_NUM = getNumberConfig(process.env.NEXT_PUBLIC_MAX_ITERATIONS_NUM, DatasetAttr.DATA_PUBLIC_MAX_ITERATIONS_NUM, 99)
|
||||
|
Reference in New Issue
Block a user