@@ -79,7 +79,11 @@ describe('refreshModelsForAccount credential discovery', () => {
7979 expect ( result ) . toMatchObject ( {
8080 accountId : account . id ,
8181 refreshed : true ,
82+ status : 'success' ,
83+ errorCode : null ,
84+ errorMessage : '' ,
8285 modelCount : 2 ,
86+ modelsPreview : [ 'claude-sonnet-4-5-20250929' , 'claude-opus-4-6' ] ,
8387 tokenScanned : 0 ,
8488 discoveredByCredential : true ,
8589 } ) ;
@@ -95,4 +99,121 @@ describe('refreshModelsForAccount credential discovery', () => {
9599 const tokenRows = await db . select ( ) . from ( schema . tokenModelAvailability ) . all ( ) ;
96100 expect ( tokenRows ) . toHaveLength ( 0 ) ;
97101 } ) ;
102+
103+ it ( 'marks runtime health unhealthy when model discovery fails' , async ( ) => {
104+ getApiTokenMock . mockResolvedValue ( null ) ;
105+ getModelsMock . mockRejectedValue ( new Error ( 'HTTP 401: invalid token' ) ) ;
106+
107+ const site = await db . insert ( schema . sites ) . values ( {
108+ name : 'site-fail' ,
109+ url : 'https://site-fail.example.com' ,
110+ platform : 'new-api' ,
111+ status : 'active' ,
112+ } ) . returning ( ) . get ( ) ;
113+
114+ const account = await db . insert ( schema . accounts ) . values ( {
115+ siteId : site . id ,
116+ username : 'fail-user' ,
117+ accessToken : '' ,
118+ apiToken : 'sk-invalid' ,
119+ status : 'active' ,
120+ extraConfig : JSON . stringify ( { credentialMode : 'apikey' } ) ,
121+ } ) . returning ( ) . get ( ) ;
122+
123+ const result = await refreshModelsForAccount ( account . id ) ;
124+
125+ expect ( result ) . toMatchObject ( {
126+ accountId : account . id ,
127+ refreshed : true ,
128+ modelCount : 0 ,
129+ modelsPreview : [ ] ,
130+ tokenScanned : 0 ,
131+ status : 'failed' ,
132+ errorCode : 'unauthorized' ,
133+ } ) ;
134+
135+ const latest = await db . select ( ) . from ( schema . accounts )
136+ . where ( eq ( schema . accounts . id , account . id ) )
137+ . get ( ) ;
138+ const parsed = JSON . parse ( latest ! . extraConfig || '{}' ) ;
139+ expect ( parsed . runtimeHealth ?. state ) . toBe ( 'unhealthy' ) ;
140+ expect ( parsed . runtimeHealth ?. source ) . toBe ( 'model-discovery' ) ;
141+ expect ( parsed . runtimeHealth ?. reason ) . toBe ( '模型获取失败,API Key 已无效' ) ;
142+ expect ( parsed . runtimeHealth ?. checkedAt ) . toMatch ( / \d { 4 } - \d { 2 } - \d { 2 } T / ) ;
143+ } ) ;
144+
145+ it ( 'returns structured result when account missing' , async ( ) => {
146+ const result = await refreshModelsForAccount ( 9999 ) ;
147+
148+ expect ( result ) . toMatchObject ( {
149+ accountId : 9999 ,
150+ refreshed : false ,
151+ status : 'failed' ,
152+ errorCode : 'account_not_found' ,
153+ errorMessage : '账号不存在' ,
154+ modelCount : 0 ,
155+ modelsPreview : [ ] ,
156+ reason : 'account_not_found' ,
157+ } ) ;
158+ } ) ;
159+
160+ it ( 'returns structured result when site disabled' , async ( ) => {
161+ const site = await db . insert ( schema . sites ) . values ( {
162+ name : 'site-disabled' ,
163+ url : 'https://site-disabled.example.com' ,
164+ platform : 'new-api' ,
165+ status : 'disabled' ,
166+ } ) . returning ( ) . get ( ) ;
167+
168+ const account = await db . insert ( schema . accounts ) . values ( {
169+ siteId : site . id ,
170+ username : 'disabled-user' ,
171+ accessToken : 'session-token' ,
172+ apiToken : null ,
173+ status : 'active' ,
174+ } ) . returning ( ) . get ( ) ;
175+
176+ const result = await refreshModelsForAccount ( account . id ) ;
177+
178+ expect ( result ) . toMatchObject ( {
179+ accountId : account . id ,
180+ refreshed : false ,
181+ status : 'skipped' ,
182+ errorCode : 'site_disabled' ,
183+ errorMessage : '站点已禁用' ,
184+ modelCount : 0 ,
185+ modelsPreview : [ ] ,
186+ reason : 'site_disabled' ,
187+ } ) ;
188+ } ) ;
189+
190+ it ( 'returns structured result when account inactive' , async ( ) => {
191+ const site = await db . insert ( schema . sites ) . values ( {
192+ name : 'site-inactive' ,
193+ url : 'https://site-inactive.example.com' ,
194+ platform : 'new-api' ,
195+ status : 'active' ,
196+ } ) . returning ( ) . get ( ) ;
197+
198+ const account = await db . insert ( schema . accounts ) . values ( {
199+ siteId : site . id ,
200+ username : 'inactive-user' ,
201+ accessToken : 'session-token' ,
202+ apiToken : null ,
203+ status : 'disabled' ,
204+ } ) . returning ( ) . get ( ) ;
205+
206+ const result = await refreshModelsForAccount ( account . id ) ;
207+
208+ expect ( result ) . toMatchObject ( {
209+ accountId : account . id ,
210+ refreshed : false ,
211+ status : 'skipped' ,
212+ errorCode : 'adapter_or_status' ,
213+ errorMessage : '平台不可用或账号未激活' ,
214+ modelCount : 0 ,
215+ modelsPreview : [ ] ,
216+ reason : 'adapter_or_status' ,
217+ } ) ;
218+ } ) ;
98219} ) ;
0 commit comments