Merge commit '9c339239' into sandboxed-agent-rebase

Made-with: Cursor

# Conflicts:
#	api/README.md
#	api/controllers/console/app/workflow_draft_variable.py
#	api/core/agent/cot_agent_runner.py
#	api/core/agent/fc_agent_runner.py
#	api/core/app/apps/advanced_chat/app_runner.py
#	api/core/plugin/backwards_invocation/model.py
#	api/core/prompt/advanced_prompt_transform.py
#	api/core/workflow/nodes/base/node.py
#	api/core/workflow/nodes/llm/llm_utils.py
#	api/core/workflow/nodes/llm/node.py
#	api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py
#	api/core/workflow/nodes/question_classifier/question_classifier_node.py
#	api/core/workflow/runtime/graph_runtime_state.py
#	api/extensions/storage/base_storage.py
#	api/factories/variable_factory.py
#	api/pyproject.toml
#	api/services/variable_truncator.py
#	api/uv.lock
#	web/app/account/oauth/authorize/page.tsx
#	web/app/components/app/configuration/config-var/config-modal/field.tsx
#	web/app/components/base/alert.tsx
#	web/app/components/base/chat/chat/answer/human-input-content/executed-action.tsx
#	web/app/components/base/chat/chat/answer/more.tsx
#	web/app/components/base/chat/chat/answer/operation.tsx
#	web/app/components/base/chat/chat/answer/workflow-process.tsx
#	web/app/components/base/chat/chat/citation/index.tsx
#	web/app/components/base/chat/chat/citation/popup.tsx
#	web/app/components/base/chat/chat/citation/progress-tooltip.tsx
#	web/app/components/base/chat/chat/citation/tooltip.tsx
#	web/app/components/base/chat/chat/question.tsx
#	web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx
#	web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx
#	web/app/components/base/markdown-blocks/form.tsx
#	web/app/components/base/prompt-editor/plugins/hitl-input-block/component-ui.tsx
#	web/app/components/base/tag-management/panel.tsx
#	web/app/components/base/tag-management/trigger.tsx
#	web/app/components/header/account-setting/index.tsx
#	web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx
#	web/app/components/header/account-setting/model-provider-page/provider-added-card/index.tsx
#	web/app/signin/utils/post-login-redirect.ts
#	web/eslint-suppressions.json
#	web/package.json
#	web/pnpm-lock.yaml
This commit is contained in:
Novice
2026-03-23 09:00:45 +08:00
1009 changed files with 76072 additions and 18166 deletions

View File

@ -0,0 +1,40 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import ParamItem from '..'
describe('ParamItem Slider onChange', () => {
const defaultProps = {
id: 'test_param',
name: 'Test Param',
enable: true,
onChange: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
it('should divide slider value by 100 when max < 5', async () => {
const user = userEvent.setup()
render(<ParamItem {...defaultProps} value={0.5} min={0} max={1} />)
const slider = screen.getByRole('slider')
await user.click(slider)
await user.keyboard('{ArrowRight}')
// max=1 < 5, so slider value change (50->51) becomes 0.51
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 0.51)
})
it('should not divide slider value when max >= 5', async () => {
const user = userEvent.setup()
render(<ParamItem {...defaultProps} value={5} min={1} max={10} />)
const slider = screen.getByRole('slider')
await user.click(slider)
await user.keyboard('{ArrowRight}')
// max=10 >= 5, so value remains raw (5->6)
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 6)
})
})

View File

@ -0,0 +1,179 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import ParamItem from '..'
describe('ParamItem', () => {
const defaultProps = {
id: 'test_param',
name: 'Test Param',
value: 0.5,
enable: true,
max: 1,
onChange: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render the parameter name', () => {
render(<ParamItem {...defaultProps} />)
expect(screen.getByText('Test Param')).toBeInTheDocument()
})
it('should render a tooltip trigger by default', () => {
const { container } = render(<ParamItem {...defaultProps} tip="Some tip text" />)
// Tooltip trigger icon should be rendered (the data-state div)
expect(container.querySelector('[data-state]')).toBeInTheDocument()
})
it('should not render tooltip trigger when noTooltip is true', () => {
const { container } = render(<ParamItem {...defaultProps} noTooltip tip="Hidden tip" />)
// No tooltip trigger icon should be rendered
expect(container.querySelector('[data-state]')).not.toBeInTheDocument()
})
it('should render a switch when hasSwitch is true', () => {
render(<ParamItem {...defaultProps} hasSwitch />)
expect(screen.getByRole('switch')).toBeInTheDocument()
})
it('should not render a switch by default', () => {
render(<ParamItem {...defaultProps} />)
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
})
it('should render InputNumber and Slider', () => {
render(<ParamItem {...defaultProps} />)
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
expect(screen.getByRole('slider')).toBeInTheDocument()
})
})
describe('Props', () => {
it('should apply custom className', () => {
const { container } = render(<ParamItem {...defaultProps} className="my-custom-class" />)
expect(container.firstChild).toHaveClass('my-custom-class')
})
it('should disable InputNumber when enable is false', () => {
render(<ParamItem {...defaultProps} enable={false} />)
expect(screen.getByRole('spinbutton')).toBeDisabled()
})
it('should disable Slider when enable is false', () => {
render(<ParamItem {...defaultProps} enable={false} />)
expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true')
})
it('should set switch value based on enable prop', () => {
render(<ParamItem {...defaultProps} hasSwitch enable={true} />)
const toggle = screen.getByRole('switch')
expect(toggle).toHaveAttribute('aria-checked', 'true')
})
})
describe('User Interactions', () => {
it('should call onChange with id and value when InputNumber changes', async () => {
const user = userEvent.setup()
const StatefulParamItem = () => {
const [value, setValue] = useState(defaultProps.value)
return (
<ParamItem
{...defaultProps}
value={value}
onChange={(key: string, nextValue: number) => {
defaultProps.onChange(key, nextValue)
setValue(nextValue)
}}
/>
)
}
render(<StatefulParamItem />)
const input = screen.getByRole('spinbutton')
await user.clear(input)
await user.type(input, '0.8')
expect(defaultProps.onChange).toHaveBeenLastCalledWith('test_param', 0.8)
})
it('should pass scaled value to slider when max < 5', () => {
render(<ParamItem {...defaultProps} value={0.5} />)
const slider = screen.getByRole('slider')
// When max < 5, slider value = value * 100 = 50
expect(slider).toHaveAttribute('aria-valuenow', '50')
})
it('should pass raw value to slider when max >= 5', () => {
render(<ParamItem {...defaultProps} value={5} max={10} />)
const slider = screen.getByRole('slider')
// When max >= 5, slider value = value = 5
expect(slider).toHaveAttribute('aria-valuenow', '5')
})
it('should call onSwitchChange with id and value when switch is toggled', async () => {
const user = userEvent.setup()
const onSwitchChange = vi.fn()
render(<ParamItem {...defaultProps} hasSwitch onSwitchChange={onSwitchChange} />)
await user.click(screen.getByRole('switch'))
expect(onSwitchChange).toHaveBeenCalledWith('test_param', expect.any(Boolean))
})
it('should call onChange with id when increment button is clicked', async () => {
const user = userEvent.setup()
render(<ParamItem {...defaultProps} value={0.5} step={0.1} />)
const incrementBtn = screen.getByRole('button', { name: /increment/i })
await user.click(incrementBtn)
// step=0.1, so 0.5 + 0.1 = 0.6, clamped to [0,1] → 0.6
expect(defaultProps.onChange).toHaveBeenCalledWith('test_param', 0.6)
})
})
describe('Edge Cases', () => {
it('should correctly scale slider value when max < 5', () => {
render(<ParamItem {...defaultProps} value={0.5} min={0} />)
// Slider should get value * 100 = 50, min * 100 = 0, max * 100 = 100
const slider = screen.getByRole('slider')
expect(slider).toHaveAttribute('aria-valuemax', '100')
})
it('should not scale slider value when max >= 5', () => {
render(<ParamItem {...defaultProps} value={5} min={1} max={10} />)
const slider = screen.getByRole('slider')
expect(slider).toHaveAttribute('aria-valuemax', '10')
})
it('should use default step of 0.1 and min of 0 when not provided', () => {
render(<ParamItem {...defaultProps} />)
const input = screen.getByRole('spinbutton')
// Component renders without error with default step/min
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
expect(input).toHaveAttribute('step', '0.1')
expect(input).toHaveAttribute('min', '0')
})
})
})

View File

@ -0,0 +1,145 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useState } from 'react'
import ScoreThresholdItem from '../score-threshold-item'
describe('ScoreThresholdItem', () => {
const defaultProps = {
value: 0.7,
enable: true,
onChange: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render the translated parameter name', () => {
render(<ScoreThresholdItem {...defaultProps} />)
expect(screen.getByText('appDebug.datasetConfig.score_threshold')).toBeInTheDocument()
})
it('should render tooltip trigger', () => {
const { container } = render(<ScoreThresholdItem {...defaultProps} />)
// Tooltip trigger icon should be rendered
expect(container.querySelector('[data-state]')).toBeInTheDocument()
})
it('should render InputNumber and Slider', () => {
render(<ScoreThresholdItem {...defaultProps} />)
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
expect(screen.getByRole('slider')).toBeInTheDocument()
})
})
describe('Props', () => {
it('should apply custom className', () => {
const { container } = render(<ScoreThresholdItem {...defaultProps} className="custom-cls" />)
expect(container.firstChild).toHaveClass('custom-cls')
})
it('should render switch when hasSwitch is true', () => {
render(<ScoreThresholdItem {...defaultProps} hasSwitch />)
expect(screen.getByRole('switch')).toBeInTheDocument()
})
it('should forward onSwitchChange to ParamItem', async () => {
const onSwitchChange = vi.fn()
render(<ScoreThresholdItem {...defaultProps} hasSwitch onSwitchChange={onSwitchChange} />)
// Verify the switch rendered (onSwitchChange forwarded internally)
expect(screen.getByRole('switch')).toBeInTheDocument()
await userEvent.click(screen.getByRole('switch'))
expect(onSwitchChange).toHaveBeenCalledTimes(1)
})
it('should disable controls when enable is false', () => {
render(<ScoreThresholdItem {...defaultProps} enable={false} />)
expect(screen.getByRole('spinbutton')).toBeDisabled()
expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true')
})
})
describe('Value Clamping', () => {
it('should clamp values to minimum of 0', () => {
render(<ScoreThresholdItem {...defaultProps} />)
const input = screen.getByRole('spinbutton')
expect(input).toHaveAttribute('min', '0')
})
it('should clamp values to maximum of 1', () => {
render(<ScoreThresholdItem {...defaultProps} />)
const input = screen.getByRole('spinbutton')
expect(input).toHaveAttribute('max', '1')
})
it('should use step of 0.01', () => {
render(<ScoreThresholdItem {...defaultProps} />)
const input = screen.getByRole('spinbutton')
expect(input).toHaveAttribute('step', '0.01')
})
it('should call onChange with rounded value when input changes', async () => {
const user = userEvent.setup()
const StatefulScoreThresholdItem = () => {
const [value, setValue] = useState(defaultProps.value)
return (
<ScoreThresholdItem
{...defaultProps}
value={value}
onChange={(key, nextValue) => {
defaultProps.onChange(key, nextValue)
setValue(nextValue)
}}
/>
)
}
render(<StatefulScoreThresholdItem />)
const input = screen.getByRole('spinbutton')
await user.clear(input)
await user.type(input, '0.55')
expect(defaultProps.onChange).toHaveBeenLastCalledWith('score_threshold', 0.55)
})
it('should call onChange with clamped value via increment button', async () => {
const user = userEvent.setup()
render(<ScoreThresholdItem {...defaultProps} value={0.5} />)
const incrementBtn = screen.getByRole('button', { name: /increment/i })
await user.click(incrementBtn)
// step=0.01, so 0.5 + 0.01 = 0.51, clamped to [0,1] → 0.51
expect(defaultProps.onChange).toHaveBeenCalledWith('score_threshold', 0.51)
})
it('should call onChange with clamped value via decrement button', async () => {
const user = userEvent.setup()
render(<ScoreThresholdItem {...defaultProps} value={0.5} />)
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
await user.click(decrementBtn)
expect(defaultProps.onChange).toHaveBeenCalledWith('score_threshold', 0.49)
})
it('should clamp to max=1 when value exceeds maximum', () => {
render(<ScoreThresholdItem {...defaultProps} value={1.5} />)
const input = screen.getByRole('spinbutton')
expect(input).toHaveValue(1)
})
})
})

View File

@ -0,0 +1,130 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import TopKItem from '../top-k-item'
vi.mock('@/env', () => ({
env: {
NEXT_PUBLIC_TOP_K_MAX_VALUE: 10,
},
}))
describe('TopKItem', () => {
const defaultProps = {
value: 2,
enable: true,
onChange: vi.fn(),
}
beforeEach(() => {
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render the translated parameter name', () => {
render(<TopKItem {...defaultProps} />)
expect(screen.getByText('appDebug.datasetConfig.top_k')).toBeInTheDocument()
})
it('should render tooltip trigger', () => {
const { container } = render(<TopKItem {...defaultProps} />)
// Tooltip trigger icon should be rendered
expect(container.querySelector('[data-state]')).toBeInTheDocument()
})
it('should render InputNumber and Slider', () => {
render(<TopKItem {...defaultProps} />)
expect(screen.getByRole('spinbutton')).toBeInTheDocument()
expect(screen.getByRole('slider')).toBeInTheDocument()
})
})
describe('Props', () => {
it('should apply custom className', () => {
const { container } = render(<TopKItem {...defaultProps} className="custom-cls" />)
expect(container.firstChild).toHaveClass('custom-cls')
})
it('should disable controls when enable is false', () => {
render(<TopKItem {...defaultProps} enable={false} />)
expect(screen.getByRole('spinbutton')).toBeDisabled()
expect(screen.getByRole('slider')).toHaveAttribute('aria-disabled', 'true')
})
})
describe('Value Limits', () => {
it('should use step of 1', () => {
render(<TopKItem {...defaultProps} />)
const input = screen.getByRole('spinbutton')
expect(input).toHaveAttribute('step', '1')
})
it('should use minimum of 1', () => {
render(<TopKItem {...defaultProps} />)
const input = screen.getByRole('spinbutton')
expect(input).toHaveAttribute('min', '1')
})
it('should use maximum from env (10)', () => {
render(<TopKItem {...defaultProps} />)
const input = screen.getByRole('spinbutton')
expect(input).toHaveAttribute('max', '10')
})
it('should render slider with max >= 5 so no scaling is applied', () => {
render(<TopKItem {...defaultProps} />)
const slider = screen.getByRole('slider')
// max=10 >= 5 so slider shows raw values
expect(slider).toHaveAttribute('aria-valuemax', '10')
})
it('should not render a switch (no hasSwitch prop)', () => {
render(<TopKItem {...defaultProps} />)
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call onChange with clamped integer value via increment button', async () => {
const user = userEvent.setup()
render(<TopKItem {...defaultProps} value={5} />)
const incrementBtn = screen.getByRole('button', { name: /increment/i })
await user.click(incrementBtn)
// step=1, so 5 + 1 = 6, clamped to [1,10] → 6
expect(defaultProps.onChange).toHaveBeenCalledWith('top_k', 6)
})
it('should call onChange with clamped integer value via decrement button', async () => {
const user = userEvent.setup()
render(<TopKItem {...defaultProps} value={5} />)
const decrementBtn = screen.getByRole('button', { name: /decrement/i })
await user.click(decrementBtn)
// step=1, so 5 - 1 = 4, clamped to [1,10] → 4
expect(defaultProps.onChange).toHaveBeenCalledWith('top_k', 4)
})
it('should call onChange with integer value when slider changes', async () => {
const user = userEvent.setup()
render(<TopKItem {...defaultProps} value={2} />)
const slider = screen.getByRole('slider')
await user.click(slider)
await user.keyboard('{ArrowRight}')
expect(defaultProps.onChange).toHaveBeenLastCalledWith('top_k', 3)
})
})
})

View File

@ -35,6 +35,11 @@ const ScoreThresholdItem: FC<Props> = ({
notOutRangeValue = Math.min(VALUE_LIMIT.max, notOutRangeValue)
onChange(key, notOutRangeValue)
}
const safeValue = Math.min(
VALUE_LIMIT.max,
Math.max(VALUE_LIMIT.min, Number.parseFloat(value.toFixed(2))),
)
return (
<ParamItem
className={className}
@ -42,7 +47,7 @@ const ScoreThresholdItem: FC<Props> = ({
name={t('datasetConfig.score_threshold', { ns: 'appDebug' })}
tip={t('datasetConfig.score_thresholdTip', { ns: 'appDebug' }) as string}
{...VALUE_LIMIT}
value={value}
value={safeValue}
enable={enable}
onChange={handleParamChange}
hasSwitch={hasSwitch}