forked from ericc-ch/copilot-api
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathversion-detection.test.ts
More file actions
349 lines (295 loc) · 11.6 KB
/
version-detection.test.ts
File metadata and controls
349 lines (295 loc) · 11.6 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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
import { describe, test, expect, mock, beforeEach } from "bun:test"
import { setSystemTime } from "bun:test"
import type { state as StateType } from "../src/lib/state"
import type { getCopilotChatVersion as GetCopilotChatVersion } from "../src/services/get-copilot-chat-version"
import type { getVSCodeVersion as GetVSCodeVersion } from "../src/services/get-vscode-version"
// ---------------------------------------------------------------------------
// We test the modules by mocking global `fetch` before importing them.
// Each describe block re-imports after resetting the module registry so we
// get fresh module-level caches for every suite.
// ---------------------------------------------------------------------------
// Helpers ----------------------------------------------------------------
function makeFetchMock(handler: (url: string, init?: RequestInit) => Response) {
return mock((url: string | URL | Request, init?: RequestInit) => {
const urlStr = url instanceof Request ? url.url : url.toString()
return Promise.resolve(handler(urlStr, init))
}) as unknown as typeof fetch
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
})
}
function textResponse(body: string, status = 200): Response {
return new Response(body, {
status,
headers: { "Content-Type": "text/plain" },
})
}
// Module type aliases for properly typed dynamic imports
type VSCodeVersionModule = { getVSCodeVersion: typeof GetVSCodeVersion }
type CopilotChatVersionModule = {
getCopilotChatVersion: typeof GetCopilotChatVersion
}
type StateModule = { state: typeof StateType }
// ---------------------------------------------------------------------------
// getVSCodeVersion tests
// ---------------------------------------------------------------------------
describe("getVSCodeVersion", () => {
let callCount = 0
beforeEach(() => {
callCount = 0
})
test("returns version from official VS Code API (primary path)", async () => {
globalThis.fetch = makeFetchMock((_url) => {
callCount++
return jsonResponse(["1.99.0", "1.98.0"])
})
const mod = (await import(
`../src/services/get-vscode-version.ts?t=${Date.now()}`
)) as VSCodeVersionModule
const version = await mod.getVSCodeVersion()
expect(version).toBe("1.99.0")
expect(callCount).toBe(1)
})
test("falls back to AUR when official API fails", async () => {
let requestIndex = 0
globalThis.fetch = makeFetchMock((_url) => {
const i = requestIndex++
if (i === 0) throw new Error("network error")
// AUR PKGBUILD response
return textResponse("pkgver=1.88.0\narch=(x86_64)")
})
const mod = (await import(
`../src/services/get-vscode-version.ts?t=${Date.now() + 1}`
)) as VSCodeVersionModule
const version = await mod.getVSCodeVersion()
expect(version).toBe("1.88.0")
})
test("returns hardcoded fallback when both official API and AUR fail", async () => {
globalThis.fetch = makeFetchMock((_url) => {
throw new Error("offline")
})
const mod = (await import(
`../src/services/get-vscode-version.ts?t=${Date.now() + 2}`
)) as VSCodeVersionModule
const version = await mod.getVSCodeVersion()
expect(version).toBe("1.104.3")
})
test("cache prevents second fetch within TTL", async () => {
let fetchCallCount = 0
globalThis.fetch = makeFetchMock((_url) => {
fetchCallCount++
return jsonResponse(["1.99.5"])
})
const mod = (await import(
`../src/services/get-vscode-version.ts?t=${Date.now() + 3}`
)) as VSCodeVersionModule
const v1 = await mod.getVSCodeVersion()
const v2 = await mod.getVSCodeVersion()
expect(v1).toBe("1.99.5")
expect(v2).toBe("1.99.5")
// fetch should only have been called once
expect(fetchCallCount).toBe(1)
})
// T1 — VS Code API returns malformed JSON (non-array body {}): falls back to AUR
test("T1: falls back to AUR when official API returns non-array body", async () => {
let requestIndex = 0
globalThis.fetch = makeFetchMock((_url) => {
const i = requestIndex++
if (i === 0) return jsonResponse({}) // non-array — triggers "Unexpected response shape"
// AUR PKGBUILD response
return textResponse("pkgver=1.90.0\narch=(x86_64)")
})
const mod = (await import(
`../src/services/get-vscode-version.ts?t=${Date.now() + 10}`
)) as VSCodeVersionModule
const version = await mod.getVSCodeVersion()
expect(version).toBe("1.90.0")
expect(requestIndex).toBe(2)
})
// T2 — AUR PKGBUILD missing pkgver= line: returns hardcoded fallback
test("T2: returns hardcoded fallback when AUR PKGBUILD has no pkgver line", async () => {
let requestIndex = 0
globalThis.fetch = makeFetchMock((_url) => {
const i = requestIndex++
if (i === 0) throw new Error("network error")
// AUR response missing pkgver=
return textResponse("pkgdesc='VSCode'\npkgrel=1\n")
})
const mod = (await import(
`../src/services/get-vscode-version.ts?t=${Date.now() + 11}`
)) as VSCodeVersionModule
const version = await mod.getVSCodeVersion()
expect(version).toBe("1.104.3")
})
// T5 — TTL expiry triggers refetch
test("T5: TTL expiry triggers a new fetch", async () => {
const CACHE_TTL = 24 * 60 * 60 * 1000
let fetchCount = 0
globalThis.fetch = makeFetchMock((_url) => {
fetchCount++
return jsonResponse(["1.99.0"])
})
const mod = (await import(
`../src/services/get-vscode-version.ts?t=${Date.now() + 12}`
)) as VSCodeVersionModule
// First call — populates cache
await mod.getVSCodeVersion()
expect(fetchCount).toBe(1)
// Advance clock past TTL
setSystemTime(new Date(Date.now() + CACHE_TTL + 1))
try {
// Second call — cache expired, should fetch again
await mod.getVSCodeVersion()
expect(fetchCount).toBe(2)
} finally {
// Always reset system time
setSystemTime()
}
})
})
// ---------------------------------------------------------------------------
// getCopilotChatVersion tests
// ---------------------------------------------------------------------------
describe("getCopilotChatVersion", () => {
const MARKETPLACE_URL =
"https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery"
const validMarketplaceResponse = {
results: [
{
extensions: [
{
versions: [{ version: "0.30.1" }],
},
],
},
],
}
test("returns version from Marketplace API", async () => {
globalThis.fetch = makeFetchMock((url) => {
expect(url).toBe(MARKETPLACE_URL)
return jsonResponse(validMarketplaceResponse)
})
const mod = (await import(
`../src/services/get-copilot-chat-version.ts?t=${Date.now()}`
)) as CopilotChatVersionModule
const version = await mod.getCopilotChatVersion()
expect(version).toBe("0.30.1")
})
test("returns hardcoded fallback on network error", async () => {
globalThis.fetch = makeFetchMock((_url) => {
throw new Error("connection refused")
})
const mod = (await import(
`../src/services/get-copilot-chat-version.ts?t=${Date.now() + 1}`
)) as CopilotChatVersionModule
const version = await mod.getCopilotChatVersion()
expect(version).toBe("0.26.7")
})
test("returns hardcoded fallback when API response has unexpected shape", async () => {
globalThis.fetch = makeFetchMock((_url) => {
return jsonResponse({ results: [] })
})
const mod = (await import(
`../src/services/get-copilot-chat-version.ts?t=${Date.now() + 2}`
)) as CopilotChatVersionModule
const version = await mod.getCopilotChatVersion()
expect(version).toBe("0.26.7")
})
test("cache prevents second fetch within TTL", async () => {
let fetchCallCount = 0
globalThis.fetch = makeFetchMock((_url) => {
fetchCallCount++
return jsonResponse(validMarketplaceResponse)
})
const mod = (await import(
`../src/services/get-copilot-chat-version.ts?t=${Date.now() + 3}`
)) as CopilotChatVersionModule
const v1 = await mod.getCopilotChatVersion()
const v2 = await mod.getCopilotChatVersion()
expect(v1).toBe("0.30.1")
expect(v2).toBe("0.30.1")
expect(fetchCallCount).toBe(1)
})
// T3 — Marketplace returns HTTP 503
test("T3: returns fallback when Marketplace returns HTTP 503", async () => {
globalThis.fetch = makeFetchMock((_url) => {
return new Response("Service Unavailable", { status: 503 })
})
const mod = (await import(
`../src/services/get-copilot-chat-version.ts?t=${Date.now() + 10}`
)) as CopilotChatVersionModule
const version = await mod.getCopilotChatVersion()
expect(version).toBe("0.26.7")
})
// T4 — Marketplace returns version: "" (empty string)
test("T4: returns fallback when Marketplace version is empty string", async () => {
globalThis.fetch = makeFetchMock((_url) => {
return jsonResponse({
results: [{ extensions: [{ versions: [{ version: "" }] }] }],
})
})
const mod = (await import(
`../src/services/get-copilot-chat-version.ts?t=${Date.now() + 11}`
)) as CopilotChatVersionModule
const version = await mod.getCopilotChatVersion()
expect(version).toBe("0.26.7")
})
// T7 — Format validation rejects CRLF-injected version
test("T7: rejects version with CRLF injection and returns fallback", async () => {
globalThis.fetch = makeFetchMock((_url) => {
return jsonResponse({
results: [
{
extensions: [{ versions: [{ version: "1.0.0\r\nEvil: header" }] }],
},
],
})
})
const mod = (await import(
`../src/services/get-copilot-chat-version.ts?t=${Date.now() + 12}`
)) as CopilotChatVersionModule
const version = await mod.getCopilotChatVersion()
expect(version).toBe("0.26.7")
})
})
// ---------------------------------------------------------------------------
// State interface test — shape check
// ---------------------------------------------------------------------------
describe("State type includes copilotChatVersion", () => {
test("state object accepts copilotChatVersion field", async () => {
const { state } = (await import(
`../src/lib/state.ts?t=${Date.now()}`
)) as StateModule
// Field must be optionally present (undefined by default)
expect(state.copilotChatVersion).toBeUndefined()
// Should be assignable without TS errors (runtime check)
state.copilotChatVersion = "0.26.7"
expect(state.copilotChatVersion).toBe("0.26.7")
})
})
// ---------------------------------------------------------------------------
// T6 — api-config header uses fallback when copilotChatVersion is undefined
// ---------------------------------------------------------------------------
describe("copilotHeaders fallback", () => {
test("T6: editor-plugin-version uses fallback string when state.copilotChatVersion is undefined", async () => {
const { copilotHeaders } = await import("../src/lib/api-config")
const minimalState = {
accountType: "individual",
manualApprove: false,
rateLimitWait: false,
showToken: false,
copilotToken: "tok",
vsCodeVersion: "1.99.0",
copilotChatVersion: undefined,
}
const headers = copilotHeaders(
minimalState as Parameters<typeof copilotHeaders>[0],
false,
)
expect(headers["editor-plugin-version"]).not.toBe("copilot-chat/undefined")
expect(headers["editor-plugin-version"]).toBe("copilot-chat/0.26.7")
})
})