22 * resolveProfileModel + migrateProxyProfile 单测 —— proxy 热切换按家族映射模型 + 旧配置迁移。
33 *
44 * resolveProfileModel:按 body.model 家族(opus/sonnet/haiku)映射到 profile 对应字段,
5- * fable/mythos/未识别 → ANTHROPIC_MODEL;无新字段但有 activeModel(旧数据)→ 整体替换回退。
5+ * fable/mythos/未识别 → ANTHROPIC_MODEL 兜底;无新字段但有 activeModel(旧数据)→ 整体替换回退。
6+ * 家族字段优先于 ANTHROPIC_MODEL;字段留空时回落到 ANTHROPIC_MODEL。
7+ * [1m] 后缀默认忽略(剥除后比较)。
68 * 目标为空 / 等于旧值 / 入参非法 → null(不改写)。
79 * migrate*:老 { models, activeModel } → ANTHROPIC_MODEL,幂等。
810 */
@@ -31,16 +33,60 @@ describe('resolveProfileModel', () => {
3133 assert . equal ( resolveProfileModel ( 'claude-fable-5' , fam ) , 'PRIMARY' ) ;
3234 assert . equal ( resolveProfileModel ( 'mythos-1' , fam ) , 'PRIMARY' ) ;
3335 } ) ;
34- it ( '未识别家族 → null(原样透传,不兜底替换)' , ( ) => {
35- assert . equal ( resolveProfileModel ( 'some-random-model' , fam ) , null ) ;
36- assert . equal ( resolveProfileModel ( 'gpt-4o' , fam ) , null ) ;
36+ it ( '未识别家族 → ANTHROPIC_MODEL 兜底(热切换开启时)' , ( ) => {
37+ assert . equal ( resolveProfileModel ( 'some-random-model' , fam ) , 'PRIMARY' ) ;
38+ assert . equal ( resolveProfileModel ( 'gpt-4o' , fam ) , 'PRIMARY' ) ;
39+ assert . equal ( resolveProfileModel ( 'K3' , fam ) , 'PRIMARY' ) ; // K3/kimi alias → PRIMARY
40+ assert . equal ( resolveProfileModel ( 'deepseek-v4' , fam ) , 'PRIMARY' ) ;
3741 } ) ;
38- it ( '家族字段留空 → 该家族不替换(null)' , ( ) => {
42+ it ( '未识别家族 + 无 ANTHROPIC_MODEL → null(无兜底目标)' , ( ) => {
43+ const noPrimary = { ANTHROPIC_DEFAULT_OPUS_MODEL : 'OPUS-T' } ;
44+ assert . equal ( resolveProfileModel ( 'some-random-model' , noPrimary ) , null ) ;
45+ assert . equal ( resolveProfileModel ( 'gpt-4o' , noPrimary ) , null ) ;
46+ } ) ;
47+ it ( '家族字段留空 → 回落到 ANTHROPIC_MODEL' , ( ) => {
3948 const partial = { ANTHROPIC_MODEL : 'PRIMARY' , ANTHROPIC_DEFAULT_HAIKU_MODEL : 'HAIKU-T' } ;
40- assert . equal ( resolveProfileModel ( 'claude-opus-4-8' , partial ) , null ) ; // opus 空 → 不回落到 PRIMARY
41- assert . equal ( resolveProfileModel ( 'claude-3-5-haiku' , partial ) , 'HAIKU-T' ) ;
49+ assert . equal ( resolveProfileModel ( 'claude-opus-4-8' , partial ) , 'PRIMARY' ) ; // opus 空 → PRIMARY 兜底
50+ assert . equal ( resolveProfileModel ( 'claude-sonnet-4' , partial ) , 'PRIMARY' ) ; // sonnet 空 → PRIMARY 兜底
51+ assert . equal ( resolveProfileModel ( 'claude-3-5-haiku' , partial ) , 'HAIKU-T' ) ; // haiku 有专属字段
4252 assert . equal ( resolveProfileModel ( 'claude-fable-5' , partial ) , 'PRIMARY' ) ;
4353 } ) ;
54+ it ( 'ANTHROPIC_MODEL 留空 + 家族字段留空 → 该家族不替换(null)' , ( ) => {
55+ // Only HAIKU field set, no PRIMARY, opus/sonnet both empty → no fallback target
56+ const haikuOnly = { ANTHROPIC_DEFAULT_HAIKU_MODEL : 'HAIKU-T' } ;
57+ assert . equal ( resolveProfileModel ( 'claude-opus-4-8' , haikuOnly ) , null ) ;
58+ assert . equal ( resolveProfileModel ( 'claude-sonnet-4' , haikuOnly ) , null ) ;
59+ assert . equal ( resolveProfileModel ( 'claude-3-5-haiku' , haikuOnly ) , 'HAIKU-T' ) ;
60+ } ) ;
61+ it ( '[1m] suffix in profile fields is stripped before comparison' , ( ) => {
62+ const with1m = {
63+ ANTHROPIC_MODEL : 'claude-sonnet-5[1m]' ,
64+ ANTHROPIC_DEFAULT_OPUS_MODEL : 'claude-opus-4-8[1m]' ,
65+ } ;
66+ assert . equal ( resolveProfileModel ( 'claude-fable-5' , with1m ) , 'claude-sonnet-5' ) ;
67+ // opus field strips to 'claude-opus-4-8' which equals oldModel → null (no-op)
68+ assert . equal ( resolveProfileModel ( 'claude-opus-4-8' , with1m ) , null ) ;
69+ } ) ;
70+ it ( '[1m] suffix case-insensitive strip' , ( ) => {
71+ assert . equal (
72+ resolveProfileModel ( 'claude-opus-4-8' , { ANTHROPIC_DEFAULT_OPUS_MODEL : 'CLAUDE-OPUS-4[1M]' } ) ,
73+ 'CLAUDE-OPUS-4'
74+ ) ;
75+ } ) ;
76+ it ( 'K3[1m] → strips to K3' , ( ) => {
77+ assert . equal (
78+ resolveProfileModel ( 'gpt-4o' , { ANTHROPIC_MODEL : 'K3[1m]' } ) ,
79+ 'K3'
80+ ) ;
81+ } ) ;
82+ it ( 'target equals oldModel after [1m] strip → null (no-op)' , ( ) => {
83+ // Profile field carries [1m], oldModel doesn't → no-op
84+ assert . equal ( resolveProfileModel ( 'my-model' , { ANTHROPIC_MODEL : 'my-model[1m]' } ) , null ) ;
85+ // Both oldModel and profile field carry [1m] → symmetric strip → no-op
86+ assert . equal ( resolveProfileModel ( 'claude-opus-4-8[1m]' , { ANTHROPIC_DEFAULT_OPUS_MODEL : 'claude-opus-4-8[1m]' } ) , null ) ;
87+ // oldModel carries [1m], profile field doesn't → no-op (oldModel also stripped before compare)
88+ assert . equal ( resolveProfileModel ( 'claude-opus-4-8[1m]' , { ANTHROPIC_DEFAULT_OPUS_MODEL : 'claude-opus-4-8' } ) , null ) ;
89+ } ) ;
4490 it ( '目标等于旧值 → null(无需改写)' , ( ) => {
4591 // fable 命中 primary,且旧值已是 PRIMARY
4692 assert . equal ( resolveProfileModel ( 'PRIMARY' , fam ) , null ) ;
@@ -61,8 +107,8 @@ describe('resolveProfileModel', () => {
61107 it ( '新字段存在时忽略 legacy activeModel' , ( ) => {
62108 const mixed = { ANTHROPIC_MODEL : 'PRIMARY' , activeModel : 'LEGACY-T' } ;
63109 assert . equal ( resolveProfileModel ( 'claude-fable-5' , mixed ) , 'PRIMARY' ) ;
64- // opus 无字段 → 不替换 (不回退到 legacy)
65- assert . equal ( resolveProfileModel ( 'claude-opus-4-8' , mixed ) , null ) ;
110+ // opus 无字段 → PRIMARY 兜底 (不回退到 legacy)
111+ assert . equal ( resolveProfileModel ( 'claude-opus-4-8' , mixed ) , 'PRIMARY' ) ;
66112 } ) ;
67113 it ( '空 profile / 空 model / 非法入参 → null' , ( ) => {
68114 assert . equal ( resolveProfileModel ( 'claude-opus-4-8' , { } ) , null ) ;
0 commit comments