dify/web/utils/model-config.spec.ts

820 lines
21 KiB
TypeScript

/**
* Test suite for model configuration transformation utilities
*
* This module handles the conversion between two different representations of user input forms:
* 1. UserInputFormItem: The form structure used in the UI
* 2. PromptVariable: The variable structure used in prompts and model configuration
*
* Key functions:
* - userInputsFormToPromptVariables: Converts UI form items to prompt variables
* - promptVariablesToUserInputsForm: Converts prompt variables back to form items
* - formatBooleanInputs: Ensures boolean inputs are properly typed
*/
import {
formatBooleanInputs,
promptVariablesToUserInputsForm,
userInputsFormToPromptVariables,
} from './model-config'
import type { UserInputFormItem } from '@/types/app'
import type { PromptVariable } from '@/models/debug'
describe('Model Config Utilities', () => {
describe('userInputsFormToPromptVariables', () => {
/**
* Test handling of null or undefined input
* Should return empty array when no inputs provided
*/
it('should return empty array for null input', () => {
const result = userInputsFormToPromptVariables(null)
expect(result).toEqual([])
})
/**
* Test conversion of text-input (string) type
* Text inputs are the most common form field type
*/
it('should convert text-input to string prompt variable', () => {
const userInputs: UserInputFormItem[] = [
{
'text-input': {
label: 'User Name',
variable: 'user_name',
required: true,
max_length: 100,
default: '',
hide: false,
},
},
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result).toHaveLength(1)
expect(result[0]).toEqual({
key: 'user_name',
name: 'User Name',
required: true,
type: 'string',
max_length: 100,
options: [],
is_context_var: false,
hide: false,
default: '',
})
})
/**
* Test conversion of paragraph type
* Paragraphs are multi-line text inputs
*/
it('should convert paragraph to paragraph prompt variable', () => {
const userInputs: UserInputFormItem[] = [
{
paragraph: {
label: 'Description',
variable: 'description',
required: false,
max_length: 500,
default: '',
hide: false,
},
},
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result[0]).toEqual({
key: 'description',
name: 'Description',
required: false,
type: 'paragraph',
max_length: 500,
options: [],
is_context_var: false,
hide: false,
default: '',
})
})
/**
* Test conversion of number type
* Number inputs should preserve numeric constraints
*/
it('should convert number input to number prompt variable', () => {
const userInputs: UserInputFormItem[] = [
{
number: {
label: 'Age',
variable: 'age',
required: true,
default: '',
hide: false,
},
} as any,
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result[0]).toEqual({
key: 'age',
name: 'Age',
required: true,
type: 'number',
options: [],
hide: false,
default: '',
})
})
/**
* Test conversion of checkbox (boolean) type
* Checkboxes are converted to 'checkbox' type in prompt variables
*/
it('should convert checkbox to checkbox prompt variable', () => {
const userInputs: UserInputFormItem[] = [
{
checkbox: {
label: 'Accept Terms',
variable: 'accept_terms',
required: true,
default: '',
hide: false,
},
} as any,
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result[0]).toEqual({
key: 'accept_terms',
name: 'Accept Terms',
required: true,
type: 'checkbox',
options: [],
hide: false,
default: '',
})
})
/**
* Test conversion of select (dropdown) type
* Select inputs include options array
*/
it('should convert select input to select prompt variable', () => {
const userInputs: UserInputFormItem[] = [
{
select: {
label: 'Country',
variable: 'country',
required: true,
options: ['USA', 'Canada', 'Mexico'],
default: 'USA',
hide: false,
},
},
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result[0]).toEqual({
key: 'country',
name: 'Country',
required: true,
type: 'select',
options: ['USA', 'Canada', 'Mexico'],
is_context_var: false,
hide: false,
default: 'USA',
})
})
/**
* Test conversion of file upload type
* File inputs include configuration for allowed types and upload methods
*/
it('should convert file input to file prompt variable', () => {
const userInputs: UserInputFormItem[] = [
{
file: {
label: 'Profile Picture',
variable: 'profile_pic',
required: false,
allowed_file_types: ['image'],
allowed_file_extensions: ['.jpg', '.png'],
allowed_file_upload_methods: ['local_file', 'remote_url'],
default: '',
hide: false,
},
} as any,
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result[0]).toEqual({
key: 'profile_pic',
name: 'Profile Picture',
required: false,
type: 'file',
config: {
allowed_file_types: ['image'],
allowed_file_extensions: ['.jpg', '.png'],
allowed_file_upload_methods: ['local_file', 'remote_url'],
number_limits: 1,
},
hide: false,
default: '',
})
})
/**
* Test conversion of file-list type
* File lists allow multiple file uploads with a max_length constraint
*/
it('should convert file-list input to file-list prompt variable', () => {
const userInputs: UserInputFormItem[] = [
{
'file-list': {
label: 'Documents',
variable: 'documents',
required: true,
allowed_file_types: ['document'],
allowed_file_extensions: ['.pdf', '.docx'],
allowed_file_upload_methods: ['local_file'],
max_length: 5,
default: '',
hide: false,
},
} as any,
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result[0]).toEqual({
key: 'documents',
name: 'Documents',
required: true,
type: 'file-list',
config: {
allowed_file_types: ['document'],
allowed_file_extensions: ['.pdf', '.docx'],
allowed_file_upload_methods: ['local_file'],
number_limits: 5,
},
hide: false,
default: '',
})
})
/**
* Test conversion of external_data_tool type
* External data tools have custom configuration and icons
*/
it('should convert external_data_tool to prompt variable', () => {
const userInputs: UserInputFormItem[] = [
{
external_data_tool: {
label: 'API Data',
variable: 'api_data',
type: 'api',
enabled: true,
required: false,
config: { endpoint: 'https://api.example.com' },
icon: 'api-icon',
icon_background: '#FF5733',
hide: false,
},
} as any,
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result[0]).toEqual({
key: 'api_data',
name: 'API Data',
required: false,
type: 'api',
enabled: true,
config: { endpoint: 'https://api.example.com' },
icon: 'api-icon',
icon_background: '#FF5733',
is_context_var: false,
hide: false,
})
})
/**
* Test handling of dataset_query_variable
* When a variable matches the dataset_query_variable, is_context_var should be true
*/
it('should mark variable as context var when matching dataset_query_variable', () => {
const userInputs: UserInputFormItem[] = [
{
'text-input': {
label: 'Query',
variable: 'query',
required: true,
max_length: 200,
default: '',
hide: false,
},
},
]
const result = userInputsFormToPromptVariables(userInputs, 'query')
expect(result[0].is_context_var).toBe(true)
})
/**
* Test conversion of multiple mixed input types
* Should handle an array with different input types correctly
*/
it('should convert multiple mixed input types', () => {
const userInputs: UserInputFormItem[] = [
{
'text-input': {
label: 'Name',
variable: 'name',
required: true,
max_length: 50,
default: '',
hide: false,
},
},
{
number: {
label: 'Age',
variable: 'age',
required: false,
default: '',
hide: false,
},
} as any,
{
select: {
label: 'Gender',
variable: 'gender',
required: true,
options: ['Male', 'Female', 'Other'],
default: '',
hide: false,
},
},
]
const result = userInputsFormToPromptVariables(userInputs)
expect(result).toHaveLength(3)
expect(result[0].type).toBe('string')
expect(result[1].type).toBe('number')
expect(result[2].type).toBe('select')
})
})
describe('promptVariablesToUserInputsForm', () => {
/**
* Test conversion of string prompt variable back to text-input
*/
it('should convert string prompt variable to text-input', () => {
const promptVariables: PromptVariable[] = [
{
key: 'user_name',
name: 'User Name',
required: true,
type: 'string',
max_length: 100,
options: [],
},
]
const result = promptVariablesToUserInputsForm(promptVariables)
expect(result).toHaveLength(1)
expect(result[0]).toEqual({
'text-input': {
label: 'User Name',
variable: 'user_name',
required: true,
max_length: 100,
default: '',
hide: undefined,
},
})
})
/**
* Test conversion of paragraph prompt variable
*/
it('should convert paragraph prompt variable to paragraph input', () => {
const promptVariables: PromptVariable[] = [
{
key: 'description',
name: 'Description',
required: false,
type: 'paragraph',
max_length: 500,
options: [],
},
]
const result = promptVariablesToUserInputsForm(promptVariables)
expect(result[0]).toEqual({
paragraph: {
label: 'Description',
variable: 'description',
required: false,
max_length: 500,
default: '',
hide: undefined,
},
})
})
/**
* Test conversion of number prompt variable
*/
it('should convert number prompt variable to number input', () => {
const promptVariables: PromptVariable[] = [
{
key: 'age',
name: 'Age',
required: true,
type: 'number',
options: [],
},
]
const result = promptVariablesToUserInputsForm(promptVariables)
expect(result[0]).toEqual({
number: {
label: 'Age',
variable: 'age',
required: true,
default: '',
hide: undefined,
},
})
})
/**
* Test conversion of checkbox prompt variable
*/
it('should convert checkbox prompt variable to checkbox input', () => {
const promptVariables: PromptVariable[] = [
{
key: 'accept_terms',
name: 'Accept Terms',
required: true,
type: 'checkbox',
options: [],
},
]
const result = promptVariablesToUserInputsForm(promptVariables)
expect(result[0]).toEqual({
checkbox: {
label: 'Accept Terms',
variable: 'accept_terms',
required: true,
default: '',
hide: undefined,
},
})
})
/**
* Test conversion of select prompt variable
*/
it('should convert select prompt variable to select input', () => {
const promptVariables: PromptVariable[] = [
{
key: 'country',
name: 'Country',
required: true,
type: 'select',
options: ['USA', 'Canada', 'Mexico'],
default: 'USA',
},
]
const result = promptVariablesToUserInputsForm(promptVariables)
expect(result[0]).toEqual({
select: {
label: 'Country',
variable: 'country',
required: true,
options: ['USA', 'Canada', 'Mexico'],
default: 'USA',
hide: undefined,
},
})
})
/**
* Test filtering of invalid prompt variables
* Variables without key or name should be filtered out
*/
it('should filter out variables with empty key or name', () => {
const promptVariables: PromptVariable[] = [
{
key: '',
name: 'Empty Key',
required: true,
type: 'string',
options: [],
},
{
key: 'valid',
name: '',
required: true,
type: 'string',
options: [],
},
{
key: ' ',
name: 'Whitespace Key',
required: true,
type: 'string',
options: [],
},
{
key: 'valid_key',
name: 'Valid Name',
required: true,
type: 'string',
options: [],
},
]
const result = promptVariablesToUserInputsForm(promptVariables)
expect(result).toHaveLength(1)
expect((result[0] as any)['text-input']?.variable).toBe('valid_key')
})
/**
* Test conversion of external data tool prompt variable
*/
it('should convert external data tool prompt variable', () => {
const promptVariables: PromptVariable[] = [
{
key: 'api_data',
name: 'API Data',
required: false,
type: 'api',
enabled: true,
config: { endpoint: 'https://api.example.com' },
icon: 'api-icon',
icon_background: '#FF5733',
},
]
const result = promptVariablesToUserInputsForm(promptVariables)
expect(result[0]).toEqual({
external_data_tool: {
label: 'API Data',
variable: 'api_data',
enabled: true,
type: 'api',
config: { endpoint: 'https://api.example.com' },
required: false,
icon: 'api-icon',
icon_background: '#FF5733',
hide: undefined,
},
})
})
/**
* Test that required defaults to true when not explicitly set to false
*/
it('should default required to true when not false', () => {
const promptVariables: PromptVariable[] = [
{
key: 'test1',
name: 'Test 1',
required: undefined,
type: 'string',
options: [],
},
{
key: 'test2',
name: 'Test 2',
required: false,
type: 'string',
options: [],
},
]
const result = promptVariablesToUserInputsForm(promptVariables)
expect((result[0] as any)['text-input']?.required).toBe(true)
expect((result[1] as any)['text-input']?.required).toBe(false)
})
})
describe('formatBooleanInputs', () => {
/**
* Test that null or undefined inputs are handled gracefully
*/
it('should return inputs unchanged when useInputs is null', () => {
const inputs = { key1: 'value1', key2: 'value2' }
const result = formatBooleanInputs(null, inputs)
expect(result).toEqual(inputs)
})
it('should return inputs unchanged when useInputs is undefined', () => {
const inputs = { key1: 'value1', key2: 'value2' }
const result = formatBooleanInputs(undefined, inputs)
expect(result).toEqual(inputs)
})
/**
* Test conversion of boolean input values to actual boolean type
* This is important for proper type handling in the backend
* Note: checkbox inputs are converted to type 'checkbox' by userInputsFormToPromptVariables
*/
it('should convert boolean inputs to boolean type', () => {
const useInputs: PromptVariable[] = [
{
key: 'accept_terms',
name: 'Accept Terms',
required: true,
type: 'checkbox',
options: [],
},
{
key: 'subscribe',
name: 'Subscribe',
required: false,
type: 'checkbox',
options: [],
},
]
const inputs = {
accept_terms: 'true',
subscribe: '',
other_field: 'value',
}
const result = formatBooleanInputs(useInputs, inputs)
expect(result).toEqual({
accept_terms: true,
subscribe: false,
other_field: 'value',
})
})
/**
* Test that non-boolean inputs are not affected
*/
it('should not modify non-boolean inputs', () => {
const useInputs: PromptVariable[] = [
{
key: 'name',
name: 'Name',
required: true,
type: 'string',
options: [],
},
{
key: 'age',
name: 'Age',
required: true,
type: 'number',
options: [],
},
]
const inputs = {
name: 'John Doe',
age: 30,
}
const result = formatBooleanInputs(useInputs, inputs)
expect(result).toEqual(inputs)
})
/**
* Test handling of truthy and falsy values for boolean conversion
* Note: checkbox inputs are converted to type 'checkbox' by userInputsFormToPromptVariables
*/
it('should handle various truthy and falsy values', () => {
const useInputs: PromptVariable[] = [
{
key: 'bool1',
name: 'Bool 1',
required: true,
type: 'checkbox',
options: [],
},
{
key: 'bool2',
name: 'Bool 2',
required: true,
type: 'checkbox',
options: [],
},
{
key: 'bool3',
name: 'Bool 3',
required: true,
type: 'checkbox',
options: [],
},
{
key: 'bool4',
name: 'Bool 4',
required: true,
type: 'checkbox',
options: [],
},
]
const inputs = {
bool1: 1,
bool2: 0,
bool3: 'yes',
bool4: null as any,
}
const result = formatBooleanInputs(useInputs, inputs)
expect(result?.bool1).toBe(true)
expect(result?.bool2).toBe(false)
expect(result?.bool3).toBe(true)
expect(result?.bool4).toBe(false)
})
/**
* Test that the function creates a new object and doesn't mutate the original
* Note: checkbox inputs are converted to type 'checkbox' by userInputsFormToPromptVariables
*/
it('should not mutate original inputs object', () => {
const useInputs: PromptVariable[] = [
{
key: 'flag',
name: 'Flag',
required: true,
type: 'checkbox',
options: [],
},
]
const inputs = { flag: 'true', other: 'value' }
const originalInputs = { ...inputs }
formatBooleanInputs(useInputs, inputs)
expect(inputs).toEqual(originalInputs)
})
})
describe('Round-trip conversion', () => {
/**
* Test that converting from UserInputForm to PromptVariable and back
* preserves the essential data (though some fields may have defaults applied)
*/
it('should preserve data through round-trip conversion', () => {
const originalUserInputs: UserInputFormItem[] = [
{
'text-input': {
label: 'Name',
variable: 'name',
required: true,
max_length: 50,
default: '',
hide: false,
},
},
{
select: {
label: 'Type',
variable: 'type',
required: false,
options: ['A', 'B', 'C'],
default: 'A',
hide: false,
},
},
]
const promptVars = userInputsFormToPromptVariables(originalUserInputs)
const backToUserInputs = promptVariablesToUserInputsForm(promptVars)
expect(backToUserInputs).toHaveLength(2)
expect((backToUserInputs[0] as any)['text-input']?.variable).toBe('name')
expect((backToUserInputs[1] as any).select?.variable).toBe('type')
expect((backToUserInputs[1] as any).select?.options).toEqual(['A', 'B', 'C'])
})
})
})