Skip to content

Commit a60adf4

Browse files
authored
Merge pull request #50 from cita-777/codex/model-refresh-health
Codex/model refresh health
2 parents 803c01f + 747b985 commit a60adf4

File tree

4 files changed

+367
-77
lines changed

4 files changed

+367
-77
lines changed

src/server/services/modelService.discovery.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)