11import { buildApiHandler } from "../../index"
22import { KimiCodeHandler } from "../kimi-code"
3- import type { Mock } from "vitest"
4-
5- vi . mock ( "../../../integrations/kimi-code/oauth" , ( ) => {
6- const mockGetAccessToken = vi . fn ( ) . mockResolvedValue ( "oauth-token" )
7- const mockForceRefreshAccessToken = vi . fn ( ) . mockResolvedValue ( "refreshed-token" )
8- return {
9- kimiCodeOAuthManager : {
10- getAccessToken : mockGetAccessToken ,
11- forceRefreshAccessToken : mockForceRefreshAccessToken ,
12- } ,
13- mockGetAccessToken,
14- mockForceRefreshAccessToken,
15- }
16- } )
173
18- vi . mock ( "../fetchers/modelCache" , ( ) => {
19- const mockGetModels = vi . fn ( ) . mockRejectedValue ( new Error ( "offline" ) )
20- return {
21- getModels : mockGetModels ,
22- mockGetModels,
23- }
24- } )
4+ const { mockGetAccessToken, mockForceRefreshAccessToken, mockGetModels } = vi . hoisted ( ( ) => ( {
5+ mockGetAccessToken : vi . fn ( ) ,
6+ mockForceRefreshAccessToken : vi . fn ( ) ,
7+ mockGetModels : vi . fn ( ) ,
8+ } ) )
9+
10+ vi . mock ( "../../../integrations/kimi-code/oauth" , ( ) => ( {
11+ kimiCodeOAuthManager : {
12+ getAccessToken : mockGetAccessToken ,
13+ forceRefreshAccessToken : mockForceRefreshAccessToken ,
14+ } ,
15+ } ) )
2516
26- const { mockGetAccessToken, mockForceRefreshAccessToken } = await import ( "../../../integrations/kimi-code/oauth" )
27- const { mockGetModels } = await import ( "../fetchers/modelCache" )
17+ vi . mock ( "../fetchers/modelCache" , ( ) => ( { getModels : mockGetModels } ) )
2818
2919describe ( "KimiCodeHandler" , ( ) => {
3020 beforeEach ( ( ) => {
3121 vi . clearAllMocks ( )
32- ; ( mockGetAccessToken as any ) . mockResolvedValue ( "oauth-token" )
33- ; ( mockForceRefreshAccessToken as any ) . mockResolvedValue ( "refreshed-token" )
34- ; ( mockGetModels as any ) . mockRejectedValue ( new Error ( "offline" ) )
22+ mockGetAccessToken . mockResolvedValue ( "oauth-token" )
23+ mockForceRefreshAccessToken . mockResolvedValue ( "refreshed-token" )
24+ mockGetModels . mockRejectedValue ( new Error ( "offline" ) )
3525 } )
3626
3727 it ( "is dispatched separately from Moonshot and preserves an unknown selected model" , ( ) => {
@@ -65,7 +55,7 @@ describe("KimiCodeHandler", () => {
6555 } catch {
6656 // expected - mock is incomplete
6757 }
68- expect ( mockGetAccessToken as any ) . not . toHaveBeenCalled ( )
58+ expect ( mockGetAccessToken ) . not . toHaveBeenCalled ( )
6959 } )
7060
7161 it ( "uses OAuth token when auth method is oauth or not specified" , async ( ) => {
@@ -78,11 +68,11 @@ describe("KimiCodeHandler", () => {
7868 } catch {
7969 // expected - mock will fail
8070 }
81- expect ( mockGetAccessToken as any ) . toHaveBeenCalled ( )
71+ expect ( mockGetAccessToken ) . toHaveBeenCalled ( )
8272 } )
8373
8474 it ( "throws error when OAuth is required but no token available" , async ( ) => {
85- ; ( mockGetAccessToken as any ) . mockResolvedValueOnce ( null )
75+ mockGetAccessToken . mockResolvedValueOnce ( null )
8676 const handler = new KimiCodeHandler ( { kimiCodeAuthMethod : "oauth" } )
8777 const gen = handler . createMessage ( "system" , [ { role : "user" , content : "test" } ] )
8878 await expect ( async ( ) => {
@@ -105,9 +95,7 @@ describe("KimiCodeHandler", () => {
10595 it ( "retries with forced refresh on 401 when using OAuth" , async ( ) => {
10696 const handler = new KimiCodeHandler ( { kimiCodeAuthMethod : "oauth" } )
10797 const fetchSpy = vi . spyOn ( globalThis , "fetch" )
108- fetchSpy . mockResolvedValueOnce (
109- new Response ( null , { status : 401 } ) ,
110- )
98+ fetchSpy . mockResolvedValueOnce ( new Response ( null , { status : 401 } ) )
11199 fetchSpy . mockResolvedValueOnce (
112100 new Response ( JSON . stringify ( { choices : [ { message : { content : "ok" } , finish_reason : "stop" } ] } ) , {
113101 status : 200 ,
@@ -121,25 +109,36 @@ describe("KimiCodeHandler", () => {
121109 } catch {
122110 // expected - mock is incomplete
123111 }
124- expect ( mockForceRefreshAccessToken as any ) . toHaveBeenCalled ( )
112+ expect ( mockForceRefreshAccessToken ) . toHaveBeenCalledOnce ( )
113+ } )
114+
115+ it ( "force-refreshes and retries exactly once after a non-streaming OAuth 401" , async ( ) => {
116+ const handler = new KimiCodeHandler ( { kimiCodeAuthMethod : "oauth" } )
117+ const unauthorized = Object . assign ( new Error ( "Unauthorized" ) , { status : 401 } )
118+ const createCompletion = vi
119+ . spyOn ( ( handler as any ) . client . chat . completions , "create" )
120+ . mockRejectedValueOnce ( unauthorized )
121+ . mockResolvedValueOnce ( { choices : [ { message : { content : "retried" } } ] } )
122+
123+ await expect ( handler . completePrompt ( "test" ) ) . resolves . toBe ( "retried" )
124+ expect ( mockForceRefreshAccessToken ) . toHaveBeenCalledOnce ( )
125+ expect ( createCompletion ) . toHaveBeenCalledTimes ( 2 )
125126 } )
126127
127128 it ( "does not retry on 401 when using API key auth" , async ( ) => {
128129 const handler = new KimiCodeHandler ( { kimiCodeAuthMethod : "api-key" , kimiCodeApiKey : "key" } )
129- const fetchSpy = vi . spyOn ( globalThis , "fetch" ) . mockResolvedValueOnce (
130- new Response ( null , { status : 401 } ) ,
131- )
130+ const fetchSpy = vi . spyOn ( globalThis , "fetch" ) . mockResolvedValueOnce ( new Response ( null , { status : 401 } ) )
132131 const gen = handler . createMessage ( "system" , [ { role : "user" , content : "test" } ] )
133132 await expect ( async ( ) => {
134133 for await ( const chunk of gen ) {
135134 // consume
136135 }
137136 } ) . rejects . toThrow ( )
138- expect ( mockForceRefreshAccessToken as any ) . not . toHaveBeenCalled ( )
137+ expect ( mockForceRefreshAccessToken ) . not . toHaveBeenCalled ( )
139138 } )
140139
141140 it ( "fetches models during prepareRequest" , async ( ) => {
142- ; ( mockGetModels as any ) . mockResolvedValueOnce ( { "test-model" : { maxTokens : 1000 } } )
141+ mockGetModels . mockResolvedValueOnce ( { "test-model" : { maxTokens : 1000 } } )
143142 const handler = new KimiCodeHandler ( { kimiCodeAuthMethod : "api-key" , kimiCodeApiKey : "key" } )
144143 const gen = handler . createMessage ( "system" , [ { role : "user" , content : "test" } ] )
145144 try {
@@ -149,11 +148,11 @@ describe("KimiCodeHandler", () => {
149148 } catch {
150149 // expected
151150 }
152- expect ( mockGetModels as any ) . toHaveBeenCalled ( )
151+ expect ( mockGetModels ) . toHaveBeenCalled ( )
153152 } )
154153
155154 it ( "continues when model discovery fails" , async ( ) => {
156- ; ( mockGetModels as any ) . mockRejectedValueOnce ( new Error ( "discovery failed" ) )
155+ mockGetModels . mockRejectedValueOnce ( new Error ( "discovery failed" ) )
157156 const handler = new KimiCodeHandler ( { kimiCodeAuthMethod : "api-key" , kimiCodeApiKey : "key" } )
158157 const gen = handler . createMessage ( "system" , [ { role : "user" , content : "test" } ] )
159158 try {
@@ -163,11 +162,27 @@ describe("KimiCodeHandler", () => {
163162 } catch {
164163 // expected - different error
165164 }
166- expect ( mockGetModels as any ) . toHaveBeenCalled ( )
165+ expect ( mockGetModels ) . toHaveBeenCalled ( )
166+ } )
167+
168+ it . each ( [
169+ [ "failure" , ( ) => Promise . reject ( new Error ( "offline" ) ) ] ,
170+ [ "empty response" , ( ) => Promise . resolve ( { } ) ] ,
171+ ] ) ( "does not repeatedly block requests after model discovery %s" , async ( _case , discovery ) => {
172+ mockGetModels . mockImplementationOnce ( discovery )
173+ vi . spyOn ( globalThis , "fetch" ) . mockImplementation (
174+ async ( ) => new Response ( JSON . stringify ( { choices : [ { message : { content : "ok" } } ] } ) , { status : 200 } ) ,
175+ )
176+ const handler = new KimiCodeHandler ( { kimiCodeAuthMethod : "api-key" , kimiCodeApiKey : "key" } )
177+
178+ await handler . completePrompt ( "first" )
179+ await handler . completePrompt ( "second" )
180+
181+ expect ( mockGetModels ) . toHaveBeenCalledOnce ( )
167182 } )
168183
169184 it ( "uses discovered model info when available" , async ( ) => {
170- ; ( mockGetModels as any ) . mockResolvedValueOnce ( { "kimi-for-coding" : { maxTokens : 8000 , contextWindow : 128000 } } )
185+ mockGetModels . mockResolvedValueOnce ( { "kimi-for-coding" : { maxTokens : 8000 , contextWindow : 128000 } } )
171186 const handler = new KimiCodeHandler ( { kimiCodeAuthMethod : "api-key" , kimiCodeApiKey : "key" } )
172187 const gen = handler . createMessage ( "system" , [ { role : "user" , content : "test" } ] )
173188 try {
0 commit comments