Skip to content

Commit 8197efd

Browse files
committed
Bump coverage
1 parent 56b50f5 commit 8197efd

3 files changed

Lines changed: 314 additions & 0 deletions

File tree

src/api/providers/__tests__/moonshot.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,42 @@ describe("MoonshotHandler", () => {
285285
expect(result.cacheWriteTokens).toBe(0)
286286
expect(result.cacheReadTokens).toBeUndefined()
287287
})
288+
289+
it("should handle cached_tokens at top level (not in prompt_tokens_details)", () => {
290+
class TestMoonshotHandler extends MoonshotHandler {
291+
public testProcessUsageMetrics(usage: any) {
292+
return this.processUsageMetrics(usage)
293+
}
294+
}
295+
296+
const testHandler = new TestMoonshotHandler(mockOptions)
297+
298+
const usage = {
299+
prompt_tokens: 100,
300+
completion_tokens: 50,
301+
cached_tokens: 15,
302+
}
303+
304+
const result = testHandler.testProcessUsageMetrics(usage)
305+
306+
expect(result.cacheReadTokens).toBe(15)
307+
})
308+
309+
it("should handle null usage gracefully", () => {
310+
class TestMoonshotHandler extends MoonshotHandler {
311+
public testProcessUsageMetrics(usage: any) {
312+
return this.processUsageMetrics(usage)
313+
}
314+
}
315+
316+
const testHandler = new TestMoonshotHandler(mockOptions)
317+
318+
const result = testHandler.testProcessUsageMetrics(null)
319+
320+
expect(result.inputTokens).toBe(0)
321+
expect(result.outputTokens).toBe(0)
322+
expect(result.cacheReadTokens).toBeUndefined()
323+
})
288324
})
289325

290326
describe("addMaxTokensIfNeeded", () => {

src/api/providers/fetchers/__tests__/moonshot.spec.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,103 @@ describe("getMoonshotModels", () => {
7878

7979
expect(globalThis.fetch).toHaveBeenCalledWith("https://api.moonshot.cn/v1/models", expect.any(Object))
8080
})
81+
82+
it("throws when response data is not an array", async () => {
83+
globalThis.fetch = vi.fn().mockResolvedValue({
84+
ok: true,
85+
json: vi.fn().mockResolvedValue({ data: "not-an-array" }),
86+
}) as unknown as typeof fetch
87+
88+
await expect(getMoonshotModels("https://api.moonshot.ai/v1", "mock-key")).rejects.toThrow(
89+
"Unexpected response format",
90+
)
91+
})
92+
93+
it("throws when response data is missing", async () => {
94+
globalThis.fetch = vi.fn().mockResolvedValue({
95+
ok: true,
96+
json: vi.fn().mockResolvedValue({}),
97+
}) as unknown as typeof fetch
98+
99+
await expect(getMoonshotModels("https://api.moonshot.ai/v1", "mock-key")).rejects.toThrow(
100+
"Unexpected response format",
101+
)
102+
})
103+
104+
it("skips models with empty or non-string ID", async () => {
105+
globalThis.fetch = vi.fn().mockResolvedValue({
106+
ok: true,
107+
json: vi.fn().mockResolvedValue({
108+
data: [{ id: "" }, { id: 123 }, { id: null }, { id: "kimi-k2-0905-preview" }],
109+
}),
110+
}) as unknown as typeof fetch
111+
112+
const models = await getMoonshotModels("https://api.moonshot.ai/v1", "mock-key")
113+
114+
expect(Object.keys(models)).toHaveLength(1)
115+
expect(models["kimi-k2-0905-preview"]).toBeDefined()
116+
})
117+
118+
it("includes Authorization header when apiKey provided", async () => {
119+
globalThis.fetch = vi.fn().mockResolvedValue({
120+
ok: true,
121+
json: vi.fn().mockResolvedValue({ data: [] }),
122+
}) as unknown as typeof fetch
123+
124+
await getMoonshotModels("https://api.moonshot.ai/v1", "my-secret-key")
125+
126+
expect(globalThis.fetch).toHaveBeenCalledWith(
127+
"https://api.moonshot.ai/v1/models",
128+
expect.objectContaining({
129+
headers: expect.objectContaining({
130+
Authorization: "Bearer my-secret-key",
131+
}),
132+
}),
133+
)
134+
})
135+
136+
it("does not include Authorization header when no apiKey", async () => {
137+
globalThis.fetch = vi.fn().mockResolvedValue({
138+
ok: true,
139+
json: vi.fn().mockResolvedValue({ data: [] }),
140+
}) as unknown as typeof fetch
141+
142+
await getMoonshotModels("https://api.moonshot.ai/v1", undefined)
143+
144+
const callArgs = (globalThis.fetch as any).mock.calls[0][1].headers
145+
expect(callArgs["Authorization"]).toBeUndefined()
146+
})
147+
148+
it("mixes known and unknown models in same response", async () => {
149+
globalThis.fetch = vi.fn().mockResolvedValue({
150+
ok: true,
151+
json: vi.fn().mockResolvedValue({
152+
data: [{ id: "kimi-k2-0905-preview" }, { id: "some-new-model" }],
153+
}),
154+
}) as unknown as typeof fetch
155+
156+
const models = await getMoonshotModels("https://api.moonshot.ai/v1", "mock-key")
157+
158+
expect(models["kimi-k2-0905-preview"]).toEqual(moonshotModels["kimi-k2-0905-preview"])
159+
expect(models["some-new-model"]).toEqual({
160+
maxTokens: 16_000,
161+
contextWindow: 262_144,
162+
supportsImages: false,
163+
supportsPromptCache: true,
164+
description: "Moonshot model: some-new-model",
165+
})
166+
})
167+
168+
it("handles HTTP error with unreadable body gracefully", async () => {
169+
globalThis.fetch = vi.fn().mockResolvedValue({
170+
ok: false,
171+
status: 500,
172+
statusText: "Internal Server Error",
173+
text: vi.fn().mockRejectedValue(new Error("network error")),
174+
}) as unknown as typeof fetch
175+
176+
await expect(getMoonshotModels("https://api.moonshot.ai/v1", "mock-key")).rejects.toThrow(
177+
"HTTP 500: Internal Server Error",
178+
)
179+
})
81180
})

src/core/webview/__tests__/webviewMessageHandler.routerModels.spec.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,183 @@ describe("webviewMessageHandler - requestRouterModels provider filter", () => {
295295
baseUrl: "http://stored:4000",
296296
})
297297
})
298+
299+
it("fetches Moonshot models when stored Moonshot credentials exist", async () => {
300+
mockProvider.getState.mockResolvedValue({
301+
apiConfiguration: {
302+
moonshotApiKey: "stored-moonshot-key",
303+
moonshotBaseUrl: "https://api.moonshot.ai/v1",
304+
},
305+
})
306+
307+
getModelsMock.mockImplementation(async (options: any) => {
308+
if (options?.provider === "moonshot") {
309+
return { "kimi-k2-0905-preview": { contextWindow: 262144, supportsPromptCache: true } }
310+
}
311+
312+
switch (options?.provider) {
313+
case "openrouter":
314+
return { "openrouter/qwen2.5": { contextWindow: 32768, supportsPromptCache: false } }
315+
case "requesty":
316+
return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } }
317+
case "vercel-ai-gateway":
318+
return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } }
319+
case "litellm":
320+
return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } }
321+
default:
322+
return {}
323+
}
324+
})
325+
326+
await webviewMessageHandler(
327+
mockProvider as any,
328+
{
329+
type: "requestRouterModels",
330+
} as any,
331+
)
332+
333+
expect(getModelsMock).toHaveBeenCalledWith({
334+
provider: "moonshot",
335+
apiKey: "stored-moonshot-key",
336+
baseUrl: "https://api.moonshot.ai/v1",
337+
})
338+
339+
const call = (mockProvider.postMessageToWebview as any).mock.calls.find(
340+
(c: any[]) => c[0]?.type === "routerModels",
341+
)
342+
expect(call).toBeTruthy()
343+
expect(call[0].routerModels.moonshot).toEqual({
344+
"kimi-k2-0905-preview": { contextWindow: 262144, supportsPromptCache: true },
345+
})
346+
})
347+
348+
it("flushes Moonshot cache when explicit apiKey provided via message values", async () => {
349+
getModelsMock.mockResolvedValue({
350+
"kimi-k2-0905-preview": { contextWindow: 262144, supportsPromptCache: true },
351+
})
352+
353+
await webviewMessageHandler(
354+
mockProvider as any,
355+
{
356+
type: "requestRouterModels",
357+
values: {
358+
moonshotApiKey: "new-moonshot-key",
359+
moonshotBaseUrl: "https://api.moonshot.cn/v1",
360+
},
361+
} as any,
362+
)
363+
364+
// flushModels should have been called for moonshot
365+
const moonshotFlushCalls = flushModelsMock.mock.calls.filter((c: any[]) => c[0]?.provider === "moonshot")
366+
expect(moonshotFlushCalls.length).toBe(1)
367+
expect(moonshotFlushCalls[0][0]).toEqual({
368+
provider: "moonshot",
369+
apiKey: "new-moonshot-key",
370+
baseUrl: "https://api.moonshot.cn/v1",
371+
})
372+
373+
// getModels should use the provided credentials
374+
const moonshotCalls = getModelsMock.mock.calls.filter((c: any[]) => c[0]?.provider === "moonshot")
375+
expect(moonshotCalls.length).toBe(1)
376+
expect(moonshotCalls[0][0]).toEqual({
377+
provider: "moonshot",
378+
apiKey: "new-moonshot-key",
379+
baseUrl: "https://api.moonshot.cn/v1",
380+
})
381+
})
382+
383+
it("does not flush Moonshot cache when using stored credentials", async () => {
384+
mockProvider.getState.mockResolvedValue({
385+
apiConfiguration: {
386+
moonshotApiKey: "stored-moonshot-key",
387+
},
388+
})
389+
390+
getModelsMock.mockImplementation(async (options: any) => {
391+
if (options?.provider === "moonshot") {
392+
return { "kimi-k2-0905-preview": { contextWindow: 262144, supportsPromptCache: true } }
393+
}
394+
395+
switch (options?.provider) {
396+
case "openrouter":
397+
return { "openrouter/qwen2.5": { contextWindow: 32768, supportsPromptCache: false } }
398+
case "requesty":
399+
return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } }
400+
case "vercel-ai-gateway":
401+
return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } }
402+
case "litellm":
403+
return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } }
404+
default:
405+
return {}
406+
}
407+
})
408+
409+
await webviewMessageHandler(
410+
mockProvider as any,
411+
{
412+
type: "requestRouterModels",
413+
} as any,
414+
)
415+
416+
// flushModels should NOT have been called for moonshot
417+
const moonshotFlushCalls = flushModelsMock.mock.calls.filter((c: any[]) => c[0]?.provider === "moonshot")
418+
expect(moonshotFlushCalls.length).toBe(0)
419+
420+
// getModels should still have been called with stored credentials
421+
const moonshotCalls = getModelsMock.mock.calls.filter((c: any[]) => c[0]?.provider === "moonshot")
422+
expect(moonshotCalls.length).toBe(1)
423+
expect(moonshotCalls[0][0]).toEqual({
424+
provider: "moonshot",
425+
apiKey: "stored-moonshot-key",
426+
baseUrl: undefined,
427+
})
428+
})
429+
430+
it("posts a Moonshot provider error and keeps an empty aggregate entry when fetch fails", async () => {
431+
mockProvider.getState.mockResolvedValue({
432+
apiConfiguration: {
433+
moonshotApiKey: "stored-moonshot-key",
434+
},
435+
})
436+
437+
getModelsMock.mockImplementation(async (options: any) => {
438+
if (options?.provider === "moonshot") {
439+
throw new Error("Moonshot API error")
440+
}
441+
442+
switch (options?.provider) {
443+
case "openrouter":
444+
return { "openrouter/qwen2.5": { contextWindow: 32768, supportsPromptCache: false } }
445+
case "requesty":
446+
return { "requesty/model": { contextWindow: 8192, supportsPromptCache: false } }
447+
case "vercel-ai-gateway":
448+
return { "vercel/model": { contextWindow: 8192, supportsPromptCache: false } }
449+
case "litellm":
450+
return { "litellm/model": { contextWindow: 8192, supportsPromptCache: false } }
451+
default:
452+
return {}
453+
}
454+
})
455+
456+
await webviewMessageHandler(
457+
mockProvider as any,
458+
{
459+
type: "requestRouterModels",
460+
} as any,
461+
)
462+
463+
// Should have posted an error for moonshot
464+
const errorCall = (mockProvider.postMessageToWebview as any).mock.calls.find(
465+
(c: any[]) => c[0]?.type === "singleRouterModelFetchResponse" && c[0]?.values?.provider === "moonshot",
466+
)
467+
expect(errorCall).toBeTruthy()
468+
expect(errorCall[0].success).toBe(false)
469+
470+
// Aggregate entry should still be empty
471+
const call = (mockProvider.postMessageToWebview as any).mock.calls.find(
472+
(c: any[]) => c[0]?.type === "routerModels",
473+
)
474+
expect(call).toBeTruthy()
475+
expect(call[0].routerModels.moonshot).toEqual({})
476+
})
298477
})

0 commit comments

Comments
 (0)