import { describe, expect, it, vi } from 'vitest' import { createEmptyModelPolicy, createEmptyRoutingGroupConfig, getDefaultModelPolicy, getModelPolicy, getModelScheduling, modelSchedulingRuleId, setDefaultProviderPriorityOverrides, setModelKeyPriorityOverridesForFormat, upsertModelPolicy, upsertModelSchedulingRule, type RoutingRule, } from '../utils/routingPolicy' import { createSchedulingPolicy, normalizeProviderSchedulingConfig, readSchedulingPolicies, schedulingPolicyEditorConfig, validateSchedulingPolicies, writeSchedulingPolicies, } from '../utils/schedulingPolicies' describe('strategy-scoped scheduling policies', () => { it('normalizes legacy Key scheduling without mutating unrelated rules, scopes, or historical priorities', () => { const config = createEmptyRoutingGroupConfig() config.default_policy.priority_mode = 'global_key' config.disabled_providers = ['disabled'] config.model_policies = [{ ...createEmptyModelPolicy('*'), key_priority_overrides: { key: 5 }, provider_priority_overrides: { provider: 3 } }] config.rules = [{ id: 'custom-rule', priority: 1, enabled: true, phase: 'client_request', stop_processing: true, conditions: { field: 'model', op: 'prefix', value: 'example' }, actions: [ { type: 'set_scheduling', priority_mode: 'global_key', scheduling_mode: 'load_balance', sticky_key_attempts: 4 }, { type: 'set_scheduling', scheduling_mode: 'fixed_order' }, { type: 'set_header', name: 'x-test', value: 'kept' }, ], }] const before = JSON.stringify(config) const normalized = normalizeProviderSchedulingConfig(config) expect(normalized.default_policy.priority_mode).toBe('provider') expect(normalized.disabled_providers).toEqual(config.disabled_providers) expect(normalized.model_policies).toEqual(config.model_policies) expect(normalized.rules[0]).toEqual({ ...config.rules[0], actions: [ { type: 'set_scheduling', priority_mode: 'provider', scheduling_mode: 'load_balance', sticky_key_attempts: 4 }, ...config.rules[0].actions.slice(1), ] }) expect(readSchedulingPolicies(config).every(entry => entry.priorityMode === 'provider')).toBe(true) expect(createSchedulingPolicy(config).priorityMode).toBe('provider') expect(JSON.stringify(config)).toBe(before) }) it('starts with one all-model strategy', () => { const entries = readSchedulingPolicies(createEmptyRoutingGroupConfig()) expect(entries).toHaveLength(1) expect(entries[0]).toMatchObject({ scope: 'all', priorityMode: 'provider', schedulingMode: 'cache_affinity' }) expect(validateSchedulingPolicies(entries)).toBeNull() }) it('generates unique policy ids on HTTP pages without crypto.randomUUID', () => { vi.stubGlobal('crypto', {}) try { const config = createEmptyRoutingGroupConfig() expect(createSchedulingPolicy(config).id).not.toBe(createSchedulingPolicy(config).id) expect(readSchedulingPolicies(config)).toHaveLength(1) } finally { vi.unstubAllGlobals() } }) it('persists all models as a wildcard rather than enumerating the current catalog', () => { const config = createEmptyRoutingGroupConfig() const entry = createSchedulingPolicy(config, 'all') entry.models = ['model-a', 'model-b'] entry.priorityMode = 'global_key' entry.schedulingMode = 'load_balance' entry.policy.provider_priority_overrides = { provider: 2 } const saved = JSON.parse(JSON.stringify(writeSchedulingPolicies(config, [entry]))) expect(saved.rules).toEqual([]) expect(saved.model_policies.map((policy: { model: string }) => policy.model)).toEqual(['*']) expect(readSchedulingPolicies(saved)[0]).toMatchObject({ scope: 'all', models: [] }) expect(getDefaultModelPolicy(saved).provider_priority_overrides).toEqual({ provider: 2 }) for (const model of ['model-a', 'model-b', 'future-model']) { expect(getModelScheduling(saved, model)).toMatchObject({ priority_mode: 'provider', scheduling_mode: 'load_balance' }) } }) it('persists one strategy for multiple models with shared rankings', () => { const config = createEmptyRoutingGroupConfig() const entry = createSchedulingPolicy(config) entry.models = ['model-a', 'model-b'] entry.priorityMode = 'global_key' entry.schedulingMode = 'fixed_order' entry.policy = { ...entry.policy, allowed_providers: ['provider-a'], provider_priority_overrides: { 'provider-a': 2 }, key_priority_overrides_by_format: { 'openai:chat': { 'key-a': 1 } }, pool_priority_overrides: { 'pool-a': 3 }, pool_policy_overrides: { 'pool-a': { scheduling_presets: [{ preset: 'cache_affinity', enabled: true }] } }, } const saved = writeSchedulingPolicies(config, [entry]) expect(saved.rules).toHaveLength(1) expect(saved.rules[0].conditions).toEqual({ any: [ { field: 'model', op: 'eq', value: 'model-a' }, { field: 'model', op: 'eq', value: 'model-b' }, ] }) for (const model of entry.models) { expect(getModelScheduling(saved, model)).toMatchObject({ priority_mode: 'provider', scheduling_mode: 'fixed_order' }) expect(getModelPolicy(saved, model)).toEqual({ ...entry.policy, model }) } expect(getModelScheduling(saved, 'other-model').scheduling_mode).toBe('cache_affinity') const reloaded = readSchedulingPolicies(JSON.parse(JSON.stringify(saved))) expect(reloaded).toHaveLength(1) expect(reloaded[0]).toMatchObject({ id: entry.id, models: ['model-a', 'model-b'], policy: entry.policy }) expect(schedulingPolicyEditorConfig(saved, reloaded[0]).model_policies).toEqual([entry.policy]) }) it('retains group exclusions in the isolated ranking editor config', () => { const config = createEmptyRoutingGroupConfig() config.disabled_providers = ['disabled-provider'] const entry = createSchedulingPolicy(config) const editorConfig = schedulingPolicyEditorConfig(config, entry) expect(editorConfig.disabled_providers).toEqual(['disabled-provider']) editorConfig.disabled_providers.push('local-only') expect(config.disabled_providers).toEqual(['disabled-provider']) }) it('retains separate strategies even when their settings are identical', () => { const config = createEmptyRoutingGroupConfig() const first = { ...createSchedulingPolicy(config), models: ['model-a'] } const second = { ...createSchedulingPolicy(config), models: ['model-b'] } const entries = readSchedulingPolicies(writeSchedulingPolicies(config, [first, second])) expect(entries.map(entry => entry.id)).toEqual([first.id, second.id]) }) it('loads legacy per-model policies, including rules without a ranking policy', () => { let config = createEmptyRoutingGroupConfig() for (const model of ['model-a', 'model-b']) { config = upsertModelPolicy(config, { ...createEmptyModelPolicy(model), provider_priority_overrides: { provider: 2 } }) config = upsertModelSchedulingRule(config, model, { priority_mode: 'provider', scheduling_mode: 'load_balance' }) } config = upsertModelSchedulingRule(config, 'model-c', { priority_mode: 'global_key', scheduling_mode: 'fixed_order' }) const entries = readSchedulingPolicies(config) expect(entries).toHaveLength(2) expect(entries[0].models).toEqual(['model-a', 'model-b']) expect(entries[1].models).toEqual(['model-c']) const saved = writeSchedulingPolicies(config, entries) for (const model of ['model-a', 'model-b', 'model-c', 'other']) { expect(getModelScheduling(saved, model)).toEqual({ ...getModelScheduling(config, model), priority_mode: 'provider' }) expect(getModelPolicy(saved, model)).toEqual(getModelPolicy(config, model)) } expect(saved.rules.every(rule => !rule.id.startsWith('ui_model_scheduling:'))).toBe(true) }) it('preserves group execution options, failover rules, and custom routing rules', () => { const config = createEmptyRoutingGroupConfig() config.default_policy.cancel_on_client_disconnect = true config.default_policy.sticky_key_attempts = 5 config.default_policy.max_transfer_count = 7 config.default_policy.failover_rules.error_stop_patterns = [{ pattern: '', status_codes: [429] }] const rule: RoutingRule = { id: 'custom-header-rule', priority: 3, enabled: true, phase: 'provider_request', conditions: { field: 'model', op: 'prefix', value: 'model-' }, actions: [{ type: 'set_header', name: 'x-test', value: 'kept' }], stop_processing: false, } config.rules.push(rule) const entry = { ...createSchedulingPolicy(config), models: ['model-a'] } const saved = writeSchedulingPolicies(config, [entry]) expect(saved.default_policy).toEqual(config.default_policy) expect(saved.rules[0]).toEqual(rule) expect(config.model_policies).toEqual([]) expect(config.rules).toEqual([rule]) }) it('keeps the wildcard fallback before specific rankings and preserves per-format keys', () => { let config = setDefaultProviderPriorityOverrides(createEmptyRoutingGroupConfig(), { provider: 4 }) config = setModelKeyPriorityOverridesForFormat(config, 'model-a', 'openai:chat', { key: 2 }) const entries = readSchedulingPolicies(config) expect(entries.map(entry => entry.scope)).toEqual(['selected', 'all']) const saved = writeSchedulingPolicies(config, entries) expect(saved.model_policies.map(policy => policy.model)).toEqual(['*', 'model-a']) expect(getModelPolicy(saved, 'model-a').key_priority_overrides_by_format).toEqual({ 'openai:chat': { key: 2 } }) expect(getModelPolicy(saved, '*').provider_priority_overrides).toEqual({ provider: 4 }) }) it('removes obsolete model rules when a model leaves a strategy', () => { const config = createEmptyRoutingGroupConfig() const entry = { ...createSchedulingPolicy(config), models: ['model-a', 'model-b'], schedulingMode: 'load_balance' as const } const previous = writeSchedulingPolicies(config, [entry]) const saved = writeSchedulingPolicies(previous, [{ ...entry, models: ['model-b', 'model-c'] }]) expect(saved.model_policies.map(policy => policy.model)).toEqual(['model-b', 'model-c']) expect(saved.rules).toHaveLength(1) expect(getModelScheduling(saved, 'model-a').scheduling_mode).toBe('cache_affinity') expect(getModelScheduling(saved, 'model-c').scheduling_mode).toBe('load_balance') }) it('preserves legacy model retry overrides and prefix matching', () => { const config = upsertModelSchedulingRule(createEmptyRoutingGroupConfig(), 'legacy-*', { priority_mode: 'provider', scheduling_mode: 'fixed_order', }) const rule = config.rules.find(rule => rule.id === modelSchedulingRuleId('legacy-*'))! rule.actions = [{ type: 'set_scheduling', priority_mode: 'provider', scheduling_mode: 'fixed_order', sticky_key_attempts: 4 }] const saved = writeSchedulingPolicies(config, readSchedulingPolicies(config)) expect(getModelScheduling(saved, 'legacy-model').sticky_key_attempts).toBe(4) expect(getModelScheduling(saved, 'other-model').scheduling_mode).toBe('cache_affinity') }) it('rejects empty or overlapping scopes', () => { const config = createEmptyRoutingGroupConfig() const first = createSchedulingPolicy(config) expect(validateSchedulingPolicies([first])).toContain('选择至少一个') first.models = ['model-a'] expect(validateSchedulingPolicies([first, { ...createSchedulingPolicy(config), models: ['model-a'] }])).toContain('不能重复') expect(validateSchedulingPolicies([createSchedulingPolicy(config, 'all'), createSchedulingPolicy(config, 'all')])).toContain('只能有一条') expect(validateSchedulingPolicies([])).not.toBeNull() }) })