Files
Supra4E8C ebb5b7d0c0 Refactor provider credential weight normalization tests and update API key structure
- Updated test cases in `providerWeightTransformers.test.ts` to reflect changes in API key structure from 'api-key-entries' to 'keys'.
- Introduced new tests in `providerWorkbenchUi.test.ts` to verify the editing surface of provider workbench.
- Modified `sponsorCustomEndpoint.test.ts` to adapt to the new API key structure.
- Adjusted `visualConfigAntigravitySensitiveWords.test.ts` and `visualConfigConcurrency.test.ts` to accommodate new YAML structure for sensitive words and concurrency settings.
- Enhanced `visualConfigDefaults.test.ts` to ensure proper handling of boolean defaults and YAML structure.
- Updated `visualConfigDevinSensitiveWords.test.ts` to reflect changes in the YAML structure for sensitive words.
- Refined `visualConfigDisableImageGeneration.test.ts` to align with new multimedia settings.
- Added comprehensive tests in `visualConfigV8.test.ts` to validate the v8 visual config contract and ensure scalar read/write path parity.
- Improved validation tests in `visualConfigValidation.test.ts` to handle new routing and requests structure.
- Cleaned up `xaiApiKeyProvider.test.ts` by removing unnecessary API client mocks and focusing on normalization of the xAI API key contract.
2026-09-28 12:55:16 +08:00

206 lines
7.1 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { parse as parseYaml } from 'yaml';
import i18n from '../src/i18n';
import { SectionStreaming } from '../src/features/config/components/sections/SectionStreaming';
import { getVisualConfigValidationErrors } from '../src/hooks/useVisualConfig';
import { DEFAULT_VISUAL_VALUES } from '../src/types/visualConfig';
import { runVisualConfig } from './helpers/visualConfig';
const signedFields = [
'maxRetryCredentials',
'maxRetryInterval',
'authAutoRefreshWorkers',
'streaming.keepaliveSeconds',
'streaming.bootstrapRetries',
'streaming.nonstreamKeepaliveInterval',
] as const;
function withSignedValues(value: string) {
return {
...structuredClone(DEFAULT_VISUAL_VALUES),
maxRetryCredentials: value,
maxRetryInterval: value,
authAutoRefreshWorkers: value,
streaming: {
keepaliveSeconds: value,
bootstrapRetries: value,
nonstreamKeepaliveInterval: value,
},
};
}
describe('visual config validation', () => {
for (const value of [
'',
' ',
'-1',
' -16 ',
'0',
'16',
String(Number.MIN_SAFE_INTEGER),
String(Number.MAX_SAFE_INTEGER),
]) {
test(`accepts backend-supported integer sentinel values: ${JSON.stringify(value)}`, () => {
const errors = getVisualConfigValidationErrors(withSignedValues(value));
for (const field of signedFields) expect(errors[field]).toBeUndefined();
});
}
for (const value of [
'-1.5',
'1.5',
'abc',
'1e3',
'Infinity',
'9'.repeat(400),
'-9007199254740992',
'9007199254740992',
'-9007199254740993',
'9007199254740993',
'-9999999999999999999',
'9999999999999999999',
]) {
test(`rejects invalid integer input: ${value.slice(0, 20)}`, () => {
const errors = getVisualConfigValidationErrors(withSignedValues(value));
for (const field of signedFields) expect(errors[field]).toBe('integer');
});
}
test('keeps non-negative and bounded fields strict', () => {
const errors = getVisualConfigValidationErrors({
...structuredClone(DEFAULT_VISUAL_VALUES),
requestRetry: '-1',
logsMaxTotalSizeMb: '-1',
errorLogsMaxFiles: '-1',
port: '65536',
});
expect(errors.requestRetry).toBe('non_negative_integer');
expect(errors.logsMaxTotalSizeMb).toBe('non_negative_integer');
expect(errors.errorLogsMaxFiles).toBe('non_negative_integer');
expect(errors.port).toBe('port_range');
});
test.each(['-9007199254740993', '9007199254740993', '-9999999999999999999'])(
'does not serialize unsafe integers even if validation is bypassed: %s',
(value) => {
const yaml = 'routing:\n retry:\n max-retry-interval: 10\n';
const config = runVisualConfig(yaml, [withSignedValues(value)]);
for (const field of signedFields) {
expect(config.visualValidationErrors[field]).toBe('integer');
}
expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({
routing: { retry: { 'max-retry-interval': 10 } },
});
}
);
test.each([Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER])(
'serializes safe integer boundaries exactly: %s',
(value) => {
const config = runVisualConfig('{}', [{ maxRetryInterval: String(value) }]);
expect(config.visualValidationErrors.maxRetryInterval).toBeUndefined();
expect(parseYaml(config.applyVisualChangesToYaml('{}'))).toEqual({
routing: { retry: { 'max-retry-interval': value } },
});
}
);
test('rejects unsafe integers in non-negative fields', () => {
const errors = getVisualConfigValidationErrors({
...structuredClone(DEFAULT_VISUAL_VALUES),
requestRetry: '9007199254740993',
logsMaxTotalSizeMb: '9007199254740993',
errorLogsMaxFiles: '9007199254740993',
});
expect(errors.requestRetry).toBe('non_negative_integer');
expect(errors.logsMaxTotalSizeMb).toBe('non_negative_integer');
expect(errors.errorLogsMaxFiles).toBe('non_negative_integer');
});
test('loading negative sentinels does not block an unrelated visual edit', () => {
const yaml = `routing:
retry:
max-retry-credentials: -1
max-retry-interval: -1
requests:
nonstream-keepalive-interval: -1
streaming:
keepalive-seconds: -1
bootstrap-retries: -1
oauth:
auth-auto-refresh-workers: -1
`;
const config = runVisualConfig(yaml, [{ debug: true }]);
expect(Object.values(config.visualValidationErrors).some(Boolean)).toBe(false);
expect(parseYaml(config.applyVisualChangesToYaml(yaml))).toEqual({
...parseYaml(yaml),
observability: { logs: { debug: true } },
});
});
test('writes user-entered negative sentinels as YAML integers', () => {
const config = runVisualConfig('{}', [withSignedValues('-1')]);
expect(parseYaml(config.applyVisualChangesToYaml('{}'))).toEqual({
routing: { retry: { 'max-retry-credentials': -1, 'max-retry-interval': -1 } },
oauth: { 'auth-auto-refresh-workers': -1 },
requests: {
'nonstream-keepalive-interval': -1,
streaming: { 'keepalive-seconds': -1, 'bootstrap-retries': -1 },
},
});
});
test('streaming badges recognize non-positive integers but not invalid input', () => {
for (const [value, disabled] of [
['-1', true],
['0', true],
['', true],
['1', false],
['-1.5', false],
] as const) {
const values = withSignedValues(value);
const markup = renderToStaticMarkup(
createElement(SectionStreaming, {
values,
validationErrors: getVisualConfigValidationErrors(values),
disabled: false,
onChange: () => {},
})
);
const label = i18n.t('config_management.visual.sections.streaming.disabled');
expect(markup.split(`>${label}</span>`).length - 1).toBe(disabled ? 2 : 0);
}
});
test('integer validation and sentinel hints exist in all four locales', async () => {
for (const locale of ['en', 'zh-CN', 'zh-TW', 'ru']) {
const json = await Bun.file(`src/i18n/locales/${locale}.json`).json();
const visual = json.config_management.visual;
expect(visual.validation.integer).toBeString();
expect(visual.sections.network.max_retry_interval_hint).toBeString();
}
});
test('requires Redis usage retention to be empty or within 1..3600', () => {
const values = structuredClone(DEFAULT_VISUAL_VALUES);
values.redisUsageQueueRetentionSeconds = '';
expect(getVisualConfigValidationErrors(values).redisUsageQueueRetentionSeconds).toBeUndefined();
values.redisUsageQueueRetentionSeconds = '0';
expect(getVisualConfigValidationErrors(values).redisUsageQueueRetentionSeconds).toBe(
'integer_range_1_3600'
);
values.redisUsageQueueRetentionSeconds = '3601';
expect(getVisualConfigValidationErrors(values).redisUsageQueueRetentionSeconds).toBe(
'integer_range_1_3600'
);
values.redisUsageQueueRetentionSeconds = '3600';
expect(getVisualConfigValidationErrors(values).redisUsageQueueRetentionSeconds).toBeUndefined();
});
});