-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathkimi-code.spec.ts
More file actions
198 lines (178 loc) · 7.06 KB
/
Copy pathkimi-code.spec.ts
File metadata and controls
198 lines (178 loc) · 7.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
import { buildApiHandler } from "../../index"
import { KimiCodeHandler } from "../kimi-code"
const { mockGetAccessToken, mockForceRefreshAccessToken, mockGetModels } = vi.hoisted(() => ({
mockGetAccessToken: vi.fn(),
mockForceRefreshAccessToken: vi.fn(),
mockGetModels: vi.fn(),
}))
vi.mock("../../../integrations/kimi-code/oauth", () => ({
kimiCodeOAuthManager: {
getAccessToken: mockGetAccessToken,
forceRefreshAccessToken: mockForceRefreshAccessToken,
},
}))
vi.mock("../fetchers/modelCache", () => ({ getModels: mockGetModels }))
describe("KimiCodeHandler", () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetAccessToken.mockResolvedValue("oauth-token")
mockForceRefreshAccessToken.mockResolvedValue("refreshed-token")
mockGetModels.mockRejectedValue(new Error("offline"))
})
it("is dispatched separately from Moonshot and preserves an unknown selected model", () => {
const handler = buildApiHandler({
apiProvider: "kimi-code",
kimiCodeAuthMethod: "api-key",
kimiCodeApiKey: "kimi-key",
apiModelId: "future-kimi-model",
})
expect(handler).toBeInstanceOf(KimiCodeHandler)
expect(handler.getModel().id).toBe("future-kimi-model")
})
it("uses kimi-for-coding only when no model is selected", () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "kimi-key" })
expect(handler.getModel().id).toBe("kimi-for-coding")
})
it("uses API key when auth method is api-key", async () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "my-api-key" })
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
new Response(JSON.stringify({ choices: [{ message: { content: "response" }, finish_reason: "stop" }] }), {
status: 200,
}),
)
try {
for await (const chunk of gen) {
// consume
}
} catch {
// expected - mock is incomplete
}
expect(mockGetAccessToken).not.toHaveBeenCalled()
})
it("uses OAuth token when auth method is oauth or not specified", async () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" })
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
try {
for await (const chunk of gen) {
// consume
}
} catch {
// expected - mock will fail
}
expect(mockGetAccessToken).toHaveBeenCalled()
})
it("throws error when OAuth is required but no token available", async () => {
mockGetAccessToken.mockResolvedValueOnce(null)
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" })
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
await expect(async () => {
for await (const chunk of gen) {
// consume
}
}).rejects.toThrow("Not authenticated with Kimi Code")
})
it("throws error when API key auth is missing the key", async () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key" })
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
await expect(async () => {
for await (const chunk of gen) {
// consume
}
}).rejects.toThrow("Kimi Code API key is required")
})
it("retries with forced refresh on 401 when using OAuth", async () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" })
const fetchSpy = vi.spyOn(globalThis, "fetch")
fetchSpy.mockResolvedValueOnce(new Response(null, { status: 401 }))
fetchSpy.mockResolvedValueOnce(
new Response(JSON.stringify({ choices: [{ message: { content: "ok" }, finish_reason: "stop" }] }), {
status: 200,
}),
)
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
try {
for await (const chunk of gen) {
// consume
}
} catch {
// expected - mock is incomplete
}
expect(mockForceRefreshAccessToken).toHaveBeenCalledOnce()
})
it("force-refreshes and retries exactly once after a non-streaming OAuth 401", async () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "oauth" })
const unauthorized = Object.assign(new Error("Unauthorized"), { status: 401 })
const createCompletion = vi
.spyOn((handler as any).client.chat.completions, "create")
.mockRejectedValueOnce(unauthorized)
.mockResolvedValueOnce({ choices: [{ message: { content: "retried" } }] })
await expect(handler.completePrompt("test")).resolves.toBe("retried")
expect(mockForceRefreshAccessToken).toHaveBeenCalledOnce()
expect(createCompletion).toHaveBeenCalledTimes(2)
})
it("does not retry on 401 when using API key auth", async () => {
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" })
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(new Response(null, { status: 401 }))
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
await expect(async () => {
for await (const chunk of gen) {
// consume
}
}).rejects.toThrow()
expect(mockForceRefreshAccessToken).not.toHaveBeenCalled()
})
it("fetches models during prepareRequest", async () => {
mockGetModels.mockResolvedValueOnce({ "test-model": { maxTokens: 1000 } })
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" })
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
try {
for await (const chunk of gen) {
// consume
}
} catch {
// expected
}
expect(mockGetModels).toHaveBeenCalled()
})
it("continues when model discovery fails", async () => {
mockGetModels.mockRejectedValueOnce(new Error("discovery failed"))
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" })
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
try {
for await (const chunk of gen) {
// consume
}
} catch {
// expected - different error
}
expect(mockGetModels).toHaveBeenCalled()
})
it.each([
["failure", () => Promise.reject(new Error("offline"))],
["empty response", () => Promise.resolve({})],
])("does not repeatedly block requests after model discovery %s", async (_case, discovery) => {
mockGetModels.mockImplementationOnce(discovery)
vi.spyOn(globalThis, "fetch").mockImplementation(
async () => new Response(JSON.stringify({ choices: [{ message: { content: "ok" } }] }), { status: 200 }),
)
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" })
await handler.completePrompt("first")
await handler.completePrompt("second")
expect(mockGetModels).toHaveBeenCalledOnce()
})
it("uses discovered model info when available", async () => {
mockGetModels.mockResolvedValueOnce({ "kimi-for-coding": { maxTokens: 8000, contextWindow: 128000 } })
const handler = new KimiCodeHandler({ kimiCodeAuthMethod: "api-key", kimiCodeApiKey: "key" })
const gen = handler.createMessage("system", [{ role: "user", content: "test" }])
try {
for await (const chunk of gen) {
// consume
}
} catch {
// expected
}
const model = handler.getModel()
expect(model.info.maxTokens).toBe(8000)
})
})