| 1 | import { afterEach, describe, expect, it, vi } from "vitest"; |
| 2 | import { |
| 3 | cliReleaseChannel, |
| 4 | desktopReleaseChannel, |
| 5 | handleCLIRelease, |
| 6 | handleDesktopReleaseManifest, |
| 7 | } from "./desktop_release"; |
| 8 | import worker from "./index"; |
| 9 | |
| 10 | const sha256 = "a".repeat(64); |
| 11 | |
| 12 | function desktopManifest(version: string, base?: string) { |
| 13 | const releaseBase = base ?? `https://dl.reasonix.io/desktop-${version}/`; |
| 14 | const asset = (name: string) => { |
| 15 | const url = releaseBase + name; |
| 16 | return { url, sig: `${url}.minisig`, size: 42, sha256 }; |
| 17 | }; |
| 18 | return { |
| 19 | version, |
| 20 | download_page: "https://reasonix.io/?download=desktop#start", |
| 21 | platforms: { |
| 22 | "darwin-arm64": asset("Reasonix-darwin-arm64.zip"), |
| 23 | "darwin-amd64": asset("Reasonix-darwin-amd64.zip"), |
| 24 | "windows-amd64": asset("Reasonix-windows-amd64-installer.exe"), |
| 25 | "windows-arm64": asset("Reasonix-windows-arm64-installer.exe"), |
| 26 | "linux-amd64": asset("Reasonix-linux-amd64.tar.gz"), |
| 27 | }, |
| 28 | native_packages: { |
| 29 | "linux-amd64": asset("Reasonix-linux-amd64.deb"), |
| 30 | }, |
| 31 | downloads: { |
| 32 | "Reasonix-darwin-universal.dmg": asset("Reasonix-darwin-universal.dmg"), |
| 33 | "Reasonix-windows-amd64.zip": asset("Reasonix-windows-amd64.zip"), |
| 34 | }, |
| 35 | }; |
| 36 | } |
| 37 | |
| 38 | function desktopManifestText(version: string, base?: string): string { |
| 39 | return JSON.stringify(desktopManifest(version, base)); |
| 40 | } |
| 41 | |
| 42 | function githubDesktopRelease(version: string, overrides: Record<string, unknown> = {}) { |
| 43 | const tag = `desktop-${version}`; |
| 44 | return { |
| 45 | tag_name: tag, |
| 46 | draft: false, |
| 47 | prerelease: false, |
| 48 | assets: [{ |
| 49 | name: "latest.json", |
| 50 | browser_download_url: |
| 51 | `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/latest.json`, |
| 52 | size: 42, |
| 53 | }], |
| 54 | ...overrides, |
| 55 | }; |
| 56 | } |
| 57 | |
| 58 | const cliAssets = [ |
| 59 | "reasonix-darwin-amd64.tar.gz", |
| 60 | "reasonix-darwin-arm64.tar.gz", |
| 61 | "reasonix-linux-amd64.tar.gz", |
| 62 | "reasonix-linux-arm64.tar.gz", |
| 63 | "reasonix-windows-amd64.zip", |
| 64 | "reasonix-windows-arm64.zip", |
| 65 | "SHA256SUMS", |
| 66 | ]; |
| 67 | |
| 68 | const cliRelease = (tag: string, prerelease: boolean) => ({ |
| 69 | tag_name: tag, |
| 70 | prerelease, |
| 71 | html_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/tag/${tag}`, |
| 72 | assets: cliAssets.map((name) => ({ |
| 73 | name, |
| 74 | browser_download_url: `https://github.com/esengine/DeepSeek-Reasonix/releases/download/${tag}/${name}`, |
| 75 | size: 42, |
| 76 | })), |
| 77 | }); |
| 78 | |
| 79 | afterEach(() => { |
| 80 | vi.unstubAllGlobals(); |
| 81 | }); |
| 82 | |
| 83 | describe("desktop Preview release gateway", () => { |
| 84 | it("recognizes Preview and the legacy Canary compatibility route", () => { |
| 85 | expect(desktopReleaseChannel("/v1/desktop/releases/preview/latest.json")).toBe("preview"); |
| 86 | expect(desktopReleaseChannel("/v1/desktop/releases/canary/latest.json")).toBe("canary"); |
| 87 | expect(desktopReleaseChannel("/v1/desktop/releases/rc/latest.json")).toBeNull(); |
| 88 | }); |
| 89 | |
| 90 | it("serves a complete Preview manifest from the canonical pointer first", async () => { |
| 91 | const fetchMock = vi.fn(async (_url: string) => |
| 92 | new Response(desktopManifestText("v1.2.0-preview.7"), { status: 200 }), |
| 93 | ); |
| 94 | vi.stubGlobal("fetch", fetchMock); |
| 95 | |
| 96 | const response = await handleDesktopReleaseManifest("preview"); |
| 97 | |
| 98 | expect(response.status).toBe(200); |
| 99 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 100 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-preview"); |
| 101 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 102 | expect(fetchMock.mock.calls[0]?.[0]).toBe("https://dl.reasonix.io/preview/latest.json"); |
| 103 | }); |
| 104 | |
| 105 | it("keeps serving the signed rolling Preview manifest during pointer migration", async () => { |
| 106 | const legacy = desktopManifest( |
| 107 | "v1.18.0-preview.62", |
| 108 | "https://dl.reasonix.io/desktop-preview/", |
| 109 | ); |
| 110 | Reflect.deleteProperty(legacy, "downloads"); |
| 111 | const fetchMock = vi.fn(async () => |
| 112 | new Response(JSON.stringify(legacy), { status: 200 }), |
| 113 | ); |
| 114 | vi.stubGlobal("fetch", fetchMock); |
| 115 | |
| 116 | const response = await handleDesktopReleaseManifest("preview"); |
| 117 | |
| 118 | expect(response.status).toBe(200); |
| 119 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-preview"); |
| 120 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 121 | }); |
| 122 | |
| 123 | it("rejects a new-format Preview manifest that still uses rolling assets", async () => { |
| 124 | const rolling = desktopManifest( |
| 125 | "v1.18.0-preview.63", |
| 126 | "https://dl.reasonix.io/desktop-preview/", |
| 127 | ); |
| 128 | const fetchMock = vi |
| 129 | .fn() |
| 130 | .mockResolvedValueOnce(new Response(JSON.stringify(rolling), { status: 200 })) |
| 131 | .mockResolvedValueOnce(new Response("missing", { status: 404 })); |
| 132 | vi.stubGlobal("fetch", fetchMock); |
| 133 | |
| 134 | const response = await handleDesktopReleaseManifest("preview"); |
| 135 | |
| 136 | expect(response.status).toBe(502); |
| 137 | expect(fetchMock).toHaveBeenCalledTimes(2); |
| 138 | }); |
| 139 | |
| 140 | it("continues to the compatibility pointer after an invalid 200 response", async () => { |
| 141 | const invalid = desktopManifest("v1.2.0-preview.8"); |
| 142 | invalid.platforms["darwin-arm64"].size = 0; |
| 143 | const fetchMock = vi |
| 144 | .fn() |
| 145 | .mockResolvedValueOnce(new Response(JSON.stringify(invalid), { status: 200 })) |
| 146 | .mockResolvedValueOnce(new Response(desktopManifestText("v1.2.0-preview.7"), { status: 200 })); |
| 147 | vi.stubGlobal("fetch", fetchMock); |
| 148 | |
| 149 | const response = await handleDesktopReleaseManifest("preview"); |
| 150 | |
| 151 | expect(response.status).toBe(200); |
| 152 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-canary-compat"); |
| 153 | expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([ |
| 154 | "https://dl.reasonix.io/preview/latest.json", |
| 155 | "https://dl.reasonix.io/canary/latest.json", |
| 156 | ]); |
| 157 | }); |
| 158 | |
| 159 | it("rejects hostile URLs and incomplete Desktop manifests", async () => { |
| 160 | const cases: Array<[string, (manifest: ReturnType<typeof desktopManifest>) => void]> = [ |
| 161 | ["malicious host", (manifest) => { |
| 162 | const url = "https://evil.invalid/desktop-v1.2.0-preview.7/Reasonix-darwin-arm64.zip"; |
| 163 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 164 | }], |
| 165 | ["userinfo", (manifest) => { |
| 166 | const url = "https://dl.reasonix.io@evil.invalid/desktop-v1.2.0-preview.7/Reasonix-darwin-arm64.zip"; |
| 167 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 168 | }], |
| 169 | ["http", (manifest) => { |
| 170 | const url = "http://dl.reasonix.io/desktop-v1.2.0-preview.7/Reasonix-darwin-arm64.zip"; |
| 171 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 172 | }], |
| 173 | ["wrong channel path", (manifest) => { |
| 174 | const url = "https://dl.reasonix.io/preview/Reasonix-darwin-arm64.zip"; |
| 175 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 176 | }], |
| 177 | ["wrong filename", (manifest) => { |
| 178 | const url = "https://dl.reasonix.io/desktop-v1.2.0-preview.7/Reasonix-darwin-amd64.zip"; |
| 179 | Object.assign(manifest.platforms["darwin-arm64"], { url, sig: `${url}.minisig` }); |
| 180 | }], |
| 181 | ["missing asset", (manifest) => { |
| 182 | delete (manifest.platforms as Partial<typeof manifest.platforms>)["windows-arm64"]; |
| 183 | }], |
| 184 | ["missing website download", (manifest) => { |
| 185 | delete (manifest.downloads as Partial<typeof manifest.downloads>)["Reasonix-darwin-universal.dmg"]; |
| 186 | }], |
| 187 | ["invalid website download", (manifest) => { |
| 188 | manifest.downloads["Reasonix-windows-amd64.zip"].size = 0; |
| 189 | }], |
| 190 | ["bad SHA", (manifest) => { |
| 191 | manifest.platforms["darwin-arm64"].sha256 = "A".repeat(64); |
| 192 | }], |
| 193 | ["zero size", (manifest) => { |
| 194 | manifest.platforms["darwin-arm64"].size = 0; |
| 195 | }], |
| 196 | ["size above release maximum", (manifest) => { |
| 197 | manifest.platforms["darwin-arm64"].size = 1073741825; |
| 198 | }], |
| 199 | ["bad signature", (manifest) => { |
| 200 | manifest.platforms["darwin-arm64"].sig += "?mirror=1"; |
| 201 | }], |
| 202 | ["wrong download page", (manifest) => { |
| 203 | manifest.download_page = "https://evil.invalid/download"; |
| 204 | }], |
| 205 | ]; |
| 206 | |
| 207 | for (const [name, mutate] of cases) { |
| 208 | const manifest = desktopManifest("v1.2.0-preview.7"); |
| 209 | mutate(manifest); |
| 210 | const fetchMock = vi |
| 211 | .fn() |
| 212 | .mockResolvedValueOnce(new Response(JSON.stringify(manifest), { status: 200 })) |
| 213 | .mockResolvedValueOnce(new Response("missing", { status: 404 })); |
| 214 | vi.stubGlobal("fetch", fetchMock); |
| 215 | |
| 216 | const response = await handleDesktopReleaseManifest("preview"); |
| 217 | |
| 218 | expect(response.status, name).toBe(502); |
| 219 | expect(fetchMock, name).toHaveBeenCalledTimes(2); |
| 220 | vi.unstubAllGlobals(); |
| 221 | } |
| 222 | }); |
| 223 | }); |
| 224 | |
| 225 | describe("desktop Stable GitHub fallback", () => { |
| 226 | it("accepts the exact versioned R2 asset directory", async () => { |
| 227 | const fetchMock = vi.fn(async (_url: string) => |
| 228 | new Response(desktopManifestText("v1.18.0"), { status: 200 }), |
| 229 | ); |
| 230 | vi.stubGlobal("fetch", fetchMock); |
| 231 | |
| 232 | const response = await handleDesktopReleaseManifest("stable"); |
| 233 | |
| 234 | expect(response.status).toBe(200); |
| 235 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-stable"); |
| 236 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 237 | }); |
| 238 | |
| 239 | it("keeps serving a legacy Stable manifest that predates website downloads", async () => { |
| 240 | const legacy = desktopManifest("v1.17.21"); |
| 241 | Reflect.deleteProperty(legacy, "downloads"); |
| 242 | const fetchMock = vi.fn(async () => |
| 243 | new Response(JSON.stringify(legacy), { status: 200 }), |
| 244 | ); |
| 245 | vi.stubGlobal("fetch", fetchMock); |
| 246 | |
| 247 | const response = await handleDesktopReleaseManifest("stable"); |
| 248 | |
| 249 | expect(response.status).toBe(200); |
| 250 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-stable"); |
| 251 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 252 | }); |
| 253 | |
| 254 | it("accepts null but rejects an empty downloads object as legacy", async () => { |
| 255 | const nullDownloads = desktopManifest("v1.17.21"); |
| 256 | Reflect.set(nullDownloads, "downloads", null); |
| 257 | const nullFetch = vi.fn(async () => |
| 258 | new Response(JSON.stringify(nullDownloads), { status: 200 }), |
| 259 | ); |
| 260 | vi.stubGlobal("fetch", nullFetch); |
| 261 | |
| 262 | const legacyResponse = await handleDesktopReleaseManifest("stable"); |
| 263 | expect(legacyResponse.status).toBe(200); |
| 264 | expect(nullFetch).toHaveBeenCalledTimes(1); |
| 265 | |
| 266 | const emptyDownloads = desktopManifest("v1.17.21"); |
| 267 | Reflect.set(emptyDownloads, "downloads", {}); |
| 268 | const emptyFetch = vi |
| 269 | .fn() |
| 270 | .mockResolvedValueOnce(new Response(JSON.stringify(emptyDownloads), { status: 200 })) |
| 271 | .mockResolvedValueOnce(new Response("missing", { status: 404 })); |
| 272 | vi.stubGlobal("fetch", emptyFetch); |
| 273 | |
| 274 | const invalidResponse = await handleDesktopReleaseManifest("stable"); |
| 275 | expect(invalidResponse.status).toBe(502); |
| 276 | expect(emptyFetch).toHaveBeenCalledTimes(2); |
| 277 | }); |
| 278 | |
| 279 | it("uses /releases/latest and requires the release tag to match the manifest version", async () => { |
| 280 | const githubBase = |
| 281 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/"; |
| 282 | const invalidR2 = desktopManifest("v1.19.0"); |
| 283 | invalidR2.platforms["darwin-arm64"].size = 0; |
| 284 | const fetchMock = vi |
| 285 | .fn() |
| 286 | .mockResolvedValueOnce(new Response(JSON.stringify(invalidR2), { status: 200 })) |
| 287 | .mockResolvedValueOnce(new Response(JSON.stringify(githubDesktopRelease("v1.18.0")), { status: 200 })) |
| 288 | .mockResolvedValueOnce(new Response(desktopManifestText("v1.18.0", githubBase), { status: 200 })); |
| 289 | vi.stubGlobal("fetch", fetchMock); |
| 290 | |
| 291 | const response = await handleDesktopReleaseManifest("stable"); |
| 292 | const body = await response.json() as { version?: string }; |
| 293 | |
| 294 | expect(response.status).toBe(200); |
| 295 | expect(body.version).toBe("v1.18.0"); |
| 296 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-desktop-release"); |
| 297 | expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([ |
| 298 | "https://dl.reasonix.io/latest/latest.json", |
| 299 | "https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases/latest", |
| 300 | `${githubBase}latest.json`, |
| 301 | ]); |
| 302 | }); |
| 303 | |
| 304 | it("rejects a GitHub manifest whose version disagrees with the latest release tag", async () => { |
| 305 | const manifestBase = |
| 306 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.17.9/"; |
| 307 | const fetchMock = vi |
| 308 | .fn() |
| 309 | .mockResolvedValueOnce(new Response("missing", { status: 404 })) |
| 310 | .mockResolvedValueOnce(new Response(JSON.stringify(githubDesktopRelease("v1.18.0")), { status: 200 })) |
| 311 | .mockResolvedValueOnce(new Response(desktopManifestText("v1.17.9", manifestBase), { status: 200 })); |
| 312 | vi.stubGlobal("fetch", fetchMock); |
| 313 | |
| 314 | const response = await handleDesktopReleaseManifest("stable"); |
| 315 | |
| 316 | expect(response.status).toBe(502); |
| 317 | expect(fetchMock).toHaveBeenCalledTimes(3); |
| 318 | }); |
| 319 | |
| 320 | it("rejects a non-canonical or zero-byte latest.json release asset", async () => { |
| 321 | for (const asset of [ |
| 322 | { |
| 323 | name: "latest.json", |
| 324 | browser_download_url: |
| 325 | "https://evil.invalid/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/latest.json", |
| 326 | size: 42, |
| 327 | }, |
| 328 | { |
| 329 | name: "latest.json", |
| 330 | browser_download_url: |
| 331 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/desktop-v1.18.0/latest.json", |
| 332 | size: 0, |
| 333 | }, |
| 334 | ]) { |
| 335 | const release = githubDesktopRelease("v1.18.0", { assets: [asset] }); |
| 336 | const fetchMock = vi |
| 337 | .fn() |
| 338 | .mockResolvedValueOnce(new Response("missing", { status: 404 })) |
| 339 | .mockResolvedValueOnce(new Response(JSON.stringify(release), { status: 200 })); |
| 340 | vi.stubGlobal("fetch", fetchMock); |
| 341 | |
| 342 | const response = await handleDesktopReleaseManifest("stable"); |
| 343 | |
| 344 | expect(response.status).toBe(502); |
| 345 | expect(fetchMock).toHaveBeenCalledTimes(2); |
| 346 | vi.unstubAllGlobals(); |
| 347 | } |
| 348 | }); |
| 349 | }); |
| 350 | |
| 351 | describe("release gateway HTTP method contract", () => { |
| 352 | const env = {} as Parameters<typeof worker.fetch>[1]; |
| 353 | |
| 354 | it("answers CORS preflight without loading an upstream release", async () => { |
| 355 | const fetchMock = vi.fn(); |
| 356 | vi.stubGlobal("fetch", fetchMock); |
| 357 | |
| 358 | const response = await worker.fetch( |
| 359 | new Request("https://crash.reasonix.io/v1/cli/releases/stable/latest.json", { |
| 360 | method: "OPTIONS", |
| 361 | }), |
| 362 | env, |
| 363 | ); |
| 364 | |
| 365 | expect(response.status).toBe(204); |
| 366 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 367 | expect(response.headers.get("access-control-allow-methods")).toBe("GET, HEAD, OPTIONS"); |
| 368 | expect(response.headers.get("access-control-max-age")).toBe("86400"); |
| 369 | expect(response.headers.get("allow")).toBe("GET, HEAD, OPTIONS"); |
| 370 | expect(fetchMock).not.toHaveBeenCalled(); |
| 371 | }); |
| 372 | |
| 373 | it("serves HEAD with GET status and headers but no body", async () => { |
| 374 | const fetchMock = vi.fn(async (_url: string) => |
| 375 | new Response(desktopManifestText("v1.2.0-preview.7"), { status: 200 }), |
| 376 | ); |
| 377 | vi.stubGlobal("fetch", fetchMock); |
| 378 | |
| 379 | const response = await worker.fetch( |
| 380 | new Request("https://crash.reasonix.io/v1/desktop/releases/preview/latest.json", { |
| 381 | method: "HEAD", |
| 382 | }), |
| 383 | env, |
| 384 | ); |
| 385 | |
| 386 | expect(response.status).toBe(200); |
| 387 | expect(await response.text()).toBe(""); |
| 388 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 389 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-preview"); |
| 390 | expect(fetchMock).toHaveBeenCalledTimes(1); |
| 391 | }); |
| 392 | |
| 393 | it("returns a CORS-aware 405 for unsupported release methods", async () => { |
| 394 | const fetchMock = vi.fn(); |
| 395 | vi.stubGlobal("fetch", fetchMock); |
| 396 | |
| 397 | const response = await worker.fetch( |
| 398 | new Request("https://crash.reasonix.io/v1/cli/releases/preview/latest.json", { |
| 399 | method: "POST", |
| 400 | }), |
| 401 | env, |
| 402 | ); |
| 403 | |
| 404 | expect(response.status).toBe(405); |
| 405 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 406 | expect(response.headers.get("allow")).toBe("GET, HEAD, OPTIONS"); |
| 407 | expect(fetchMock).not.toHaveBeenCalled(); |
| 408 | }); |
| 409 | }); |
| 410 | |
| 411 | describe("CLI public release gateway", () => { |
| 412 | it("recognizes only Stable and Preview routes", () => { |
| 413 | expect(cliReleaseChannel("/v1/cli/releases/stable/latest.json")).toBe("stable"); |
| 414 | expect(cliReleaseChannel("/v1/cli/releases/preview/latest.json")).toBe("preview"); |
| 415 | expect(cliReleaseChannel("/v1/cli/releases/rc/latest.json")).toBeNull(); |
| 416 | }); |
| 417 | |
| 418 | it("serves a complete strict Preview pointer from R2", async () => { |
| 419 | const fetchMock = vi.fn(async (_url: string) => |
| 420 | new Response(JSON.stringify(cliRelease("v1.18.0-preview.1", true)), { status: 200 }), |
| 421 | ); |
| 422 | vi.stubGlobal("fetch", fetchMock); |
| 423 | |
| 424 | const response = await handleCLIRelease("preview"); |
| 425 | const body = await response.json() as { tag_name?: string }; |
| 426 | |
| 427 | expect(body.tag_name).toBe("v1.18.0-preview.1"); |
| 428 | expect(response.headers.get("access-control-allow-origin")).toBe("*"); |
| 429 | expect(response.headers.get("x-reasonix-release-source")).toBe("r2-cli-preview"); |
| 430 | expect(fetchMock.mock.calls[0]?.[0]).toBe("https://dl.reasonix.io/cli/preview/latest.json"); |
| 431 | }); |
| 432 | |
| 433 | it("rewrites release notes to the canonical repository tag URL", async () => { |
| 434 | const release = cliRelease("v1.18.0", false); |
| 435 | release.html_url = "https://evil.invalid/phishing"; |
| 436 | const fetchMock = vi.fn(async () => |
| 437 | new Response(JSON.stringify(release), { status: 200 }), |
| 438 | ); |
| 439 | vi.stubGlobal("fetch", fetchMock); |
| 440 | |
| 441 | const response = await handleCLIRelease("stable"); |
| 442 | const body = await response.json() as { html_url?: string }; |
| 443 | |
| 444 | expect(body.html_url).toBe( |
| 445 | "https://github.com/esengine/DeepSeek-Reasonix/releases/tag/v1.18.0", |
| 446 | ); |
| 447 | }); |
| 448 | |
| 449 | it("falls back after an invalid 200 and strictly filters GitHub releases", async () => { |
| 450 | const invalidPointer = cliRelease("v1.18.0-preview.20", true); |
| 451 | invalidPointer.assets[0]!.size = 0; |
| 452 | const releases = [ |
| 453 | cliRelease("v1.19.0-rc.1", true), |
| 454 | cliRelease("v1.18.0-preview.2", true), |
| 455 | cliRelease("v1.18.0-preview.12", true), |
| 456 | cliRelease("v1.18.0-preview.13", false), |
| 457 | cliRelease("v1.17.21", false), |
| 458 | ]; |
| 459 | const fetchMock = vi |
| 460 | .fn() |
| 461 | .mockResolvedValueOnce(new Response(JSON.stringify(invalidPointer), { status: 200 })) |
| 462 | .mockResolvedValueOnce(new Response(JSON.stringify(releases), { status: 200 })); |
| 463 | vi.stubGlobal("fetch", fetchMock); |
| 464 | |
| 465 | const response = await handleCLIRelease("preview"); |
| 466 | const body = await response.json() as { tag_name?: string }; |
| 467 | |
| 468 | expect(body.tag_name).toBe("v1.18.0-preview.12"); |
| 469 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 470 | expect(fetchMock.mock.calls[1]?.[0]).toContain("releases?per_page=100"); |
| 471 | }); |
| 472 | |
| 473 | it("requires every CLI asset URL to be canonical", async () => { |
| 474 | const invalidURLs = [ |
| 475 | "https://evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz", |
| 476 | "https://github.com@evil.invalid/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz", |
| 477 | "http://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-amd64.tar.gz", |
| 478 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.19.0/reasonix-darwin-amd64.tar.gz", |
| 479 | "https://github.com/esengine/DeepSeek-Reasonix/releases/download/v1.20.0/reasonix-darwin-arm64.tar.gz", |
| 480 | ]; |
| 481 | |
| 482 | for (const browserDownloadURL of invalidURLs) { |
| 483 | const invalid = cliRelease("v1.20.0", false); |
| 484 | invalid.assets[0]!.browser_download_url = browserDownloadURL; |
| 485 | const fetchMock = vi |
| 486 | .fn() |
| 487 | .mockResolvedValueOnce(new Response(JSON.stringify(invalid), { status: 200 })) |
| 488 | .mockResolvedValueOnce(new Response(JSON.stringify([cliRelease("v1.19.0", false)]), { status: 200 })); |
| 489 | vi.stubGlobal("fetch", fetchMock); |
| 490 | |
| 491 | const response = await handleCLIRelease("stable"); |
| 492 | const body = await response.json() as { tag_name?: string }; |
| 493 | |
| 494 | expect(body.tag_name).toBe("v1.19.0"); |
| 495 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 496 | vi.unstubAllGlobals(); |
| 497 | } |
| 498 | }); |
| 499 | |
| 500 | it("rejects non-positive, malformed, and oversized CLI asset sizes", async () => { |
| 501 | const invalidSizes: unknown[] = [ |
| 502 | 0, |
| 503 | -1, |
| 504 | "42", |
| 505 | undefined, |
| 506 | 1.5, |
| 507 | 1073741825, |
| 508 | Number.MAX_SAFE_INTEGER + 1, |
| 509 | NaN, |
| 510 | ]; |
| 511 | for (const size of invalidSizes) { |
| 512 | const invalid = cliRelease("v1.20.0", false); |
| 513 | (invalid.assets[0] as { size?: unknown }).size = size; |
| 514 | const fetchMock = vi |
| 515 | .fn() |
| 516 | .mockResolvedValueOnce(new Response(JSON.stringify(invalid), { status: 200 })) |
| 517 | .mockResolvedValueOnce(new Response(JSON.stringify([cliRelease("v1.19.0", false)]), { status: 200 })); |
| 518 | vi.stubGlobal("fetch", fetchMock); |
| 519 | |
| 520 | const response = await handleCLIRelease("stable"); |
| 521 | const body = await response.json() as { tag_name?: string }; |
| 522 | |
| 523 | expect(body.tag_name, `size ${String(size)}`).toBe("v1.19.0"); |
| 524 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 525 | vi.unstubAllGlobals(); |
| 526 | } |
| 527 | }); |
| 528 | |
| 529 | it("rejects duplicate required CLI assets", async () => { |
| 530 | const invalid = cliRelease("v1.20.0", false); |
| 531 | invalid.assets.push({ ...invalid.assets[0]! }); |
| 532 | const fetchMock = vi |
| 533 | .fn() |
| 534 | .mockResolvedValueOnce(new Response(JSON.stringify(invalid), { status: 200 })) |
| 535 | .mockResolvedValueOnce(new Response(JSON.stringify([cliRelease("v1.19.0", false)]), { status: 200 })); |
| 536 | vi.stubGlobal("fetch", fetchMock); |
| 537 | |
| 538 | const response = await handleCLIRelease("stable"); |
| 539 | const body = await response.json() as { tag_name?: string }; |
| 540 | |
| 541 | expect(body.tag_name).toBe("v1.19.0"); |
| 542 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 543 | }); |
| 544 | |
| 545 | it("rejects incomplete releases and compares huge Stable versions exactly", async () => { |
| 546 | const incomplete = cliRelease("v100000000000000000001.0.0", false); |
| 547 | incomplete.assets.pop(); |
| 548 | const releases = [ |
| 549 | incomplete, |
| 550 | cliRelease("v99999999999999999999.999.999", false), |
| 551 | cliRelease("v100000000000000000000.0.0", false), |
| 552 | ]; |
| 553 | const fetchMock = vi |
| 554 | .fn() |
| 555 | .mockResolvedValueOnce(new Response(JSON.stringify(incomplete), { status: 200 })) |
| 556 | .mockResolvedValueOnce(new Response(JSON.stringify(releases), { status: 200 })); |
| 557 | vi.stubGlobal("fetch", fetchMock); |
| 558 | |
| 559 | const response = await handleCLIRelease("stable"); |
| 560 | const body = await response.json() as { tag_name?: string }; |
| 561 | |
| 562 | expect(body.tag_name).toBe("v100000000000000000000.0.0"); |
| 563 | expect(response.headers.get("x-reasonix-release-source")).toBe("github-cli-releases"); |
| 564 | expect(fetchMock).toHaveBeenCalledTimes(2); |
| 565 | }); |
| 566 | }); |
| 567 |